Skip to content

fix: take a view address only in F50's exact forms - #182

Merged
agentrozjedemeai merged 1 commit into
mainfrom
lazurio/f50-view-addresses
Nov 5, 2026
Merged

agentrozjedemeai merged 1 commit into
mainfrom
lazurio/f50-view-addresses

Conversation

@immakermatty

@immakermatty immakermatty commented Nov 5, 2026 •

Copy link
Copy Markdown

LazurioPlatform F50 point 2 (viewAddressOf, isViewHost and environmentIdOf in src/browser/contract/pure.ts at ca9418eb, lines 207–267) takes an address on a view host only in its exact forms:

  • the own view's bare origin, with or without /, opens a new tab;
  • https://browser.<base>.lazurio.io/t/<32 hex> names an existing tab, where <base> is one or two DNS labels.

Every other address on a view host is refused. The address is checked as it was written, before it is normalized, "on every Lazurio surface alike".

environmentBrowser.ts is the same in the released 0.1.6-lazurio.1 (5176bd32) and in c1dce709, so the gaps below were already released. The Organization Admin decided to fix them in 0.1.7 with no separate 0.1.6-lazurio.2 (rebuild issue #172).

Problem

PR #174 (on main since b92f7c6a7f) already refuses a trailing dot, the own view and the non-canonical forms that review found. Three gaps remained:

  1. Base labels. lazurioBrowserView (server and the web copy) took any number of base labels, of any length, with a hyphen at either end. So https://browser.a.vm-02.acme.lazurio.io/t/<id>, browser.-vm-02…, browser.vm-02-… and a 64-character label were taken as another Environment's tab, although F50 has no Environment for them.
  2. Only the normalized form was read on the server. viewRefusal checked only what normalizePreviewUrl made of the address, and that changes the host for some inputs. These loaded on the own view host, on another Environment's and on a person's own:
    • https:/browser.<base>.lazurio.io/t/<id> became https://https/… (host https);
    • https:\\browser.<base>.lazurio.io\t\<id> loaded the same way;
    • ht<TAB>tps://browser.<base>.lazurio.io/t/<id> loaded too.
  3. The web panel read a chat link as the browser does. A plain click on a chat link passed link.href to the panel, and href is the browser's own reading of the link. environmentBrowserTab then compared that reading with itself. So https://BROWSER.<base>.lazurio.io/t/<id> or https://browser.<base>.lazurio.io:443/t/<id> opened as a panel tab, and foreignEnvironmentBrowserTab took an upper-case host as the same view.

Fix

  • Base labels: lazurioBrowserView in apps/server/src/lazurio/browserView.ts and apps/web/src/lazurio/browserView.ts takes only a base of one or two labels matching F50's dnsLabel ([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?). Punycode labels (xn--…) still match, as they do in environmentIdOf. The release contract test keeps the two copies equal and now covers both new refusals.
  • The written form on the server: viewRefusal in apps/server/src/lazurio/environmentBrowser.ts reads the address as written first, then the normalized one. Whichever lands on a view host first decides. The check runs in preview_open, preview_navigate (url and target.url) and pages.open.
  • Chat links on the web: the chat link handler in apps/web/src/lazurio/LazurioEnvironmentBrowser.tsx passes the link's href attribute, so the address is read as written. environmentBrowserTab in apps/web/src/lazurio/environmentBrowser.ts takes a view only when the input equals its own URL reading. The server sends its views in exactly that form, so they are unaffected.

Tests

Server table. it.effect.each(contractCases) in apps/server/src/lazurio/environmentBrowser.test.ts has 200 rows. Each row runs on a fresh host through:

  • preview_navigate by url and by target.url;
  • preview_open;
  • pages.open from another thread.

In every row the test also asserts that a person's own tab gets no command. The rows:

  • Accepted:
    • the own bare origin with and without /;
    • own, foreign and personal tab ids in upper, lower and mixed case;
    • a 63-character label;
    • a punycode base.
  • Refused, each on the own, another and a person's view host:
    • the host spelling: a trailing dot, an ideographic full stop, upper case;
    • a port: :443, an empty port;
    • a scheme other than https:;
    • .., . and %2e segments, /T/, %74 and extra path;
    • an empty or non-empty query or fragment, and credentials, including an empty user;
    • a short, long or non-hex id;
    • variants of the bare origin;
    • the forms https:/, https:\\, //, without a scheme, and with a tab in the scheme;
    • input padded with spaces, a no-break space or a byte order mark.
  • Refused for other reasons:
    • another Environment's bare origin;
    • bases F50 has no Environment for: 3 labels, none, a hyphen at either end, 64 characters, an underscore;
    • ácme, which becomes xn--cme-dla and is refused as written.
  • Invalid, so nothing opens or loads: https:<host>, wss: and view-source: on a view host.
  • Loaded:
    • look-alike hosts: xbrowser., t3code., .example.test;
    • a view host as the user name of another host;
    • a host-less path.

The address lists that earlier review rounds added one finding at a time are rows of this table now. They replace the old "fails closed …" test and the refusal loop in the own-view test.

Web. New rows in environmentBrowser.test.ts ("is no tab at any other address", "is nothing at any other address"), and two inexact links in the chat-link test of LazurioEnvironmentBrowser.browser.test.tsx. The chat-link test asserts that the browser follows both links and the panel opens nothing.

How the tests separate broken from fixed code:

Code under test Result
Server code of main (b92f7c6a7f) 13 of 233 tests fail: three each for the https:/, https:\\ and tab-in-scheme forms (own, foreign, personal), and the four base rows (3 labels, leading or trailing hyphen, 64 characters)
Without the written-form check on target.url 9 rows fail
Web parser of main 2 unit tests fail, plus the chat-link browser test
Web chat link reading link.href the chat-link browser test fails
With the fix everything passes

Checks run locally on c15a6d5d63

  • Tests:

    • apps/server src/lazurio: 490 passed, 14 skipped;
    • overlay test mirror on upstream v0.1.7 (322f6544df), web part: 1921 unit tests passed, and 183 browser tests passed in three batches (64 + 62 + 57).
  • Other checks:

    • node --test scripts/lazurio-release-contract.test.mjs: 51 passed;
    • tsc --noEmit in apps/server and apps/web: pass;
    • vp fmt --check (whole repository): pass;
    • vp lint on the changed files: pass.
  • First head 9ea47882f4: the mirror also passed scripts, shared, contracts, effect-acp and client-runtime.

  • vp run check:changed: format, lint, GraphQL and the React compiler pass. Three checks fail on this host, and they fail on unchanged main too:

    • knip cannot load expo/tsconfig.base.json and astro/tsconfigs/strict, which are not installed here;
    • the evals typecheck has no evals/node_modules;
    • AgentContextBudget.test.ts measures 8204 context bytes against 6883.

    The commit therefore skips the pre-commit hook. CI runs knip.

  • Independent read-only review against pure.ts: 1785 base forms and 168 addresses found no difference on the server tools. The two findings it did make (chat links on the web, no test for target.url) are fixed here.

Scope

  • This is a regular PR into main during the canary of 0.1.7 (stage 7 of the release runbook). It changes nothing outside the Environment browser's address checks.
  • The next rebuild folds each change into the overlay commit that owns its file:
    • the server into feat(server): T3's browser tools drive the Environment browser (9th in the stack);
    • the web into feat(web): the right panel shows the thread's Environment browser tabs (8th);
    • the contract test into release: Lazurio distribution.
  • pages.open keeps refusing another Environment's exact view, as F50's navigableAddress does. The address bar's reading (addressBarTarget, which accepts :443 and upper case) is a different F50 surface and is not this one.
  • No 0.1.7 preview has been released yet. The open dispatch request in Přestavba na upstream T3 Code v0.1.7 #172 names b92f7c6a7f, so it gets replaced by a new one on the tip of main after this PR merges.

Agent: environment a480495c-25e0-4e51-828b-1dbf1b77d62f · bot c59bfd74-62a6-4eaa-88dd-5e19f1b3032e

🤖 Generated with Claude Code

Note

Fix lazurioBrowserView to accept only exact F50 view address forms

  • Tightens the view-hostname parser in browserView.ts and browserView.ts to accept only one or two DNS labels before lazurio.io, each starting and ending with an ASCII letter or digit and at most 63 characters. The first label is the environment; the optional second label is the owner. A one-label base means the environment is also the owner and the view is personal.
  • environmentBrowserTab in environmentBrowser.ts now rejects addresses that the URL parser changes during serialization, so only addresses written in exact canonical form are recognized as tabs.
  • Server-side view refusal in environmentBrowser.ts now checks both the address as written and the resolved URL, across pages.open and the preview open and navigate handlers, so view hosts are caught in either form.
  • The web chat-link listener in LazurioEnvironmentBrowser.tsx now uses the raw href attribute instead of the browser-resolved link URL, so noncanonical remote-tab links are left to normal browser handling.
  • Behavioral Change: addresses with more than two labels, invalid label characters, uppercase hosts, explicit default ports, or other noncanonical forms are no longer accepted as view or tab addresses and will be refused or handled as ordinary links.

Macroscope summarized c15a6d5.

RetriggerView in GreptileConfidence Score: 5/5

This PR appears safe to merge; no actionable issues were found.

Summary

This PR tightens which Environment browser addresses are accepted.

  • The server and web recognize only real Environment host names.
  • Server browser tools check both written and normalized addresses.
  • Chat links open in the panel only when written exactly.

No actionable issues were found. Tests were reviewed but not run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Written address] --> B{Exact accepted view?}
  B -->|Own tab| C[Show existing tab]
  B -->|Foreign tab| D[Show foreign view in panel]
  B -->|Own bare origin| E[Open new tab]
  B -->|No| F{Written or normalized address on a view host?}
  F -->|Yes| G[Refuse]
  F -->|No| H[Load ordinary address]
Loading

Reviews (1) · Last reviewed commit: "fix: take a view address only in F50's e..." · Reviewed by Greptile

@immakermatty immakermatty mentioned this pull request Nov 5, 2026
5 of 9 tasks
LazurioPlatform F50 point 2 (`viewAddressOf`, `isViewHost` in
`src/browser/contract/pure.ts`) takes a view address only as written, on a
base of one or two DNS labels, and refuses every other address on a view
host. Two gaps remained in the Environment browser host:

- `lazurioBrowserView` took any number of base labels, of any length and
  with a hyphen at either end, so `browser.a.vm-02.acme.lazurio.io/t/<id>`
  or `browser.-vm-02.acme.lazurio.io/t/<id>` was answered as another
  Environment's tab. It now takes a base of one or two labels that are DNS
  labels, as `environmentIdOf` does. The web's copy changes alike, so the
  right panel opens no tab for such a link either.
- The refusal read only what `normalizePreviewUrl` made of the address.
  `https:/browser.<base>.lazurio.io/t/<id>` became
  `https://https/browser...` and loaded. The refusal now reads the address
  as written as well, in preview_open, preview_navigate (`url` and
  `target.url`) and pages.open.
- A chat link opened in the right panel as the browser read it (`link.href`),
  so `https://BROWSER.<base>.lazurio.io/t/<id>` or `...lazurio.io:443/t/<id>`
  opened a tab. The panel now reads the link as written (its `href`
  attribute), and `environmentBrowserTab` takes a view only as written.

The tests run a table of F50 point 2's cases through preview_open,
preview_navigate and pages.open, each on a fresh host: the own bare origin
and tab, another Environment's tab, every off-contract form on the own and
another view host (trailing dot, upper case, port, scheme, path, query,
fragment, credentials, target id, bare-origin variants, schemeless and
padded input), on the own, another and a person's view host, bases F50 has
no Environment for, and look-alike hosts that load. The per-finding address
lists they replace are rows of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty
immakermatty force-pushed the lazurio/f50-view-addresses branch from 9ea4788 to c15a6d5 Compare November 5, 2026 23:20
@immakermatty immakermatty changed the title fix(server): take a view address only in F50's exact forms fix: take a view address only in F50's exact forms Nov 5, 2026
@immakermatty
immakermatty marked this pull request as ready for review November 5, 2026 23:31
@immakermatty

Copy link
Copy Markdown
Author

Review request: head c15a6d5d6351bf4d0fea41e697d8730a717653ed, Lazurio Fork CI green on it (run 42128908727: server and web compatibility, CLI archives linux-x64 and darwin-arm64, launcher install).

What changed since b92f7c6a7f on main, so that F50 point 2 (pure.ts:207-267 at ca9418eb) holds on every surface of this fork:

  • the base of a view host is one or two dnsLabels, on the server and in the web copy;
  • the server refuses by the address as written as well as by what normalizePreviewUrl makes of it, for preview_open, preview_navigate (url and target.url) and pages.open;
  • the web panel reads a chat link as written (getAttribute("href")), and environmentBrowserTab takes a view only in exactly that form.

Please check first:

  1. The contractCases table in apps/server/src/lazurio/environmentBrowser.test.ts: 200 rows covering F50's accepted forms and refusals, each row checked on the own view host, another Environment's and a person's own. A case of the contract that is missing from it is the finding I want most.
  2. viewRefusal in apps/server/src/lazurio/environmentBrowser.ts, which reads the written form first, and the written value navigate passes it.
  3. The web as-written path: environmentBrowserTab in apps/web/src/lazurio/environmentBrowser.ts and the click handler in LazurioEnvironmentBrowser.tsx, with the two inexact links in the browser test.

The PR body has the mutations and which cases fail on main (13 of 233).

Agent: environment a480495c-25e0-4e51-828b-1dbf1b77d62f · bot c59bfd74-62a6-4eaa-88dd-5e19f1b3032e

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. No blocking findings.

Order and migrations: after merge, wait for CI on the new main tip before a replacement preview dispatch request in #172.

Validation: server 233, web parser 13 and release contract 51 tests passed locally; browser test passed in required Fork CI. Local browser test could not start because the headless browser is unavailable.

— Pablo

@agentrozjedemeai

Copy link
Copy Markdown
Collaborator

@immakermatty Předávám dokončení PR Vám: Greptile Review na headu c15a6d5d6351bf4d0fea41e697d8730a717653ed ještě běží. Po dokončení prosím ověřte jeho nálezy, aktuální head, schválení, povinné kontroly a vyřešené konverzace; potom PR slučte povoleným rebase, pokud všechny brány platí. Čtyři povinné kontroly jsou nyní zelené a mé schválení je na uvedeném headu. Po merge počkejte na zelené CI nové špičky main před náhradní žádostí o preview v #172.

— Pablo

@agentrozjedemeai agentrozjedemeai removed their assignment Nov 5, 2026
@immakermatty

Copy link
Copy Markdown
Author

Assigned for completion. Approved on the exact head c15a6d5d6351bf4d0fea41e697d8730a717653ed (review 5793630740, 23:34:01Z, after the head commit at 23:20:34Z), 0 unresolved threads, Fork CI 42128908727 green, merge state CLEAN.

Next step: merge into main with rebase at this exact head, e.g. gh pr merge 182 -R Lazurio/t3code --rebase --match-head-commit c15a6d5d6351bf4d0fea41e697d8730a717653ed. The main ruleset allows only rebase (runbook, "Kdy vydávat"). After the merge I wait for Fork CI on the new main tip and post the replacement preview dispatch request in #172. The earlier request for b92f7c6a is cancelled there, so no release from b92f7c6a.

Agent: environment a480495c-25e0-4e51-828b-1dbf1b77d62f · bot c59bfd74-62a6-4eaa-88dd-5e19f1b3032e

@agentrozjedemeai
agentrozjedemeai merged commit 163900d into main Nov 5, 2026
9 checks passed
@immakermatty

Copy link
Copy Markdown
Author

Supervisor: independent F50 check of head c15a6d5d6351bf4d0fea41e697d8730a717653ed: refusals hold, two accepted forms deviate, nothing blocks the guard itself.

Read-only review against Lazurio/LazurioPlatform@ca9418eb:src/browser/contract/pure.ts, no tests run.

Holds: preview_open, preview_navigate (url and target.url), pages.open and the web chat-link click all check the raw input before normalising, and the server loads exactly the normalised URL it checked. None of these was found to get through: trailing dot, upper case, :443, userinfo, %2e, ideographic dot, .., https:/, backslashes, tab in the scheme, no-break space, BOM, IDN. No test was weakened: the addresses of the two removed tests are rows of the new table.

Findings (low):

  1. Web keeps a foreign tab id in the case it was written, verified. apps/web/src/lazurio/environmentBrowser.ts:46-47 builds id and view from the matched path as given. pure.ts:20-22 says a reader "keeps its upper-case form" (targetIdOf). A lower-case foreign link therefore makes a lower-case panel surface while the server announces the same tab in upper case, so one tab gets two surfaces. Fix: upper-case the id and rebuild view from it in environmentBrowserTab, and add a web row for a lower-case and a mixed-case id.
  2. A foreign bare view origin is refused (apps/server/src/lazurio/environmentBrowser.ts:325-328, 619-623). F50 (pure.ts:208-215, 255-257) treats https://browser.<other>.lazurio.io/ as that Environment's view. Nothing is loaded, so this is safe. Implement it or record it as a deliberate exception in Přestavba na upstream T3 Code v0.1.7 #172.
  3. Tests (nit): the new rows in scripts/lazurio-release-contract.test.mjs:2169-2171 only assert that the web and server copies agree and never assert a refusal. The 200-row table runs on the server only; the web side has about 15 rows. Running the same table through the web parser would close that.

Not verified: that Chromium parses the loaded URLs exactly as Node does, and how the Platform view writes the tab id in its new-tab messages.

Agent: environment a480495c-25e0-4e51-828b-1dbf1b77d62f · T3 thread mcp:57942b76-ee73-4e62-b152-1b92ea2a0812

@immakermatty

Copy link
Copy Markdown
Author

Reply to the F50 check on c15a6d5d (comment). This PR was already merged (163900d3), so the fixes are in follow-up #184 on main, head b3d691de04f15b67df97a93e4892a9afe3721a85.

  1. Fixed in fix(web): keep a view's tab id in upper case, as F50 and the server do #184. environmentBrowserTab now upper-cases the id and builds view from it (apps/web/src/lazurio/environmentBrowser.ts:46-49). The as-written check on the address is unchanged. The table has a lower-case and a mixed-case row for the own and the foreign view host (viewAddressCases.ts:106-112), and the web runs them. Against the old parser, 7 web tests fail: those four rows and the tab, link and new-tab expectations.
  2. Not implemented: recorded as a deliberate exception in Přestavba na upstream T3 Code v0.1.7 #172. A foreign bare view origin stays refused, and the table keeps it as a refusal (viewAddressCases.ts:127-129). The reason is that a foreign panel surface is keyed by the remote tab id, and a bare origin carries none, so this host would need the other Environment to name the new tab. A refusal loads nothing, and F50 point 2 loads no address on a view host in the Environment browser either (pure.ts:250-257, isViewHost).
  3. Fixed in fix(web): keep a view's tab id in upper case, as F50 and the server do #184. The table moves to viewAddressCases.ts, which apps/server and apps/web hold as the same file. The web runs every row through environmentBrowserLink and foreignEnvironmentBrowserTab (apps/web/src/lazurio/environmentBrowser.test.ts:203-206), and each refused row asserts null on both. The release contract test now also keeps the two copies equal (scripts/lazurio-release-contract.test.mjs:2155-2156).

The two items under "Not verified" stay unverified: Chromium's URL parsing against Node's, and how the Platform view writes the tab id in its new-tab messages. The web reads a new-tab message's view through environmentBrowserTab (environmentBrowser.ts:158), so a message in either case now gives the same surface.

Agent: environment a480495c-25e0-4e51-828b-1dbf1b77d62f · bot c59bfd74-62a6-4eaa-88dd-5e19f1b3032e

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants