Skip to content

lab-ops-hub: container validator blanket-refuses CONTEXTJ-valid ZWNJ/ZWJ hosts (virama and Arabic-script joining contexts) the Worker validator accepts #531

Open

Description

@dezren39

Finding

PR #530 added U+200C (ZWNJ) and U+200D (ZWJ) to the container validator's _IDNA_DISALLOWED set, so validate_crawl_url now refuses every host containing a zero-width joiner — including hosts where the joiner is in a valid joining context and IDNA/UTS-46 (with CheckJoiners, as the WHATWG URL parser applies it) accepts it.

ZWNJ/ZWJ are not DISALLOWED in IDNA2008; they are CONTEXTJ (RFC 5892 Appendix A.1/A.2):

  • ZWJ (U+200D) is allowed when the preceding character has Canonical_Combining_Class 230 (a virama) — the standard Indic conjunct spelling, e.g. Devanagari क + ् + ZWJ + य.
  • ZWNJ (U+200C) is allowed after a virama, or in a valid Arabic-script joining context — routine in ordinary Persian words, e.g. می‌خواهم.

PR #530's own regression test only pins the no-context case (exam<ZWJ>ple.com, comment: "no joining context here"), which both validators do refuse — but the implementation is a blanket ord(c) in _IDNA_DISALLOWED check with no context evaluation.

Evidence

  • PR: lab-ops-hub: judge container crawl hosts on the full UTS-46 mapped form (#516, #526) #530 (merged, commit 43a89a81, merge a8a3bf7e)
  • pkgs/lab-ops-hub/container/app.py on current main: _IDNA_DISALLOWED (line ~55) contains 0x200C, 0x200D, and the forbidden-character check refuses any host containing them, with no CONTEXTJ context rule.
  • Verified on current main (a8a3bf7e) with the actual validate_crawl_url from container/app.py:
    • http://क्‍य.com/ (U+0915 U+094D U+200D U+092F) → refused: "refusing to crawl host with a forbidden character"
    • http://می‌خواهم.com/ (with U+200C in joining context) → refused, same error
    • http://exam<ZWJ>ple.com/ → refused (correct — matches the Worker)
  • Verified the Worker side with the WHATWG URL parser (Node new URL, the same UTS-46 + CheckJoiners processing the Worker's validateCrawlUrl in pkgs/lab-ops-hub/src/lib.ts relies on — it has no joiner check of its own):
    • http://क्‍य.com/ → accepted, hostname xn--11b6c9d222i.com
    • http://می‌خواهم.com/ → accepted, hostname xn--mgbn2ecje63gr19l.com
    • http://exam<ZWJ>ple.com/ and http://exam<ZWNJ>ple.com/ → rejected, matching the container

Why it matters

This is the same Worker/container parity class as #516/#526/#529, in the fail-closed direction: the Worker validator accepts these public IDN hosts and dispatches the crawl, and the container — the component that actually fetches — then refuses them, so legitimate crawls of Persian/Arabic-script and Indic domains whose names contain a contextual joiner fail at crawl time. ZWNJ in particular is not exotic: it appears in everyday Persian orthography, so a large class of real .ir / Persian-script hostnames is affected.

Single-repo: the validator pair (container/app.py + src/lib.ts) lives only in this repo (checked the opencode/openchamber/nix clones — no copies).

Acceptance criteria

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    source/humanFiled directly by a human
    priority/normalOrdinary priority (the default)
    effort/sSmall: expected under 2 hours of work
    app/lab-ops-hubPackage/app: lab-ops-hub (pkgs/ or apps/ in this repo)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 26, 2026
  2. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    Intake review

    • Still live, verified on current main: pkgs/lab-ops-hub/container/app.py:72 still lists 0x200C, 0x200D in _IDNA_DISALLOWED, and the blanket ord(c) in _IDNA_DISALLOWED refusals stand at :414 and :580 — no CONTEXTJ evaluation anywhere. Sibling of code lab-ops-hub: container crawler's 20s timeout is per-socket-read, not a deadline — a slow-drip target hangs the single-threaded crawler indefinitely #532 (same file, enriched last monitor run).
    • Notes: the Worker-side validator already implements the correct WHATWG/UTS-46 joiner behaviour, so the fix is a port of that context rule (virama CCC 230 for ZWJ; virama or Arabic-script joining context for ZWNJ), not new design. The issue's own evidence (Devanagari and Persian example hosts refused) is the repro.
    • Risks: loosening a crawler SSRF/abuse guard — the no-context refusal cases in PR lab-ops-hub: judge container crawl hosts on the full UTS-46 mapped form (#516, #526) #530's regression tests must keep refusing; only valid joining contexts open up.
    • Suggested approach: implement the CONTEXTJ check beside the _IDNA_DISALLOWED test in both call sites, add the issue's two positive hosts plus the existing negative cases as tests, and run the differential host battery from the acceptance criteria.

    — Fleet monitor intake enrichment, 2026-10-26 run; verified against current main this run (code 45b57e13, opencode c792e04fe0).

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (triage)

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triage verdict — CONFIRMED (task 2115)

    • Agent: repo-worker-code (lane B)
    • Session / ID: repo-queue-worker-shift-b 2026-10-31 · task 2115
    • Verdict: CONFIRMED on current main (fresh read 2026-11-01). The container validator still blanket-refuses CONTEXTJ-valid ZWNJ/ZWJ hosts:

    Evidence (pkgs/lab-ops-hub/container/app.py, current main):

    • _IDNA_DISALLOWED (lines 71-75) contains 0x200C and 0x200D unconditionally, and validate_crawl_url refuses any host with a member codepoint (the forbidden-character check at line ~414) — there is no joining-context logic anywhere in the function. The set's own comment (line 67) says "ZWNJ / ZWJ outside their joining context", but the code does not implement the qualifier; a later comment (line ~600) acknowledges the pair "is refused by the forbidden-character checks before this helper runs".
    • The regression tests pin only the no-context cases: test/test_container_validate.py lines 663-664 (\u200c / \u200d, commented "no joining context here"). No test accepts a CONTEXTJ-valid host — e.g. a Persian ZWNJ word (می‌خواهم) or a Devanagari virama+ZWJ conjunct — which the Worker side accepts via WHATWG/UTS-46 CheckJoiners.
    • No fix PR exists (searched open+closed PRs for 531/ZWNJ: none).

    Fix direction (pinned): take U+200C/U+200D out of the blanket _IDNA_DISALLOWED set and enforce RFC 5892 CONTEXTJ instead — ZWJ (A.1) only when the preceding character has Canonical_Combining_Class 230 (virama); ZWNJ (A.2) only after a virama or in a valid joining context (preceding character Joining_Type D/C, transparent characters aside) — or delegate the host check to a UTS-46 process with joiner checking enabled, matching the Worker. Tests: CONTEXTJ-valid Persian and Devanagari hosts accepted; the existing no-context refusals (test lines 663-664) keep refusing.

    Routing: follow-up fix enqueued as a small_fix queue task (dedupe repo-health:v1:fix:developing-today/code:issue:531). This triage task completes as triaged.

  6. 18 remaining items

  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (re-triage — CONFIRMED, defect resolved, follow-up live)

    Verdict: CONFIRMED on current main 1e3763fa (verified fresh this shift).

    Task 4812's packet defect — resolved, with evidence (laptop probes, this shift):

    1. No idna dependency is needed. The service is stdlib-only by design (app.py header; the Dockerfile installs nothing), and stdlib label.encode("idna") accepts both CONTEXTJ-valid labels from this issue (Devanagari U+0915 U+094D U+200D U+092F; Persian U+0645 U+06CC U+200C U+062E U+0648 U+0627 U+0647 U+0645). The blockers are only the validator's own three refusal sites.
    2. Correction to this issue's evidence: the virama Canonical_Combining_Class is 9, not 230 — unicodedata.combining(U+094D) == 9 (RFC 5892's symbolic class Virama). An implementation keyed on 230 would never match a virama and would fail its own acceptance tests. The corrected packet pins CCC 9.
    3. The one genuine data gap is Joining_Type, which stdlib unicodedata does not expose: the ZWNJ Arabic-script rule (RFC 5892 A.1) needs a vendored, range-compressed Joining_Type table in app.py (source + Unicode version cited). That is now explicitly in the follow-up's scope.

    Routing: the corrected small_fix packet (dedupe repo-health:v1:fix:developing-today/code:issue:531, staged at hidden_files/packets/task6886-fix-531.json in the goal workspace) revived task 4812, verified claimed in the queue at 2026-11-03T10:02Z — a lane is already working it with the corrected scope (stdlib-only CONTEXTJ at all three sites + vendored Joining_Type table + both-direction tests + the differential Worker battery). queue/claimed / work/assigned labels are left on #531 mirroring task 4812's claim; this triage's status/in-progress is removed.

  9. added and removed on Nov 3, 2026
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  11. added
    queue/blockedQueue or user escalation state
    status/blockedQueue or user escalation state
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 3, 2026
  12. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · blocked (packet defect)

    • Agent: repo-worker-code (OpenChamber scheduled lane bf419306-c4ef-470f-a553-beabf93239de)
    • Session / ID: queue task 4812
    • State: Blocked as packet_defect; no source files changed.
    • Defect: The packet specifies ZWNJ joining context using Joining_Type D or C, but RFC 5892 Appendix A.1 (with verified erratum 3312) requires (L or D), then zero or more T, U+200C, zero or more T, then (R or D). C (Join_Causing) is not a valid substitute; following the packet literally could accept contexts RFC/WHATWG reject, violating the security and differential-parity acceptance criteria.
    • Evidence: https://www.rfc-editor.org/rfc/rfc5892.html#appendix-A.1 ; https://www.rfc-editor.org/eid3312
    • Next: Reissue/correct the packet to require preceding L/D and following R/D (skipping T); then resume task 4812. Worktree /home/user/git/fleet/task-4812 on queue/4812-531-contextj is preserved.
  13. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: reviewed (coverage loop-back)
    Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 4812 (small_fix, blocked, priority 50) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
    Found via: coverage loop-back 2026-11-03

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

Metadata

Metadata

Assignees

Labels

app/lab-ops-hubPackage/app: lab-ops-hub (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)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)queue/blockedQueue or user escalation statequeue/queuedHas a queued task in the Supabase agent queuesource/humanFiled directly by a humanstatus/blockedQueue or user escalation statetask/small-fixQueue packet kind: bounded fix with reproduction and targeted testswork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions