Skip to content

id(world): joined-phase metrics frame is never capability-checked — the capability it carries is discarded, unlike query-phase metrics and joined-phase info #1506

Closed

Description

@dezren39

Problem

The metrics session frame is capability-authorized in the query phase but never checked at all in the joined phase. The frame always carries a capability (ClientFrame::Metrics { capability: String }, pkgs/id/src/world_session/types.rs:87-89; the shell contract lists metrics in the opening, query, and joined phases), and the two phases treat it inconsistently:

  • Query phase (pkgs/id/src/world_session/mod.rs:1324-1352): the handler takes the frame's capability, builds a JoinCapability, and gates on service.world().snapshot(token) — exactly like info. A bogus capability gets join denied. The scripted-session test pins this: metrics_frame_reports_each_host_metric_after_a_scripted_session (pkgs/id/src/world_session/tests.rs:193, bogus capability refused at :214, valid capability served at :230).
  • Joined phase (pkgs/id/src/world_session/mod.rs:2124-2134): the comment says "Like info, the joined phase may read the world's metrics with the session's own capability", but the handler matches ClientFrame::Metrics { .. } — discarding the capability — and calls world.metrics().await. WorldHandle::metrics (pkgs/id/src/world/handle.rs:116-128) takes no token parameter, so no validation is possible on this path. Neither the frame's capability nor the session's join token is ever presented.

Contrast the joined-phase info handler directly above it (mod.rs:2108-2123), which passes token.clone() to world.snapshot(...) and refuses with join denied when the capability no longer joins.

Why it matters

  • A participant whose capability has been revoked keeps read access to the world's host metrics for as long as their frames win the race against the session loop's revocation branch (mod.rs:1836-1843 breaks the loop when the revocation broadcast is received; a metrics frame already selected is served first). The codebase's stated enforcement model is per-delivery re-validation — "Projecting the event re-validates the capability on every delivery, which is what disconnects a revoked participant" (mod.rs:1796-1798) — and the metrics path bypasses it entirely.
  • The metrics are operational data about a world the participant may no longer have any grant for: live session count, command/journal/checkpoint rates and latencies, frame bytes (MetricsSnapshot, pkgs/id/src/world/metrics.rs:83-108).
  • The query phase and its test establish the intended authorization ("holding a capability that snapshots the world is enough to read its metrics", mod.rs:1326-1327); the joined phase silently not enforcing it is a defect in F11.7 slice 1 (id(world): F11.7 host metrics, Prometheus endpoint and tracing spans #553, PR feat(id): F11.7 slice 1 — host metrics core + world stats surfacing (#553) #1437), not a design difference — the joined handler's own comment claims the info pattern it does not implement.

Scope note for triage: the joined-phase caps handler (mod.rs:2136-2149) has the same token-less shape via WorldHandle::caps_report (handle.rs:372). It predates F11.7 and has no authorizing query-phase twin, so it is not filed here — triage should rule whether the fix pattern extends to it in the same pass or a follow-up.

Acceptance criteria

  • Joined-phase metrics validates the session's capability the way joined-phase info does (gate on snapshot(token), or a token-taking metrics path on WorldHandle); a metrics frame from a session whose capability no longer joins is refused (join denied / session ended), never served.
  • A scripted-session test mirrors the query-phase test: join, revoke the participant, send a joined-phase metrics frame, assert no metrics reply is served. The existing query-phase metrics test and the metrics unit tests still pass.
  • The joined-phase handler's comment matches the implemented behaviour.
  • Triage records a ruling for joined-phase caps (same fix pass or a follow-up issue).

Found by new-issue-creator (lane A) on main 931f1771, from a line-by-line read of the F11.7 slice-1 wiring (PR #1437). Related: #553 (the F11.7 epic — this is a defect in its shipped slice 1, not a duplicate).

Activity

  1. developing-today commented on Oct 27, 2026

    @developing-today
    Contributor
  2. added
    kind/securitySecurity issue or security hardening
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/lowNice to have; do when convenient
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 27, 2026
  3. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane A, slug new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-27 14:05 CDT
    • Found via: filed this run
    • Task: Triage the joined-phase metrics authorization gap: the joined handler discards the frame's capability and WorldHandle::metrics takes no token, while the query-phase twin gates on snapshot(token) and joined-phase info re-validates. Rule on the joined-phase caps sibling in the same pass.
    • Expectation: effort/s — gate joined-phase metrics on the session capability like info, plus a join→revoke→metrics scripted-session test.
    • Links: F11.7 epic id(world): F11.7 host metrics, Prometheus endpoint and tracing spans #553; slice-1 PR feat(id): F11.7 slice 1 — host metrics core + world stats surfacing (#553) #1437
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Oct 28, 2026
  5. 71 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 31, 2026
  9. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Agent note — pickup (resume, merge shift)

  10. added a commit that references this issue on Oct 31, 2026
  11. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Agent note — done (merged)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/securitySecurity issue or security hardeningpriority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions