Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9e5a917
fix(test): promote Bun 1.4.4 with independent runner role verification
luvs01 Oct 23, 2026
1deb67d
docs: align Docker Bun comments with unified role defaults
luvs01 Oct 23, 2026
0dd4582
fix(link): preserve literal SSH include names on Bun 1.4.4
luvs01 Oct 23, 2026
41f08ff
test: use a draining TLS origin for early upload replies
luvs01 Oct 23, 2026
a9c3d12
test: confine draining origin to buffered HTTP/1 upload
luvs01 Oct 23, 2026
f011a94
merge: align test-runner role branch with dev 2.93.0 dependency baseline
luvs01 Oct 24, 2026
c32aec6
Merge dev security dependency baseline into 7629 SSH-config validation
luvs01 Oct 24, 2026
8b4c34c
Merge branch 'dev' into codex/test-runner-role-verification-20261023
lidge-jun Oct 24, 2026
a593f93
Merge commit '26598df2c4c07d781e344ac6119953fed77ecb27' into codex/pr…
luvs01 Oct 24, 2026
00b6c0b
Merge commit '8b4c34c7e6103314dcb061f0f82a8eaafdb4d386' into codex/pr…
luvs01 Oct 24, 2026
f091b1c
test(server): isolate port reclaim fixture identity and clock
luvs01 Oct 24, 2026
967a885
test: observe body before buffered early upload reply
luvs01 Oct 24, 2026
ba67ddb
test: close direct upload before fixture teardown
luvs01 Oct 24, 2026
226b45d
Merge commit 'ba67ddb501cc9ee2871025d8a6b812074afbdd08' into codex/77…
luvs01 Oct 24, 2026
c530d3a
Merge dev into port fixture parent preserving deterministic clock
luvs01 Oct 25, 2026
fa01399
Merge refreshed port fixture parent into SSH include glob fix
luvs01 Oct 25, 2026
f9594c0
Merge remote-tracking branch 'origin/dev' into sync-7629
lidge-jun Oct 26, 2026
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
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/guides/remote-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ The dashboard does not ask you to enter a token. It probes the host first, and i

The Add Child sheet explains that the Child uses this Home's providers over SSH and lists the prerequisites above. Host aliases come from `~/.ssh/config` on the Home running OpenCodex, not necessarily the computer displaying the browser. If discovery succeeds with no hosts, add a `Host` entry like this, then choose **Rescan hosts**. You can also enter an existing alias manually; selecting or entering an alias enables **Test connection**. The sheet links to this guide.

Host discovery also follows top-level `Include` directives. Parentheses and braces in an included filename stay literal when combined with wildcards: for example, `Include conf.d/*(1).conf` finds `backup(1).conf`, and `Include conf.d/shard-{1..3}-*.conf` looks for filenames containing `{1..3}`. An `Include` inside a `Host` or `Match` block is conditional and is not used for discovery.

```sshconfig
Host devbox
HostName devbox.example.com
Expand Down
38 changes: 35 additions & 3 deletions src/link/ssh-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
import { isSshAlias } from "./ssh-argv";

/**
Expand Down Expand Up @@ -117,11 +117,43 @@ export interface LoadHostCandidatesOptions {
glob?: (pattern: string) => string[];
}

/** OpenSSH Include uses glob(..., GLOB_TILDE): parentheses and braces are literal. */
function normalizeIncludeGlob(pattern: string): string {
let normalized = "";
for (let i = 0; i < pattern.length; i += 1) {
const char = pattern[i]!;
if (char === "\\" && i + 1 < pattern.length) {
normalized += char + pattern[++i];
continue;
}
if (char === "[") {
let end = i + 1;
if (pattern[end] === "!" || pattern[end] === "^") end += 1;
if (pattern[end] === "]") end += 1;
for (; end < pattern.length; end += 1) {
if (pattern[end] === "\\") end += 1;
else if (pattern[end] === "]") break;
}
if (end < pattern.length) {
normalized += pattern.slice(i, end + 1);
i = end;
continue;
}
// Keep a malformed suffix intact: generated quoting must not close its class.
return normalized + pattern.slice(i);
}
// Character classes work on both Bun versions, without enabling 1.4.4's extglobs.
normalized += char === "(" ? "[(]" : char === "{" ? "[{]" : char;
}
return normalized;
}

function defaultGlob(pattern: string): string[] {
if (!/[*?[]/.test(pattern)) return [pattern];
const base = dirname(pattern.slice(0, pattern.search(/[*?[]/) + 1));
const relative = pattern.slice(base.length + 1);
return [...new Bun.Glob(relative).scanSync({ cwd: base, absolute: true, onlyFiles: true })].sort();
// Only native Windows separators become slashes; POSIX backslash escapes stay intact.
const relative = pattern.slice(base.length + 1).split(sep).join("/");
return [...new Bun.Glob(normalizeIncludeGlob(relative)).scanSync({ cwd: base, absolute: true, onlyFiles: true })].sort();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion structure/remote-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Every remote `ocx` call goes through `remoteOcxArgv`, a quoted POSIX `sh -c` scr

`src/link/remote-platform.ts` recognises a Windows Child from one remote result: exit 125, a MINGW/MSYS/CYGWIN/Windows `uname -s`, or the cmd.exe or PowerShell "is not recognized" refusal of a POSIX command when the SSH shell has no POSIX sh. Any other non-transport failure (not 0, 125, 127 or 255) gets one locale-independent follow-up, the fixed `cmd /c echo %OS%` (`WINDOWS_OS_PROBE`) against the same strict host key, which prints `Windows_NT` under cmd and PowerShell in any display language. Enrollment maps all three to `remote_platform_unsupported` at probe or host confirmation, before host trust, key issuance or a tunnel, and the refusal names Hub mode (`ocx hub invite`, `ocx connect`). `ocx link cleanup` (`src/cli/link-cleanup.ts`) applies the same classifier to existing Home-initiated records over their pinned SSH trust and only reports: a Windows Child gets the recovery order disconnect on the Child, forced revoke on the Home, then Hub mode, because an ordinary revoke's remote `ocx disconnect` goes through the POSIX prelude that exits 125. Coverage: `tests/cli/cli-link-cleanup.test.ts`, `tests/server/link-enrollment.test.ts`.

`src/link/ssh-config.ts` lists host candidates from `~/.ssh/config`. Arguments are split with the rules of OpenSSH's `argv_split`. Pattern hosts, `Match` blocks and aliases that fail the alias check produce no candidates, and only top-level `Include` directives are followed, because an include inside a `Host` or `Match` block is conditional. A candidate is an offer, not trust.
`src/link/ssh-config.ts` lists host candidates from `~/.ssh/config`. Arguments are split with the rules of OpenSSH's `argv_split`. Pattern hosts, `Match` blocks and aliases that fail the alias check produce no candidates, and only top-level `Include` directives are followed, because an include inside a `Host` or `Match` block is conditional. Include discovery shields literal parentheses and braces from Bun's extended-glob and brace expansion, preserving existing escapes and character classes; wildcard matching, traversal options and lexical ordering remain with `Bun.Glob`. Coverage: `tests/clients/link-ssh-config.test.ts`. A candidate is an offer, not trust.

`src/link/ssh-runner.ts` drains tunnel stdout and stderr with a 64 KiB cap per stream. After the SSH process exits, either reader that has not reached EOF gets 250 ms to consume remaining bytes, then is cancelled and its lock released without waiting for the underlying cancellation promise. Already consumed diagnostics are retained for host-key, auth and network classification, so an inherited pipe cannot indefinitely hold the supervisor's exit classification or pause retries and the outage clock. This grace applies to spawned tunnels; bounded one-shot commands retain their command timeout.

Expand Down
46 changes: 45 additions & 1 deletion tests/clients/link-ssh-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { loadHostCandidates, parseHostCandidates, splitSshArgs } from "../../src/link/ssh-config";
import { removeTreeWithRetry } from "../helpers/remove-tree";

Expand Down Expand Up @@ -77,6 +77,50 @@ test("top-level Include follows relative globs but ignores conditional includes"
]);
});

for (const fixture of [
{ name: "star before parentheses", pattern: "conf.d/*(1).conf", files: ["conf.d/backup(1).conf", "conf.d/1.conf"], selected: [0] },
{ name: "question mark before parentheses", pattern: "conf.d/?(work).conf", files: ["conf.d/x(work).conf", "conf.d/work.conf"], selected: [0] },
{ name: "plus before parentheses", pattern: "conf.d/*+(work).conf", files: ["conf.d/a+(work).conf", "conf.d/awork.conf"], selected: [0] },
{ name: "at sign before parentheses", pattern: "conf.d/*@(work).conf", files: ["conf.d/a@(work).conf", "conf.d/awork.conf"], selected: [0] },
{ name: "exclamation mark before parentheses", pattern: "conf.d/*!(work).conf", files: ["conf.d/a!(work).conf", "conf.d/aother.conf"], selected: [0] },
{ name: "numeric brace range", pattern: "conf.d/shard-{1..3}-*.conf", files: ["conf.d/shard-{1..3}-a.conf", "conf.d/shard-1-a.conf"], selected: [0] },
{ name: "brace alternatives", pattern: "conf.d/{work,home}-*.conf", files: ["conf.d/{work,home}-a.conf", "conf.d/work-a.conf", "conf.d/home-a.conf"], selected: [0] },
{ name: "literal directory after a wildcard", pattern: "conf.*/@(work)/*.conf", files: ["conf.a/@(work)/a.conf", "conf.a/work/a.conf"], selected: [0] },
{ name: "ordinary question marks and classes", pattern: "conf.d/[ab]?.conf", files: ["conf.d/a1.conf", "conf.d/b2.conf", "conf.d/c3.conf", "conf.d/a12.conf"], selected: [0, 1] },
{ name: "parentheses and braces inside a class", pattern: "conf.d/[({]*.conf", files: ["conf.d/(a.conf", "conf.d/{b.conf", "conf.d/c.conf"], selected: [0, 1] },
{ name: "a leading closing bracket inside a class", pattern: "conf.d/[]({]*.conf", files: ["conf.d/(a.conf", "conf.d/]b.conf", "conf.d/{c.conf", "conf.d/d.conf"], selected: [0, 1, 2] },
{ name: "a negated class", pattern: "conf.d/[!({]*.conf", files: ["conf.d/(a.conf", "conf.d/{b.conf", "conf.d/c.conf"], selected: [2] },
{ name: "already quoted parentheses", pattern: "conf.d/*[(]1[)].conf", files: ["conf.d/backup(1).conf", "conf.d/1.conf"], selected: [0] },
{ name: "already quoted braces", pattern: "conf.d/*[{]1..3[}].conf", files: ["conf.d/shard{1..3}.conf", "conf.d/shard1.conf"], selected: [0] },
{ name: "an unclosed class cannot consume generated quoting", pattern: "conf.d/[[[ab*(c).conf", files: ["conf.d/bc).conf", "conf.d/c.conf"], selected: [] },
]) {
test(`Include glob compatibility: ${fixture.name}`, () => {
const home = tempHome("glob");
const sshDir = join(home, ".ssh");
mkdirSync(sshDir, { recursive: true });
writeFileSync(join(sshDir, "config"), `Include ${fixture.pattern}\n`);
for (const [index, file] of fixture.files.entries()) {
const path = join(sshDir, file);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, `Host host${index}.example.test\n`);
}
expect(loadHostCandidates({ home }).map(candidate => candidate.alias)).toEqual(
fixture.selected.map(index => `host${index}.example.test`),
);
});
}

test.skipIf(process.platform === "win32")("Include preserves an escaped parenthesis in a POSIX glob", () => {
const home = tempHome("escaped-glob");
const sshDir = join(home, ".ssh");
const includeDir = join(sshDir, "conf.d");
mkdirSync(includeDir, { recursive: true });
writeFileSync(join(sshDir, "config"), String.raw`Include conf.d/*\(1).conf`);
writeFileSync(join(includeDir, "backup(1).conf"), "Host literal.example.test\n");
writeFileSync(join(includeDir, "1.conf"), "Host expanded.example.test\n");
expect(loadHostCandidates({ home }).map(candidate => candidate.alias)).toEqual(["literal.example.test"]);
});

test("Include recursion stops at the OpenSSH nesting limit", () => {
const home = tempHome("recursive");
const sshDir = join(home, ".ssh");
Expand Down
Loading