Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pc-filebridge",
"version": "0.2.1",
"version": "0.2.2",
"description": "Read files and create new files on explicitly allowed PC folders without overwrite or delete capabilities.",
"author": {
"name": "Gexiro Global Enterprises Ltd."
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
- name: Install locked dependencies
run: npm ci --ignore-scripts
- name: Build digest-pinned runtime image
run: docker build --pull -f deploy/docker/Dockerfile -t pc-filebridge:0.2.1 .
run: docker build --pull -f deploy/docker/Dockerfile -t pc-filebridge:0.2.2 .
- name: Verify container policy, tunnel version, and fail-closed startup
run: npm run check:container
- name: Scan complete runtime image
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:

- name: Verify hardened VPS container
run: |
docker build --pull -f deploy/docker/Dockerfile -t pc-filebridge:0.2.1 .
docker build --pull -f deploy/docker/Dockerfile -t pc-filebridge:0.2.2 .
npm run check:container
docker compose -f deploy/docker/compose.example.yml -f deploy/docker/compose.host-ca.example.yml config --quiet
sh scripts/run-image-vulnerability-gate.sh
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.2.2 - 2026-08-30

- Fixed Windows autostart durability: the tunnel task now has no finite execution limit and is not stopped by battery or idle transitions.
- Added a Windows PowerShell 5.1-compatible role-bound auto-volume task installer with optional isolated runtime state.
- Canonicalized role casing before task registration and case-sensitive contract lookup.
- Added a regression contract for the required long-running Scheduled Task settings.

## 0.2.1 - 2026-08-30

- Derive a stable, opaque volume id from host, disk, partition, volume, and serial identity; drive-letter, label, health, filesystem, and bus changes do not change that id.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ For ChatGPT, create a dedicated Secure MCP Tunnel and a dedicated runtime API ke

The tunnel runtime must run on the computer that owns the configured folders. A VPS deployment can remain available while personal devices are offline, but it exposes files stored in its persistent server volume; it does not make a powered-off PC disk remotely readable. See [VPS deployment](docs/VPS_DEPLOYMENT.md).

Version 0.2.1 separates the main PC, laptop, and infrastructure into three role-bound tunnels. Their tunnel identifiers, runtime aliases, and root ids must never be reused across roles. The Windows and Docker launchers verify the operator-visible tunnel name before starting, and local ChatGPT connectors require an explicit `armed` operator gate. This prevents a PC and VPS poller from silently serving different backends through one connector.
Version 0.2.2 separates the main PC, laptop, and infrastructure into three role-bound tunnels. Their tunnel identifiers, runtime aliases, and root ids must never be reused across roles. The Windows and Docker launchers verify the operator-visible tunnel name before starting, and local ChatGPT connectors require an explicit `armed` operator gate. This prevents a PC and VPS poller from silently serving different backends through one connector.

ChatGPT cannot securely infer which physical device opened a conversation. Operators must select the clearly named PC, laptop, or infrastructure connector. Local Codex can instead use its device-local stdio configuration.

Expand Down Expand Up @@ -114,13 +114,13 @@ After a successful manual connection:

The installer creates a new per-user scheduled task and refuses to replace an existing task.

For role-bound automatic volume discovery, configure the task or a hidden per-user Startup shortcut to run:
For role-bound automatic volume discovery, install the dedicated per-user task:

```powershell
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\Connect-PCFileBridgeRoleTunnel-Task.ps1 -Role pc-local
.\scripts\Install-PCFileBridgeRoleAutostart.ps1 -Role pc-local
```

The task script runs a singleton monitor and does not contain a tunnel identifier or API key in its arguments.
If the protected tunnel runtime intentionally uses a separate local application-data directory, pass its absolute path with `-RuntimeLocalAppData`. The task script runs a singleton monitor and does not contain a tunnel identifier or API key in its arguments. Both installers create long-running tasks with no finite execution limit and do not stop them on battery or idle transitions; they refuse to replace an existing task.

## Full-drive mode

Expand Down
2 changes: 1 addition & 1 deletion deploy/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ARG TUNNEL_ASSETS_IMAGE=ghcr.io/openai/tunnel-client:v0.0.13@sha256:4c6b1441a6ca
ARG TUNNEL_COMMIT=4b5267f823be0b046bb883aacb51603cfde3a0ea
ARG OTEL_VERSION=v1.44.0
ARG X_NET_VERSION=v0.56.0
ARG APP_VERSION=0.2.1
ARG APP_VERSION=0.2.2

FROM ${NODE_IMAGE} AS build
WORKDIR /src
Expand Down
2 changes: 1 addition & 1 deletion deploy/docker/compose.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ services:
build:
context: ../..
dockerfile: deploy/docker/Dockerfile
image: pc-filebridge:0.2.1
image: pc-filebridge:0.2.2
container_name: pc-filebridge
restart: unless-stopped
init: true
Expand Down
2 changes: 1 addition & 1 deletion docs/TUNNEL_ROLES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tunnel roles and device isolation

PC FileBridge 0.2.1 uses three distinct Secure MCP Tunnel identities. A tunnel identifier must never be shared by two roles or by two active pollers.
PC FileBridge 0.2.2 uses three distinct Secure MCP Tunnel identities. A tunnel identifier must never be shared by two roles or by two active pollers.

| Role | Required tunnel name | Runtime alias | Root ids |
|---|---|---|---|
Expand Down
2 changes: 1 addition & 1 deletion mcp/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13504,7 +13504,7 @@ async function main() {
const configPath = path2.resolve(process.env.FILEBRIDGE_CONFIG ?? path2.join(pluginRoot, "config", "roots.local.json"));
const policy = await FileBridgePolicy.fromFile(configPath);
const server = new McpServer(
{ name: "pc-filebridge", version: "0.2.1" },
{ name: "pc-filebridge", version: "0.2.2" },
{
instructions: "Create-only filesystem bridge. Use only configured root IDs and relative paths. Reads are bounded and secret-redacted. Writes may create a new file or directory only. Overwrite, append, patch, rename, move, link traversal, and delete are unavailable and must never be claimed."
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pc-filebridge",
"version": "0.2.1",
"version": "0.2.2",
"description": "Create-only filesystem MCP server for ChatGPT, Codex, and other MCP clients.",
"license": "Apache-2.0",
"repository": {
Expand Down
17 changes: 15 additions & 2 deletions scripts/Connect-PCFileBridgeRoleTunnel-Task.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,23 @@ param(
[ValidateSet('pc-local', 'laptop-local')]
[string]$Role = 'pc-local',
[ValidateRange(5, 3600)]
[int]$PollSeconds = 15
[int]$PollSeconds = 15,
[string]$RuntimeLocalAppData = ''
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Role = $Role.ToLowerInvariant()

& (Join-Path $PSScriptRoot 'Start-PCFileBridgeVolumeMonitor.ps1') -Role $Role -PollSeconds $PollSeconds
$previousLocalAppData = $env:LOCALAPPDATA
try {
if (-not [string]::IsNullOrWhiteSpace($RuntimeLocalAppData)) {
if (-not [IO.Path]::IsPathRooted($RuntimeLocalAppData)) { throw 'RUNTIME_LOCALAPPDATA_MUST_BE_ABSOLUTE' }
$runtimeRoot = [IO.Path]::GetFullPath($RuntimeLocalAppData)
if (-not (Test-Path -LiteralPath $runtimeRoot -PathType Container)) { throw 'RUNTIME_LOCALAPPDATA_UNAVAILABLE' }
$env:LOCALAPPDATA = $runtimeRoot
}
& (Join-Path $PSScriptRoot 'Start-PCFileBridgeVolumeMonitor.ps1') -Role $Role -PollSeconds $PollSeconds
} finally {
$env:LOCALAPPDATA = $previousLocalAppData
}
2 changes: 1 addition & 1 deletion scripts/Install-PCFileBridgeAutostart.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ $arguments = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{0}" -Tu
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments -WorkingDirectory ([IO.Path]::GetDirectoryName($connectScript))
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $account
$principal = New-ScheduledTaskPrincipal -UserId $account -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
$settings = New-ScheduledTaskSettingsSet -Compatibility Win8 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1)

Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'Starts the private OpenAI Secure MCP Tunnel for PC FileBridge after user logon.' | Out-Null
Write-Output "AUTOSTART_INSTALLED task=$TaskName overwrite=false"
46 changes: 46 additions & 0 deletions scripts/Install-PCFileBridgeRoleAutostart.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[CmdletBinding()]
param(
[ValidateSet('pc-local', 'laptop-local')]
[string]$Role = 'pc-local',
[string]$TaskName = '',
[ValidateRange(5, 3600)]
[int]$PollSeconds = 15,
[string]$RuntimeLocalAppData = ''
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Role = $Role.ToLowerInvariant()

if ([string]::IsNullOrWhiteSpace($TaskName)) {
$TaskName = if ($Role -ceq 'pc-local') { 'PC FileBridge PC Local' } else { 'PC FileBridge Laptop Local' }
Comment thread
dzeusking-dev marked this conversation as resolved.
}
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
throw 'The scheduled task already exists; this installer never replaces it.'
}

$taskScript = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot 'Connect-PCFileBridgeRoleTunnel-Task.ps1') -ErrorAction Stop).Path
$account = [Security.Principal.WindowsIdentity]::GetCurrent().Name
$argumentParts = @(
'-NoLogo',
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy', 'Bypass',
'-File', ('"' + $taskScript + '"'),
'-Role', $Role,
'-PollSeconds', [string]$PollSeconds
)
if (-not [string]::IsNullOrWhiteSpace($RuntimeLocalAppData)) {
if (-not [IO.Path]::IsPathRooted($RuntimeLocalAppData)) { throw 'RUNTIME_LOCALAPPDATA_MUST_BE_ABSOLUTE' }
$runtimeRoot = [IO.Path]::GetFullPath($RuntimeLocalAppData)
if (-not (Test-Path -LiteralPath $runtimeRoot -PathType Container)) { throw 'RUNTIME_LOCALAPPDATA_UNAVAILABLE' }
$argumentParts += @('-RuntimeLocalAppData', ('"' + $runtimeRoot + '"'))
}
$arguments = $argumentParts -join ' '
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments -WorkingDirectory ([IO.Path]::GetDirectoryName($taskScript))
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $account
$principal = New-ScheduledTaskPrincipal -UserId $account -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -Compatibility Win8 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1)

Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Starts the $Role PC FileBridge auto-volume monitor after user logon." | Out-Null
Write-Output "ROLE_AUTOSTART_INSTALLED task=$TaskName role=$Role overwrite=false"
1 change: 1 addition & 0 deletions scripts/Start-PCFileBridgeVolumeMonitor.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ param(

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Role = $Role.ToLowerInvariant()

$pluginRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..') -ErrorAction Stop).Path
if ([string]::IsNullOrWhiteSpace($ConnectScriptPath)) { $ConnectScriptPath = Join-Path $PSScriptRoot 'Connect-PCFileBridgeRoleTunnel.ps1' }
Expand Down
4 changes: 2 additions & 2 deletions scripts/container-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const image = process.env.FILEBRIDGE_IMAGE ?? "pc-filebridge:0.2.1";
const image = process.env.FILEBRIDGE_IMAGE ?? "pc-filebridge:0.2.2";
const expectedTunnelVersion = "0.0.13";
const expectedNodeVersion = "v24.20.0";
const expectedTools = [
Expand Down Expand Up @@ -74,7 +74,7 @@ try {
}
if (!failedClosed) throw new Error("Container did not fail closed when secrets were absent.");

const client = new Client({ name: "pc-filebridge-container-smoke", version: "0.2.1" });
const client = new Client({ name: "pc-filebridge-container-smoke", version: "0.2.2" });
const transport = new StdioClientTransport({
command: "docker",
args: [
Expand Down
2 changes: 1 addition & 1 deletion scripts/mcp-config-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ try {
await request("initialize", {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: { name: "pc-filebridge-config-smoke", version: "0.2.1" },
clientInfo: { name: "pc-filebridge-config-smoke", version: "0.2.2" },
});
send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });

Expand Down
2 changes: 1 addition & 1 deletion scripts/mcp-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const expectedTools = [
"stat_path",
];

const client = new Client({ name: "pc-filebridge-smoke", version: "0.2.1" });
const client = new Client({ name: "pc-filebridge-smoke", version: "0.2.2" });
const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const fixtureDirectory = await mkdtemp(path.join(pluginRoot, ".pc-filebridge-mcp-smoke-"));
const configPath = path.join(fixtureDirectory, "roots.json");
Expand Down
1 change: 1 addition & 0 deletions scripts/package-contents-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const required = [
"config/tunnel-roles.json",
"scripts/Connect-PCFileBridgeRoleTunnel.ps1",
"scripts/Connect-PCFileBridgeRoleTunnel-Task.ps1",
"scripts/Install-PCFileBridgeRoleAutostart.ps1",
"scripts/Set-PCFileBridgeGate.ps1",
"scripts/Start-PCFileBridgeVolumeMonitor.ps1",
"scripts/Test-PCFileBridgeRoleConfig.ps1",
Expand Down
2 changes: 1 addition & 1 deletion scripts/run-image-vulnerability-gate.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

set -eu

image="${FILEBRIDGE_IMAGE:-pc-filebridge:0.2.1}"
image="${FILEBRIDGE_IMAGE:-pc-filebridge:0.2.2}"
scan_dir="$(mktemp -d "${TMPDIR:-/tmp}/pc-filebridge-scan.XXXXXX")"
source_container=""
govuln_container=""
Expand Down
44 changes: 44 additions & 0 deletions scripts/tunnel-role-contract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,50 @@ import { fileURLToPath } from "node:url";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const contract = JSON.parse(await readFile(path.join(root, "config", "tunnel-roles.json"), "utf8"));
const autostartInstallers = await Promise.all(
["Install-PCFileBridgeAutostart.ps1", "Install-PCFileBridgeRoleAutostart.ps1"].map((name) =>
readFile(path.join(root, "scripts", name), "utf8"),
),
);
const roleTask = await readFile(
path.join(root, "scripts", "Connect-PCFileBridgeRoleTunnel-Task.ps1"),
"utf8",
);
const volumeMonitor = await readFile(
path.join(root, "scripts", "Start-PCFileBridgeVolumeMonitor.ps1"),
"utf8",
);
for (const roleEntryPoint of [autostartInstallers[1], roleTask, volumeMonitor]) {
if (!roleEntryPoint.includes("$Role = $Role.ToLowerInvariant()")) {
throw new Error("Role entry points must normalize case before case-sensitive contract checks.");
}
}
for (const windowsPowerShellScript of [...autostartInstallers, roleTask]) {
if (windowsPowerShellScript.includes("IsPathFullyQualified")) {
throw new Error("Windows PowerShell 5.1 scripts must not call IsPathFullyQualified.");
}
}
const requiredAutostartSettings = [
"-Compatibility Win8",
"-AllowStartIfOnBatteries",
"-DontStopIfGoingOnBatteries",
"-StartWhenAvailable",
"-DontStopOnIdleEnd",
"-ExecutionTimeLimit ([TimeSpan]::Zero)",
"-MultipleInstances IgnoreNew",
"-RestartCount 5",
"-RestartInterval (New-TimeSpan -Minutes 1)",
];
for (const autostartInstaller of autostartInstallers) {
for (const setting of requiredAutostartSettings) {
if (!autostartInstaller.includes(setting)) {
throw new Error(`Autostart installer is missing durable setting: ${setting}`);
}
}
if (/ExecutionTimeLimit\s+\(New-TimeSpan/i.test(autostartInstaller)) {
throw new Error("Autostart installer must not impose a finite execution time limit.");
}
}
const volumePolicy = (rootIdPrefix) => ({
enabled: true,
driveType: 3,
Expand Down
2 changes: 1 addition & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async function main(): Promise<void> {
const configPath = path.resolve(process.env.FILEBRIDGE_CONFIG ?? path.join(pluginRoot, "config", "roots.local.json"));
const policy = await FileBridgePolicy.fromFile(configPath);
const server = new McpServer(
{ name: "pc-filebridge", version: "0.2.1" },
{ name: "pc-filebridge", version: "0.2.2" },
{
instructions:
"Create-only filesystem bridge. Use only configured root IDs and relative paths. " +
Expand Down