Skip to content

docs(eventing): fix doc/code inconsistencies from the agentdocs review - #900

Merged
mrsabath merged 4 commits into
mainfrom
docs/fix-eventing-doc-inconsistencies
Oct 7, 2026
Merged

mrsabath merged 4 commits into
mainfrom
docs/fix-eventing-doc-inconsistencies

Conversation

@mrsabath

@mrsabath mrsabath commented Oct 7, 2026

Copy link
Copy Markdown
Member

From the review of all nine eventing/agentdocs/ files. Every fix below was verified against the code before the doc was edited — the repo's own §8.5/§8.6 discipline — and the code fix is reproduced by a test. This PR covers the review's inconsistencies section only; gaps and improvements are separate conversations.

1. SIGNED_ATTRS drift (5 sites in DESIGN_PHASE3)

§2.6's opening, §8.3's rule, IMPLEMENTATION_REPORT3's T4 row and shared/signing.py:185-219 all say Phase 3 added three attributes to SIGNED_ATTRS in ONE change: userkey, depth, agent. But five other sites in the same document still said two:

  • header (status line, T4 parenthetical)
  • §2.6 (the "has to land with depth in one change rather than two" sentence)
  • §7.7 ("this is the second canonicalisation-breaking change in Phase 3")
  • §9 step 4 ("SIGNED_ATTRS += userkey, depth")
  • §11 T4 row

All five now name three attributes and the one-change rule. §8.3 itself was already correct and untouched.

2. ER_USERKEY false positive — code fix

The runner refused to boot whenever REQUEST_TOPIC != "requests" and ER_USERKEY was unset. That false-positives every Phase 1 single-tenant deployment that renames the default — and the shipped k8s/base/configmap.yaml:18 does exactly that (REQUEST_TOPIC: kev1-requests, no ER_USERKEY): a fresh Phase 1 cluster boots no runner at all. Reproduced by executing ercfg.load() with the configmap's env.

The design's own text (§3.3) says the refusal guards per-user runners. The condition now keys on the per-user topic shape — {prefix}-{userkey}-requests (§3.1) — via a new tenancy.userkey_in_topic():

  • kev1-requests (renamed default) → boots, single-tenant.
  • kev1-gh-mrsabath-4c1d9e07-requests with no ER_USERKEY → still refuses.
  • Same topic with a mismatched ER_USERKEY → now also refuses (previously it booted and mis-attributed).
  • EventBridge's StoreRegistry.for_event remains the actual filing control, so a false negative in the shape check is counted (unattributed), never guessed.

userkey_in_topic lives in shared/tenancy.py next to is_valid_userkey because that module is "the ONLY place an identity becomes a name" — recognising one inside a topic name belongs there. The USERKEY_RE core is shared so the recognizer can't drift from the producer.

DESIGN_PHASE3.md §3.3 and §8.2 updated to describe the shape-keyed refusal. Tests: 10 new (TopicSet round-trip, each of the four suffixes, the renamed-default regression, mismatch refusal, non-userkey shapes).

3. EB_TOPIC_PREFIX default

§3.1 said "{prefix} defaults to the namespace (kev1)" — implying derivation from the namespace. The code has a literal "kev1" default (eventbridge/config.py:91), and §8.1's table row agrees. Reworded to the literal default and why a literal is right (names stay identical to Phase 1's shipped kev1- topics in any namespace).

4. README_PHASE1 — two stale claims

  • "submitter is not signed" — false since Phase 2: submitter, submitteriss, groupid joined SIGNED_ATTRS (DESIGN_PHASE2 §4.2). Corrected, keeping the honest-limit framing: Kafka is still plaintext; the claim is EventBridge's assertion, not proof of who typed.
  • "roughly 100 ms per sign/verify" — Report1 §9 measured 222 ms sign / 227 ms verify. Replaced with the measured figures and the 190–230 ms re-run band (KubeCon §2.4's convention).

5. README_PHASE0 — test count

"20 passed in ~0.6s" → Report0's record: 58 passed in 7.27s.

6. Assorted smaller ones

  • KubeCon §9 changelog: rows now chronological (10-05, 10-05, 10-06).
  • Report1 §3 table: sign/verify row cited "§6"; the figures live in §9. Fixed.
  • DESIGN_PHASE0 §0 CLI skeleton: --watch used action="store_true", default=True — a combination that can never be false. Now argparse.BooleanOptionalAction like the shipped CLI (skills/eventbridge/eventbridge-cli.py:659).
  • DESIGN_PHASE2 §5 table: added the missing ER_VERIFY_KEY_PATH row — the keyset row already referenced it as the fallback, but no row defined it.
  • Code docstrings: the ~150–200 ms signing estimates in kafka_out.py, emit.py and signing.py (×2) replaced with the measured 222/227 ms (Report1 §9). KubeCon M4 explicitly flags these as estimates-vs-measured; the convention there is docstrings get corrected in place.

Verification

  • eventing suite: 907 passed, 7 skipped (baseline 897 + 10 new), Python 3.14.
  • ruff check . and ruff format --check . from the repo root, ruff 0.11.4 (CI-exact): clean.
  • The suite is intentionally untouched in behaviour: the three original wiring tests still pass under their corrected names, and the new refusal is a strict subset of the old one on the safe side.

🤖 Generated with Claude Code

Six verified inconsistencies between the eventing docs and the tree,
each checked against the code before editing (the repo's own §8.5/§8.6
discipline):

1. SIGNED_ATTRS drift: DESIGN_PHASE3's header, §2.6, §7.7, §9 step 4 and
   §11 T4 still described T4 as adding two attributes ("userkey, depth"),
   contradicting §8.3's rule, §2.6's own opening, Report3's T4 row and
   shared/signing.py, which all say three (userkey, depth, agent) in ONE
   change. All five sites now agree.

2. ER_USERKEY false positive (code fix): the runner refused to start
   whenever REQUEST_TOPIC != "requests" and ER_USERKEY was unset, which
   false-positives every Phase 1 single-tenant deployment — the shipped
   k8s/base/configmap.yaml sets REQUEST_TOPIC: kev1-requests and boots no
   runner at all. The refusal now keys on the per-user topic shape
   ({prefix}-{userkey}-requests, §3.1) via tenancy.userkey_in_topic(), so
   a renamed default boots and a per-user topic still demands its
   matching key — mismatched too, not just missing. DESIGN_PHASE3 §3.3
   and §8.2 updated to match; 10 new tests (round-trip, each suffix,
   renamed-default regression, mismatch refusal, non-userkey shapes).

3. EB_TOPIC_PREFIX default: §3.1 claimed "{prefix} defaults to the
   namespace (kev1)"; the code has a literal "kev1" default and §8.1's
   row agrees. Reworded to the literal default and why.

4. README_PHASE1: "submitter is not signed" — it joined SIGNED_ATTRS in
   Phase 2 (DESIGN_PHASE2 §4.2), kept the honest-limit framing (Kafka
   still plaintext, the claim is about EventBridge's assertion).
   "roughly 100 ms per sign/verify" — Report1 §9 measured 222/227 ms;
   replaced with the measured figures and the 190-230 ms re-run band.

5. README_PHASE0: pinned the suite count to Report0's record (58 passed
   in 7.27s, not "20 passed in ~0.6s").

6. KubeCon §9 changelog rows reordered chronologically (10-05, 10-05,
   10-06); Report1's §3 table now cites §9 for the sign/verify figures
   (the section they live in); DESIGN_PHASE0's CLI skeleton --watch uses
   argparse.BooleanOptionalAction like the shipped CLI, not
   store_true+default=True, a combination that can never be false;
   DESIGN_PHASE2 §5 gains the missing ER_VERIFY_KEY_PATH row that the
   keyset row already referenced; the ~150-200 ms estimates in four code
   docstrings replaced with the measured 222/227 ms (Report1 §9), which
   KubeCon M4 flags as estimates-vs-measured.

eventing suite: 907 passed, 7 skipped (baseline 897 + 10 new).
ruff check / format --check: clean, CI-exact invocation.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Mariusz Sabath <mrsabath@gmail.com>
@mrsabath
mrsabath requested a review from a team as a code owner October 7, 2026 02:19
The §3.3 refusal covers two distinct faults but reported both as
"ER_USERKEY is required": an unset variable, and one that disagrees with
the userkey embedded in REQUEST_TOPIC. For the second the message is
wrong — the variable *was* supplied, it just names another tenant — and
an operator told a variable they set is missing looks in the wrong place.

The exit now names which fault it is and, on a mismatch, prints both the
supplied key and the embedded one, so a mis-rendered deployment is
diagnosable from the exit line without reading the chart.

Behaviour is unchanged: the same four inputs boot or refuse exactly as
before, only the text differs. The mismatch test asserts the new wording,
that "is required" is absent, and that both keys appear.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Mariusz Sabath <mrsabath@gmail.com>

@aslom aslom left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

I verified every documentation correction in this PR against the code at head, and all of
them hold: submitter really is in SIGNED_ATTRS (shared/signing.py:195), the three
Phase 3 attributes really do land in one change (signing.py:196-210), 222 ms / 227 ms
really is in IMPLEMENTATION_REPORT1 §9 and not §6 (line 827), Report0 really records
58 passed in 7.27s, the shipped CLI really uses argparse.BooleanOptionalAction
(skills/eventbridge/eventbridge-cli.py:659), ER_VERIFY_KEY_PATH really defaults to empty
with the keyset falling back to it (eventrunner/config.py:100-105), and the 190-230 ms
band is sourced rather than invented (KUBECON_NA_2026.md §2.4, "re-running it here gave
190/198 ms median"). README_PHASE0's "four tests in tests/test_router.py" still reads
correctly after the count change — test_router.py has exactly 4. The §2 bug is real and
worth the code fix: k8s/base/configmap.yaml:18 does ship REQUEST_TOPIC: kev1-requests
with no ER_USERKEY, so the old condition did refuse to boot the shipped Phase 1 deployment.

One must-fix in the new code. PER_USER_TOPIC_RE tries to recover {userkey} from
{prefix}-{userkey}-{suffix}, but both prefix and the userkey's slug may contain -,
so that grammar is ambiguous and no quantifier choice resolves it. The shipped non-greedy
{1,32}? misparses a legal dashed prefix and reintroduces exactly the false-positive boot
refusal this PR set out to remove; flipping it to greedy only moves the failure onto
multi-dash slugs, which are ordinary (the module's own docstring example is
oi-alice-example-com-9e8f7a6b). Details and a tested alternative are inline.

Author: mrsabath (MEMBER — maintainer)
Areas reviewed: Python (shared/tenancy.py, eventrunner/config.py, shared/signing.py, eventrunner/emit.py, eventbridge/kafka_out.py), tests, Docs (9 agentdocs files)
Agent/IDE config (.claude/.vscode): none
Commits: 2 commits, all signed-off: yes
CI status: passing (11/11 — eventing-test, lint, codeql, trivy-scan, hadolint, dependency-review, test-startup, DCO)

Comment thread eventing/shared/tenancy.py Outdated
Comment thread eventing/eventrunner/config.py Outdated
Comment thread eventing/agentdocs/DESIGN_PHASE3.md Outdated
Review: #900, three comments — one must-fix, one suggestion, one nit.

- must-fix: `userkey_in_topic`'s split is ambiguous, and the non-greedy
  quantifier picked the wrong end — a legal dashed prefix (`prod-eu-…`)
  made a correctly-configured runner refuse to boot; the greedy flip
  moves the failure to multi-dash slugs, which userkey()'s own docstring
  shows are ordinary. The grammar cannot be fixed by quantifier, so the
  guard no longer decomposes the name at all: `userkey_in_topic` keeps
  only the yes/no per-user question (every valid split means per-user),
  its docstring now forbids using the captured key for identity, and a
  new `tenancy.topic_names_userkey(topic, userkey)` anchors on the key
  the runner already holds — `-{userkey}-{suffix}`, no split. All eight
  of the reviewer's cases pass, plus dashed-prefix and multi-dash-slug
  boot tests, and the dashed-prefix case he asked for in
  test_userkey_in_topic_*.
- suggestion (taken): the inverse misconfiguration — ER_USERKEY set
  while REQUEST_TOPIC is a shared topic — now refuses too. emit.py keys
  its stamp on userkey presence, not tenancy mode, so that runner would
  have consumed the shared topic and filed every response as one
  tenant's: the harm §3.3 names, from the other direction. ER_USERKEY
  is also now shape-checked (`is_valid_userkey`) at boot — a typo'd key
  names nobody and StoreRegistry counts every stamped response
  `unattributed`, and a topic rendered from the same typo'd key anchors
  against it consistently, so only the shape check catches it.
- nit: §3.1's EB_TOPIC_PREFIX paragraph reflowed to the file's width.

Rebased onto 370ef6b, which split the refusal message by fault ("is
required" for unset, "must match" for a wrong key). That split is
preserved and carried into the anchored check: the message names the
fault and the supplied key, and REQUEST_TOPIC rides in the message
because the anchoring check does not extract an embedded key to print.

DESIGN_PHASE3 §3.3 and §8.2 describe the two-directional refusal and
the anchoring rationale; the Cfg docstring matches.

eventing suite: 915 passed, 7 skipped (897 baseline; +18 across the
three commits). ruff check / format --check: clean, CI-exact invocation.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Mariusz Sabath <mrsabath@gmail.com>
@mrsabath

mrsabath commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@aslom all three of your comments are addressed in 2e94bcb — replies with details are on each thread:

  • must-fix (ambiguous split): reproduced both failure modes before fixing; the guard now anchors on the key in hand via tenancy.topic_names_userkey (your exact shape, -{userkey}-{suffix}, no split), userkey_in_topic keeps only the yes/no per-user question and its docstring forbids identity use of the captured key. All eight of your cases pass, plus dashed-prefix and multi-dash-slug boot tests and the dashed-prefix case you asked for in test_userkey_in_topic_*.
  • suggestion (inverse direction): taken — ER_USERKEY on a shared topic now refuses at boot, and the key is shape-checked (is_valid_userkey), the only check that can catch a typo'd key since a topic rendered from the same typo anchors against it consistently.
  • nit: reflowed.

One wrinkle: this rebased onto 370ef6b (the fault-naming refusal messages), which landed between my original commit and the review — your message split is preserved and carried into the anchored check.

Suite: 915 passed, 7 skipped; ruff clean; all CI green. Since your review was filed as a comment rather than an approval, the branch protection gate is still asking for one — mind converting when you're satisfied? Thanks!

@aslom aslom left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review of 2e94bcb0 against my three findings on 370ef6b9. All three are addressed,
and I re-derived the fix rather than taking the reply's word for it.

The must-fix is genuinely fixed. topic_names_userkey anchors on -{userkey}-{suffix}
with no split, and I re-ran the full scenario matrix against the new tenancy.py driving
load()'s three checks in order: 10/10 correct, including the two that failed before —
prod-eu-gh-alice-ed66acf0-requests with the correct key now boots, and the multi-dash-slug
case (oi-alice-ex-ample-com-1f806a21) that a greedy quantifier would have broken also
boots. The renamed-default case still boots and every mismatch still refuses. I also
confirmed userkey_in_topic has exactly one production caller (config.py:208) which
uses only the is not None answer — so the docstring's "never compared for identity" is
enforced in practice, not just documented.

The suggestion was taken in both halves, and the reasoning in the reply about the shape
check is correct and checks out: store_registry.py:167 does say "An INVALID key counts as
unattributed too, not just a missing one", so a typo'd key really is invisible to the
topic-name checks and only is_valid_userkey can catch it. I verified the new shape check
cannot break an existing deployment — ER_USERKEY is set nowhere in k8s/, the overlays,
or scripts/. Not making the inverse refusal conditional on tenancy mode is the right call
for the reason given: emit.py:77-78 keys the stamp on key presence alone.

The nit is fixed — §3.1's paragraph is now uniform at 83-86 columns with no short line.

One residual I chased and am satisfied is a non-issue, recorded so it is not rediscovered as
a surprise: the suffix anchor can match a different tenant's topic when one valid userkey
is a dash-boundary suffix of another, e.g. topic_names_userkey("kev1-oi-alice-gh-bob-1234abcd-requests", "gh-bob-1234abcd") is True. That requires the two keys to share a trailing digest — a
32-bit collision between two real identities, which is precisely the property userkey()'s
digest exists to prevent, and a fabricated key is operator-controlled anyway. No change wanted.

One suggestion left, inline: two of the older userkey_in_topic tests still assert the split
for identity, which is now the one thing its docstring says you may not do.

Author: mrsabath (MEMBER — maintainer)
Areas reviewed: Python (shared/tenancy.py, eventrunner/config.py), tests, Docs (DESIGN_PHASE3 §3.1/§3.3/§11)
Agent/IDE config (.claude/.vscode): none
Commits: 3 commits, all signed-off: yes
CI status: passing (11/11 on 2e94bcb0)

Comment thread eventing/tests/test_tenancy.py Outdated
… split

Review: #900, the remaining suggestion on the approval review — the two
tests that still pinned the split for identity, the use the new
userkey_in_topic docstring rules out.

test_userkey_in_topic_round_trips_topicset asserted that whatever
TopicSet derives, userkey_in_topic recognises the key back out of it —
a round-trip contract the module disowns: it holds only because the
fixture prefix "kev1" has no dash, and the ambiguity test twelve lines
below asserts the opposite ("prod-eu-…" splits shortest-first). So the
file asserted both "the split round-trips" and "the split is arbitrary",
and a future narrowing (return None on several valid splits, or a
quantifier flip) would fail these two as if regressions when they
actually encode a contract the fix repudiated.

Narrowed both to the claim that survives, per the reviewer's shape:

- renamed to test_userkey_in_topic_recognises_topicset_names_as_per_user;
  recognition (is not None) plus the anchored round-trip
  topic_names_userkey(method(uk), uk), which holds for every prefix and
  slug rather than only the dash-free ones.
- test_userkey_in_topic_recognises_each_suffix likewise asserts
  recognition plus the anchored check, with a docstring pointing at the
  ambiguity test.

The captured key is now asserted nowhere, matching the docstring.

eventing suite: 915 passed, 7 skipped. ruff check / format --check:
clean, CI-exact invocation.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Mariusz Sabath <mrsabath@gmail.com>
@mrsabath
mrsabath merged commit 54c47b3 into main Oct 7, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants