Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c5e57b5
R23 R2: add fail-closed Pro read-only tool profile
bitmaster162 Sep 18, 2026
08e71e5
R23 R2: test Pro read-only MCP profile
bitmaster162 Sep 18, 2026
e4913e3
R23 R2: add Windows Secure MCP Tunnel launcher
bitmaster162 Sep 18, 2026
eefc08b
R23 R2: test Windows tunnel launcher boundary
bitmaster162 Sep 18, 2026
e803f57
R23 R2: document Secure MCP Tunnel boundary
bitmaster162 Sep 18, 2026
d2d7170
R23 R2: annotate remote tools as read-only
bitmaster162 Sep 18, 2026
f614808
R23 R2: test read-only MCP annotations
bitmaster162 Sep 18, 2026
d659872
R23 R2: align capability status with Pro profile
bitmaster162 Sep 18, 2026
54aa989
R23 R2: test truthful Pro capability status
bitmaster162 Sep 18, 2026
77e470b
R23 R2: bound tunnel profile identifier
bitmaster162 Sep 18, 2026
ddfd605
R23 R2: test tunnel profile validation
bitmaster162 Sep 18, 2026
a8e1e25
R23 R2: execute credential-free Windows tunnel plan in CI
bitmaster162 Sep 18, 2026
2c85304
R23 R2: prove Pro read-only boundary over stdio JSON-RPC
bitmaster162 Sep 18, 2026
d31af26
CI: preserve exact wheel with SHA receipt before review tests
bitmaster162 Sep 18, 2026
8df1121
R23 R2: repair PowerShell profile validation block
bitmaster162 Sep 18, 2026
6b637b7
R23 R2: drop unused wheel preservation experiment
bitmaster162 Sep 18, 2026
47229b5
R23 R2: canonicalize Windows tunnel launcher
bitmaster162 Sep 18, 2026
4039cfa
R23 R2: bind tunnel id to official format
bitmaster162 Sep 18, 2026
81c57be
R23 R2: test official tunnel id contract
bitmaster162 Sep 18, 2026
3d3e095
R23 R2: document tunnel id and runtime permission contract
bitmaster162 Sep 18, 2026
a91bc3f
R23 R2: scope launcher tests to source-tree qualification
bitmaster162 Sep 18, 2026
0a0ecce
Merge master into R23 Remote Commander Tunnel R2
bitmaster162 Sep 18, 2026
002aa9f
R23 R2: pin tunnel health listener to loopback
bitmaster162 Sep 18, 2026
7163c1c
R23 R2: test loopback-only tunnel health listener
bitmaster162 Sep 18, 2026
d7f8fb3
R23 R2: document loopback health listener
bitmaster162 Sep 18, 2026
c29b3c8
R23 R2: refresh merged baseline and qualification state
bitmaster162 Sep 18, 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
86 changes: 82 additions & 4 deletions continuityos/remote_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
"Report the ContinuityOS Remote Commander capability boundary: "
"enabled state, allowed roots, read limits, and governed execution path."
),
"annotations": {
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False,
},
"inputSchema": {
"type": "object",
"additionalProperties": False,
Expand All @@ -79,6 +85,12 @@
"Read-only host identity and runtime information. Does not return environment "
"variables, credentials, or process contents."
),
"annotations": {
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False,
},
"inputSchema": {
"type": "object",
"additionalProperties": False,
Expand All @@ -91,6 +103,12 @@
"List one directory inside an explicitly allowed remote root. Sensitive files "
"and credential directories are omitted. Read-only."
),
"annotations": {
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False,
},
"inputSchema": {
"type": "object",
"additionalProperties": False,
Expand All @@ -111,6 +129,12 @@
"Read a bounded UTF-8 text file inside an explicitly allowed remote root. "
"Known credential/key paths are denied. Read-only."
),
"annotations": {
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False,
},
"inputSchema": {
"type": "object",
"additionalProperties": False,
Expand All @@ -130,6 +154,21 @@

TOOLS = [*BASE_TOOLS, *REMOTE_TOOLS]

TOOL_PROFILE_FULL = "full"
TOOL_PROFILE_CHATGPT_PRO_READONLY = "chatgpt-pro-readonly"
_REMOTE_TOOL_NAMES = frozenset(tool["name"] for tool in REMOTE_TOOLS)
_TOOL_PROFILES = {
TOOL_PROFILE_FULL: None,
TOOL_PROFILE_CHATGPT_PRO_READONLY: _REMOTE_TOOL_NAMES,
}


def _tool_profile(value: str | None) -> str:
profile = (value or TOOL_PROFILE_FULL).strip().lower()
if profile not in _TOOL_PROFILES:
raise ValueError(f"unknown remote tool profile: {profile}")
return profile


def _env_enabled(value: str | None) -> bool:
return (value or "").strip().lower() in {"1", "true", "yes", "on"}
Expand Down Expand Up @@ -317,19 +356,46 @@ def __init__(
*,
remote_enabled: bool | None = None,
remote_roots=None,
tool_profile: str = TOOL_PROFILE_FULL,
):
super().__init__(db, policy_path, db_source)
self.remote = RemoteSurface(
enabled=remote_enabled,
roots=remote_roots,
)
self.tool_profile = _tool_profile(tool_profile)

@property
def tools(self) -> list[dict]:
allowed = _TOOL_PROFILES[self.tool_profile]
if allowed is None:
return list(TOOLS)
return [tool for tool in TOOLS if tool["name"] in allowed]

def _require_tool_visible(self, name: str) -> None:
allowed = _TOOL_PROFILES[self.tool_profile]
if allowed is not None and name not in allowed:
raise PermissionError(
f"tool hidden by remote tool profile {self.tool_profile}: {name}"
)

def call(self, name, args):
self._require_tool_visible(name)
if name == "capability_status":
self.turns += 1
return json.dumps(
self.remote.status(), ensure_ascii=False, indent=2
)
status = self.remote.status()
status["tool_profile"] = self.tool_profile
status["advertised_tools"] = [tool["name"] for tool in self.tools]
if self.tool_profile == TOOL_PROFILE_CHATGPT_PRO_READONLY:
status["mode"] = "read_only_host_surface"
status["mutating_execution"] = {
"available": False,
"direct_shell": False,
"reason": "hidden_by_tool_profile",
}
else:
status["mutating_execution"]["available"] = True
return json.dumps(status, ensure_ascii=False, indent=2)
if name == "system_info":
self.turns += 1
return json.dumps(
Expand Down Expand Up @@ -372,6 +438,17 @@ def main() -> None:
help="Path to one JSON policy, or YAML when PyYAML is installed",
)
parser.add_argument("--enable-remote", action="store_true", default=None)
parser.add_argument(
"--tool-profile",
choices=[TOOL_PROFILE_FULL, TOOL_PROFILE_CHATGPT_PRO_READONLY],
default=os.environ.get(
"CONTINUITYOS_REMOTE_TOOL_PROFILE", TOOL_PROFILE_FULL
),
help=(
"Advertised/callable tool surface. chatgpt-pro-readonly exposes "
"only bounded read-only Remote Commander tools."
),
)
parser.add_argument(
"--remote-root",
action="append",
Expand All @@ -384,6 +461,7 @@ def main() -> None:
args.policy,
remote_enabled=args.enable_remote,
remote_roots=args.remote_root,
tool_profile=args.tool_profile,
)

for line in sys.stdin:
Expand Down Expand Up @@ -418,7 +496,7 @@ def main() -> None:
{
"jsonrpc": "2.0",
"id": message_id,
"result": {"tools": TOOLS},
"result": {"tools": server.tools},
}
)
elif method == "tools/call":
Expand Down
99 changes: 99 additions & 0 deletions docs/REMOTE_COMMANDER_R2_TUNNEL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# ContinuityOS Remote Commander R2 — Secure MCP Tunnel

## Baseline

R1 is merged to `master` as
`c184ff7280a6e310cdaa2b6903b45209bfc4e8f0`.
R2 is synchronized on top of that merged baseline.

R2 does not require ChatGPT Pro to build or validate locally.

## Purpose

R2 connects the private stdio Remote Commander MCP to OpenAI Secure MCP Tunnel
without exposing an inbound port.

Data path:

`ChatGPT/OpenAI -> OpenAI tunnel control plane <- outbound HTTPS tunnel-client -> local stdio ContinuityOS Remote MCP`

The tunnel is transport only. ContinuityOS remains the authority boundary.

Official implementation reference:
`https://github.com/openai/tunnel-client`.

## Pro read-only profile

When launched with:

`--tool-profile chatgpt-pro-readonly`

the server advertises and accepts only:

- `capability_status`
- `system_info`
- `fs_list`
- `fs_read`

The restriction is enforced twice:

1. `tools/list` omits every base memory/write/execution tool.
2. `tools/call` rejects hidden tool names even if a client attempts a direct call.

This means `remember`, `upsert`, `forget`, `preflight_exec`, and
`execute_preflight` are unavailable on this profile.

## Windows launcher

Use:

`scripts/windows/ContinuityOS-RemoteTunnel.ps1`

Safe pre-Pro validation:

```powershell
.\scripts\windows\ContinuityOS-RemoteTunnel.ps1 -Mode Plan -RemoteRoot C:\path\to\allowed\root
```

After OpenAI tunnel credentials exist, set them only in the current process
environment. Do not commit them or place them in the MCP command:

```powershell
$env:CONTROL_PLANE_API_KEY = "<runtime-key>"
$env:CONTROL_PLANE_TUNNEL_ID = "<tunnel-id>"

.\scripts\windows\ContinuityOS-RemoteTunnel.ps1 -Mode Init -RemoteRoot C:\path\to\allowed\root
.\scripts\windows\ContinuityOS-RemoteTunnel.ps1 -Mode Doctor
.\scripts\windows\ContinuityOS-RemoteTunnel.ps1 -Mode Run
```

The launcher passes no API key or bearer token on the command line. The official
`tunnel-client` inherits the runtime key from the environment.

`CONTROL_PLANE_TUNNEL_ID` must match `tunnel_` plus exactly 32 lowercase hex
characters. The runtime API key should be restricted to Tunnels Read + Use;

## Security invariants

- no public MCP listener is created by ContinuityOS;
- no public MCP endpoint is required;
- tunnel-client health/UI is explicitly pinned to loopback `127.0.0.1:8080`;
- no arbitrary `--mcp-command` input is accepted by the launcher;
- the MCP child command is fixed to `continuityos.remote_mcp_server`;
- the remote root must already exist;
- credential/key paths remain denied by R1;
- Pro tunnel surface is read-only at the server, not merely in the ChatGPT UI;
- R2 does not merge, deploy, trade, touch wallets, or grant capital authority.

## Qualification

R2 is code-complete only when:

1. R1 review-gates are green on its synchronized head;
2. R2 unit/static tests pass on Linux and Windows CI;
3. R2 CodeQL and P0 checks are green;
4. Windows CI proves credential-free `Plan` mode and loopback-only listener settings;
5. once tunnel credentials are available, `Init -> Doctor -> Run` is tested with
the official OpenAI tunnel-client.

The final step is runtime qualification and cannot be claimed from CI alone.
117 changes: 117 additions & 0 deletions scripts/windows/ContinuityOS-RemoteTunnel.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
[CmdletBinding()]
param(
[ValidateSet("Plan", "Init", "Doctor", "Run")]
[string]$Mode = "Plan",

[string]$TunnelId = $env:CONTROL_PLANE_TUNNEL_ID,
[string]$Profile = "continuityos-remote",
[string]$RemoteRoot = (Get-Location).Path,
[string]$TunnelClient = "tunnel-client",
[string]$Python = "python"
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

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
}

function Require-ControlPlaneKey {
if ([string]::IsNullOrWhiteSpace($env:CONTROL_PLANE_API_KEY)) {
throw "CONTROL_PLANE_API_KEY must be supplied through the process environment."
}
}

function Validate-TunnelId {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
throw "TunnelId is required for Init."
}
if ($Value -notmatch '^tunnel_[0-9a-f]{32}$') {
throw "TunnelId has an invalid format."
}
}

if (
[string]::IsNullOrWhiteSpace($Profile) -or
$Profile.Length -gt 64 -or
$Profile -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$'
) {
throw "Profile has an invalid format."
}

$pythonExe = Require-Command -Name $Python
$root = (Resolve-Path -LiteralPath $RemoteRoot).Path
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
throw "RemoteRoot must be an existing directory."
}

# The child MCP is deliberately fixed. No arbitrary shell text is accepted.
# The Pro profile is enforced again inside remote_mcp_server for both tools/list
# and tools/call; the tunnel is transport only.
$quotedPython = '"' + $pythonExe.Replace('"', '""') + '"'
$quotedRoot = '"' + $root.Replace('"', '""') + '"'
$mcpCommand = (
$quotedPython +
" -m continuityos.remote_mcp_server" +
" --enable-remote" +
" --tool-profile chatgpt-pro-readonly" +
" --remote-root " + $quotedRoot
)

$plan = [ordered]@{
schema = "continuityos.remote_tunnel_plan/v1"
mode = $Mode
profile = $Profile
tunnel_id_present = -not [string]::IsNullOrWhiteSpace($TunnelId)
control_plane_key_present = -not [string]::IsNullOrWhiteSpace($env:CONTROL_PLANE_API_KEY)
remote_root = $root
python = $pythonExe
mcp_transport = "stdio"
mcp_tool_profile = "chatgpt-pro-readonly"
public_mcp_listener = $false
health_listener = "127.0.0.1:8080"
health_listener_scope = "loopback"
direct_shell = $false
}

if ($Mode -eq "Plan") {
$plan | ConvertTo-Json -Depth 4
exit 0
}

$tunnelExe = Require-Command -Name $TunnelClient
Require-ControlPlaneKey

switch ($Mode) {
"Init" {
Validate-TunnelId -Value $TunnelId
& $tunnelExe init `
--sample sample_mcp_stdio_local `
--profile $Profile `
--tunnel-id $TunnelId `
--health-listen-addr 127.0.0.1:8080 `
--mcp-command $mcpCommand
if ($LASTEXITCODE -ne 0) {
throw "tunnel-client init failed with exit code $LASTEXITCODE"
}
}
"Doctor" {
& $tunnelExe doctor --profile $Profile --explain
if ($LASTEXITCODE -ne 0) {
throw "tunnel-client doctor failed with exit code $LASTEXITCODE"
}
}
"Run" {
& $tunnelExe run --profile $Profile
if ($LASTEXITCODE -ne 0) {
throw "tunnel-client run failed with exit code $LASTEXITCODE"
}
}
}
Loading
Loading