feat(core/relay): add NIP-AM kind 44200 (agent turn metrics) with relay plumbing#1445
Draft
wpfleger96 wants to merge 4 commits into
Draft
feat(core/relay): add NIP-AM kind 44200 (agent turn metrics) with relay plumbing#1445wpfleger96 wants to merge 4 commits into
wpfleger96 wants to merge 4 commits into
Conversation
…ay plumbing Add kind:44200 (KIND_AGENT_TURN_METRIC) as the durable, p-gated, owner-encrypted per-turn token-usage event defined in NIP-AM (docs/nips/NIP-AM.md, PR #1441). Changes: - buzz-core/kind.rs: add KIND_AGENT_TURN_METRIC = 44200 to P_GATED_KINDS and ALL_KINDS; compile-time asserts confirm regular (non-ephemeral, non-replaceable) kind shape - buzz-core/agent_turn_metric.rs (new): AgentTurnMetricPayload type matching the NIP schema (harness+timestamp required; nullable token fields; turn/cumulative objects; sessionId+turnSeq required when cumulative present; deltaReliable; stopReason enum); encrypt_agent_turn_metric/decrypt_agent_turn_metric helpers reusing encrypt_observer_payload/decrypt_observer_payload from observer.rs; round-trip, wrong-key, null-field, and stop-reason tests - buzz-relay/handlers/req.rs: extend p_gated_filters_authorized to deny the ids-filter exemption for kind:44200 (same carve-out shape as KIND_DM_VISIBILITY); new test covering both {kinds:[44200], ids:[...]} deny and the kindless-ids pass-through path (with documented defense-in-depth note) - buzz-relay/handlers/ingest.rs: validate_agent_turn_metric_envelope (p tag, agent tag == event.pubkey, no h tag, NIP-44 content); async is_agent_owner ownership check; required_scope_for_kind → MessagesWrite; is_global_only_kind addition; envelope and ownership tests - migrations/0001_initial_schema.sql + schema/schema.sql: add 44200 to the NULL search_tsv CASE so the p_gated_persistent_kinds_have_storage_null_tsvector drift test passes No emit logic, no adapters — Task B (goose adapter, buzz-acp) is a separate PR. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
reader_authorized_for_event in filter.rs now gates KIND_AGENT_TURN_METRIC alongside KIND_DM_VISIBILITY — reader must match the #p tag (owner). This single function closes all kindless-ids retrieval paths: WS historical pull (req.rs:330, req.rs:652), HTTP bridge (bridge.rs:608, bridge.rs:863), and live fan-out (event.rs). Live fan-out extended likewise: owner_only_kind now covers both 44200 and 30622, so kindless-ids subscriptions cannot receive 44200 events for non-owners. Tests added: reader_authorized_for_event_gates_agent_turn_metric_by_p (owner allow, non-owner deny, authoring-agent deny). Case-2 rationale in the existing req.rs test updated: pass-through at the filter-authorization gate is correct because the result-level gate is now the enforcement point for this path. NIP-AM ref: docs/nips/NIP-AM.md at 19889ba (PR #1441). Resolves blocking gap from PR #1445 review. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…pat for NIP-AM Two Thufir-flagged IMPORTANT fixes for PR #1445. Count gate (COUNT existence leak): - Add RESULT_GATED_KINDS = [KIND_DM_VISIBILITY, KIND_AGENT_TURN_METRIC] to kind.rs — explicit list of kinds that require per-event owner verification even for COUNT queries. - Add filter_can_match_result_gated_kinds() to req.rs — returns true when filter has no kinds constraint (wildcard) or includes a result-gated kind. - Add result_gated_count_safe_for_pushdown() to req.rs — safe to use fast SQL count_events() only when filter's #p tag is non-empty and all values equal the authenticated reader's pubkey. - Apply the guard in count.rs (WS): both with-channel and without-channel fast-path conditions now require !needs_result_gated_filtering; both fallback loops now call reader_authorized_for_event per event. - Apply the guard in bridge.rs (HTTP): same two fast-path conditions and same two fallback loops. - 6 unit tests covering wildcard/explicit/safe-pushdown/unsafe cases. StopReason forward-compatibility: - Replace #[derive(Deserialize)] on StopReason with a custom impl that maps any unrecognized string to StopReason::Unknown instead of returning an error; NIP-AM requires consumers to accept future stopReason values. - Add test unknown_stop_reason_maps_to_unknown_not_error: future value tool_limit deserializes to Unknown; token counts remain intact. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Pure formatting pass — no logic changes. Fixes just fmt-check failure in CI (Rust Lint job 84653617621). Import group reflow in agent_turn_metric.rs and line-length wrapping in ingest.rs and req.rs. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the relay-side infrastructure for NIP-AM agent turn metrics (kind 44200), as specified in
docs/nips/NIP-AM.md(PR #1441, Thufir-cleared). This is Task A of Phase 2; Task B (goose adapter,buzz-acp) is a separate parallel PR.No emit logic, no adapters. This PR lands the kind, its full read-gate enforcement (filter-level and result-level), storage/search handling, payload type, COUNT existence-leak closure, and StopReason forward-compatibility.
Changes
crates/buzz-core/src/kind.rsKIND_AGENT_TURN_METRIC = 44200constant referencing NIP-AMP_GATED_KINDSandALL_KINDSRESULT_GATED_KINDS = [KIND_DM_VISIBILITY, KIND_AGENT_TURN_METRIC]— kinds requiring per-event owner verification in COUNT queriescrates/buzz-core/src/agent_turn_metric.rs(new)AgentTurnMetricPayloadstruct matching NIP-AM schema:harness/timestamprequired; nullableturn/cumulativeTokenCounts;sessionId+turnSeqrequired when cumulative present;deltaReliabledefaulting true;StopReasonenumDeserializeforStopReason: unrecognized values map toUnknown(NIP-AM forward-compat requirement — consumers must not reject future stop reasons)encrypt_agent_turn_metric/decrypt_agent_turn_metrichelperscrates/buzz-core/src/filter.rsreader_authorized_for_eventextended to gateKIND_AGENT_TURN_METRICalongsideKIND_DM_VISIBILITY— reader must match the#ptag. Closes all kindless-ids retrieval paths (WS historical, HTTP bridge, live fan-out).crates/buzz-relay/src/handlers/event.rsprivate_event_owner, covers both 30622 and 44200)crates/buzz-relay/src/handlers/req.rsp_gated_filters_authorized: deny ids-filter exemption for kind 44200filter_can_match_result_gated_kinds(): returns true when filter has no kinds constraint or includes a result-gated kindresult_gated_count_safe_for_pushdown(): safe to use fast SQL COUNT only when filter's#pis non-empty and all values equal the authenticated reader's pubkeycrates/buzz-relay/src/handlers/count.rs(WS COUNT)!needs_result_gated_filteringreader_authorized_for_eventper eventcrates/buzz-relay/src/api/bridge.rs(HTTP COUNT)crates/buzz-relay/src/handlers/ingest.rsvalidate_agent_turn_metric_envelope: exactly-one-p, exactly-one-agent=event.pubkey, nohtag, NIP-44 contentis_agent_ownerownership checkrequired_scope_for_kind→MessagesWritefor 44200is_global_only_kindincludes 44200migrations/0001_initial_schema.sql+schema/schema.sqlNULL tsvectorCASE — encrypted ciphertext must not enter FTS indexesRead-gate coverage
{kinds:[44200], ids:[...]}filterp_gated_filters_authorizedcarve-out (req.rs){ids:[...]}historical pull (WS + HTTP bridge)reader_authorized_for_event(filter.rs){ids:[...]}live subscriptionprivate_event_ownerfan-out carve-out (event.rs)result_gated_count_safe_for_pushdown+ per-event fallback (count.rs,bridge.rs)NULL tsvectorstorageRelated
docs/nips/NIP-AM.md, Thufir pass-2 cleared)buzz-acp): feat(buzz-acp): add goose usage adapter for NIP-AM turn metrics #1446 (duncan/nip-am-goose-adapter)