Provider-neutral local execution gateway for Web AI clients.
Web Agent Gateway (WAG) is an open-source, local-first, least-authority gateway for AI clients that need bounded access to user-approved developer resources. WAG keeps identity, policy, approvals, secrets, durable ownership, and capability admission under local control instead of exposing a generic remote-machine surface.
The current Browser Inspect v2 profile is intentionally read-only and exposes exactly five methods: health, workspace.open, repo.search, repo.snapshot, and file.read. It does not expose raw shell/process access, Git writes, file mutation, browser mutation, or generic forwarding.
The Windows x64 native-host preview is currently unsigned and must not be treated as Authenticode-trusted. Supported preview downloads come from the immutable GitHub Release, not from transient GitHub Actions artifacts.
- Latest preview release
- Windows native-host installation and removal
- Security policy
- Privacy policy
- Code-signing policy
- Contributing
Provide a provider-neutral, least-authority local trust/capability gateway for AI hosts that need bounded access to local resources, while keeping identity, policy, approvals, secrets, durable ownership, and audit under WAG control.
WAG has two normative product goals:
- Replace Remote Desktop Commander for trusted private-local WebChat -> machine workflows: repository work, local filesystem operations, bounded commands, process lifecycle and interactive terminal sessions.
- Give WebChat agents local development outcomes comparable to Claude Code/Codex while improving on generic desktop bridges with caller-owned workspace identity, secret redaction, exact-path Git CAS, autonomous-stop revalidation, durable effect receipts and a separate browser trust plane.
ChatGPT Web is the current reference provider and first direct DC-replacement target. Provider neutrality is architectural: future WebChat providers must reuse the same WAG authority/capability contracts through the best available native/standard adapter. "Claude Code/Codex-like" means outcome parity, not raw-shell or agent-platform parity.
Approved mission design: docs/superpowers/specs/2026-09-17-webchat-local-coding-mission-lock-design.md. Normative ADR: docs/adr/0018-lock-webchat-local-coding-mission.md. Dated evidence base: docs/research/2026-09-17-wag-webchat-local-coding-mission-lock.md.
- Git history and tagged evidence.
docs/superpowers/specs/approved designs.docs/adr/architecture decisions.docs/research/dated research receipts.docs/benchmarks/empirical evidence.- Chat history is never canonical project state.
COMPOSE, do not fork wholesale:
- DevSpace: upstream local execution backend/donor.
- LocalAnt: security/policy/approval donor.
- Official MCP SDK: protocol boundary.
- Host-native/local MCP where available; Cloudflare Quick Tunnel remains historical benchmark infrastructure only.
- Our code: thin gateway, policy, semantic tools, telemetry, compatibility.
Trusted Caller Context v1 and the internal Durable Verify Job Core v1 have passed their acceptance gates. Trusted Adapter Admission v1 remains the authority boundary for browser callers, and Browser Adapter v1 is preserved as historical three-tool evidence.
The current read-only browser profile is Browser Inspect v2. It uses a distinct adapter identity/protocol revision and exposes exactly health, workspace.open, repo.search, repo.snapshot, and file.read. It does not expose verify.run, mutation, public jobs, process/PTY, Git writes, browser mutation, or generic forwarding.
The Windows native-host pipeline now records strict unsigned-candidate provenance, normalizes WAG-owned PE VersionInfo before candidate recording, verifies runtime/license inputs, and independently verifies signed-candidate Authenticode continuity. WAG has not yet published or accepted a trusted signed native-host release; Windows application-control acceptance remains gated on an externally signed candidate that passes the existing verifier without relaxation.
The MCP v2 compatibility spike passed for the core/dual-era path, and the later MCP SDK v2 Production Migration v1 moved production to the split v2 TypeScript SDK packages without separately opting into protocol 2026-07-28. Browser Admission HTTP Mode Split v1 removed the unreachable browser generic bearer, the historical file.patch protocol was retired after durable-mutation acceptance, MCP v1 experimental Tasks were retired after revalidation, and the remaining generic bearer HTTP compatibility mode was subsequently removed after its consumers were reduced to historical benchmark/spike harnesses. Current HTTP remains Browser Admission only; Business/private MCP remains stdio. The private-local plane is now intentionally automation-first and may grow when a measured DC-parity gap has a bounded acceptance test.
The supported private deployment direction is ChatGPT Business + OpenAI Secure MCP Tunnel + the production-built Gateway stdio command. DevSpace remains a separately supervised loopback-only process; the Gateway does not expose a public listener.
Local pre-upgrade verification uses placeholders only:
npm ci
npm run build
$env:DEVSPACE_OAUTH_OWNER_TOKEN = '<local-owner-secret>'
node .\dist\cli.js doctor --config C:\path\to\private.json
node .\dist\cli.js serve-stdio --config C:\path\to\private.jsonThe private JSON config is non-secret and contains only approved roots, the loopback DevSpace base/resource URLs, and bounded verify profiles. DEVSPACE_OAUTH_OWNER_TOKEN is accepted only from the environment; access and refresh tokens remain in process memory.
After a Business upgrade and Secure MCP Tunnel provisioning, the intended external setup is:
tunnel-client init --profile web-agent-gateway --tunnel-id <tunnel-id> --mcp-command "node C:\path\to\web-agent-gateway\dist\cli.js serve-stdio --config C:\path\to\private.json"
tunnel-client doctor --profile web-agent-gateway --explain
tunnel-client run --profile web-agent-gatewayThose tunnel/account steps are a post-upgrade acceptance gate, not local pre-upgrade evidence. Do not commit owner tokens, control-plane API keys, tunnel credentials, or machine-specific private config files.
By default serve-stdio exposes exactly five tools: health, workspace.open, repo.snapshot, file.read, verify.run.
A local operator may opt in to the repository-engineering profile that carries WAG's Desktop Commander replacement scope. It is off unless the private config asks for it, and it can never be requested by the model, the provider, the transport, or repository content:
inspect adds the read-only set repo.list, repo.search and repo.diff. On the private stdio/WAG Local surface, mutation adds autonomous bounded mutation tools and gitCommit adds exact-path CAS commits. Those effects execute under the trusted AUTONOMOUS_LOCAL profile with caller-owned workspace identity and immediate kill-switch revalidation; no Goal Lease or per-change human approval is required. The browser operator is a separate proposal/review surface and remains human-approved for filesystem and Git effects. file.create only creates — it refuses a path that already exists, both when planned and again at execution.
The operator review server's origin is announced on stderr; its single-use bootstrap token is not. A stdio gateway's stderr belongs to whichever process spawned it — in the supported deployment that is the remote-facing tunnel client — so the token is written to <statePath>.operator-url instead and removed on shutdown. Open that URL locally to review and approve.
With inspect, mutation and git commit enabled, the private stdio runtime exposes the full autonomous repository loop, including command.run, bounded mutations (file.replace, secret-safe file.edit_block, suffix-guarded file.append) and exact-path commit. The current server assembly also publishes the local-machine surface: machine.open, machine.describe, machine.list, machine.read, machine.search, machine.info, machine.mkdir, machine.move, machine.delete, machine.command.run, machine.process.start, machine.process.list, machine.process.inspect, machine.process.terminate (with per-session restart-recoverable WAG-started process records), and machine.terminal.open/output/input/close.
ChatGPT may keep a frozen tool snapshot for an already-published connector. WAG therefore keeps a compatibility route through the existing tool names: workspace.open can fall back to an autonomous local-machine workspace when DevSpace cannot admit the root, and repo.list, file.read and command.run dispatch by the durable workspace backend. This lets an older 16-tool connector reach local-machine work without a manual connector refresh.
gitCommit requires mutation because both share the same durable store. On private stdio, git.commit executes autonomously after WAG revalidates the caller-owned workspace, exact path set, repository identity, branch/HEAD/tree CAS and emergency stop. On the browser operator plane the same commit object remains proposal-only and requires local human review.
WAG commits with git plumbing against a private index — read-tree, hash-object --no-filters, update-index --cacheinfo, write-tree, commit-tree, update-ref — never git add and never git commit. The commit message is never shell syntax and your real index is never used as WAG's staging area. Browser proposals still build scratch objects without moving a ref until human approval; private-stdio commits instead pass the autonomous-local policy and CAS checks before the ref move.
The committed blob is the bytes you reviewed. The one transformation WAG applies is the end-of-line conversion git itself would apply — because on a core.autocrlf=true checkout every text file is CRLF on disk and LF in the object, so committing disk bytes verbatim would rewrite the repository — and the paths it converted are named on the review page. A path whose attributes ask for a content filter or a working-tree-encoding is refused rather than silently transformed.
WAG binds the resulting tree delta, not just the paths you named, and refuses anything that is not an addition or a modification — so a directory replaced by a file cannot quietly delete the subtree it shadows. It also binds the repository itself: the worktree root, the git dir and the common dir must all still match at approval, which is what stops a repository-supplied core.worktree, a workspace nested inside a larger repository, and an inherited GIT_DIR from redirecting the commit somewhere else.
verify.run executes one locally configured profile by name. The profile names an argv, a bounded environment, a timeout and an output budget; the model chooses the name and nothing else.
The argv reaches the process directly rather than through a shell, and the environment is built upwards from an allowlist (ADR-0025). Nothing the operator's shell exported and nothing the execution backend added reaches the verification unless WAG names it or the profile declares it — NODE_OPTIONS is excluded by construction, because it can inject --require into every Node-based run. Cancellation and abandonment reap the whole process tree; unrelated processes are untouched.
What WAG does not do is isolate a verification from the network. Enforcing that would mean changing machine-wide firewall or security policy, which WAG will not do. The narrower guarantee it does give is that a verification reaches the network with no credential, token or proxy setting WAG passed it: profile environment keys matching TOKEN, SECRET, PASSWORD, API_KEY, PRIVATE_KEY or CREDENTIAL are rejected at config load, and nothing else is inherited.
command.run accepts an opened workspace_id, a bounded argv array and optional timeout/output limits. It reuses the same WAG-owned runner as verify.run: argv is data rather than shell text, caller-supplied environment is not accepted, executable resolution is controlled, timeout/output budgets are bounded, and timeout cleanup reaps the whole spawned process tree. The contract intentionally rejects raw shell strings rather than attempting to emulate an interactive terminal.
Before every command, WAG revalidates the caller-owned workspace record and its live workspace identity, then checks the emergency kill switch again immediately before process creation. Local-machine workspaces use the same existing command.run schema through the compatibility bridge, so a frozen connector snapshot does not require a separate machine tool to run bounded argv.
This is not an OS sandbox. An authorized process runs as the local user with the workspace as its working directory; repository code can have filesystem side effects outside that directory and can use the network if the OS permits it. WAG therefore declares command.run destructive/open-world. Caller-supplied environment and implicit credentials remain unavailable. Interactive work uses WAG-owned machine.terminal.* sessions with bounded input/output, secret redaction, caller/workspace ownership and kill-switch checks. A shell may also be invoked explicitly as argv through machine.command.run; WAG never treats a caller-provided shell string as ambient authority.
Every git subprocess WAG starts goes through one policy (src/safe-git.ts, ADR-0024). The repository is untrusted and so is the environment WAG inherited, so the policy: builds the child environment upwards from an allowlist, so no GIT_* and no unrelated secret is inherited; pins core.hooksPath at an empty directory; enumerates the repository's configured content filters and config-defined hooks and disables each one by name; overrides every configuration key whose value git would execute; passes --literal-pathspecs, so a filename means itself; and disables the pager, external diff and textconv.
Both halves of that matter and neither is obvious. Pinning core.hooksPath does nothing to the hook.<name>.command hooks Git 2.53 added, which a repository sets in its own config and which fire on plain plumbing. And --no-ext-diff --no-textconv do nothing to a clean filter, which git diff runs. Both were measured on this machine and both are covered by tests with proven-armed controls; docs/research/2026-09-20-wag-git-execution-surface.md has the reproductions.
What WAG cannot control is the execution backend's own git: opening a workspace is the backend's operation and it writes its own review checkpoint. Those invocations are attributed in the tests rather than claimed away. The branch moves only by compare-and-swap against the approved parent, so a branch that moved since the preview makes the commit fail rather than clobber. main and master are protected by default.
Because the real index is deliberately left alone, after a WAG commit git status shows the committed paths as staged reversions against the new HEAD until you refresh the index yourself (git reset -- <paths>, which changes no file content). Nothing is lost, and WAG's own next commit is unaffected.
Not available in v1: amend, merge commits, empty commits, signing, force, reset, checkout, branch creation or deletion, push/fetch/pull, committing on a detached HEAD, and committing a deletion or rename.
repo.list returns the immediate tracked and untracked-not-ignored entries of one directory. repo.diff returns the bounded working-tree diff against HEAD, with the bodies of path-policy-sensitive files (.env, .npmrc, .git-credentials and the rest of the denylist) withheld. Every response is capped at 64 KiB and reports whether it was truncated. A caller-supplied path is never interpolated into a shell command: it reaches git as a single literal argv pathspec.
WAG Local now targets Desktop Commander replacement for the trusted private-local workflows used by this project. It includes bounded local filesystem metadata/search/read/create/replace/mkdir/move/delete, paged/multi-file text reads, native bounded image reads (PNG/JPEG/WEBP/GIF), argv execution, owned process lifecycle and interactive terminal sessions. Supported local images are returned as native MCP image content; the existing frozen file.read compatibility bridge can use the same path, so image inspection does not require a connector-catalog refresh. It intentionally does not copy DC's weaker boundaries: there is no caller-supplied environment, no implicit credential inheritance, no unrestricted Git/remote-effect surface, and git.push remains non-grantable. DevSpace repository roots and caller-owned workspace identity remain enforced; local-machine workspaces are explicit WAG records rather than ambient machine authority.
Authority: docs/adr/0020-make-private-stdio-the-dc-replacement-surface.md, docs/adr/0021-complete-the-bounded-repository-inspection-set.md , docs/adr/0024-isolate-the-git-execution-surface.md and docs/adr/0025-run-verifications-through-an-argv-runner.md. Design: docs/superpowers/specs/2026-09-19-wag-dc-replacement-v1-design.md. Acceptance: docs/superpowers/plans/2026-09-19-wag-dc-replacement-v1-acceptance.md.
The accepted read-only browser profile is Browser Inspect v2, and Browser Verify Approval v1 added a verification proposal under browser.chatgpt.native.verify.v3. Both remain exactly as accepted.
A successor adapter, browser.chatgpt.native.operator.v4 on protocol 4, extends that same proposal class to the three reviewed changes (ADR-0026). Its surface is the accepted seven tools plus five proposal and result operations:
health workspace.open repo.search repo.snapshot file.read
verify.preview verify.result
mutation.preview file.create mutation.result
git.commit git.commit.result
Every consequential tool there is a proposal. git.commit proposes; it does not commit. The coordinators behind them are the same ones the private stdio surface uses, so there is one review contract rather than a browser-shaped copy of it, and the local operator remains the only authority that can turn a proposal into a change.
The browser is never given the operator origin, its bootstrap token, its session cookie or its CSRF token — the discovery file the native host reads carries only the admission URL and its one-time admission bootstrap. A request id is correlation evidence, not a credential: another browser session that knows one cannot read the record, reuse the workspace, or propose against it.
v1, v2 and v3 sessions never acquire v4 authority. The adapter id and protocol revision are distinct precisely so that cannot happen by talking a newer dialect.
web-agent-gateway serve-browser-operator --config <absolute-path>The profile is asked for in the config — repositoryEngineering.mutation and .gitCommit, as on the stdio surface — not implied by the command. The command binds the loopback admission server and the operator review server, and never speaks stdio.
It publishes %LOCALAPPDATA%\WebAgentGateway\browser-adapter-v4.json, which is exactly the path the native host reads by default, so neither side is configured with the other's. That file carries the admission URL and its one-time bootstrap and nothing else. The operator review origin is announced on stderr; its single-use bootstrap is not — that goes to <statePath>.operator-url, mode 0600, removed on shutdown.
The browser side needs the extension loaded and the native host registered per user. On Windows the registration is one HKCU NativeMessagingHosts value under the key for the Chromium-family browser in use. Note, measured on 2026-09-20: Google Chrome refuses to side-load an unpacked extension at all — --load-extension and --disable-extensions-except are ignored in Google-Chrome-branded builds since Chrome 137, and the --enable-unsafe-extension-debugging escape hatch was deleted in Chrome 149. Microsoft Edge 153 still accepts --load-extension; Chromium and Chrome for Testing builds always have. For an automated harness, the documented successor is the CDP Extensions.loadUnpacked command on the browser-level target.
Attachment is automatic. An extension reload orphans the content script in tabs that are already open, so the worker re-injects it into allowed tabs on install and startup, and opening the side panel re-attaches and rescans before it answers — you never have to reload the conversation by hand.
A rescan is idempotent. A proposal is identified by the WAG session, the tab, the provider's own message id, the tool and the exact arguments, so the same assistant message stays one proposal across a DOM rescan, a page reload, a service-worker suspension, an extension reload and a side-panel reopen. Two different messages proposing byte-identical payloads are still two proposals. A turn with no provider message id is refused rather than given an unstable identity.
Two gestures stay human: Run in the side panel, and Approve on the local operator review server. Neither is automated, and they are deliberately on different channels. The native host connects lazily when a proposal is run; there is no manual reconnect step.
The review window defaults to one minute and can be raised to five (repositoryEngineering.mutation.reviewTtlMs). The browser profile usually wants the longer value, because the operator switches windows between those two gestures. The window is not what makes approval safe — approval re-reads the file and refuses on any drift, and a commit revalidates branch, HEAD, tree and identity and moves the ref by compare-and-swap.
Two bounds apply to every proposal, because they bound different things. Each caller may hold 8 live proposals awaiting review, which is what keeps the operator's list legible; and each caller may make 30 proposal attempts per minute, charged before any backend work, which is what a record count cannot bound — a proposal that fails while being computed leaves no record but still ran the planner. On v4 the session correlation must be a server-minted session_<uuid>; rebinding the same one is the service-worker reconnect path, but a correlation a caller chose is not accepted.
The Windows native host is currently a development/pre-release component. Installation changes one per-user Chromium Native Messaging registration and stores exact-owned files under %LOCALAPPDATA%.
See docs/native-host-installation.md for the system-change warning, preparation, verification, and ownership-safe removal procedure.
See docs/policies/code-signing-policy.md for signing scope, manual approval, provenance, official-release rules, and the current pre-SignPath status.
External changes require maintainer review. See CONTRIBUTING.md for contribution, verification, security, and code-signing expectations.
Security reports: SECURITY.md.
Privacy behavior: docs/policies/privacy.md.
Web Agent Gateway is licensed under the Apache License 2.0. See LICENSE.
The Windows native-host executable incorporates Node.js and bundled MIT-licensed JavaScript dependencies. See THIRD_PARTY_NOTICES.md and third_party/native-host/ for exact tracked license material.