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
2 changes: 1 addition & 1 deletion eventing/agentdocs/DESIGN_PHASE0.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def cmd_cont(args):
if __name__ == "__main__":
p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="op", required=True)
r = sub.add_parser("run"); r.add_argument("prompt"); r.add_argument("--max-turns", type=int, default=3); r.add_argument("--watch", action="store_true", default=True); r.set_defaults(fn=cmd_run)
r = sub.add_parser("run"); r.add_argument("prompt"); r.add_argument("--max-turns", type=int, default=3); r.add_argument("--watch", action=argparse.BooleanOptionalAction, default=True); r.set_defaults(fn=cmd_run)
w = sub.add_parser("watch"); w.add_argument("correlationid"); w.set_defaults(fn=cmd_watch)
c = sub.add_parser("cont"); c.add_argument("correlationid"); c.add_argument("prompt"); c.set_defaults(fn=cmd_cont)
args = p.parse_args(); args.fn(args)
Expand Down
1 change: 1 addition & 0 deletions eventing/agentdocs/DESIGN_PHASE2.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ unaffected until an operator opts in:
| `ER_SIGNING_KEY_PATH` | empty | Seed EventRunner signs terminal responses with. |
| `ER_SIGNING_KID` | empty | Names this runner's key. Needed once more than one runner is approved. |
| `ER_REQUIRE_SIGNATURE` | `false` | Refuse unsigned or badly-signed requests. |
| `ER_VERIFY_KEY_PATH` | empty | Single public key requests are verified against. Empty falls back to the public half of `ER_SIGNING_KEY_PATH`'s seed. |
| `ER_VERIFY_KEYSET_PATH` | empty | Approved-key set for requests. Empty falls back to `ER_VERIFY_KEY_PATH`'s single key. |

Seed paths name **Secret** mounts and are env-only, never `config.toml`. Keyset
Expand Down
54 changes: 36 additions & 18 deletions eventing/agentdocs/DESIGN_PHASE3.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Status: draft (revision 1) — **partly implemented.** §9's steps 1-4 are in the tree:
T1 (`shared/tenancy.py`), T2 (`TopicSet` threaded through both services), T3 (`Caller`,
the registry, `ce_userkey`), T4 (`SIGNED_ATTRS` += `userkey`, `depth`), T13 (`AgentSpec`)
the registry, `ce_userkey`), T4 (`SIGNED_ATTRS` += `userkey`, `depth`, `agent`), T13 (`AgentSpec`)
and T22 (the Secret-vs-ConfigMap test). **T5 is also in** — per-user stores
(`store_registry.py`), the global `correlationid → userkey` index (`owner_index.py`), and
the `Minter` uniqueness check that replaces the startup seeding loop (§2.6).
Expand Down Expand Up @@ -340,7 +340,7 @@ deployment worse than an unsigned one because an operator believes it is atteste
response is written into and which ntfy topic it is announced on, so an unsigned,
mutable `userkey` would let anything with write access to a topic file an event into
another user's history. See §8.3 on the canonicalisation break that adding it causes,
and why it has to land with `depth` in one change rather than two.
and why it has to land with `depth` and `agent` in one change rather than three.

**The trap.** It is tempting to salt the session derivation —
`uuid5(NAMESPACE, userkey + "/" + corr)` — so two tenants can never collide in
Expand Down Expand Up @@ -400,13 +400,14 @@ The brief's shape, with `{userid}` replaced by `{userkey}` for the reasons in §
unchanged. The two topics this table adds have no Phase 2 predecessor to stay compatible
with, so they are derived from the prefix instead: `{prefix}-events` and `{prefix}-dead`.

`{prefix}` defaults to the namespace (`kev1`), configurable as `EB_TOPIC_PREFIX`,
exactly as Phase 1 §5 prefixes the shared-broker topics today. It is validated as
`[a-zA-Z0-9-]{1,32}`: it is the one component of these names `_slug` never sees, and `.`
or `_` in it reproduces precisely the JMX metric collision §3.1's naming rules below warn
about. Longest realistic
name: `kev1-` + 2 + 1 + 24 + 1 + 8 + `-responses` = 51 characters, against Kafka's
249 — comfortable, and worth having checked rather than assumed.
`{prefix}` defaults to `kev1`, configurable as `EB_TOPIC_PREFIX`. It is not derived
from the namespace: a literal default keeps the names identical to Phase 1's shipped
`kev1-` topics regardless of the namespace the chart lands in, and it matches §8.1's
row. It is validated as `[a-zA-Z0-9-]{1,32}`: it is the one component of these names
`_slug` never sees, and `.` or `_` in it reproduces precisely the JMX metric collision
§3.1's naming rules below warn about. Longest realistic name: `kev1-` + 2 + 1 + 24 +
1 + 8 + `-responses` = 51 characters, against Kafka's 249 — comfortable, and worth
having checked rather than assumed.

Two naming rules that bite if ignored:

Expand Down Expand Up @@ -571,10 +572,27 @@ Three things this buys that are worth naming:
the ownership check on transcript writes possible at all (§6.3).

`ER_USERKEY` is the new required variable in multi-tenant mode, and the runner
**refuses to start without it** when `REQUEST_TOPIC` does not equal the bridge's
single-tenant default. A runner that stamps no `userkey` on its responses produces
events EventBridge cannot file, and a silent default here would route one user's
output into another user's store.
**refuses to start** on a mismatch between it and `REQUEST_TOPIC`, in either
direction: a per-user topic (`{prefix}-{userkey}-requests`, §3.1) the key does not
name, and — the inverse, which is just as wrong — a key set while `REQUEST_TOPIC`
is not that user's per-user topic. In the inverse direction the runner would
consume the shared topic while stamping `ce_userkey` on every response (the stamp
keys on the key's presence, not on tenancy mode), filing every response as one
tenant's. A runner that stamps no userkey on its responses produces events
EventBridge cannot file, and a silent default here would route one user's output
into another user's store. The key must also have `userkey()`'s shape
(`is_valid_userkey`): a typo'd key names nobody, and StoreRegistry counts every
stamped response `unattributed`.

The match itself anchors on the key the runner holds rather than decomposing the
topic name: `{prefix}-{userkey}-{suffix}` is an ambiguous grammar — dashes are
legal in both the prefix and the slug inside `userkey` — so any split picks an
arbitrary answer (`prod-eu-gh-alice-…-requests` splits shortest-first and embeds
the wrong key), and a split-based check would refuse a correctly-configured
deployment with a dashed prefix. `topic_names_userkey` needs no split. The refusal
deliberately does not key on "REQUEST_TOPIC is not the single-tenant default"
either: single-tenant deployments rename the default freely (the shipped Phase 1
configmap sets `kev1-requests`), and a renamed default is not a tenancy signal.

### 3.4 KEDA: one ScaledObject per user

Expand Down Expand Up @@ -1948,8 +1966,8 @@ whose depth is `>= EB_TRIGGER_MAX_DEPTH` (default **3**) and records a
`trigger.dropped` with reason `depth-exceeded`.

`depth` joins `signing.SIGNED_ATTRS`, and it must: an unsigned counter can be reset to
zero in flight, which turns the whole control off. See §8.3 — this is the second
canonicalisation-breaking change in Phase 3 and it has to land together with `userkey`.
zero in flight, which turns the whole control off. See §8.3 — it is one of the three
canonicalisation-breaking attributes of Phase 3, and they land in one change.

**2. Responses are not trigger-eligible by default.** `EB_TRIGGER_ON_RESPONSES=false`.
Agent chaining — "when the triager finishes, run the fixer" — is a legitimate and
Expand Down Expand Up @@ -2040,7 +2058,7 @@ upgrades and changes nothing behaves exactly as it did in Phase 2.

| Variable | Default | Effect |
|---|---|---|
| `ER_USERKEY` | empty | Which tenant this runner serves (§3.3). Stamped on every response as `ce_userkey`. Required once `REQUEST_TOPIC` is not the default. |
| `ER_USERKEY` | empty | Which tenant this runner serves (§3.3). Stamped on every response as `ce_userkey`. Must name the tenant `REQUEST_TOPIC` serves, in either direction: a per-user topic without the matching key refuses, and a key without its per-user topic refuses too. |
| `ER_AGENT_DIR` | `/etc/rossoctl/agents` | Where baked `AgentSpec`s live (§5.2). |
| `ER_AGENT_NAME` | `default` | Fallback agent when neither the trigger nor the registry names one. |
| `ER_SKILL_SOURCES` | empty | Allowed source prefixes. **Empty means no fetching at all** (§5.4 gate 1). |
Expand Down Expand Up @@ -2093,7 +2111,7 @@ The dependency order is real — several steps are unsafe before the one above t
3. **The registry and `Caller`** (§2.4, §2.5), still in `single` mode. `ce_userkey` is
stamped on events but nothing routes on it — the Phase 2 §4.4 "audit before enforce"
pattern, applied to tenancy.
4. **`SIGNED_ATTRS` += `userkey`, `depth`** (§8.3). One change, both services.
4. **`SIGNED_ATTRS` += `userkey`, `depth`, `agent`** (§8.3). One change, both services.
5. **Per-user stores** (§6.1) and **owner-scoped reads** (§6.2), behind
`EB_TENANCY_MODE=multi`. Now `multi` is usable by one real tenant.
6. **Transcript auth** (§6.3). Must come before any second tenant exists: §6's opening
Expand Down Expand Up @@ -2163,7 +2181,7 @@ In the spirit of Phase 2 §3, the things a demo of this must not claim:
| **T1** | `shared/tenancy.py`: `userkey`, `_canonicalise`, `_slug`, `TopicSet`, `ntfy_topic`. Tests: collision cases, the GitHub case-folding agreement with `ghauth.is_allowed`, topic/DNS/ntfy legality over a corpus of adversarial identifiers. | — |
| **T2** | Thread `TopicSet` through `kafka_out`, `kafka_in`, `RequestsMirror`, `GroupMirror`, `eventrunner/consume`. `single` mode only; zero behaviour change. | T1 |
| **T3** | `Caller`, the registry loader, `auth.resolve` returning `userkey`; `ce.EXT_USERKEY`; stamp it on requests. | T1 |
| **T4** | `SIGNED_ATTRS` += `userkey`, `depth`, in one change, both services. | T3, T13 |
| **T4** | `SIGNED_ATTRS` += `userkey`, `depth`, `agent`, in one change, both services. | T3, T13 |
| **T5** | `StoreRegistry` + per-user store files + the global `correlationid → userkey` index + lazy `Minter` seeding (§2.6). | T3 |
| **T6** | `Consumer.ensure_subscribed` with the blocking assignment wait, `auto_offset_reset=earliest`, and submit-path ordering. Test the race explicitly: publish before subscribe must fail the test. | T2 |
| **T7** | Owner-scoped reads: `404` not `403`, the group-list filter, and the single-tenant bypass. | T5 |
Expand Down
2 changes: 1 addition & 1 deletion eventing/agentdocs/IMPLEMENTATION_REPORT1.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Highlights, each re-measuring something the design had asserted:
| Concurrency (T1.10) | ≤ partition count | peak **4** replicas for 3 correlations |
| Image pull | 5.1 s cold (245 MB) | 454 ms – 2.45 s warm |
| Rebalance floor (Gap A) | ~3 s | consistent with the 5.0 s total |
| Ed25519 sign / verify | not estimated | **222 ms / 227 ms** — see §6 |
| Ed25519 sign / verify | not estimated | **222 ms / 227 ms** — see §9 |

---

Expand Down
2 changes: 1 addition & 1 deletion eventing/agentdocs/KUBECON_NA_2026.md
Original file line number Diff line number Diff line change
Expand Up @@ -774,5 +774,5 @@ been read rather than run. Confirm it before spending anything on either CNI opt
| Date | Change |
|---|---|
| 2026-10-05 | Created. Captures the accepted abstract, status against it as of `main`, the SPIRE decision, the W1–W6 plan, and the 30-minute slide/demo shape |
| 2026-10-06 | M1 and M2 filed as #895 and #896; §5 gains a tracking table |
| 2026-10-05 | Added §5, the M1-M4 MVP cut (~2 days) with sequencing, explicit cuts and the talk it supports. Corrected §4/W3: Kind's default CNI is kindnetd and does not enforce NetworkPolicy, so the `NetworkPolicy` MVP was unbuildable as written — three options tabulated, M3 recommends substituting the #888 asymmetry for the egress beat. §7's risk table and §8's observations updated; the 30-minute shape renumbered §5→§6, risks §6→§7, observations §7→§8 |
| 2026-10-06 | M1 and M2 filed as #895 and #896; §5 gains a tracking table |
2 changes: 1 addition & 1 deletion eventing/agentdocs/README_PHASE0.md
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ Run the unit tests any time you want to reprove the DESIGN §4.5 contract withou

```bash
.venv/bin/python -m pytest tests/ -q
# → .................... 20 passed in ~0.6s
# → .......................................................... 58 passed in 7.27s
```

The four tests in `tests/test_router.py` cover FIFO ordering per correlationid, cross-correlation parallelism, the concurrency cap, and slot re-arm after drain.
Expand Down
14 changes: 9 additions & 5 deletions eventing/agentdocs/README_PHASE1.md
Original file line number Diff line number Diff line change
Expand Up @@ -694,9 +694,11 @@ Two limits worth stating plainly:
`/continue` stay open by design — `/continue` because authenticating it would
put a long-lived bearer token inside every ntfy notification; it relies on the
unguessable `correlationid` as a capability URL instead.
- `submitter` is **not signed**, and Kafka is plaintext. The claim is "EventBridge
refuses unauthenticated submissions and records who it believes submitted this",
not "this event proves who submitted it."
- `submitter` is signed (it joined `signing.SIGNED_ATTRS` in Phase 2, DESIGN_PHASE2
§4.2), and Kafka is still plaintext. The claim is "EventBridge refuses
unauthenticated submissions and records who it believes submitted this — and on a
signed request, the signature proves EventBridge asserted that name", not "this
event proves who typed it."

Setting `EB_AUTH_TOKENS` will 401 the `/eventbridge` CLI, the three e2e scripts
and the README `curl`s until they are taught to send the header.
Expand All @@ -718,8 +720,10 @@ rm /tmp/seed.hex

Signing is implemented in pure Python (`cryptography` is a C extension and
banned), verified against the RFC 8032 test vectors. It is off by default partly
because the pure-Python scalar multiplication costs roughly 100 ms per
sign/verify, which is real per-event overhead — see `IMPLEMENTATION_REPORT1.md`.
because the pure-Python scalar multiplication is real per-event overhead:
IMPLEMENTATION_REPORT1.md §9 measured **222 ms to sign and 227 ms to verify** on
the reference laptop, and later hardware has measured inside a 190–230 ms band —
fine for the terminal event, wrong for every intermediate one.

---

Expand Down
3 changes: 2 additions & 1 deletion eventing/eventbridge/kafka_out.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class Producer:
signed input — it cannot be swapped to impersonate another key.

**Cost.** The Ed25519 here is pure Python (`cryptography` is a C extension and is
banned by §1.1) and takes ~150-200 ms per signature. `publish_request` is called
banned by §1.1) and takes 190-230 ms per signature (measured: 222 ms sign / 227 ms
verify, IMPLEMENTATION_REPORT1 §9). `publish_request` is called
once per group member by `GroupService.submit_members`, in a loop, inside one HTTP
request — so a 100-member batch spends ~20 s signing while the caller waits. That
is accepted: a batch launch is an operator action, not a hot path. It is recorded
Expand Down
Loading
Loading