diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml
index fb37ecbe158..d3d4ec16ec2 100644
--- a/.github/workflows/ssh-relay-runtime-artifacts.yml
+++ b/.github/workflows/ssh-relay-runtime-artifacts.yml
@@ -263,6 +263,7 @@ jobs:
src/main/ssh/ssh-relay-release-asset.test.ts \
src/main/ssh/ssh-relay-artifact-selector.test.ts \
src/main/ssh/ssh-relay-libc-detection.test.ts \
+ src/main/ssh/ssh-relay-linux-kernel-detection.test.ts \
src/main/ssh/ssh-relay-artifact-download.test.ts \
src/main/ssh/ssh-relay-artifact-extraction.test.ts \
src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts \
@@ -809,6 +810,7 @@ jobs:
src/main/ssh/ssh-relay-release-asset.test.ts `
src/main/ssh/ssh-relay-artifact-selector.test.ts `
src/main/ssh/ssh-relay-libc-detection.test.ts `
+ src/main/ssh/ssh-relay-linux-kernel-detection.test.ts `
src/main/ssh/ssh-relay-artifact-download.test.ts `
src/main/ssh/ssh-relay-artifact-extraction.test.ts `
src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts `
diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs
index 2d23dc5eeb5..3ada4e78f36 100644
--- a/config/scripts/ssh-relay-runtime-workflow.test.mjs
+++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs
@@ -121,6 +121,7 @@ describe('SSH relay runtime artifact workflow', () => {
'ssh-relay-release-asset.test.ts',
'ssh-relay-artifact-selector.test.ts',
'ssh-relay-libc-detection.test.ts',
+ 'ssh-relay-linux-kernel-detection.test.ts',
'ssh-relay-artifact-download.test.ts',
'ssh-relay-artifact-extraction.test.ts',
'ssh-relay-artifact-cache-lock-release.test.ts',
diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
index 70231968a75..6b9e1adb377 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
@@ -9,9 +9,10 @@ keeps commands, hashes, runner identities, timings, and failure details.
A checked box means the work has evidence in the detailed ledger. Design approval alone does not
complete a box.
-Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest
-remaining disconnected host-evidence boundary after exact-head acceptance of Linux libc detection,
-2026-07-15, Codex implementation owner.**
+Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — require exact-head
+all-six native CI for the disconnected marked Linux kernel probe and kernel-only distro-suffix
+parser accepted locally by `E-M5-LINUX-KERNEL-DETECTION-LOCAL-001`, 2026-07-15, Codex implementation
+owner.**
`E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit
`22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks,
Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests,
@@ -118,6 +119,30 @@ inside budget. Windows arm64 completes exact-byte PTY/watcher smoke, then retain
hosted build-26200 rejection against required 26100; this is the aggregate workflow's sole failure.
Real SSH/distro behavior, remaining host evidence, composition, and every production/default path
remain open; legacy remains the sole production path.
+`E-M5-LINUX-KERNEL-DETECTION-AUDIT-001` fixes the next narrow boundary: one disconnected,
+15-second cancellable POSIX-shell/no-Node marked `uname -r` probe plus a kernel-only numeric-prefix
+parser. Common supported Rocky/RHEL releases such as `4.18.0-553.5.1.el8_10.x86_64` currently fail
+the generic parser because its suffix grammar excludes `_`; the kernel parser will admit only the
+bounded distro suffix alphabet `[0-9A-Za-z._+~-]` without relaxing libc, macOS, or Windows version
+parsing. Missing, noisy, incomplete, duplicate, malformed, oversized, or invalid evidence returns
+unknown; ordinary probe failure remains availability evidence and cancellation propagates. The
+reviewed 4.18 floor is unchanged. Host-evidence composition, product callers, live SSH/distro
+proof, transfer/install, settings, fallback, tuple enablement, publication, and defaults stay
+absent; legacy remains the sole production path.
+`E-M5-LINUX-KERNEL-DETECTION-LOCAL-RED-001` records the expected two-file failure before
+implementation: the purpose suite collects zero tests because its detection module is absent, and
+the existing selector passes 24 cases but fails three Rocky/RHEL assertions. Supported and
+below-floor `el8_10` releases both return `unknown-kernel`, proving the underscore suffix gap. Exact
+Node 24 and all-six native execution remain required after local green.
+`E-M5-LINUX-KERNEL-DETECTION-LOCAL-001` is green: 19/19 purpose tests, 60/60 focused+workflow tests,
+394/394 non-full-size relay tests with three declared skips, 280/280 release-script tests, typecheck,
+lint, format, reliability, max-lines, localization, diff, and protected-resolver gates pass. One
+15-second cancellable marked POSIX command returns only a unique strict `uname -r`; supported
+Rocky/RHEL, Ubuntu, and Alpine suffixes select against the unchanged 4.18 floor, while malformed,
+oversized, noisy, unavailable, or invalid evidence returns unknown and cancellation propagates.
+Libc, macOS, and PowerShell grammars remain strict. This is contract evidence, not live SSH/distro
+proof; full host composition and every production/default path remain absent. Exact-head all-six
+native proof is next.
`E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`:
all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden
E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The
diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
index e07e32c9af9..51d5d2e36b2 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
Date created: 2026-07-14
Last updated: 2026-07-15
-Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining disconnected host-evidence boundary after exact-head acceptance of Linux libc detection**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
-Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-LIBC-DETECTION-CI-001` closes implementation commit `d8b17a354`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 executes the full runtime smoke and retains only the declared hosted build-26200 rejection against required 26100. Every native client runs the new 15-case marked/no-Node libc contract. This remains contract evidence, not live SSH/distro evidence; no host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
+Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: require exact-head all-six native CI for the disconnected marked Linux kernel probe and kernel-only distro-suffix parser accepted locally by E-M5-LINUX-KERNEL-DETECTION-LOCAL-001**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
+Session checkpoint: **LOCAL CLOSED / CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-KERNEL-DETECTION-LOCAL-001` passes the 19-case purpose contract, selector and native-workflow gates, 394 non-full-size relay cases, 280 release-script cases, typecheck, lint, format, reliability, localization, max-lines, diff, and protected-resolver gates. The reviewed Linux 4.18 floor is unchanged. No host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693)
@@ -15077,6 +15077,102 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 2
settings, fallback, tuple enablement, publication, or defaults. Legacy remains the sole production
path; no tuple is enabled.
+### E-M5-LINUX-KERNEL-DETECTION-AUDIT-001 — marked kernel evidence and distro suffix boundary
+
+- Date/owner: 2026-07-15, Codex implementation owner.
+- Audit: complete rereads of current OS/architecture detection, the accepted marked libc probe, the
+ signed-manifest selector/version parser, and their purpose tests. Common supported Rocky/RHEL 8
+ kernels report releases such as `4.18.0-553.5.1.el8_10.x86_64`; the selector's generic numeric
+ parser rejects `_` in that suffix and would return `unknown-kernel` despite a valid supported
+ numeric prefix. This is an implementation-detail gap, not a change to the reviewed 4.18 floor or
+ the primary plan.
+- Finding: add one purpose-named `ssh-relay-linux-kernel-detection` module that consumes an explicit
+ SSH connection and optional cancellation signal, executes one 15-second POSIX-shell/no-Node
+ marked `uname -r`, and returns an exact bounded release string or undefined. Add a kernel-specific
+ parser to the selector that compares only the authenticated probe's strict numeric prefix while
+ admitting the bounded real-world suffix alphabet `[0-9A-Za-z._+~-]`; keep the generic libc/macOS/
+ PowerShell version grammar unchanged.
+- Safety contract: only one complete marker-qualified line counts; arbitrary or marker-concatenated
+ startup noise, duplicate/unterminated markers, whitespace/control characters, oversized releases,
+ missing commands, and unparseable numeric prefixes yield unknown. Ordinary execution failure is
+ compatibility evidence; `AbortError` propagates. The parser must accept exact-minimum Rocky/RHEL,
+ Ubuntu, and Alpine suffixes but reject whitespace, slash, colon, and arbitrary punctuation.
+- Required RED: missing purpose module plus a selector assertion showing a supported Rocky-style
+ `4.18.0-...el8_10...` release currently returns `unknown-kernel`; then marked valid/noisy/
+ malformed/bounded/unavailable/cancelled probe cases, kernel-suffix accept/reject boundaries,
+ existing selector regressions, and native-workflow inclusion.
+- Deliberately absent: libc composition, libstdc++/GLIBCXX, macOS/Windows/translation evidence,
+ Electron/startup/product callers, live SSH or distro cells, cache acquisition, transfer/install,
+ settings, fallback, tuple enablement, release publication, and defaults. Legacy remains the sole
+ production path and the primary HTML plan is unchanged.
+
+### E-M5-LINUX-KERNEL-DETECTION-LOCAL-RED-001 — kernel probe and Rocky suffix boundaries fail first
+
+- Date/owner: 2026-07-15, Codex implementation owner.
+- Source: uncommitted RED tests atop exact accepted evidence head `54d523a81`.
+- Command: `pnpm vitest run src/main/ssh/ssh-relay-linux-kernel-detection.test.ts src/main/ssh/ssh-relay-artifact-selector.test.ts`.
+- Expected result: FAIL, 2 failed files; the purpose suite has zero collected tests because
+ `ssh-relay-linux-kernel-detection` does not exist, and the selector has 24 passed / 3 failed.
+ Supported `4.18.0-553.5.1.el8_10.x86_64` returns legacy/`unknown-kernel`; the equivalent
+ below-floor `4.17.99-553.5.1.el8_10.x86_64` also returns `unknown-kernel` instead of
+ `kernel-too-old`.
+- Environment: local macOS arm64 worktree; pnpm 10.24.0 under Node 26.0.0 emits the repository's
+ expected Node-24 engine warning. Exact Node 24 and all-six native execution remain required after
+ local green.
+- Boundary: this is failure-before-implementation evidence only. It authorizes only the audited
+ disconnected probe, selector parser, purpose tests, and native workflow inclusion; no production
+ caller or behavior is connected.
+
+### E-M5-LINUX-KERNEL-DETECTION-LOCAL-001 — disconnected marked kernel probe and strict selector
+
+- Date/owner: 2026-07-15, Codex implementation owner.
+- Implementation: `ssh-relay-linux-kernel-detection.ts` performs one 15-second cancellable POSIX
+ command, brackets the optional `uname -r` result with exact Orca markers, and returns only one
+ complete, unique, strict release or undefined. Startup noise outside the markers is ignored;
+ empty/multi-line/duplicate/unterminated/marker-concatenated evidence, releases above 256
+ characters, invalid numeric prefixes, whitespace/control characters, slash, colon, and arbitrary
+ punctuation are rejected. Ordinary execution failure returns undefined and `AbortError`
+ propagates. The selector's kernel-only parser admits the audited distro suffix alphabet after a
+ numeric kernel prefix and compares the prefix to the unchanged reviewed 4.18 floor. The generic
+ libc/macOS/PowerShell grammar remains strict, with direct underscore-regression assertions.
+- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts
+--maxWorkers=1 src/main/ssh/ssh-relay-linux-kernel-detection.test.ts` — PASS, 1 file / 19 tests in
+ 209 ms Vitest, 1.08 s wall, 132,005,888-byte maximum RSS. Rocky/RHEL, Ubuntu, and Alpine releases;
+ outside noise; empty/multiple/duplicate/unterminated/concatenated/oversized/malformed evidence;
+ invalid suffixes; missing command; cancellation; one signal-qualified 15-second exec; marked
+ `uname -r`; and no Node/npm/Python/Perl/archive/checksum dependency all pass.
+- Focused plus native-workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config
+config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-linux-kernel-detection.test.ts
+src/main/ssh/ssh-relay-artifact-selector.test.ts
+src/main/ssh/ssh-remote-platform-detection.test.ts
+config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 60 tests in 3.32 s Vitest,
+ 5.26 s wall, 132,300,800-byte maximum RSS while run beside independent typecheck/format gates.
+ Both POSIX and PowerShell native commands now require the purpose suite exactly once. Supported
+ Rocky/RHEL, Ubuntu, and Alpine suffixes select; a Rocky release below 4.18 reports
+ `kernel-too-old`; invalid suffixes report `unknown-kernel`; libc, macOS, and PowerShell versions
+ do not inherit the kernel grammar.
+- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config
+config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS,
+ 34 files / 394 tests; 3 declared full-size files/tests skipped; 15.00 s Vitest, 16.38 s wall,
+ 246,890,496-byte maximum RSS. One preceding diagnostic invocation quoted the shell glob, selected
+ zero files, and exited 1; the exact checklist command above was then run unquoted and passed.
+- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config
+config/vitest.config.ts --maxWorkers=1 --silent=true
+config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 18.76 s Vitest,
+ 20.26 s wall, 190,480,384-byte maximum RSS.
+- Repository gates: `pnpm run typecheck`, targeted `oxlint`, targeted `oxfmt --check`, `pnpm lint`,
+ and `pnpm run check:max-lines-ratchet` pass. Full lint includes switch exhaustiveness, 41
+ reliability gates, no new max-lines bypass, bundled skill-guide verification, 9,837 localization
+ references and locale parity, and zero localization coverage candidates; existing unrelated lint
+ warnings remain warnings. `git diff --check` passes and the protected Node/npm resolver pair has
+ zero diff. Local Node 26.0.0 emits only the expected Node-24 engine warning; exact Node 24 proof
+ remains the native CI gate.
+- Boundary/residual gaps: the new detector is imported only by its purpose tests. This is contract
+ evidence, not live SSH/GNU/BusyBox/Rocky/Ubuntu/Alpine execution. It does not compose libc,
+ libstdc++/GLIBCXX, macOS/Windows/translation evidence and adds no Electron/startup/product caller,
+ transfer/install, mode, setting, fallback, tuple, publication, or default behavior. Legacy remains
+ the sole production path. Require exact-head all-six native proof before advancing.
+
## Accepted Gaps
No product gap is accepted merely because it appears in this list. Each entry requires explicit
@@ -15135,10 +15231,12 @@ The project is not complete until every applicable item below is checked with ev
## Next Required Action
-Audit the next smallest disconnected host-evidence slice after exact-head acceptance of Linux libc
-detection. Do not compose full host evidence, provision production secrets/environments, add an
-Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode
-wiring, fallback, tuple enablement, release publication, or default behavior.
+Commit and push the locally accepted disconnected Linux kernel package, then require exact-head
+Node 24 proof on all six native clients plus adjacent PR, Golden E2E, and computer-use checks before
+auditing another host-evidence slice. Do not compose full host evidence, provision production
+secrets/environments, add an Electron/startup consumer, embed unreviewed keys or manifest bytes, add
+SSH transfer/install, mode wiring, fallback, tuple enablement, release publication, or default
+behavior.
Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets
behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18,
release-cut, desktop builds, publication, and every tuple separately gated.
diff --git a/src/main/ssh/ssh-relay-artifact-selector.test.ts b/src/main/ssh/ssh-relay-artifact-selector.test.ts
index f702a612a60..37d127ba1b5 100644
--- a/src/main/ssh/ssh-relay-artifact-selector.test.ts
+++ b/src/main/ssh/ssh-relay-artifact-selector.test.ts
@@ -102,6 +102,57 @@ describe('SSH relay artifact selector', () => {
).toBe('selected')
})
+ it.each(['4.18.0-553.5.1.el8_10.x86_64', '5.15.0-107-generic', '6.6.15-0-lts'])(
+ 'accepts a supported Linux distro kernel release %s',
+ (kernelVersion) => {
+ expect(
+ selectSshRelayArtifact(verifiedManifest(), {
+ ...compatibleLinuxHost,
+ kernelVersion
+ }).kind
+ ).toBe('selected')
+ }
+ )
+
+ it.each([
+ ['4.18.0-553.5.1.el8_10.x86_64', 'selected'],
+ ['4.17.99-553.5.1.el8_10.x86_64', 'kernel-too-old'],
+ ['4.18.0 bad', 'unknown-kernel'],
+ ['4.18.0/bad', 'unknown-kernel'],
+ ['4.18.0:bad', 'unknown-kernel'],
+ ['4.18.0@bad', 'unknown-kernel']
+ ] as const)('classifies the kernel suffix boundary for %s', (kernelVersion, expected) => {
+ const result = selectSshRelayArtifact(verifiedManifest(), {
+ ...compatibleLinuxHost,
+ kernelVersion
+ })
+
+ expect(result.kind === 'selected' ? result.kind : result.reason).toBe(expected)
+ })
+
+ it('keeps non-kernel version grammars strict', () => {
+ expect(
+ selectSshRelayArtifact(verifiedManifest(), {
+ ...compatibleLinuxHost,
+ libc: { family: 'glibc', version: '2.28_bad' }
+ })
+ ).toEqual({ kind: 'legacy', reason: 'unknown-libc' })
+ expect(
+ selectSshRelayArtifact(verifiedManifest(createSshRelayDarwinArtifactTestManifest()), {
+ os: 'darwin',
+ architecture: 'arm64',
+ processTranslated: false,
+ version: '13.5_bad'
+ })
+ ).toEqual({ kind: 'legacy', reason: 'unknown-os-version' })
+ expect(
+ selectSshRelayArtifact(verifiedManifest(windowsManifest()), {
+ ...compatibleWindowsHost,
+ powerShellVersion: '5.1_bad'
+ })
+ ).toEqual({ kind: 'legacy', reason: 'unknown-powershell' })
+ })
+
it('selects the detected libc family when both Linux variants exist', () => {
const manifest = createSshRelayArtifactTestManifest()
const musl = structuredClone(manifest.tuples[0])
diff --git a/src/main/ssh/ssh-relay-artifact-selector.ts b/src/main/ssh/ssh-relay-artifact-selector.ts
index c8088f21891..5c0b4d3395d 100644
--- a/src/main/ssh/ssh-relay-artifact-selector.ts
+++ b/src/main/ssh/ssh-relay-artifact-selector.ts
@@ -3,6 +3,8 @@ import { sshRelayRuntimeDownloadUrl } from './ssh-relay-release-asset'
type VerifiedSshRelayRuntimeTuple = VerifiedSshRelayArtifactManifest['tuples'][number]
+const LINUX_KERNEL_RELEASE_MAX_CHARS = 256
+
type SshRelayHostBase = {
architecture: 'x64' | 'arm64'
processTranslated: boolean
@@ -100,6 +102,24 @@ function parseNumericVersion(value: string | undefined): number[] | null {
return components.every(Number.isSafeInteger) ? components : null
}
+function parseLinuxKernelVersion(value: string | undefined): number[] | null {
+ if (!value || value.length > LINUX_KERNEL_RELEASE_MAX_CHARS) {
+ return null
+ }
+ // Why: real distro kernels include `_`, `+`, and `~`; other version fields keep the stricter
+ // generic grammar so this compatibility exception cannot weaken unrelated host evidence.
+ const match = /^(\d+(?:\.\d+){1,3})(?:-[0-9A-Za-z._+~-]+)?$/.exec(value)
+ if (!match) {
+ return null
+ }
+ const components = match[1].split('.').map(Number)
+ return components.every(Number.isSafeInteger) ? components : null
+}
+
+export function isSshRelayLinuxKernelRelease(value: string | undefined): value is string {
+ return parseLinuxKernelVersion(value) !== null
+}
+
function parseOpenSshVersion(value: string | undefined): number[] | null {
if (!value) {
return null
@@ -144,7 +164,11 @@ function selectLinux(
if (tuple.compatibility.kind !== 'linux') {
return { kind: 'legacy', reason: 'tuple-inconsistent' }
}
- const kernel = meetsVersion(host.kernelVersion, tuple.compatibility.minimumKernelVersion)
+ const kernel = meetsVersion(
+ host.kernelVersion,
+ tuple.compatibility.minimumKernelVersion,
+ parseLinuxKernelVersion
+ )
if (kernel === null) {
return { kind: 'legacy', reason: 'unknown-kernel' }
}
diff --git a/src/main/ssh/ssh-relay-linux-kernel-detection.test.ts b/src/main/ssh/ssh-relay-linux-kernel-detection.test.ts
new file mode 100644
index 00000000000..f46cf80596c
--- /dev/null
+++ b/src/main/ssh/ssh-relay-linux-kernel-detection.test.ts
@@ -0,0 +1,87 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import type { SshConnection } from './ssh-connection'
+
+const execCommandMock = vi.hoisted(() => vi.fn())
+
+vi.mock('./ssh-relay-deploy-helpers', () => ({
+ execCommand: execCommandMock
+}))
+
+const { detectSshRelayLinuxKernelRelease } = await import('./ssh-relay-linux-kernel-detection')
+
+const conn = {} as SshConnection
+const marker = '__ORCA_SSH_RELAY_KERNEL__'
+
+function segment(...lines: string[]): string {
+ return [`${marker} BEGIN`, ...lines, `${marker} END`].join('\n')
+}
+
+describe('detectSshRelayLinuxKernelRelease', () => {
+ beforeEach(() => {
+ execCommandMock.mockReset()
+ })
+
+ it.each(['4.18.0-553.5.1.el8_10.x86_64', '5.15.0-107-generic', '6.6.15-0-lts'])(
+ 'returns the exact marked distro kernel release %s',
+ async (release) => {
+ execCommandMock.mockResolvedValueOnce(
+ ['Last login: ignored', segment(release), 'post-command noise'].join('\n')
+ )
+
+ await expect(detectSshRelayLinuxKernelRelease(conn)).resolves.toBe(release)
+ }
+ )
+
+ it.each([
+ ['', 'empty segment'],
+ ['6.8.0\nsecond-line', 'multiple marked lines'],
+ [`${marker} BEGIN\n6.8.0`, 'unterminated segment'],
+ [segment('x'.repeat(257)), 'oversized release'],
+ [segment('release-6.8.0'), 'malformed numeric prefix'],
+ [segment('6.8.0 generic'), 'suffix whitespace'],
+ [segment('6.8.0/generic'), 'suffix slash'],
+ [segment('6.8.0:generic'), 'suffix colon'],
+ [segment('6.8.0@generic'), 'unsupported suffix punctuation'],
+ [segment('6.8.0\tgeneric'), 'control whitespace'],
+ ['6.8.0', 'unmarked evidence'],
+ [[segment('6.8.0'), segment('6.8.0')].join('\n'), 'duplicate complete segments'],
+ [`startup${marker} BEGIN\n6.8.0\n${marker} END`, 'marker concatenated to startup noise']
+ ])('returns unknown for %s', async (output) => {
+ execCommandMock.mockResolvedValueOnce(output)
+
+ await expect(detectSshRelayLinuxKernelRelease(conn)).resolves.toBeUndefined()
+ })
+
+ it('classifies an unavailable probe as unknown without inventing fallback policy', async () => {
+ execCommandMock.mockRejectedValueOnce(new Error('remote command unavailable'))
+
+ await expect(detectSshRelayLinuxKernelRelease(conn)).resolves.toBeUndefined()
+ })
+
+ it('propagates cancellation and passes the signal into the single bounded probe', async () => {
+ const signal = new AbortController().signal
+ const abortError = Object.assign(new Error('cancelled'), { name: 'AbortError' })
+ execCommandMock.mockRejectedValueOnce(abortError)
+
+ await expect(detectSshRelayLinuxKernelRelease(conn, { signal })).rejects.toBe(abortError)
+ expect(execCommandMock).toHaveBeenCalledTimes(1)
+ expect(execCommandMock).toHaveBeenCalledWith(conn, expect.any(String), {
+ signal,
+ timeoutMs: 15_000
+ })
+ })
+
+ it('constructs one marked POSIX-shell uname probe without runtime dependencies', async () => {
+ execCommandMock.mockResolvedValueOnce(segment('6.8.0'))
+
+ await detectSshRelayLinuxKernelRelease(conn)
+
+ const command = execCommandMock.mock.calls[0]?.[1] ?? ''
+ expect(command).toContain('command -v uname')
+ expect(command).toContain('uname -r')
+ expect(command.match(/uname -r/gu)).toHaveLength(1)
+ expect(command).toContain(`${marker} BEGIN`)
+ expect(command).toContain(`${marker} END`)
+ expect(command).not.toMatch(/\b(?:node|npm|python|perl|tar|sha256sum|shasum)\b/u)
+ })
+})
diff --git a/src/main/ssh/ssh-relay-linux-kernel-detection.ts b/src/main/ssh/ssh-relay-linux-kernel-detection.ts
new file mode 100644
index 00000000000..a3a8e153262
--- /dev/null
+++ b/src/main/ssh/ssh-relay-linux-kernel-detection.ts
@@ -0,0 +1,86 @@
+import {
+ getProcessOutputFields,
+ iterateProcessOutputLines
+} from '../../shared/process-output-field-scanner'
+import type { SshConnection } from './ssh-connection'
+import { isSshRelayLinuxKernelRelease } from './ssh-relay-artifact-selector'
+import { execCommand } from './ssh-relay-deploy-helpers'
+
+const PROBE_MARKER = '__ORCA_SSH_RELAY_KERNEL__'
+const PROBE_TIMEOUT_MS = 15_000
+
+const PROBE_COMMAND = [
+ `printf '\n%s\n' '${PROBE_MARKER} BEGIN'`,
+ 'if command -v uname >/dev/null 2>&1; then uname -r 2>&1 || :; fi',
+ `printf '%s\n' '${PROBE_MARKER} END'`
+].join('\n')
+
+function parseMarker(line: string): 'BEGIN' | 'END' | null {
+ const fields = getProcessOutputFields(line, 3)
+ if (fields.length !== 2 || fields[0] !== PROBE_MARKER) {
+ return null
+ }
+ return fields[1] === 'BEGIN' || fields[1] === 'END' ? fields[1] : null
+}
+
+function parseKernelRelease(output: string): string | undefined {
+ let activeLines: string[] | null = null
+ let release: string | undefined
+ let invalid = false
+
+ for (const line of iterateProcessOutputLines(output)) {
+ const marker = parseMarker(line)
+ if (marker === 'BEGIN') {
+ if (activeLines || release !== undefined) {
+ invalid = true
+ }
+ activeLines = []
+ continue
+ }
+ if (marker === 'END') {
+ if (!activeLines || activeLines.length !== 1 || release !== undefined) {
+ invalid = true
+ activeLines = null
+ continue
+ }
+ release = activeLines[0]
+ activeLines = null
+ continue
+ }
+ if (activeLines) {
+ if (activeLines.length === 1) {
+ invalid = true
+ } else {
+ activeLines.push(line)
+ }
+ }
+ }
+
+ if (invalid || activeLines || !isSshRelayLinuxKernelRelease(release)) {
+ return undefined
+ }
+ return release
+}
+
+function isAbortError(error: unknown): boolean {
+ return error instanceof Error && error.name === 'AbortError'
+}
+
+export async function detectSshRelayLinuxKernelRelease(
+ conn: SshConnection,
+ { signal }: { signal?: AbortSignal } = {}
+): Promise {
+ try {
+ const output = await execCommand(conn, PROBE_COMMAND, {
+ signal,
+ timeoutMs: PROBE_TIMEOUT_MS
+ })
+ return parseKernelRelease(output)
+ } catch (error) {
+ // Why: cancellation must settle the caller's work instead of becoming compatibility evidence.
+ if (isAbortError(error)) {
+ throw error
+ }
+ return undefined
+ }
+}