diff --git a/docs/REMOTE_COMMANDER_R3_RUNTIME_QUALIFICATION.md b/docs/REMOTE_COMMANDER_R3_RUNTIME_QUALIFICATION.md new file mode 100644 index 0000000..b9e0706 --- /dev/null +++ b/docs/REMOTE_COMMANDER_R3_RUNTIME_QUALIFICATION.md @@ -0,0 +1,93 @@ +# ContinuityOS Remote Commander R3 — Windows Runtime Qualification + +## Baseline + +R3 originally started from merged `master` `5a72da36f104421010718da96681c3304fc48fe1`. + +After R24 / PR #199 merged, R3 was synchronized by a normal merge commit from new `master`: + +`5e6887a3109590e190d5427a3205c3f99e9b5a3f` + +No rebase or force-push was used. + +Qualification was performed from a separate clean Windows checkout so the parallel R24 / PR #199 worktree and branch were not modified. + +## Official tunnel-client artifact + +Validated release: + +- repository: `openai/tunnel-client` +- release: `v0.0.14` +- asset: `tunnel-client-v0.0.14-windows-amd64.zip` +- expected SHA-256: `784ab8da7b5a88f0109f1fd8aaf0a1c86067430b896dddf307ef7e3cc49fa1a5` +- observed SHA-256: exact match +- binary version: `0.0.14+0f870e50a973fa820d4c409000059e181e8d242b` + +The official binary advertises `sample_mcp_stdio_local` and the MCP command profile contract used by ContinuityOS R2. + +## Credential-free Windows result + +The merged launcher `scripts/windows/ContinuityOS-RemoteTunnel.ps1` produced: + +- schema: `continuityos.remote_tunnel_plan/v1` +- MCP transport: `stdio` +- MCP tool profile: `chatgpt-pro-readonly` +- public MCP listener: `false` +- health/UI listener: `127.0.0.1:8080` +- health/UI scope: `loopback` +- direct shell: `false` +- control-plane key present: `false` + +The real Windows stdio MCP process was then exercised over JSON-RPC. It advertised exactly: + +- `capability_status` +- `system_info` +- `fs_list` +- `fs_read` + +A bounded `fs_read` of a benign repository file succeeded. A direct call to hidden write tool `remember` was rejected by the server-side profile boundary. + +Observed local state after the R24 synchronization: + +`LOCAL_READONLY_RUNTIME_GREEN` + +The post-sync Windows regression set covering R3 Remote Commander plus the merged R24 witnessed-replay tests passed: `44 passed`. + +## Fail-closed credential gate + +With `CONTROL_PLANE_API_KEY` and `CONTROL_PLANE_TUNNEL_ID` absent: + +- launcher `Doctor` exited non-zero and identified the missing runtime API key; +- launcher `Init` exited non-zero before creating a tunnel profile; +- no secret-like value was printed. + +This is intentional. R3 does not manufacture, recover, persist, or log OpenAI runtime credentials. + +## Reproducible harness + +Run: + +```powershell +.\scripts\windows\Test-ContinuityOS-RemoteRuntime.ps1 \ + -ExpectedHead \ + -TunnelClient C:\path\to\tunnel-client.exe +``` + +The harness fails closed unless the Git worktree is clean, the expected head matches when provided, the launcher plan preserves the read-only boundary, the official tunnel-client stdio sample is available, the MCP advertises exactly four read-only tools, a benign read succeeds, and a hidden write call is rejected. + +## Remaining live qualification + +The following is **not yet claimed**: + +`Init -> Doctor -> Run -> ChatGPT connector discovery -> tools/list -> benign read through the live OpenAI tunnel` + +That step requires operator-provided: + +- `CONTROL_PLANE_API_KEY` with Tunnels Read + Use; +- `CONTROL_PLANE_TUNNEL_ID` for the intended OpenAI workspace. + +Until those values exist at runtime, `live_tunnel_qualified=false`. + +## Parallel branch isolation + +R24 / PR #199 merged independently into master before R3. Its file set had zero overlap with the R3 Remote Commander files. R3 then synchronized from that merged master with a normal merge commit; R3 did not modify the historical R24 branch, rebase it, or force-push it. diff --git a/scripts/windows/Test-ContinuityOS-RemoteRuntime.ps1 b/scripts/windows/Test-ContinuityOS-RemoteRuntime.ps1 new file mode 100644 index 0000000..b89db3c --- /dev/null +++ b/scripts/windows/Test-ContinuityOS-RemoteRuntime.ps1 @@ -0,0 +1,153 @@ +[CmdletBinding()] +param( + [string]$RepoRoot = "", + [string]$RemoteRoot = "", + [string]$ProbeRelativePath = "README.md", + [string]$TunnelClient = "tunnel-client", + [string]$Python = "python", + [string]$ExpectedHead = "" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($RepoRoot)) { + $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +} + +function Require-Command { + param([Parameter(Mandatory = $true)][string]$Name) + $resolved = Get-Command $Name -ErrorAction Stop + if (-not $resolved.Source) { + throw "Cannot resolve executable path for $Name" + } + return $resolved.Source +} + +$repo = (Resolve-Path -LiteralPath $RepoRoot).Path +if ([string]::IsNullOrWhiteSpace($RemoteRoot)) { + $RemoteRoot = $repo +} +$root = (Resolve-Path -LiteralPath $RemoteRoot).Path +$probe = Join-Path $root $ProbeRelativePath +if (-not (Test-Path -LiteralPath $probe -PathType Leaf)) { + throw "ProbeRelativePath must identify an existing file inside RemoteRoot." +} + +$pythonExe = Require-Command -Name $Python +$tunnelExe = Require-Command -Name $TunnelClient +$launcher = Join-Path $repo "scripts\windows\ContinuityOS-RemoteTunnel.ps1" +if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) { + throw "ContinuityOS Remote Tunnel launcher is missing." +} + +Push-Location $repo +try { + $head = (git rev-parse HEAD).Trim() + $dirty = @(git status --porcelain) + if ($LASTEXITCODE -ne 0) { + throw "Unable to read Git baseline." + } + if ($dirty.Count -ne 0) { + throw "Runtime qualification requires a clean Git worktree." + } + if (-not [string]::IsNullOrWhiteSpace($ExpectedHead) -and $head -ne $ExpectedHead) { + throw "Git HEAD does not match ExpectedHead." + } + + $plan = ( + & $launcher -Mode Plan -RemoteRoot $root -TunnelClient $tunnelExe -Python $pythonExe | + Out-String + ) | ConvertFrom-Json + + if ($plan.schema -ne "continuityos.remote_tunnel_plan/v1") { + throw "Unexpected launcher plan schema." + } + if ($plan.mcp_transport -ne "stdio" -or + $plan.mcp_tool_profile -ne "chatgpt-pro-readonly" -or + $plan.public_mcp_listener -ne $false -or + $plan.health_listener_scope -ne "loopback" -or + $plan.direct_shell -ne $false) { + throw "Launcher plan violates the R3 safety contract." + } + + $tunnelVersion = (& $tunnelExe --version 2>&1 | Select-Object -First 1).ToString().Trim() + $quickstart = (& $tunnelExe help quickstart 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0 -or $quickstart -notmatch "sample_mcp_stdio_local") { + throw "tunnel-client quickstart does not advertise the local stdio sample." + } + $sample = (& $tunnelExe profiles samples show sample_mcp_stdio_local 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0 -or $sample -notmatch "mcp.command|mcp-command") { + throw "tunnel-client local stdio sample contract is unavailable." + } + + $requests = @( + @{jsonrpc="2.0"; id=1; method="initialize"; params=@{}}, + @{jsonrpc="2.0"; id=2; method="tools/list"; params=@{}}, + @{jsonrpc="2.0"; id=3; method="tools/call"; params=@{name="capability_status"; arguments=@{}}}, + @{jsonrpc="2.0"; id=4; method="tools/call"; params=@{name="fs_read"; arguments=@{path=$ProbeRelativePath; max_bytes=2048}}}, + @{jsonrpc="2.0"; id=5; method="tools/call"; params=@{name="remember"; arguments=@{text="must not write"}}} + ) + $payload = (($requests | ForEach-Object { $_ | ConvertTo-Json -Compress -Depth 8 }) -join [Environment]::NewLine) + [Environment]::NewLine + $mcpArgs = @( + "-m", "continuityos.remote_mcp_server", + "--db", ":memory:", + "--enable-remote", + "--tool-profile", "chatgpt-pro-readonly", + "--remote-root", $root + ) + $raw = $payload | & $pythonExe @mcpArgs + if ($LASTEXITCODE -ne 0) { + throw "Remote MCP stdio qualification failed." + } + $responses = @($raw | ForEach-Object { $_ | ConvertFrom-Json }) + $toolsResponse = $responses | Where-Object { $_.id -eq 2 } | Select-Object -First 1 + $statusResponse = $responses | Where-Object { $_.id -eq 3 } | Select-Object -First 1 + $readResponse = $responses | Where-Object { $_.id -eq 4 } | Select-Object -First 1 + $denyResponse = $responses | Where-Object { $_.id -eq 5 } | Select-Object -First 1 + + $tools = @($toolsResponse.result.tools | ForEach-Object { $_.name }) + $expectedTools = @("capability_status", "system_info", "fs_list", "fs_read") + if (($tools -join ",") -ne ($expectedTools -join ",")) { + throw "Unexpected ChatGPT Pro tool surface." + } + $status = $statusResponse.result.content[0].text | ConvertFrom-Json + if ($status.mode -ne "read_only_host_surface" -or $status.mutating_execution.available -ne $false) { + throw "Capability status does not report a read-only host surface." + } + $readHasError = $readResponse.result.PSObject.Properties.Name -contains "isError" + if ($readHasError -and $readResponse.result.isError -eq $true) { + throw "Benign fs_read probe failed." + } + $hiddenWriteDenied = ( + $denyResponse.result.isError -eq $true -and + $denyResponse.result.content[0].text -match "tool hidden by remote tool profile" + ) + if (-not $hiddenWriteDenied) { + throw "Hidden write tool was not rejected." + } + + [pscustomobject]@{ + schema = "continuityos.remote_runtime_qualification/v1" + status = "LOCAL_READONLY_RUNTIME_GREEN" + git_head = $head + git_clean = $true + tunnel_client_version = $tunnelVersion + tunnel_client_stdio_sample = $true + mcp_transport = $plan.mcp_transport + mcp_tool_profile = $plan.mcp_tool_profile + advertised_tools = $tools + benign_read = "pass" + hidden_write_denied = $true + public_mcp_listener = $plan.public_mcp_listener + health_listener = $plan.health_listener + health_listener_scope = $plan.health_listener_scope + direct_shell = $plan.direct_shell + control_plane_key_present = -not [string]::IsNullOrWhiteSpace($env:CONTROL_PLANE_API_KEY) + tunnel_id_present = -not [string]::IsNullOrWhiteSpace($env:CONTROL_PLANE_TUNNEL_ID) + live_tunnel_qualified = $false + } | ConvertTo-Json -Depth 6 +} +finally { + Pop-Location +} diff --git a/tests/test_remote_runtime_windows_script.py b/tests/test_remote_runtime_windows_script.py new file mode 100644 index 0000000..28b0cba --- /dev/null +++ b/tests/test_remote_runtime_windows_script.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + + +SCRIPT = ( + Path(__file__).resolve().parents[1] + / "scripts" + / "windows" + / "Test-ContinuityOS-RemoteRuntime.ps1" +) + + +def _source() -> str: + if not SCRIPT.is_file(): + pytest.skip("source-tree-only Windows runtime harness is not shipped in the wheel") + return SCRIPT.read_text(encoding="utf-8") + + +def test_runtime_harness_requires_clean_git_baseline(): + source = _source() + assert "git rev-parse HEAD" in source + assert "git status --porcelain" in source + assert "ExpectedHead" in source + assert "Runtime qualification requires a clean Git worktree." in source + + +def test_runtime_harness_proves_readonly_tool_boundary(): + source = _source() + assert "--tool-profile" in source + assert "chatgpt-pro-readonly" in source + assert '@("capability_status", "system_info", "fs_list", "fs_read")' in source + assert 'name="remember"' in source + assert "tool hidden by remote tool profile" in source + assert "LOCAL_READONLY_RUNTIME_GREEN" in source + + +def test_runtime_harness_does_not_claim_live_tunnel(): + source = _source() + assert "live_tunnel_qualified = $false" in source + assert "control_plane_key_present" in source + assert "tunnel_id_present" in source + assert "CONTROL_PLANE_API_KEY" in source + assert "CONTROL_PLANE_TUNNEL_ID" in source + + +def test_runtime_harness_keeps_public_surface_closed(): + source = _source() + assert "public_mcp_listener" in source + assert "health_listener_scope" in source + assert '"loopback"' in source + assert "direct_shell" in source