[closed] [codex] Fix visited timestamp under clock skew#2406
[closed] [codex] Fix visited timestamp under clock skew#2406juliusmarminge wants to merge 41 commits into
Conversation
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
- Discover SSH hosts and persist SSH targets - Bootstrap tunneled SSH sessions with desktop password prompts - Extend IPC and storage tests for SSH metadata
- Validate SSH targets and known-host parsing more strictly - Retry desktop SSH session refresh on auth failures - Preserve saved registry state when bearer persistence fails
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
- Resolve dev remote package specs to `t3@nightly` - Cover the dev fallback in sshEnvironment tests
- surface stdout when remote launch or pairing fails - report parse errors and invalid remote port or credential values
- Add a capped scroll area for discovered SSH hosts - Keep the manual SSH form always visible and simplify the dialog layout - Ensure the scroll area viewport respects inherited max height
- No functional change - Keep staged code style consistent
- Move SSH IPC handlers and password prompt state out of main.ts - Keep SSH environment launch and auth flow owned by sshEnvironment.ts
- Externalize askpass, remote launch, and runner helpers into script assets - Copy SSH scripts into `dist-electron` for packaging - Co-authored-by: codex <codex@users.noreply.github.com>
- Remove native password prompts from posix and Windows scripts - Fail loudly when T3_SSH_AUTH_SECRET is missing
- use type-only imports required by verbatim module syntax - fix SSH desktop build/typecheck regressions and auth test isolation - tighten browser test selectors for the add-environment dialog Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
- Fix auth-failure matching for permission denied variants - Allow remote port picker to run without a state file path - Dispose only after pending tunnel creation resolves
- add desktop and server settings for hosted HTTPS pairing - resolve and probe MagicDNS endpoints via shared Tailscale helpers - update remote pairing docs and UI to surface the new setup flow
- Preserve Tailscale Serve port when toggling exposure - Prevent reused hosted pairing tokens and double SSH responses - Refine endpoint compatibility for HTTP custom URLs
- Ignore invalid custom HTTPS endpoint URLs when building desktop exposure - Reset pairing submission state after backend errors and clarify retry guidance - Add coverage for malformed endpoint inputs
- Relax settings UI text assertions to match reachable URLs - Add `packages/tailscale/package.json` to release smoke coverage
- Move SSH parsing and discovery logic into `@t3tools/ssh` - Reuse the shared helpers from the desktop app and release smoke checks - Add coverage for host discovery, parsing, and package spec resolution
- Add auth, command, config, and tunnel exports - Update desktop SSH environment imports - Add tests for auth and command helpers
- Avoid showing the hosted pairing flow outside the static app - Keep the existing error boundary behavior for local builds
- Factor Tailscale command execution into a reusable helper - Add tests for `tailscale serve off` - Ensure hosted pairing UI cleans up Tailscale Serve after startup
- Move SSH auth, config, and tunnel logic into `packages/ssh` - Wire `apps/desktop` to the shared Effect runtime - Add tests for config, auth, and tunnel behavior
- Simplify add-environment dialog around remote and SSH pairing - Auto-detect pairing URLs and improve SSH prompt handling - Add coverage for tunnel and connection parsing behavior
- Support remote T3 runner selection for hosted pairing - Surface SSH/environment disconnect states in the UI - Improve websocket snapshot and password prompt error handling
- Add structured logging around SSH command, tunnel, and pairing flows - Let desktop SSH bootstrap failures propagate instead of timing out locally - Update reconnect test to expect the underlying SSH timeout error
Co-authored-by: codex <codex@users.noreply.github.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string { | ||
| const packageSpec = input?.packageSpec?.trim() || "t3@latest"; | ||
| const nodeScriptPath = input?.nodeScriptPath?.trim() || ""; | ||
| return stripTrailingNewlines( | ||
| applyScriptPlaceholders(REMOTE_RUNNER_SCRIPT, { | ||
| T3_PACKAGE_SPEC: packageSpec, | ||
| T3_NODE_SCRIPT_PATH: shellSingleQuote(nodeScriptPath), | ||
| }), | ||
| ); | ||
| } |
There was a problem hiding this comment.
🟢 Low src/tunnel.ts:318
packageSpec is substituted directly into the shell script without escaping, so a value like file:./path with spaces/pkg produces invalid shell syntax in the exec npx --yes @@T3_PACKAGE_SPEC@@ "$@" command. A single quote in packageSpec also breaks the printf on line 236 because it is embedded inside single quotes. Consider applying shellSingleQuote() to packageSpec to match how nodeScriptPath is handled.
export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string {
- const packageSpec = input?.packageSpec?.trim() || "t3@latest";
+ const packageSpec = shellSingleQuote(input?.packageSpec?.trim() || "t3@latest");
const nodeScriptPath = input?.nodeScriptPath?.trim() || "";
return stripTrailingNewlines(
applyScriptPlaceholders(REMOTE_RUNNER_SCRIPT, {
T3_PACKAGE_SPEC: packageSpec,
- T3_NODE_SCRIPT_PATH: shellSingleQuote(nodeScriptPath),
+ T3_NODE_SCRIPT_PATH: nodeScriptPath ? shellSingleQuote(nodeScriptPath) : "''",
}),
);
}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/ssh/src/tunnel.ts around lines 318-327:
`packageSpec` is substituted directly into the shell script without escaping, so a value like `file:./path with spaces/pkg` produces invalid shell syntax in the `exec npx --yes @@T3_PACKAGE_SPEC@@ "$@"` command. A single quote in `packageSpec` also breaks the `printf` on line 236 because it is embedded inside single quotes. Consider applying `shellSingleQuote()` to `packageSpec` to match how `nodeScriptPath` is handled.
Evidence trail:
packages/ssh/src/tunnel.ts lines 220-237 (REMOTE_RUNNER_SCRIPT template with unquoted @@T3_PACKAGE_SPEC@@ in exec and printf), lines 318-326 (buildRemoteT3RunnerScript showing packageSpec is raw while nodeScriptPath uses shellSingleQuote), lines 173-175 (shellSingleQuote definition), lines 55-58 (RemoteT3RunnerOptions interface showing packageSpec is arbitrary string)
| <MenuPopup align="end" className="min-w-52"> | ||
| {endpointCopyOptions.map((option) => ( | ||
| <MenuItem | ||
| key={option.key} |
There was a problem hiding this comment.
🟡 Medium settings/ConnectionsSettings.tsx:724
endpointCopyOptions.map at line 724 uses endpointDefaultPreferenceKey(endpoint) as the React key, but this value is intentionally non-unique—multiple desktop LAN interfaces all return "desktop-core:lan:http" and multiple loopback endpoints all return "desktop-core:loopback:http". Duplicate keys cause React warnings and incorrect list rendering. Use endpoint.id instead, which is unique per endpoint.
- key={option.key}
+ key={endpoint.id}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/settings/ConnectionsSettings.tsx around line 724:
`endpointCopyOptions.map` at line 724 uses `endpointDefaultPreferenceKey(endpoint)` as the React `key`, but this value is intentionally non-unique—multiple desktop LAN interfaces all return `"desktop-core:lan:http"` and multiple loopback endpoints all return `"desktop-core:loopback:http"`. Duplicate keys cause React warnings and incorrect list rendering. Use `endpoint.id` instead, which is unique per endpoint.
Evidence trail:
apps/web/src/components/settings/ConnectionsSettings.tsx lines 471-490 (endpointDefaultPreferenceKey function collapses multiple endpoints to same key), lines 562-576 (endpointCopyOptions built with key: endpointDefaultPreferenceKey(endpoint)), lines 724-726 (key={option.key} used in .map)
| function splitDirectiveArgs(value: string): ReadonlyArray<string> { | ||
| return value | ||
| .trim() | ||
| .split(/\s+/u) | ||
| .map((entry) => entry.trim()) | ||
| .filter((entry) => entry.length > 0); | ||
| } |
There was a problem hiding this comment.
🟢 Low src/config.ts:14
splitDirectiveArgs splits only on whitespace, so SSH config lines using = as the directive separator (e.g., Host=myserver) produce a directive value of "host=myserver". After lowercasing, this fails to match "host", causing valid Host entries to be silently ignored.
-function splitDirectiveArgs(value: string): ReadonlyArray<string> {
- return value
- .trim()
.split(/\s+/u)
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
+function splitDirectiveArgs(value: string): ReadonlyArray<string> {
+ const withSpaceSeparators = value
+ .replace(/=(?!=)/gu, " ")
+ .trim();
+ return withSpaceSeparators
+ .split(/\s+/u)
+ .map((entry) => entry.trim())
+ .filter((entry) => entry.length > 0);
}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/ssh/src/config.ts around lines 14-20:
`splitDirectiveArgs` splits only on whitespace, so SSH config lines using `=` as the directive separator (e.g., `Host=myserver`) produce a directive value of `"host=myserver"`. After lowercasing, this fails to match `"host"`, causing valid `Host` entries to be silently ignored.
Evidence trail:
packages/ssh/src/config.ts lines 14-20 (splitDirectiveArgs splits on /\s+/u only), lines 100-108 (directive matching uses normalizedDirective === "host" and === "include"). SSH config man page documents = as valid separator: 'Configuration options may be separated by whitespace or optional whitespace and exactly one =' (https://man.openbsd.org/ssh_config.5).
| const updateHeight = () => { | ||
| const nextHeight = Math.ceil(element.scrollHeight || element.getBoundingClientRect().height); | ||
| setHeight((currentHeight) => (currentHeight === nextHeight ? currentHeight : nextHeight)); | ||
| }; | ||
| const updateHeightAfterPaint = () => { | ||
| updateHeight(); | ||
| firstFrameId = window.requestAnimationFrame(() => { | ||
| updateHeight(); | ||
| secondFrameId = window.requestAnimationFrame(updateHeight); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🟢 Low settings/ConnectionsSettings.tsx:178
When updateHeightAfterPaint is called again before prior animation frames complete, the previous firstFrameId and secondFrameId are overwritten without cancelling. The orphaned frame callbacks then execute after unmount, calling setHeight on an unmounted component.
- const updateHeightAfterPaint = () => {
+ const updateHeightAfterPaint = () => {
+ if (firstFrameId !== null) {
+ window.cancelAnimationFrame(firstFrameId);
+ firstFrameId = null;
+ }
+ if (secondFrameId !== null) {
+ window.cancelAnimationFrame(secondFrameId);
+ secondFrameId = null;
+ }
updateHeight();
- firstFrameId = window.requestAnimationFrame(() => {
+ firstFrameId = window.requestAnimationFrame(() => {
updateHeight();
- secondFrameId = window.requestAnimationFrame(updateHeight);
+ secondFrameId = window.requestAnimationFrame(updateHeight);
});
};🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/settings/ConnectionsSettings.tsx around lines 178-188:
When `updateHeightAfterPaint` is called again before prior animation frames complete, the previous `firstFrameId` and `secondFrameId` are overwritten without cancelling. The orphaned frame callbacks then execute after unmount, calling `setHeight` on an unmounted component.
Evidence trail:
apps/web/src/components/settings/ConnectionsSettings.tsx lines 168-205 at REVIEWED_COMMIT. Lines 184-189: `updateHeightAfterPaint` overwrites `firstFrameId` and `secondFrameId` without cancelling prior values. Lines 195-201: cleanup only cancels the latest stored IDs, not previously overwritten ones.
| throw new Error("Desktop SSH launch did not return a pairing token."); | ||
| } | ||
|
|
||
| const bearerSession = await bootstrapDesktopSshBearerSession( |
There was a problem hiding this comment.
🟡 Medium runtime/service.ts:680
If bootstrapDesktopSshBearerSession throws (lines 680-683), the registrySnapshot from line 672 is never rolled back, leaving registry modifications from prepareSavedEnvironmentRecordForConnection (line 673) persisted despite the failure. This is inconsistent with line 689, which rolls back when didPersistBearerToken is false. Consider wrapping lines 680-691 in a try-catch that also calls persistSavedEnvironmentRegistryRollback(registrySnapshot) on failure.
Also found in 1 other location(s)
apps/desktop/src/sshScripts/remote-pick-port.cjs:6
If
filePathexists but is a directory (or a file without read permissions),fs.readFileSyncwill throw a synchronous exception at module load time. This exception is not caught because it occurs outside the async IIFE's.catch()handler on line 31. The script will crash instead of gracefully falling back todefaultPort. Consider wrapping the file read in a try-catch or usingfs.statSyncto verify it's a readable file.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/environments/runtime/service.ts around line 680:
If `bootstrapDesktopSshBearerSession` throws (lines 680-683), the `registrySnapshot` from line 672 is never rolled back, leaving registry modifications from `prepareSavedEnvironmentRecordForConnection` (line 673) persisted despite the failure. This is inconsistent with line 689, which rolls back when `didPersistBearerToken` is false. Consider wrapping lines 680-691 in a try-catch that also calls `persistSavedEnvironmentRegistryRollback(registrySnapshot)` on failure.
Evidence trail:
apps/web/src/environments/runtime/service.ts lines 670-695 at REVIEWED_COMMIT: Line 672 takes registrySnapshot, line 673 calls prepareSavedEnvironmentRecordForConnection (modifying registry), lines 680-682 call bootstrapDesktopSshBearerSession with no try-catch, lines 688-691 show explicit rollback only for the didPersistBearerToken===false case.
Also found in 1 other location(s):
- apps/desktop/src/sshScripts/remote-pick-port.cjs:6 -- If `filePath` exists but is a directory (or a file without read permissions), `fs.readFileSync` will throw a synchronous exception at module load time. This exception is not caught because it occurs outside the async IIFE's `.catch()` handler on line 31. The script will crash instead of gracefully falling back to `defaultPort`. Consider wrapping the file read in a try-catch or using `fs.statSync` to verify it's a readable file.
Closed and replaced because the original head branch included unrelated existing work. Replacement PR: #2408
Note
Add SSH tunnel environments, Tailscale Serve support, and hosted static app pairing
@t3tools/sshand@t3tools/tailscalepackages implementing SSH config parsing, tunnel orchestration, askpass helpers, and Tailscale CLI integration for status/serve management.AdvertisedEndpointcontract (remoteAccess.ts) with loopback, LAN, Tailscale IP, and MagicDNS HTTPS endpoint providers; the desktop Connections settings UI is significantly expanded to manage and select endpoints, enable Tailscale Serve, and generate hosted pairing URLs.markThreadVisitedto store the server-provided timestamp, avoiding clock skew regressions (uiStateStore.ts).📊 Macroscope summarized 9678d3b. 38 files reviewed, 13 issues evaluated, 3 issues filtered, 5 comments posted
🗂️ Filtered Issues
apps/desktop/src/sshScripts/remote-pick-port.cjs — 0 comments posted, 3 evaluated, 2 filtered
process.argv[3]is not provided and the file atfilePathis empty or doesn't exist, bothpreferredanddefaultPortbecomeNaN. SinceNumber.isInteger(NaN)returnsfalse,startfalls back todefaultPortwhich isNaN. This causes the loop to never execute becauseNaN < endis always false, silently failing the port scan. [ Failed validation ]filePathexists but is a directory (or a file without read permissions),fs.readFileSyncwill throw a synchronous exception at module load time. This exception is not caught because it occurs outside the async IIFE's.catch()handler on line 31. The script will crash instead of gracefully falling back todefaultPort. Consider wrapping the file read in a try-catch or usingfs.statSyncto verify it's a readable file. [ Cross-file consolidated ]packages/ssh/src/command.ts — 0 comments posted, 2 evaluated, 1 filtered
child.exitCode.pipe(Effect.map(Number))converts the exit code using theNumberconstructor. If the child process is terminated by a signal (rather than exiting normally),child.exitCodemay resolve tonull. SinceNumber(null)returns0, the check at line 222 (exitCode !== 0) would incorrectly pass, treating a signal-killed process as successful instead of an error. [ Failed validation ]