Skip to main content
Glama
README.md
# winops

An MCP agent that diagnoses Windows service and process faults through
**read-only, least-privilege PowerShell endpoints** — and cannot change
anything without a human saying yes.

![CI](https://github.com/HariSankar27/winops-agent/actions/workflows/ci.yml/badge.svg)

## The problem

The easy way to build an ops agent is to hand an LLM a PowerShell session and
a system prompt saying "be careful." That gives a model that read an untrusted
event log the ability to run anything, and makes "be careful" the only thing
between a hallucination and a production outage.

winops assumes the model *will* eventually be wrong or manipulated, and makes
that survivable: it constrains what the agent can even reach, and separately
gates the few things that change state.

## Two independent safety layers

**1. The agent can only reach six functions.** A PowerShell
[JEA](https://learn.microsoft.com/en-us/powershell/scripting/security/remoting/jea/overview)
endpoint runs as a `RestrictedRemoteServer` (NoLanguage mode) exposing exactly
six named, parameter-validated functions. Not `Invoke-Expression`, not raw
cmdlets, not the filesystem — regardless of what the agent asks for.

| Read-only | Mutating (approval-gated) |
|---|---|
| `Get-WinOpsServiceStatus` | `Restart-WinOpsService` |
| `Get-WinOpsFailedServices` | `Stop-WinOpsProcess` |
| `Get-WinOpsProcessInfo` | |
| `Get-WinOpsRecentErrors` | |

**2. The two mutating tools always pause for a human.** They're marked
`always_require_approval` on the MCP tool, so the agent can investigate
freely and forever, but acting requires an explicit yes.

Neither layer trusts the other. A bug in the approval logic still leaves the
blast radius at "restart one named service"; a mis-scoped JEA role still
can't act without a human. See
[docs/decisions/0002](docs/decisions/0002-two-independent-safety-layers.md).

There's a third, quieter layer: `invoke.ps1` takes the function name as a
PowerShell `[ValidateSet(...)]`, so even a fully compromised MCP server can
only pick one of six branches — it can't inject a command name.

## Architecture

```mermaid
flowchart LR
  CLI[winops CLI] --> Agent[Microsoft Agent Framework]
  Agent -->|MCPStdioTool| MCP[MCP server]
  MCP --> Client[jea.client]
  Client -->|powershell.exe, ValidateSet| Session[Invoke-Command]
  Session -->|PSRemoting, NoLanguage| JEA[(JEA endpoint)]
```

Full write-up in [docs/architecture.md](docs/architecture.md).

## Quickstart

One-time setup, in an **elevated** PowerShell (registers the JEA endpoint,
creates the `WinOpsOperators` group, enables PSRemoting):

```powershell
.\jea\register.ps1
```

Verify the endpoint is really constrained before trusting it:

```powershell
# should work
Invoke-Command -ConfigurationName WinOps -ComputerName localhost -ScriptBlock { Get-WinOpsFailedServices }
# should fail - proof the session is locked down
Invoke-Command -ConfigurationName WinOps -ComputerName localhost -ScriptBlock { Get-ChildItem C:\ }
```

Then:

```bash
cp .env.example .env          # add OPENAI_API_KEY (or AZURE_ENDPOINT)
uv sync
uv run winops diagnose-service Spooler
```

The agent investigates with the read-only tools, then proposes a fix and
waits for `y/n` before anything runs.

## Project status

| Area | State |
|---|---|
| JEA role files, MCP server, agent, CLI | Built, 13 tests passing |
| MCP tool + approval-loop logic | Tested with fakes (no API key needed) |
| The JEA endpoint itself | **Not yet run** — `register.ps1` needs elevation |
| Real LLM calls | **Not yet run** — no API key in the build environment |
| Fault-injection evals, audit log, replay | Not built (M4–M6) |

The `$using:` parameter passing in `invoke.ps1` is the documented way to send
values into a NoLanguage session, but it hasn't been exercised against a live
endpoint here — that's the first thing to check after `register.ps1`.

## Why Windows services, not Hyper-V

This was originally scoped around Hyper-V VM faults. Hyper-V isn't available
on Windows Home editions at all — a hard SKU-level restriction, not something
a package manager can install around — so the managed domain moved to services
and processes. Everything that makes the project interesting (MCP, JEA least
privilege, approval-gated mutation) is unchanged; retargeting to Hyper-V means
writing a new `WinOpsHelpers.psm1` with `Get-VM`/`Restart-VM` and a matching
role capability file. Nothing in `jea/client.py`, `mcp_server.py`, or
`agent.py` mentions services by name. See
[docs/decisions/0001](docs/decisions/0001-domain-swap-from-hyper-v.md).

## Safety and limitations

- The agent cannot see or touch the filesystem, the registry, other services'
  configuration, or anything else outside those six functions.
- Every JEA session is transcribed to `C:\ProgramData\WinOps\Transcripts`.
- Approval is per-call and never remembered; there's no "approve all".
- Tested against a **local** endpoint. Managing a remote host means pointing
  `JEA_COMPUTER_NAME` at it and dealing with WinRM auth, which isn't set up here.

## License

MIT