Skip to content

fix(enforce): fail closed on --enforce for GUI editors (Cursor) - #112

Open
johnnyfish wants to merge 1 commit into
mainfrom
fix/enforce-gui-editor-fail-closed
Open

fix(enforce): fail closed on --enforce for GUI editors (Cursor)#112
johnnyfish wants to merge 1 commit into
mainfrom
fix/enforce-gui-editor-fail-closed

Conversation

@johnnyfish

Copy link
Copy Markdown
Contributor

What

onecli run --enforce -- cursor now fails closed with a clear message instead of silently sandboxing nothing.

Why

Cursor (and other VS Code-style GUI editors) aren't launched-and-owned by onecli run: the command only opens/focuses an already-running Electron app, so there's no child process tree to confine. Under --enforce the wrap path would:

  1. sandbox-exec the launcher — which hands off to the running app and governs nothing, and
  2. inject the ephemeral per-run forwarder port (127.0.0.1:<port>) into Cursor's persistent settings.json, leaving the editor pointed at a dead proxy after the run ends.

I hit #2 live: a prior enforce-cursor run had left "http.proxy": "http://127.0.0.1:59845" in real settings.

Change

Route agents whose spec has configDir set (the GUI/VS Code-style marker that already drives injectElectronProxySettings) to a fail-closed error that points at the supported cooperative path:

--enforce is not supported for Cursor: it's a GUI editor, not a launched
process tree. Run `onecli run -- cursor` (no --enforce): OneCLI injects
gateway proxy settings into its config instead

Mirrors the existing docker-sandbox (Hermes) fail-closed case. The cooperative path (onecli run -- cursor) is unchanged and self-heals a stale port by re-injecting the real gateway URL.

Test

TestGUIEditorsMarkedForEnforceRejection: pins that Cursor carries configDir (so the fail-closed route fires) and that a CLI agent (Codex) does not.

Verification

Reproduced both CI jobs in Linux Docker before pushing:

  • golang:1.26.0: go build ./... + go test -race ./... — all pass
  • golangci/golangci-lint:v2.10: golangci-lint run ./... — 0 issues

Follows #105 (now merged).

@johnnyfish

Copy link
Copy Markdown
Contributor Author

Found and fixed a second Cursor blocker: a rotated gateway CA

Testing the GUI launch on a real machine surfaced net::ERR_CERT_AUTHORITY_INVALID on every request, even though the CA was installed and trusted.

Root cause: two different CAs share the subject name OneCLI Local Gateway CA. The keychain held one trusted months ago; the gateway had since rotated. Every name-based check agreed things were fine:

security find-certificate -c "OneCLI Local Gateway CA"   → found
security dump-trust-settings                             → trusted

Only the key differed. Verified by checking the served leaf against each:

trusted CA   82:1D:36:11...  → verification failed (unable to get local issuer)
live CA      25:0E:4A:5F...  → OK

The runbook made this reachable: it pointed add-trusted-cert at ~/.onecli/gateway-ca.pem, which was written once and never refreshed, while ca-bundle.pem kept being updated.

Changes

  • writeGatewayCACert refreshes the bare gateway-ca.pem alongside the bundle, so the documented file can't drift from the CA in use.
  • warnIfGatewayCANotTrusted runs before a sandboxed GUI editor launches and compares public keys, not names, printing the exact delete/add commands. Warns rather than fails: the sandbox and gateway are still enforcing and CLI agents are unaffected, so refusing to launch would be the worse trade.
  • Runbook deletes before adding, and documents the symptom.

Verification

Against the real machine, before the fix:

warning fired: "...is in your keychain, but it is NOT the CA this gateway is using"

After re-trusting: warning silent, security verify-cert returns certificate verification successful, and requests through the enforced forwarder report SSL certificate verify ok. The ERR_CERT_AUTHORITY_INVALID entries stop in Cursor's log.

Unit tests cover the trap directly (same-name/different-key must not compare equal, same cert must compare equal so the warning doesn't become noise, live CA findable among stale ones, empty PEM must not truncate). Linux go build + full suite green via Docker.

@johnnyfish

Copy link
Copy Markdown
Contributor Author

Second CA bug on the same machine: installed but not trusted

After fixing the rotated CA, Cursor still failed with ERR_CERT_AUTHORITY_INVALID and my new check stayed silent. Both were wrong for the same reason: I was treating "the certificate is in the keychain" as "the certificate is trusted".

The trap. The documented command was:

security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db ...

-d selects the admin trust domain, which requires root. Run as a normal user it adds the certificate, applies no trust settings at all, and exits 0. Ground truth on the machine:

security find-certificate -c "OneCLI Local Gateway CA"  → found (looks fine)
security dump-trust-settings                            → "Number of trusted certs = 1"
                                                          ...and it was an unrelated cert

Dropping -d fixed it immediately: trusted certs went 1 → 2 with OneCLI Local Gateway CA present, and requests through the enforced forwarder went to SSL certificate verify ok. Cursor's log has had zero cert errors since.

Changes

  • Dropped -d from every instruction (docs + both warning messages).
  • warnIfGatewayCANotTrusted no longer infers trust from presence: it asks the OS via dump-trust-settings and emits a distinct warning for "installed but untrusted", naming -d as the likely cause. Installed-but-untrusted and wrong-CA need different fixes, so they get different messages.
  • The trust command is one constant instead of three copies, so guidance can't drift.
  • Runbook documents the dump-trust-settings confirmation step.

Linux go build + go vet + full suite green via Docker.

Status of the Cursor demo

  • CA/TLS: fixed and verified (SSL certificate verify ok, no cert errors in Cursor's log)
  • Remaining: streaming chat still needs onecli-cloud#754. Gateway logs confirm the shape precisely: every unary gRPC call succeeds through the MITM (AvailableModels, GetTeams, IsConnected all 200), and only the server-streaming chat call never appears.

Make --enforce trustworthy end to end instead of silently degrading:

- Fail closed on --enforce for GUI editors (Cursor) and launch them
  inside the OneCLI sandbox so their traffic is actually governed.
- Make headless cursor-agent a first-class enforceable agent.
- Verify the gateway CA is trusted (not merely installed) and detect
  a rotated CA before it breaks GUI editors.
- Add transparent redirect: an SNI-recovering listener, fail-closed pf
  anchor management nested under com.apple, a setgid helper so pf can
  scope redirection by group, an opt-in transparent sandbox profile,
  and session lifecycle with fail-closed teardown.
- Cover it with live TLS proof, pf anchor, and full-chain tests, plus
  the Cursor coverage demo runbook.
@johnnyfish
johnnyfish force-pushed the fix/enforce-gui-editor-fail-closed branch from b79425d to 43979f6 Compare July 31, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant