Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/test-t3-app/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,12 @@ Shared browser dev is single-origin: Vite proxies the backend paths, so never se

The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.

### Verify a shared environment before human handoff

When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request.

Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person.

## Preserve the environment while iterating

Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
- Start the web stack with `vp run dev`. Add `--share` when someone needs to open it from another device on the tailnet.
- Browser dev is single-origin: Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the backend. Do not set `VITE_HTTP_URL` or `VITE_WS_URL` for `dev`/`dev:web`.
- Worktree paths supply stable preferred port offsets. Read the actual server and web ports from the `[dev-runner]` line because occupied ports can still shift them.
- Before handing off a `--share` URL, open its origin in a controlled browser and confirm the app loads. A successful curl is insufficient because browsers reject some otherwise reachable ports.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/scripts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@

`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname.

Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied, so treat the `[dev-runner]` output as authoritative.
Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative.

## Running multiple dev instances

Expand Down
49 changes: 49 additions & 0 deletions scripts/dev-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
devPortProbeHosts,
findFirstAvailableOffset,
getDevRunnerModeArgs,
isBrowserAllowedPort,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
Expand DownExpand Up@@ -547,6 +548,41 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);

it.effect("skips browser-blocked web ports before probing availability", () =>
Effect.gen(function* () {
const probed: Array<{ port: number; role: string | undefined }> = [];
const offset = yield* findFirstAvailableOffset({
// 5733 + 833 = 6566, which browsers block as sane-port.
startOffset: 833,
requireServerPort: true,
requireWebPort: true,
checkPortAvailability: (port, role) => {
probed.push({ port, role });
return Effect.succeed(true);
},
});

assert.equal(offset, 834);
assert.deepStrictEqual(probed, [
{ port: 14_607, role: "server" },
{ port: 6567, role: "web" },
]);
}),
);

it.effect("does not reject a server-only offset because its unused web port is blocked", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
startOffset: 833,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
});

assert.equal(offset, 833);
}),
);

it.effect("allows offsets where the non-required server port exceeds max", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
Expand DownExpand Up@@ -583,6 +619,19 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);
});

describe("isBrowserAllowedPort", () => {
it.each([6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697])(
"rejects Fetch-blocked web port %s from the worktree offset range",
(port) => {
assert.equal(isBrowserAllowedPort(port), false);
},
);

it.each([5733, 5900, 6567, 6670, 8733])("allows browser-safe web port %s", (port) => {
assert.equal(isBrowserAllowedPort(port), true);
});
});

describe("checkPortAvailabilityOnHosts", () => {
it.effect("checks overlapping hosts sequentially to avoid self-interference", () =>
Effect.gen(function* () {
Expand Down
19 changes: 19 additions & 0 deletions scripts/dev-runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,17 @@ const BASE_WEB_PORT = 5733;
const MAX_HASH_OFFSET = 3000;
const MAX_PORT = 65535;
const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
// HTTP(S) requests to these ports are blocked by the Fetch standard before a
// browser reaches the network. Keep the complete list here so explicit or
// future wider offsets cannot produce a URL that curl accepts but browsers
// reject. https://fetch.spec.whatwg.org/#port-blocking
const FETCH_BAD_PORTS = new Set([
0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
// Dev servers bind loopback, so loopback is the only interface whose
// availability decides whether we can use a port. Probing wildcards too made
// the runner walk away from a perfectly free port whenever something else held
Expand DownExpand Up@@ -91,6 +102,10 @@ export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {
return MODE_ARGS[mode];
}

export function isBrowserAllowedPort(port: number): boolean {
return !FETCH_BAD_PORTS.has(port);
}

export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(
"DevRunnerConfigurationError",
{
Expand DownExpand Up@@ -498,6 +513,10 @@ export function findFirstAvailableOffset<R = NetService.NetService>({
break;
}

if (requireWebPort && !isBrowserAllowedPort(webPort)) {
continue;
}

const checks: Array<Effect.Effect<boolean, never, R>> = [];
if (requireServerPort) {
checks.push(checkPort(serverPort, "server"));
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/test-t3-app/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,12 @@ Shared browser dev is single-origin: Vite proxies the backend paths, so never se

The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.

### Verify a shared environment before human handoff

When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request.

Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person.

## Preserve the environment while iterating

Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
- Start the web stack with `vp run dev`. Add `--share` when someone needs to open it from another device on the tailnet.
- Browser dev is single-origin: Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the backend. Do not set `VITE_HTTP_URL` or `VITE_WS_URL` for `dev`/`dev:web`.
- Worktree paths supply stable preferred port offsets. Read the actual server and web ports from the `[dev-runner]` line because occupied ports can still shift them.
- Before handing off a `--share` URL, open its origin in a controlled browser and confirm the app loads. A successful curl is insufficient because browsers reject some otherwise reachable ports.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/scripts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@

`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname.

Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied, so treat the `[dev-runner]` output as authoritative.
Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative.

## Running multiple dev instances

Expand Down
49 changes: 49 additions & 0 deletions scripts/dev-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
devPortProbeHosts,
findFirstAvailableOffset,
getDevRunnerModeArgs,
isBrowserAllowedPort,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
Expand DownExpand Up@@ -547,6 +548,41 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);

it.effect("skips browser-blocked web ports before probing availability", () =>
Effect.gen(function* () {
const probed: Array<{ port: number; role: string | undefined }> = [];
const offset = yield* findFirstAvailableOffset({
// 5733 + 833 = 6566, which browsers block as sane-port.
startOffset: 833,
requireServerPort: true,
requireWebPort: true,
checkPortAvailability: (port, role) => {
probed.push({ port, role });
return Effect.succeed(true);
},
});

assert.equal(offset, 834);
assert.deepStrictEqual(probed, [
{ port: 14_607, role: "server" },
{ port: 6567, role: "web" },
]);
}),
);

it.effect("does not reject a server-only offset because its unused web port is blocked", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
startOffset: 833,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
});

assert.equal(offset, 833);
}),
);

it.effect("allows offsets where the non-required server port exceeds max", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
Expand DownExpand Up@@ -583,6 +619,19 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);
});

describe("isBrowserAllowedPort", () => {
it.each([6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697])(
"rejects Fetch-blocked web port %s from the worktree offset range",
(port) => {
assert.equal(isBrowserAllowedPort(port), false);
},
);

it.each([5733, 5900, 6567, 6670, 8733])("allows browser-safe web port %s", (port) => {
assert.equal(isBrowserAllowedPort(port), true);
});
});

describe("checkPortAvailabilityOnHosts", () => {
it.effect("checks overlapping hosts sequentially to avoid self-interference", () =>
Effect.gen(function* () {
Expand Down
19 changes: 19 additions & 0 deletions scripts/dev-runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,17 @@ const BASE_WEB_PORT = 5733;
const MAX_HASH_OFFSET = 3000;
const MAX_PORT = 65535;
const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
// HTTP(S) requests to these ports are blocked by the Fetch standard before a
// browser reaches the network. Keep the complete list here so explicit or
// future wider offsets cannot produce a URL that curl accepts but browsers
// reject. https://fetch.spec.whatwg.org/#port-blocking
const FETCH_BAD_PORTS = new Set([
0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
// Dev servers bind loopback, so loopback is the only interface whose
// availability decides whether we can use a port. Probing wildcards too made
// the runner walk away from a perfectly free port whenever something else held
Expand DownExpand Up@@ -91,6 +102,10 @@ export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {
return MODE_ARGS[mode];
}

export function isBrowserAllowedPort(port: number): boolean {
return !FETCH_BAD_PORTS.has(port);
}

export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(
"DevRunnerConfigurationError",
{
Expand DownExpand Up@@ -498,6 +513,10 @@ export function findFirstAvailableOffset<R = NetService.NetService>({
break;
}

if (requireWebPort && !isBrowserAllowedPort(webPort)) {
continue;
}

const checks: Array<Effect.Effect<boolean, never, R>> = [];
if (requireServerPort) {
checks.push(checkPort(serverPort, "server"));
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/test-t3-app/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,12 @@ Shared browser dev is single-origin: Vite proxies the backend paths, so never se

The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.

### Verify a shared environment before human handoff

When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request.

Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person.

## Preserve the environment while iterating

Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
- Start the web stack with `vp run dev`. Add `--share` when someone needs to open it from another device on the tailnet.
- Browser dev is single-origin: Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the backend. Do not set `VITE_HTTP_URL` or `VITE_WS_URL` for `dev`/`dev:web`.
- Worktree paths supply stable preferred port offsets. Read the actual server and web ports from the `[dev-runner]` line because occupied ports can still shift them.
- Before handing off a `--share` URL, open its origin in a controlled browser and confirm the app loads. A successful curl is insufficient because browsers reject some otherwise reachable ports.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/scripts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@

`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname.

Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied, so treat the `[dev-runner]` output as authoritative.
Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative.

## Running multiple dev instances

Expand Down
49 changes: 49 additions & 0 deletions scripts/dev-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
devPortProbeHosts,
findFirstAvailableOffset,
getDevRunnerModeArgs,
isBrowserAllowedPort,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
Expand DownExpand Up@@ -547,6 +548,41 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);

it.effect("skips browser-blocked web ports before probing availability", () =>
Effect.gen(function* () {
const probed: Array<{ port: number; role: string | undefined }> = [];
const offset = yield* findFirstAvailableOffset({
// 5733 + 833 = 6566, which browsers block as sane-port.
startOffset: 833,
requireServerPort: true,
requireWebPort: true,
checkPortAvailability: (port, role) => {
probed.push({ port, role });
return Effect.succeed(true);
},
});

assert.equal(offset, 834);
assert.deepStrictEqual(probed, [
{ port: 14_607, role: "server" },
{ port: 6567, role: "web" },
]);
}),
);

it.effect("does not reject a server-only offset because its unused web port is blocked", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
startOffset: 833,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
});

assert.equal(offset, 833);
}),
);

it.effect("allows offsets where the non-required server port exceeds max", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
Expand DownExpand Up@@ -583,6 +619,19 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);
});

describe("isBrowserAllowedPort", () => {
it.each([6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697])(
"rejects Fetch-blocked web port %s from the worktree offset range",
(port) => {
assert.equal(isBrowserAllowedPort(port), false);
},
);

it.each([5733, 5900, 6567, 6670, 8733])("allows browser-safe web port %s", (port) => {
assert.equal(isBrowserAllowedPort(port), true);
});
});

describe("checkPortAvailabilityOnHosts", () => {
it.effect("checks overlapping hosts sequentially to avoid self-interference", () =>
Effect.gen(function* () {
Expand Down
19 changes: 19 additions & 0 deletions scripts/dev-runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,17 @@ const BASE_WEB_PORT = 5733;
const MAX_HASH_OFFSET = 3000;
const MAX_PORT = 65535;
const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
// HTTP(S) requests to these ports are blocked by the Fetch standard before a
// browser reaches the network. Keep the complete list here so explicit or
// future wider offsets cannot produce a URL that curl accepts but browsers
// reject. https://fetch.spec.whatwg.org/#port-blocking
const FETCH_BAD_PORTS = new Set([
0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
// Dev servers bind loopback, so loopback is the only interface whose
// availability decides whether we can use a port. Probing wildcards too made
// the runner walk away from a perfectly free port whenever something else held
Expand DownExpand Up@@ -91,6 +102,10 @@ export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {
return MODE_ARGS[mode];
}

export function isBrowserAllowedPort(port: number): boolean {
return !FETCH_BAD_PORTS.has(port);
}

export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(
"DevRunnerConfigurationError",
{
Expand DownExpand Up@@ -498,6 +513,10 @@ export function findFirstAvailableOffset<R = NetService.NetService>({
break;
}

if (requireWebPort && !isBrowserAllowedPort(webPort)) {
continue;
}

const checks: Array<Effect.Effect<boolean, never, R>> = [];
if (requireServerPort) {
checks.push(checkPort(serverPort, "server"));
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/test-t3-app/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,12 @@ Shared browser dev is single-origin: Vite proxies the backend paths, so never se

The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.

### Verify a shared environment before human handoff

When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request.

Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person.

## Preserve the environment while iterating

Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
- Start the web stack with `vp run dev`. Add `--share` when someone needs to open it from another device on the tailnet.
- Browser dev is single-origin: Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the backend. Do not set `VITE_HTTP_URL` or `VITE_WS_URL` for `dev`/`dev:web`.
- Worktree paths supply stable preferred port offsets. Read the actual server and web ports from the `[dev-runner]` line because occupied ports can still shift them.
- Before handing off a `--share` URL, open its origin in a controlled browser and confirm the app loads. A successful curl is insufficient because browsers reject some otherwise reachable ports.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/scripts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@

`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname.

Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied, so treat the `[dev-runner]` output as authoritative.
Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative.

## Running multiple dev instances

Expand Down
49 changes: 49 additions & 0 deletions scripts/dev-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
devPortProbeHosts,
findFirstAvailableOffset,
getDevRunnerModeArgs,
isBrowserAllowedPort,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
Expand DownExpand Up@@ -547,6 +548,41 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);

it.effect("skips browser-blocked web ports before probing availability", () =>
Effect.gen(function* () {
const probed: Array<{ port: number; role: string | undefined }> = [];
const offset = yield* findFirstAvailableOffset({
// 5733 + 833 = 6566, which browsers block as sane-port.
startOffset: 833,
requireServerPort: true,
requireWebPort: true,
checkPortAvailability: (port, role) => {
probed.push({ port, role });
return Effect.succeed(true);
},
});

assert.equal(offset, 834);
assert.deepStrictEqual(probed, [
{ port: 14_607, role: "server" },
{ port: 6567, role: "web" },
]);
}),
);

it.effect("does not reject a server-only offset because its unused web port is blocked", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
startOffset: 833,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
});

assert.equal(offset, 833);
}),
);

it.effect("allows offsets where the non-required server port exceeds max", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
Expand DownExpand Up@@ -583,6 +619,19 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);
});

describe("isBrowserAllowedPort", () => {
it.each([6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697])(
"rejects Fetch-blocked web port %s from the worktree offset range",
(port) => {
assert.equal(isBrowserAllowedPort(port), false);
},
);

it.each([5733, 5900, 6567, 6670, 8733])("allows browser-safe web port %s", (port) => {
assert.equal(isBrowserAllowedPort(port), true);
});
});

describe("checkPortAvailabilityOnHosts", () => {
it.effect("checks overlapping hosts sequentially to avoid self-interference", () =>
Effect.gen(function* () {
Expand Down
19 changes: 19 additions & 0 deletions scripts/dev-runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,17 @@ const BASE_WEB_PORT = 5733;
const MAX_HASH_OFFSET = 3000;
const MAX_PORT = 65535;
const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
// HTTP(S) requests to these ports are blocked by the Fetch standard before a
// browser reaches the network. Keep the complete list here so explicit or
// future wider offsets cannot produce a URL that curl accepts but browsers
// reject. https://fetch.spec.whatwg.org/#port-blocking
const FETCH_BAD_PORTS = new Set([
0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
// Dev servers bind loopback, so loopback is the only interface whose
// availability decides whether we can use a port. Probing wildcards too made
// the runner walk away from a perfectly free port whenever something else held
Expand DownExpand Up@@ -91,6 +102,10 @@ export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {
return MODE_ARGS[mode];
}

export function isBrowserAllowedPort(port: number): boolean {
return !FETCH_BAD_PORTS.has(port);
}

export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(
"DevRunnerConfigurationError",
{
Expand DownExpand Up@@ -498,6 +513,10 @@ export function findFirstAvailableOffset<R = NetService.NetService>({
break;
}

if (requireWebPort && !isBrowserAllowedPort(webPort)) {
continue;
}

const checks: Array<Effect.Effect<boolean, never, R>> = [];
if (requireServerPort) {
checks.push(checkPort(serverPort, "server"));
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/test-t3-app/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,12 @@ Shared browser dev is single-origin: Vite proxies the backend paths, so never se

The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.

### Verify a shared environment before human handoff

When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request.

Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person.

## Preserve the environment while iterating

Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
- Start the web stack with `vp run dev`. Add `--share` when someone needs to open it from another device on the tailnet.
- Browser dev is single-origin: Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the backend. Do not set `VITE_HTTP_URL` or `VITE_WS_URL` for `dev`/`dev:web`.
- Worktree paths supply stable preferred port offsets. Read the actual server and web ports from the `[dev-runner]` line because occupied ports can still shift them.
- Before handing off a `--share` URL, open its origin in a controlled browser and confirm the app loads. A successful curl is insufficient because browsers reject some otherwise reachable ports.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/scripts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@

`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname.

Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied, so treat the `[dev-runner]` output as authoritative.
Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative.

## Running multiple dev instances

Expand Down
49 changes: 49 additions & 0 deletions scripts/dev-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
devPortProbeHosts,
findFirstAvailableOffset,
getDevRunnerModeArgs,
isBrowserAllowedPort,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
Expand DownExpand Up@@ -547,6 +548,41 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);

it.effect("skips browser-blocked web ports before probing availability", () =>
Effect.gen(function* () {
const probed: Array<{ port: number; role: string | undefined }> = [];
const offset = yield* findFirstAvailableOffset({
// 5733 + 833 = 6566, which browsers block as sane-port.
startOffset: 833,
requireServerPort: true,
requireWebPort: true,
checkPortAvailability: (port, role) => {
probed.push({ port, role });
return Effect.succeed(true);
},
});

assert.equal(offset, 834);
assert.deepStrictEqual(probed, [
{ port: 14_607, role: "server" },
{ port: 6567, role: "web" },
]);
}),
);

it.effect("does not reject a server-only offset because its unused web port is blocked", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
startOffset: 833,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
});

assert.equal(offset, 833);
}),
);

it.effect("allows offsets where the non-required server port exceeds max", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
Expand DownExpand Up@@ -583,6 +619,19 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);
});

describe("isBrowserAllowedPort", () => {
it.each([6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697])(
"rejects Fetch-blocked web port %s from the worktree offset range",
(port) => {
assert.equal(isBrowserAllowedPort(port), false);
},
);

it.each([5733, 5900, 6567, 6670, 8733])("allows browser-safe web port %s", (port) => {
assert.equal(isBrowserAllowedPort(port), true);
});
});

describe("checkPortAvailabilityOnHosts", () => {
it.effect("checks overlapping hosts sequentially to avoid self-interference", () =>
Effect.gen(function* () {
Expand Down
19 changes: 19 additions & 0 deletions scripts/dev-runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,17 @@ const BASE_WEB_PORT = 5733;
const MAX_HASH_OFFSET = 3000;
const MAX_PORT = 65535;
const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
// HTTP(S) requests to these ports are blocked by the Fetch standard before a
// browser reaches the network. Keep the complete list here so explicit or
// future wider offsets cannot produce a URL that curl accepts but browsers
// reject. https://fetch.spec.whatwg.org/#port-blocking
const FETCH_BAD_PORTS = new Set([
0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
// Dev servers bind loopback, so loopback is the only interface whose
// availability decides whether we can use a port. Probing wildcards too made
// the runner walk away from a perfectly free port whenever something else held
Expand DownExpand Up@@ -91,6 +102,10 @@ export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {
return MODE_ARGS[mode];
}

export function isBrowserAllowedPort(port: number): boolean {
return !FETCH_BAD_PORTS.has(port);
}

export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(
"DevRunnerConfigurationError",
{
Expand DownExpand Up@@ -498,6 +513,10 @@ export function findFirstAvailableOffset<R = NetService.NetService>({
break;
}

if (requireWebPort && !isBrowserAllowedPort(webPort)) {
continue;
}

const checks: Array<Effect.Effect<boolean, never, R>> = [];
if (requireServerPort) {
checks.push(checkPort(serverPort, "server"));
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/test-t3-app/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,12 @@ Shared browser dev is single-origin: Vite proxies the backend paths, so never se

The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.

### Verify a shared environment before human handoff

When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request.

Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person.

## Preserve the environment while iterating

Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
- Start the web stack with `vp run dev`. Add `--share` when someone needs to open it from another device on the tailnet.
- Browser dev is single-origin: Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the backend. Do not set `VITE_HTTP_URL` or `VITE_WS_URL` for `dev`/`dev:web`.
- Worktree paths supply stable preferred port offsets. Read the actual server and web ports from the `[dev-runner]` line because occupied ports can still shift them.
- Before handing off a `--share` URL, open its origin in a controlled browser and confirm the app loads. A successful curl is insufficient because browsers reject some otherwise reachable ports.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/scripts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@

`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname.

Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied, so treat the `[dev-runner]` output as authoritative.
Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative.

## Running multiple dev instances

Expand Down
49 changes: 49 additions & 0 deletions scripts/dev-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
devPortProbeHosts,
findFirstAvailableOffset,
getDevRunnerModeArgs,
isBrowserAllowedPort,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
Expand DownExpand Up@@ -547,6 +548,41 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);

it.effect("skips browser-blocked web ports before probing availability", () =>
Effect.gen(function* () {
const probed: Array<{ port: number; role: string | undefined }> = [];
const offset = yield* findFirstAvailableOffset({
// 5733 + 833 = 6566, which browsers block as sane-port.
startOffset: 833,
requireServerPort: true,
requireWebPort: true,
checkPortAvailability: (port, role) => {
probed.push({ port, role });
return Effect.succeed(true);
},
});

assert.equal(offset, 834);
assert.deepStrictEqual(probed, [
{ port: 14_607, role: "server" },
{ port: 6567, role: "web" },
]);
}),
);

it.effect("does not reject a server-only offset because its unused web port is blocked", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
startOffset: 833,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
});

assert.equal(offset, 833);
}),
);

it.effect("allows offsets where the non-required server port exceeds max", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
Expand DownExpand Up@@ -583,6 +619,19 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);
});

describe("isBrowserAllowedPort", () => {
it.each([6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697])(
"rejects Fetch-blocked web port %s from the worktree offset range",
(port) => {
assert.equal(isBrowserAllowedPort(port), false);
},
);

it.each([5733, 5900, 6567, 6670, 8733])("allows browser-safe web port %s", (port) => {
assert.equal(isBrowserAllowedPort(port), true);
});
});

describe("checkPortAvailabilityOnHosts", () => {
it.effect("checks overlapping hosts sequentially to avoid self-interference", () =>
Effect.gen(function* () {
Expand Down
19 changes: 19 additions & 0 deletions scripts/dev-runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,17 @@ const BASE_WEB_PORT = 5733;
const MAX_HASH_OFFSET = 3000;
const MAX_PORT = 65535;
const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
// HTTP(S) requests to these ports are blocked by the Fetch standard before a
// browser reaches the network. Keep the complete list here so explicit or
// future wider offsets cannot produce a URL that curl accepts but browsers
// reject. https://fetch.spec.whatwg.org/#port-blocking
const FETCH_BAD_PORTS = new Set([
0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
// Dev servers bind loopback, so loopback is the only interface whose
// availability decides whether we can use a port. Probing wildcards too made
// the runner walk away from a perfectly free port whenever something else held
Expand DownExpand Up@@ -91,6 +102,10 @@ export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {
return MODE_ARGS[mode];
}

export function isBrowserAllowedPort(port: number): boolean {
return !FETCH_BAD_PORTS.has(port);
}

export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(
"DevRunnerConfigurationError",
{
Expand DownExpand Up@@ -498,6 +513,10 @@ export function findFirstAvailableOffset<R = NetService.NetService>({
break;
}

if (requireWebPort && !isBrowserAllowedPort(webPort)) {
continue;
}

const checks: Array<Effect.Effect<boolean, never, R>> = [];
if (requireServerPort) {
checks.push(checkPort(serverPort, "server"));
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/test-t3-app/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,12 @@ Shared browser dev is single-origin: Vite proxies the backend paths, so never se

The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.

### Verify a shared environment before human handoff

When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request.

Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person.

## Preserve the environment while iterating

Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
- Start the web stack with `vp run dev`. Add `--share` when someone needs to open it from another device on the tailnet.
- Browser dev is single-origin: Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the backend. Do not set `VITE_HTTP_URL` or `VITE_WS_URL` for `dev`/`dev:web`.
- Worktree paths supply stable preferred port offsets. Read the actual server and web ports from the `[dev-runner]` line because occupied ports can still shift them.
- Before handing off a `--share` URL, open its origin in a controlled browser and confirm the app loads. A successful curl is insufficient because browsers reject some otherwise reachable ports.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/scripts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@

`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname.

Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied, so treat the `[dev-runner]` output as authoritative.
Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative.

## Running multiple dev instances

Expand Down
49 changes: 49 additions & 0 deletions scripts/dev-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
devPortProbeHosts,
findFirstAvailableOffset,
getDevRunnerModeArgs,
isBrowserAllowedPort,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
Expand DownExpand Up@@ -547,6 +548,41 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);

it.effect("skips browser-blocked web ports before probing availability", () =>
Effect.gen(function* () {
const probed: Array<{ port: number; role: string | undefined }> = [];
const offset = yield* findFirstAvailableOffset({
// 5733 + 833 = 6566, which browsers block as sane-port.
startOffset: 833,
requireServerPort: true,
requireWebPort: true,
checkPortAvailability: (port, role) => {
probed.push({ port, role });
return Effect.succeed(true);
},
});

assert.equal(offset, 834);
assert.deepStrictEqual(probed, [
{ port: 14_607, role: "server" },
{ port: 6567, role: "web" },
]);
}),
);

it.effect("does not reject a server-only offset because its unused web port is blocked", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
startOffset: 833,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
});

assert.equal(offset, 833);
}),
);

it.effect("allows offsets where the non-required server port exceeds max", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
Expand DownExpand Up@@ -583,6 +619,19 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);
});

describe("isBrowserAllowedPort", () => {
it.each([6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697])(
"rejects Fetch-blocked web port %s from the worktree offset range",
(port) => {
assert.equal(isBrowserAllowedPort(port), false);
},
);

it.each([5733, 5900, 6567, 6670, 8733])("allows browser-safe web port %s", (port) => {
assert.equal(isBrowserAllowedPort(port), true);
});
});

describe("checkPortAvailabilityOnHosts", () => {
it.effect("checks overlapping hosts sequentially to avoid self-interference", () =>
Effect.gen(function* () {
Expand Down
19 changes: 19 additions & 0 deletions scripts/dev-runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,17 @@ const BASE_WEB_PORT = 5733;
const MAX_HASH_OFFSET = 3000;
const MAX_PORT = 65535;
const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
// HTTP(S) requests to these ports are blocked by the Fetch standard before a
// browser reaches the network. Keep the complete list here so explicit or
// future wider offsets cannot produce a URL that curl accepts but browsers
// reject. https://fetch.spec.whatwg.org/#port-blocking
const FETCH_BAD_PORTS = new Set([
0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
// Dev servers bind loopback, so loopback is the only interface whose
// availability decides whether we can use a port. Probing wildcards too made
// the runner walk away from a perfectly free port whenever something else held
Expand DownExpand Up@@ -91,6 +102,10 @@ export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {
return MODE_ARGS[mode];
}

export function isBrowserAllowedPort(port: number): boolean {
return !FETCH_BAD_PORTS.has(port);
}

export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(
"DevRunnerConfigurationError",
{
Expand DownExpand Up@@ -498,6 +513,10 @@ export function findFirstAvailableOffset<R = NetService.NetService>({
break;
}

if (requireWebPort && !isBrowserAllowedPort(webPort)) {
continue;
}

const checks: Array<Effect.Effect<boolean, never, R>> = [];
if (requireServerPort) {
checks.push(checkPort(serverPort, "server"));
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/test-t3-app/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,12 @@ Shared browser dev is single-origin: Vite proxies the backend paths, so never se

The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.

### Verify a shared environment before human handoff

When another person will use the printed pairing URL, first open the shared origin without the pairing path or fragment in the controlled browser and confirm the T3 Code app loads. This browser navigation is required even when curl succeeds because browsers block some otherwise reachable ports before making a network request.

Do not open the other person's complete pairing URL during this reachability check; doing so consumes its one-time token. If the agent also needs an authenticated browser, create and consume a separate pairing token, then leave a fresh token for the other person.

## Preserve the environment while iterating

Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
- Start the web stack with `vp run dev`. Add `--share` when someone needs to open it from another device on the tailnet.
- Browser dev is single-origin: Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the backend. Do not set `VITE_HTTP_URL` or `VITE_WS_URL` for `dev`/`dev:web`.
- Worktree paths supply stable preferred port offsets. Read the actual server and web ports from the `[dev-runner]` line because occupied ports can still shift them.
- Before handing off a `--share` URL, open its origin in a controlled browser and confirm the app loads. A successful curl is insufficient because browsers reject some otherwise reachable ports.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/scripts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@

`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname.

Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied, so treat the `[dev-runner]` output as authoritative.
Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative.

## Running multiple dev instances

Expand Down
49 changes: 49 additions & 0 deletions scripts/dev-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
devPortProbeHosts,
findFirstAvailableOffset,
getDevRunnerModeArgs,
isBrowserAllowedPort,
resolveModePortOffsets,
resolveOffset,
runDevRunnerWithInput,
Expand DownExpand Up@@ -547,6 +548,41 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
}),
);

it.effect("skips browser-blocked web ports before probing availability", () =>
Effect.gen(function* () {
const probed: Array<{ port: number; role: string | undefined }> = [];
const offset = yield* findFirstAvailableOffset({
// 5733 + 833 = 6566, which browsers block as sane-port.
startOffset: 833,
requireServerPort: true,
requireWebPort: true,
checkPortAvailability: (port, role) => {
probed.push({ port, role });
return Effect.succeed(true);
},
});

assert.equal(offset, 834);
assert.deepStrictEqual(probed, [
{ port: 14_607, role: "server" },
{ port: 6567, role: "web" },
]);
}),
);

it.effect("does not reject a server-only offset because its unused web port is blocked", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
startOffset: 833,
requireServerPort: true,
requireWebPort: false,
checkPortAvailability: () => Effect.succeed(true),
});

assert.equal(offset, 833);
}),
);

it.effect("allows offsets where the non-required server port exceeds max", () =>
Effect.gen(function* () {
const offset = yield* findFirstAvailableOffset({
Expand DownExpand Up@@ -583,6 +619,19 @@ it.layer(NodeServices.layer)("dev-runner", (it) => {
);
});

describe("isBrowserAllowedPort", () => {
it.each([6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697])(
"rejects Fetch-blocked web port %s from the worktree offset range",
(port) => {
assert.equal(isBrowserAllowedPort(port), false);
},
);

it.each([5733, 5900, 6567, 6670, 8733])("allows browser-safe web port %s", (port) => {
assert.equal(isBrowserAllowedPort(port), true);
});
});

describe("checkPortAvailabilityOnHosts", () => {
it.effect("checks overlapping hosts sequentially to avoid self-interference", () =>
Effect.gen(function* () {
Expand Down
19 changes: 19 additions & 0 deletions scripts/dev-runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,17 @@ const BASE_WEB_PORT = 5733;
const MAX_HASH_OFFSET = 3000;
const MAX_PORT = 65535;
const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";
// HTTP(S) requests to these ports are blocked by the Fetch standard before a
// browser reaches the network. Keep the complete list here so explicit or
// future wider offsets cannot produce a URL that curl accepts but browsers
// reject. https://fetch.spec.whatwg.org/#port-blocking
const FETCH_BAD_PORTS = new Set([
0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
6669, 6679, 6697, 10080,
]);
// Dev servers bind loopback, so loopback is the only interface whose
// availability decides whether we can use a port. Probing wildcards too made
// the runner walk away from a perfectly free port whenever something else held
Expand DownExpand Up@@ -91,6 +102,10 @@ export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {
return MODE_ARGS[mode];
}

export function isBrowserAllowedPort(port: number): boolean {
return !FETCH_BAD_PORTS.has(port);
}

export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(
"DevRunnerConfigurationError",
{
Expand DownExpand Up@@ -498,6 +513,10 @@ export function findFirstAvailableOffset<R = NetService.NetService>({
break;
}

if (requireWebPort && !isBrowserAllowedPort(webPort)) {
continue;
}

const checks: Array<Effect.Effect<boolean, never, R>> = [];
if (requireServerPort) {
checks.push(checkPort(serverPort, "server"));
Expand Down
Loading