Skip to content

id(group/doc/claim): the shared key=value renderer passes bidi/format controls (Cf) raw — a claim value with U+202E reaches the terminal unescaped in both the unquoted and quoted paths and visually reorders the row's author=/event= fields #3111

Closed

Description

@dezren39

Problem

The shared key=value plain-text renderer for id group, id doc and id claim — text() in pkgs/id/src/commands/group.rs:64-72, used by tokens() (group.rs:75) and reached from doc.rs print_action/print_rows (doc.rs:40,51) and claim.rs print_set/claim_fields (claim.rs:26,36) — passes Unicode format characters (category Cf) raw to the terminal in both of its paths:

Value::String(string) if string.chars().any(|c| c.is_whitespace() || c.is_control()) => {
    Value::String(string.clone()).to_string()
}
Value::String(string) => string.clone(),
  1. Unquoted path: the quoting trigger is is_whitespace() || is_control(). char::is_control() is category Cc only and is_whitespace() is the White_Space property; every Cf character — U+202A–U+202E (bidi embedding/override, incl. RIGHT-TO-LEFT OVERRIDE), U+2066–U+2069 (isolates), U+200E/U+200F (LRM/RLM), U+200B, U+FEFF, U+00AD — is neither, so a stored claim name/value carrying only an override is returned byte-identical.
  2. Quoted path: even when quoting does trigger (e.g. the value also contains a space), the quoted form is a serde_json string literal, which escapes only control characters, " and \ — the Cf character is emitted raw inside the quotes, where it still acts on terminal rendering.

A RIGHT-TO-LEFT OVERRIDE in a claim value therefore visually reorders the rest of the row on a terminal: the stored name/value sit before the row's real trailing author= and event= fields (the exact field order the #1611 fix protected against textual forgery), so the spoof the renderer exists to prevent can still be done visually. Zero-width/format characters (U+200B, U+FEFF, U+00AD) likewise reach the terminal invisibly, so two rows that look identical can carry different bytes.

How the values get there

Claim names/values are signed tag events that sync between nodes. Ingest validation (check_text, pkgs/id/src/tag_claims.rs:145-153) checks only byte length and NFC; Cf characters are NFC-stable, so a claim authored by another node with a U+202E in its name or value passes check_op, is stored, and is printed by id claim list / id claim search (and the same renderer serves id doc rows and id group output).

Evidence (verified this run, main 201e0c35)

  • Source read: group.rs:64-72 (trigger + both paths), group.rs:75 (tokens), claim.rs:26/36 (stored name/value fields placed before author=/event=), doc.rs:40/51 (shared use), tag_claims.rs:145-153 (length + NFC only).
  • Laptop Rust probe (~/git/fleet/probe-2026-10-31-laneA-group-text-cf/, rustc, replicating the exact predicate): for U+202E, U+202A, U+2066, U+200B, U+200E, U+FEFF and U+00AD — is_control=false is_whitespace=false quoted_trigger=false in every case.
  • Python unicodedata cross-check: all seven are category Cf and not White_Space.

Distinct from existing issues

Acceptance criteria

  • A claim name or value containing U+202E (alone, and combined with a space so the quoted path is taken) renders with the Cf character escaped (e.g. \u202e) in id claim list/search plain output — it never reaches the terminal raw; same for the other Cf characters named above.
  • Regression tests in the group.rs renderer tests (alongside test_newline_in_value_stays_one_line etc.) cover: Cf-only value (unquoted path) and Cf-plus-space value (quoted path).
  • Values with no structure-breaking or format characters still render byte-identically to before (existing tests, incl. test_space_free_values_render_byte_identically and test_json_output_is_unchanged, stay green); --json output is unchanged.
  • The fix is in the shared renderer (text/tokens), so id group, id doc and id claim all inherit it — no per-command copies.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    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 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Adopted as Phase 2 of design #3455 (a value cannot act on the terminal — one format-character contract for pkgs/id plain output; design doc files/designs/code-id-plain-output-rendering.md in the keep-the-four-repos-healthy goal workspace). Triage: do NOT re-file, re-scope or split this issue — record the phase structure instead.

    Gating: this phase is gated on Phase 1 (#2839) for the shared Cf set definition only — hence the status/blocked label added with this note. The implementation work itself (trigger + escaping in group::text) is otherwise independent.

    Decisions recorded on #3455 that bind this phase:

    • The character set is the shared one from Phase 1: petname.rs's Unicode 16 is_format_char set, promoted pub(crate) — no local subset.
    • The quoted path must escape Cf as \uXXXX itself: serde_json's to_string() emits Cf raw, so widening the trigger alone leaves the quoted path broken (this issue's own evidence). Keep serde_json's existing escapes for Cc/"/\ byte-identical — the existing group.rs test corpus is the regression net.
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triage verdict: CONFIRMED (queue task 4531, lane E)

    Re-verified independently against current main f82b20b58 (filing read was 201e0c3; substance unchanged):

    • pkgs/id/src/commands/group.rs:66 — text() still triggers the quoted path on is_whitespace() || is_control() only (Cc/White_Space); a grep of the whole file finds no Cf / U+202E handling at all. U+202E alone renders raw and unquoted; with a space it takes the serde_json-quoted path, which does not escape Cf either.
    • pkgs/id/src/tag_claims.rs check_text — byte length + NFC only; Cf is NFC-stable, so a remote-authored claim value carrying a bidi override passes ingest and reaches the terminal, where it can visually reorder the row's real author=/event= fields.

    Phase structure (design #3455): this is Phase 2; the status/blocked label stays — per the design-architect adoption note it records the gate on Phase 1 (#2839) for the shared Cf set definition only.

    Routing: fix task 5635 enqueued (dedupe repo-health:v1:fix:developing-today/code:issue:3111): implement Cf escaping in the shared renderer using the Phase 1 shared Cf definition when landed (park naming the dependency if not), with regression tests for the Cf-only and Cf-plus-space paths. Its closure, with #2839, unblocks the gated Phase 3b audit (task 5380).

  5. 93 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — parked
    Agent: repo-worker-code (OpenChamber scheduled lane)
    Task: repo-health-code-pilot task 5635
    At: 2026-11-02T08:44:20Z
    State: parked
    Fresh evidence: #3111 is OPEN; dependency #2839 is OPEN; its implementation PR #4264 is OPEN and authored by dezren39. The design gates Phase 2 on #2839 landing. All-state search found no active fix PR for #3111.
    Disposition: parked 30 minutes on the unmet Phase 1 dependency. No PR content, branch, worktree, checks, reviews, or source inspected or touched. Labels reflect parked state; work/assigned remains set.
    Resume condition: #2839 closes/merges; revalidate #3111 and dependency state fresh. Do not inspect/touch Drew-owned PR #4264 or its branch/worktree/checks/reviews.
    Links: #3111 · #2839 · #4264 · task 5635

  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — task 5635 parked (lane A, 2026-11-02)

    Fresh gate check: Phase 1 issue #2839 is still OPEN and its PR #4264 is still OPEN @ 90782ce. Per this task's packet, Phase 2 (#3111) consumes the shared Cf set definition from Phase 1, and the task parks naming that dependency until #2839 lands. Issue #3111 OPEN, no hard-stop labels. Parked task 5635 for 1h; resume when #2839 is closed / PR #4264 merged, then implement the group.rs renderer Cf escaping with the shared definition.

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent: repo-worker-code (lane E)
    state: parked
    task: 5635
    issue: 3111

    Lane E claimed task 5635 (Phase 2 of #3455) and revalidated the Phase 1 gate fresh at 2026-11-02T11:24Z: #2839 is still OPEN and its PR #4264 is still OPEN at 90782ce (branch queue/5439-2839-plain-field-unicode-cf); #2839 currently carries queue/claimed + status/in-progress, so Phase 1 is actively held elsewhere. Per the packet, Phase 2 does not start before the shared Cf set definition lands. Parking task 5635; resume after #2839 closes / PR #4264 merges.

  9. 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 Nov 2, 2026
  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  11. added a commit that references this issue on Nov 2, 2026
  12. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect 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