From 430ce0f526a03f4ecab71e9b3cfb9a417b099611 Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 17:47:14 +0530 Subject: [PATCH 1/9] fix(full-system): verify activation and clarify gateway state (scripts/policygate-operator.mjs) --- scripts/policygate-operator.mjs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/scripts/policygate-operator.mjs b/scripts/policygate-operator.mjs index f00da40..ec14ba5 100644 --- a/scripts/policygate-operator.mjs +++ b/scripts/policygate-operator.mjs @@ -168,14 +168,26 @@ if (action === "yolo") { const temporary=yoloLeasePath+"."+randomUUID()+".tmp"; fs.writeFileSync(temporary, JSON.stringify(leaseData, null, 2) + "\n", { mode: 0o644,flag:"wx" }); fs.renameSync(temporary,yoloLeasePath); - if(process.env.POLICYGATE_TEST_OPERATOR!=="1"&&fs.existsSync("/usr/local/bin/bridge-dev-controller")){ - const activated=spawnSync("/usr/local/bin/bridge-dev-controller",["activate"],{stdio:"inherit",env:{PATH:"/usr/bin:/bin"},timeout:30000}); + const controllerPresent=process.env.POLICYGATE_TEST_OPERATOR!=="1"&&fs.existsSync("/usr/local/bin/bridge-dev-controller"); + if(controllerPresent){ + const helper="/usr/local/bin/bridge-dev-controller"; + const activated=spawnSync(helper,["activate"],{stdio:"inherit",env:{PATH:"/usr/bin:/bin"},timeout:30000}); if(activated.status!==0){fs.unlinkSync(yoloLeasePath);throw new Error("Host controller activation failed; YOLO lease removed");} + // Exit zero alone is insufficient: the protected activation record must + // match this signed host-scoped lease before we report local success. + if(!activationFile||!verifyLeaseActivation(activationFile,leaseData,pubFile)){ + const revoked=spawnSync(helper,["mode","standard"],{stdio:"inherit",env:{PATH:"/usr/bin:/bin"},timeout:30000}); + if(fs.existsSync(yoloLeasePath))fs.unlinkSync(yoloLeasePath); + throw new Error("HOST_CONTROLLER_ACTIVATION_UNVERIFIED: local root lease revoked; "+ + (revoked.status===0?"controller restored to STANDARD.":"controller rollback unverified; inspect operator status and jobs.")); + } } - console.log(permanent?"Full System ENABLED until manually disabled.":"Full System ENABLED for "+durationMin+" minutes."); - if(!permanent)console.log(`Active until: ${new Date(expires_at).toISOString()}`); - console.log("To deactivate immediately, run: policygate-operator.mjs yolo --off"); + console.log(permanent?"Full System lease issued until manually disabled.":"Full System lease issued for "+durationMin+" minutes."); + if(!permanent)console.log(`Lease expires: ${new Date(expires_at).toISOString()}`); + console.log(controllerPresent?"Host-controller activation record VERIFIED.":"Isolated test operator: no host controller activation."); + console.log("Running MCP gateway acknowledgment is UNVERIFIED; check bridge-dev mode status and MCP agent_status."); + console.log("To deactivate immediately, run: bridge-dev mode sudo --off"); process.exit(0); } From 76d45c4dfe20caa86f52d2a0fa8392e9162b153b Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 17:47:16 +0530 Subject: [PATCH 2/9] fix(full-system): verify activation and clarify gateway state (src/cli.ts) --- src/cli.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index d630421..632f851 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -220,24 +220,26 @@ function cmdAccess(): void { console.log("Keep these credentials private. For ChatGPT Web, configure a separate HTTPS MCP route."); } -function runOperator(args: string[]): void { +function runOperator(args: string[]): boolean { if (process.platform !== "linux") { console.error("Signed operator mode control requires the hardened Linux installation."); process.exitCode = 1; - return; + return false; } const script = path.join(getActivePath("opt"), "scripts", "policygate-operator.mjs"); if (!fs.existsSync(script)) { console.error("Operator script not installed: " + script); process.exitCode = 1; - return; + return false; } // The operator script independently requires real root UID and an interactive terminal. const run = spawnSync("sudo", ["--", process.execPath, script, ...args], {stdio:"inherit"}); if (run.error || run.status !== 0) { console.error("Operator authorization failed. Use an independent local root terminal."); process.exitCode = run.status || 1; + return false; } + return true; } async function cmdMode(args: string[]) { @@ -261,8 +263,19 @@ async function cmdMode(args: string[]) { console.error("Full System (sudo) needs the optional Linux host controller; portable mode cannot elevate."); process.exitCode=1;return; } - if(args.includes("--off")) runOperator(["mode","standard"]); - else runOperator(["yolo", ...args.slice(1)]); + if(args.includes("--off")) { + runOperator(["mode","standard"]); + return; + } + if (!runOperator(["yolo", ...args.slice(1)])) return; + // An issued operator lease is not evidence that the local controller + // state actually authorizes Full System. + const mode = getActiveMode(); + if (mode.mode !== "YOLO") { + console.error("Full System not confirmed by local signed lease and controller state: " + mode.detail); + console.error("Inspect 'bridge-dev mode status' and MCP 'agent_status'; do not assume remote root access."); + process.exitCode = 1; + } return; } console.error("Usage: bridge-dev mode [read-write | sudo [--duration <1-120>] | sudo --off]"); From 897629c1b86b0d36424f62b490d0418a3958d38d Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 17:47:18 +0530 Subject: [PATCH 3/9] fix(full-system): verify activation and clarify gateway state (scripts/test-host-controller.mjs) --- scripts/test-host-controller.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/test-host-controller.mjs b/scripts/test-host-controller.mjs index ff671b7..bf32ea3 100644 --- a/scripts/test-host-controller.mjs +++ b/scripts/test-host-controller.mjs @@ -54,6 +54,8 @@ try{ const operatorEnv={...process.env,POLICYGATE_TEST_OPERATOR:'1',POLICYGATE_OPERATOR_PRIVATE_KEY:privateKey,POLICYGATE_APPROVAL_PUBLIC_KEY:config.public_key,POLICYGATE_YOLO_LEASE:config.lease,POLICYGATE_MODE_FILE:config.mode_file}; for(const args of [['yolo','--duration','0'],['yolo','--duration','121'],['yolo','--duration','1junk'],['yolo','--duration'],['yolo','--durtion','10'],['yolo','--unknown'],['yolo','--until-off','--duration','5']])assert.notEqual(spawnSync(process.execPath,['scripts/policygate-operator.mjs',...args],{env:operatorEnv,encoding:'utf8'}).status,0); const activated=spawnSync(process.execPath,['scripts/policygate-operator.mjs','mode','yolo','--duration','1'],{env:operatorEnv,encoding:'utf8'});assert.equal(activated.status,0,activated.stderr); + assert.match(activated.stdout,/Running MCP gateway acknowledgment is UNVERIFIED/); + assert.doesNotMatch(activated.stdout,/Full System ENABLED/); const issued=JSON.parse(await fs.readFile(config.lease,'utf8'));assert.ok(issued.expires_at>Date.now()&&issued.expires_at<=Date.now()+60_000); const revoked=spawnSync(process.execPath,['scripts/policygate-operator.mjs','yolo','--off'],{env:operatorEnv,encoding:'utf8'});assert.equal(revoked.status,0,revoked.stderr); const foreverCommand=spawnSync(process.execPath,['scripts/policygate-operator.mjs','yolo'],{env:operatorEnv,encoding:'utf8'}); From ae942252bef01573dede123b76a0a1eb24e0be9a Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 17:47:20 +0530 Subject: [PATCH 4/9] fix(full-system): verify activation and clarify gateway state (docs/TROUBLESHOOTING.md) --- docs/TROUBLESHOOTING.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 3c907bb..9c7b909 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -36,6 +36,28 @@ break native gateway tools. After disable/remove, stale manifests cannot authori calls. Clients that cache tools may require reconnecting or a new chat even after tool-list notifications were sent. +## Full System (sudo) CLI and verification + +On managed Linux, `bridge-dev mode sudo` requests independent operator +confirmation. Prefer `bridge-dev mode sudo --duration 60` to a permanent +lease; `bridge-dev mode sudo --off` deactivates it. The portable installer +does not provision the privileged controller. + +If `bridge-dev mode sudo` prints the outdated +`strict | standard | yolo` usage, inspect the executable selected by +`type -a bridge-dev` (fish) or `command -v bridge-dev` (POSIX shells). +The managed launcher is `/usr/local/bin/bridge-dev`, which forwards to +`/opt/bridge-dev/dist/cli.js`. An older user-local executable can shadow +it. Inspect before changing anything; **do not** overwrite the managed +gateway with the portable installer. + +An operator-signed lease and verified controller activation are not proof +that the running gateway accepted Full System. Check `bridge-dev mode +status` in the independent terminal and the actual MCP `agent_status`. +Require `full_machine_access: true` before relying on host-root access. +A mismatch is a failed acceptance, not a reason to bypass policy or issue +another permanent lease. + ## Policy and cleanup `POLICYGATE_CONTEXT_REQUIRED` means the session needs `system_context` before a From 1dcc88f88c37ef66863c27299dd96d59972ba10d Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 17:50:15 +0530 Subject: [PATCH 5/9] fix(cli): distinguish unreadable root state from confirmed sudo denial --- src/cli.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 632f851..0ea4241 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -247,10 +247,13 @@ async function cmdMode(args: string[]) { if (!target || target === "status") { const reading = getActiveMode(); const label = reading.mode === "YOLO" ? "Full System (sudo)" : - reading.mode === "STANDARD" ? "Read & Write" : "Unavailable (internal safety lock)"; + reading.mode === "STANDARD" ? "Read & Write" : + reading.mode === "STRICT" ? "Unavailable (internal safety lock)" : "Operator-only / unverified"; console.log("Mode: " + label + " (" + reading.detail + ")"); - if (reading.mode === "STRICT" || reading.mode === "UNKNOWN") + if (reading.mode === "STRICT") console.log("Protected state prevents Read & Write. Use 'bridge-dev mode read-write' from your own terminal."); + if (reading.mode === "UNKNOWN") + console.log("Mode state is not readable here. Verify with the independent operator and MCP agent_status."); return; } if (["read-write","readwrite","rw","standard"].includes(target)) { @@ -271,10 +274,16 @@ async function cmdMode(args: string[]) { // An issued operator lease is not evidence that the local controller // state actually authorizes Full System. const mode = getActiveMode(); - if (mode.mode !== "YOLO") { - console.error("Full System not confirmed by local signed lease and controller state: " + mode.detail); - console.error("Inspect 'bridge-dev mode status' and MCP 'agent_status'; do not assume remote root access."); + if (mode.mode === "STRICT" || mode.mode === "STANDARD") { + console.error("Full System not confirmed: protected state reports " + mode.mode); + console.error("Inspect the independent operator and MCP agent_status; do not assume remote root access."); process.exitCode = 1; + } else if (mode.mode === "UNKNOWN") { + // Normal user accounts may not read root-owned lease files. The root + // operator verified its own activation record, but only the running + // gateway's agent_status can confirm remote host-root authorization. + console.log("Local lease state is operator-only (" + mode.detail + ")."); + console.log("Check MCP agent_status for full_machine_access=true before relying on Full System."); } return; } From 796211f6990dbcafe15817bbf6108694ae06aae7 Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 17:59:57 +0530 Subject: [PATCH 6/9] feat(operator): add single-file managed Full System recovery helper --- scripts/bridge-dev-recover.sh | 119 ++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 scripts/bridge-dev-recover.sh diff --git a/scripts/bridge-dev-recover.sh b/scripts/bridge-dev-recover.sh new file mode 100644 index 0000000..4e103b4 --- /dev/null +++ b/scripts/bridge-dev-recover.sh @@ -0,0 +1,119 @@ +#!/bin/sh +# BridgeDev managed Linux operator recovery. No install, no service restart. +# Run locally in your own terminal; never through MCP or with sudo sh. +set -eu + +CLI=/usr/local/bin/bridge-dev +OPERATOR=/opt/bridge-dev/scripts/policygate-operator.mjs + +say() { printf '[BridgeDev] %s\n' "$*"; } +die() { say "ERROR: $*" >&2; exit 1; } +usage() { + cat <<'USAGE' +Usage: sh bridge-dev-recover.sh [check | on [minutes] | off] + check Read-only status (default); never prompts for sudo + on 60 Request a time-limited Full System lease (1-120 min) + off Revoke Full System and return to Read & Write +This file does not install, update, restart, or deploy BridgeDev. +Managed service releases use their separate, watchdog-backed deploy controller. +USAGE +} +need_host() { + [ "$(uname -s)" = Linux ] || die "Managed mode is Linux-only" + [ -x "$CLI" ] || die "Managed CLI missing at $CLI" + [ -f "$OPERATOR" ] || die "Managed operator script missing at $OPERATOR" + [ -x /usr/bin/node ] || die "Node missing at /usr/bin/node" + command -v sudo >/dev/null 2>&1 || die "sudo is unavailable" +} +operator_status() { + sudo /usr/bin/node "$OPERATOR" status +} +active_lease() { + case "$1" in + *"Current Mode : FULL SYSTEM / sudo ("*) return 0 ;; + *) return 1 ;; + esac +} +check() { + say "Selected managed CLI: $CLI" + selected=$(command -v bridge-dev 2>/dev/null || :) + if [ -n "$selected" ] && [ "$selected" != "$CLI" ]; then + say "Your PATH selects $selected instead; using the managed CLI explicitly." + fi + "$CLI" mode status || : + say "Local CLI state alone does not prove gateway root authorization." + say "Verify MCP agent_status returns full_machine_access=true before relying on sudo." + say "Read-only check complete. No mode or service changes." +} +rollback() { + say "Activation was not verified; attempting operator-controlled rollback to Read & Write." + if "$CLI" mode sudo --off; then + if result=$(operator_status); then + printf '%s\n' "$result" + if ! active_lease "$result"; then + say "Rollback verified by independent operator status." + return 0 + fi + fi + fi + say "ROLLBACK UNVERIFIED. Inspect the operator/controller and existing jobs; do not retry activation." + return 1 +} +activate() { + duration=${1:-60} + case "$duration" in + ''|*[!0-9]*) die "Duration must be an integer from 1 to 120" ;; + esac + [ "$duration" -ge 1 ] && [ "$duration" -le 120 ] || die "Duration must be 1-120 minutes" + [ -t 0 ] && [ -t 1 ] || die "Activation requires an independent interactive local terminal" + say "Checking independent operator state first; no existing lease will be overwritten." + before=$(operator_status) || die "Cannot verify operator status; no activation attempted" + printf '%s\n' "$before" + if active_lease "$before"; then + die "An existing Full System lease is active. Check/revoke it first; no new lease issued." + fi + say "Requesting Full System for $duration minute(s). The operator will ask you to confirm." + # Old CLI builds can return nonzero when root-owned lease metadata is not + # readable by the ordinary user. Do not trust that exit code alone. + cli_code=0 + "$CLI" mode sudo --duration "$duration" || cli_code=$? + if ! after=$(operator_status); then + rollback || : + die "Operator status unavailable after request; lease cannot be considered verified" + fi + printf '%s\n' "$after" + if ! active_lease "$after"; then + # If user declined, no new lease exists; still leave the base mode as-is. + if [ "$cli_code" -eq 0 ]; then + say "No Full System lease was issued (possibly declined)." + exit 1 + fi + rollback || : + die "Activation failed and rollback was attempted" + fi + if [ "$cli_code" -ne 0 ]; then + say "CLI returned $cli_code, but the independent operator reports a signed active lease." + say "This may be a stale/unreadable CLI status, not proof of gateway acceptance." + fi + say "Operator lease VERIFIED. Remote gateway acceptance is still UNVERIFIED." + say "Check MCP agent_status for full_machine_access=true. If absent, run: sh bridge-dev-recover.sh off" +} +deactivate() { + [ -t 0 ] && [ -t 1 ] || die "Deactivation requires an independent local terminal" + say "Revoking Full System using the managed operator." + "$CLI" mode sudo --off || die "Revoke command failed; inspect operator status and existing jobs" + after=$(operator_status) || die "Revoke requested, but independent status is unavailable" + printf '%s\n' "$after" + if active_lease "$after"; then + die "Full System still active; do not assume revocation" + fi + say "Full System inactive according to the operator. Confirm MCP agent_status reports full_machine_access=false." +} + +case "${1:-check}" in + --help|-h|help) usage ;; + check) [ "$#" -eq 1 ] || [ "$#" -eq 0 ] || die "Unexpected arguments"; need_host; check ;; + on) [ "$#" -le 2 ] || die "Unexpected arguments"; need_host; activate "${2:-60}" ;; + off) [ "$#" -eq 1 ] || die "Unexpected arguments"; need_host; deactivate ;; + *) usage; die "Unknown action: $1" ;; +esac From e6ed619e0a15975c70e9ad3d2a50f8466969004c Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 18:00:41 +0530 Subject: [PATCH 7/9] test/docs: cover and explain one-file managed recovery helper --- scripts/test-bridge-dev-cli.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/test-bridge-dev-cli.mjs b/scripts/test-bridge-dev-cli.mjs index f9e95bc..6bd1e9b 100644 --- a/scripts/test-bridge-dev-cli.mjs +++ b/scripts/test-bridge-dev-cli.mjs @@ -32,6 +32,22 @@ try { const invalid=run(["unknown-command","--json"]);assert.equal(invalid.status,1);assert.equal(JSON.parse(invalid.stderr).code,"BRIDGEDEV_ERROR"); const help=run(["help"]);assert.equal(help.status,0,help.stderr); assert.match(help.stdout,/mode read-write/);assert.match(help.stdout,/mode sudo --duration 60/); + const recovery=fs.readFileSync("scripts/bridge-dev-recover.sh","utf8"); + assert.match(recovery,/CLI=\/usr\/local\/bin\/bridge-dev/); + assert.match(recovery,/active_lease\(\)/); + assert.match(recovery,/rollback\(\)/); + assert.match(recovery,/full_machine_access=true/); + assert.doesNotMatch(recovery,/sudo\s+(?:rm|chown|chmod|systemctl)|\bexec\s+sudo\b/); + if(process.platform!=="win32"){ + const syntax=spawnSync("sh",["-n","scripts/bridge-dev-recover.sh"],{encoding:"utf8"}); + assert.equal(syntax.status,0,syntax.stderr); + const helpScript=spawnSync("sh",["scripts/bridge-dev-recover.sh","--help"],{encoding:"utf8"}); + assert.equal(helpScript.status,0,helpScript.stderr); + assert.match(helpScript.stdout,/check.*on.*off/); + const deniedScript=spawnSync("sh",["scripts/bridge-dev-recover.sh","on","121"],{encoding:"utf8"}); + assert.notEqual(deniedScript.status,0); + // No actual activation is possible from the test environment. + } const denied=run(["mode","sudo"]);assert.notEqual(denied.status,0); assert.match(denied.stderr,/portable mode cannot elevate/); const access=run(["access"]); From 43baae0d225e5476fd6ad0598ddc9c845af4da57 Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 18:00:42 +0530 Subject: [PATCH 8/9] test/docs: cover and explain one-file managed recovery helper --- docs/TROUBLESHOOTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 9c7b909..9db8cc1 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -38,6 +38,19 @@ tool-list notifications were sent. ## Full System (sudo) CLI and verification +One local-shell helper is available at +[`scripts/bridge-dev-recover.sh`](../scripts/bridge-dev-recover.sh). +Download/review it and run `sh bridge-dev-recover.sh check` to inspect the +managed launcher, or `sh bridge-dev-recover.sh on 60` in an independent +terminal to request an explicitly confirmed temporary lease. +`sh bridge-dev-recover.sh off` revokes the lease. The script uses the +managed `/usr/local/bin/bridge-dev`, never whichever unrelated executable +appears first on PATH. It verifies the operator's local signed lease and +attempts to revert to Read & Write when an activation fails verification. +Remote MCP acceptance still requires `agent_status` in ChatGPT. It never +installs, upgrades, restarts or deploys the service; production releases +use the separately reviewed watchdog/rollback controller. + On managed Linux, `bridge-dev mode sudo` requests independent operator confirmation. Prefer `bridge-dev mode sudo --duration 60` to a permanent lease; `bridge-dev mode sudo --off` deactivates it. The portable installer From 3ae380cab19cbc455827eea16b29f18dead5acef Mon Sep 17 00:00:00 2001 From: kmanisk Date: Sat, 14 Nov 2026 18:00:44 +0530 Subject: [PATCH 9/9] test/docs: cover and explain one-file managed recovery helper --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index a790b50..46c5fab 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,11 @@ bridge-dev mode sudo bridge-dev mode sudo --off ``` +For a **single local shell helper** with explicit verification and recovery +instructions, use [scripts/bridge-dev-recover.sh](scripts/bridge-dev-recover.sh): +`sh bridge-dev-recover.sh check`, `on 60`, or `off`. +It does not deploy code or replace the independent operator. + Full System displays `Enable Full System (sudo)? [Y/n]`. Press Enter or `y` to approve, or `n` to decline. Use `--duration 60` for an optional timed session. **The portable installer does not install the privileged host controller or automatically activate sudo.** [Linux controller setup](docs/HOST_CONTROLLER.md). ## Platforms