# JUJU — Manual Acceptance Runbook

## 0.8.2 native evidence already exercised vs. remaining manual gates

The 0.8.2 release work executed real native acceptance on the connected Windows
Server x64 / Node v24.21.0 installation and a Debian bookworm arm64 / Node 22
container, in addition to the macOS arm64 repository gate. These are recorded as
automated/native evidence in `TEST_REPORT.md`; they do **not** convert unrelated
manual scenarios into passes.

Already exercised before release: Windows private-state ACL under built-in
Administrator, 10/10 `juju setup --check` components, Playwright Chromium sandbox,
SAPI speech, interactive desktop capture/focus/UI Automation, SRT/WFP five-check
confinement, and a real MCP STDIO session with 74 tools + managed Git + sandboxed
inside/outside write behavior. Linux exercised fresh tarball setup, bubblewrap and
X11/XTEST/X-Resource/AT-SPI desktop behavior under the same-user D-Bus session.

Still manual/unclaimed: external ChatGPT custom-MCP UI, a second Windows OS user,
Windows 11/Node 22 matrix beyond the connected Server host, enabled 8.3 aliases,
Defender/OneDrive permutations, Windows ARM64, macOS Intel and pure Wayland
portal-only desktop behavior. Keep those rows `MANUAL_NOT_RUN` until their own
evidence exists.

The 0.8.0 section below is historical and preserves the original candidate gate.

## 0.8.0 Windows native claim gate — all cases MANUAL_NOT_RUN

Record OS build, architecture, Node version, package SHA-256, exact commands,
results and redacted evidence. Use disposable test workspaces/state for
adversarial cases, not production credentials. A local macOS pass or a mocked
platform value does not close any Windows gate.

| Case | Required native evidence | Current status |
|---|---|---|
| WIN-01 | Windows 11 x64 fresh install; Node 22 and 24; start/stdio/status/stop/kill; real external MCP client; compact/hybrid/full | MANUAL_NOT_RUN |
| WIN-02 | Git Bash and cmd-only environments; npm tasks, native argv, Thai/spaced paths, CRLF/BOM edit/rollback; canceled parent/child jobs | MANUAL_NOT_RUN |
| WIN-03 | Real NTFS ADS, enabled 8.3 volume aliases, mixed case, hardlinks and junctions; all protected-path reads/writes fail closed | MANUAL_NOT_RUN |
| WIN-04 | Repository-planted git.exe/rg.exe/node.exe are never selected as implicit helpers; trusted native LSP server works | MANUAL_NOT_RUN |
| WIN-05 | A second non-admin OS user cannot read private descriptors/keys or dispatch owner commands; bad proof/replay/spoof rejected | MANUAL_NOT_RUN |
| WIN-06 | Defender enabled, file-lock/read-only conflicts, Unicode profile path and OneDrive behavior recorded; no forced overwrite | MANUAL_NOT_RUN |
| WIN-07 | Explicit sandbox requests fail; unsandboxed schedule needs separate owner approval; desktop and speech limitations reported | MANUAL_NOT_RUN |
| WIN-08 | Stop 0.7.x with its old CLI, upgrade, restart and verify retained OAuth grants/ACL/trust; new CLI does not send unauthenticated legacy IPC | MANUAL_NOT_RUN |

`.github/workflows/windows.yml` must first produce real successful Node 22/24
contracts, full core and packaging evidence. Then remove its phase-0
allow-failure setting and configure required checks in the repository before
claiming Windows supported. No check is marked complete merely because the
workflow exists. See [Windows status](WINDOWS_0.8.0.md).

**This file is the owner's runbook for the acceptance checks that a developer (an AI) cannot legitimately perform.** They require a real ChatGPT account that exposes custom‑MCP / developer mode and a user‑managed tunnel on a domain you control. The developer has neither, so every case below ships as **`MANUAL_NOT_RUN`** with steps and expected results — never asserted as passed.

Cross‑references (do not invent commands beyond these):
- CLI surface — `src/cli/main.ts`
- Manual gates MAN‑01..MAN‑08 and the automated matrix — `IMPLEMENTATION_PLAN_AND_ACCEPTANCE.md` §H
- Setup runbook — `JUJU_IMPLEMENTATION_SPEC.md` §17 (17.1 what JUJU does/doesn't, 17.2 manual steps)
- Tunnel walkthrough — `docs/TUNNEL.md`; example ingress — `examples/cloudflared.yml`
- Local proxy fixture — `examples/reverse-proxy-fixture.mjs`

## Status values (JUJU_IMPLEMENTATION_SPEC §20)

Every test carries exactly one status. This file only ever sets the last one.

| Status | Meaning |
|---|---|
| `AUTOMATED_PASS` | An automated test ran and passed on real code (no mocks). |
| `AUTOMATED_FAIL` | An automated test ran and failed. |
| `MANUAL_PASS` | A human ran the manual step in their own environment and it met the expected result, with redacted evidence recorded. |
| `MANUAL_NOT_RUN` | The manual step has not been executed here. **Default for everything in this file.** Not a failure and not a pass. |
| `NOT_SUPPORTED` | Out of scope / capability intentionally absent. |

> **Read this before recording anything.** A green run of the local MCP Inspector or the automated suite **is not** a ChatGPT + tunnel pass (§20). Do not copy an *expected* result into an *actual* result. Do not promote a `MANUAL_NOT_RUN` to `MANUAL_PASS` without evidence you captured yourself.

---

## 0. Prerequisites (run once)

```bash
# Node >= 22.12, git present; ripgrep optional (enables regex search).
node --version
npm ci
npm run build          # tsc + scripts/emit-schemas.mjs -> dist/

# Environment probe (prints NO secrets). Expect node/config-dir/state-db/git ok.
node dist/cli/main.js doctor
```

In every block below, `juju` means the built CLI — if not installed globally, read
it as `node dist/cli/main.js` (or run `npm link` once). Use a **synthetic fixture
project with no secrets** as the workspace for all runs (e.g. `tests/fixtures/` or a
throwaway repo); never point acceptance at a project that holds credentials.

---

## Runbook A — Local end‑to‑end, NO domain (reverse‑proxy fixture)

**Purpose.** Exercise the real OAuth + MCP path through a proxy hop **without**
Cloudflare, a domain, or ChatGPT. `examples/reverse-proxy-fixture.mjs` stands in
for the user tunnel and forwards **all** paths (not just `/mcp`) to JUJU.

**What is already AUTOMATED (no mocks) — you do not need to hand‑drive it:**
The authorization‑code + PKCE dance (`/auth` → `/interaction` → owner approval →
`/token`), 24‑tool `tools/list`, tool dispatch, and both MCP generations are
covered by `tests/integration/m0-transport-oauth.test.ts` and
`tests/compatibility/mcp.test.ts`. The owner‑approval step in that test performs
the exact same state change that `juju auth approve` performs over IPC.

```bash
npm run test:integration     # M0: real transport + real OAuth, real SQLite
npm run test:compat          # both MCP generations, tool catalog
npm run test:security        # metadata discovery, PKCE, audience, Host/Origin
```

**What is still MANUAL in Runbook A:** driving the *real CLI* (`juju start`,
`juju auth add-client`, `juju auth approve` over the live IPC socket) with a real
proxy hop in front, and pointing an actual MCP client at the proxied URL.

### A.1 Bring up server + proxy

```bash
# 1) Global profile: public origin = the PROXY (insecure http allowed for LOCAL TEST ONLY).
juju init --public-url http://127.0.0.1:8788 --dangerously-allow-insecure-http

# 2) In the fixture project directory, grant reversible edits.
cd /path/to/fixture-project
juju trust --mode edit

# 3) Start JUJU (foreground). Binds 127.0.0.1:21730 by default.
juju start
```

```bash
# 4) In a second terminal: proxy 8788 -> 21730, forwarding ALL paths.
node examples/reverse-proxy-fixture.mjs 8788 21730
# prints: reverse-proxy fixture: http://127.0.0.1:8788 -> 127.0.0.1:21730 (all paths)
```

### A.2 Verify health + OAuth metadata through the proxy

```bash
curl -s http://127.0.0.1:8788/healthz
# Expect: {"status":"ok","name":"juju","version":"...","authConfigured":true}

curl -s http://127.0.0.1:8788/.well-known/oauth-protected-resource/mcp
# Expect JSON whose "resource" == "http://127.0.0.1:8788/mcp"

curl -s http://127.0.0.1:8788/.well-known/oauth-authorization-server
# Expect "issuer" == "http://127.0.0.1:8788", code_challenge_methods_supported includes "S256",
# scopes_supported includes juju:read juju:write juju:exec offline_access
```

### A.3 Register a client and drive the flow

```bash
# Register a static client using your MCP client's EXACT callback URL.
juju auth add-client --redirect-uri http://127.0.0.1:6274/oauth/callback --name "local inspector"
# Prints client_id (and client_secret if confidential) ONCE — copy into the client now.
```

Now connect a real MCP client (e.g. the MCP Inspector) to
`http://127.0.0.1:8788/mcp`, choose OAuth, and complete sign‑in. When the browser
reaches consent, approve from the terminal:

```bash
juju auth pending            # verify id + phrase match the browser page
juju auth approve <id>       # browser then finishes automatically
juju auth list               # grant bound to THIS workspace, no tokens shown
```

**Expected (Runbook A).** Metadata resolves through the proxy; consent completes
only after `juju auth approve`; the client lists 33 tools and `project_overview`
returns `root` == the fixture directory. **Status: `MANUAL_NOT_RUN`** for the
hand‑driven proxy path (the equivalent logic is `AUTOMATED_PASS` via the M0 test —
record that reference, not a ChatGPT pass).

---

## Runbook B — Real ChatGPT + user tunnel (the true manual gate)

This is the only path that satisfies MAN‑01..MAN‑08. It needs an account that
actually exposes custom MCP / developer mode and a domain you control. JUJU never
creates the tunnel, DNS, or credentials (§17.1).

### B.1 Named tunnel up (MAN‑02)

Follow `docs/TUNNEL.md` and `examples/cloudflared.yml`. Route **every path** of one
hostname to `127.0.0.1:21730` — OAuth discovery, `/auth`, `/token`, revocation and
the `/interaction` consent pages must all reach JUJU or sign‑in breaks.

```bash
juju init --public-url https://juju.example.com    # HTTPS required (no insecure flag)
cd /path/to/fixture-project && juju trust --mode edit
juju start
# start your cloudflared named tunnel in its own terminal (see docs/TUNNEL.md)
```

Verify over public HTTPS:

```bash
curl -s https://juju.example.com/healthz
curl -s https://juju.example.com/.well-known/oauth-protected-resource/mcp
curl -s https://juju.example.com/.well-known/oauth-authorization-server
```

**Proof:** the three responses above (HTTPS 200, correct `resource`/`issuer`).
**Do not** paste tunnel credentials or the tunnel UUID.

### B.2 Enable custom MCP / developer mode (MAN‑01)

In the account that actually has it, follow the **current** menus — do not trust a
menu path from older docs (§17.2 step 3). Record the plan tier and the menu path
you observed.

### B.3 Add the remote server and register the callback (MAN‑03)

1. Add remote MCP URL `https://juju.example.com/mcp`; choose **OAuth**.
2. Copy the **exact** callback shown in the ChatGPT UI. Register it verbatim:

```bash
juju auth add-client --redirect-uri "<EXACT callback URL from the ChatGPT UI>" --name "chatgpt"
juju auth clients        # confirm the redirect URI matches character‑for‑character
```

Record the **registration mode** used: static client via `juju auth add-client`
(above) *or* a tested CIMD/dynamic registration. A mismatched redirect URI is the
most common failure — it must match exactly.

### B.4 OAuth consent with phrase match (MAN‑03)

Start sign‑in from ChatGPT. At consent, match the browser to the terminal:

```bash
juju auth pending        # compare id + phrase + callback + scopes + workspace root to the page
juju auth approve <id>   # only after the phrase matches; browser finishes sign‑in
juju auth list           # grant present, "(this workspace)", no tokens printed
```

**Proof:** the phrase on the browser equals the phrase from `juju auth pending`;
`juju auth list` shows the grant bound to this workspace. Redact any token,
authorization code, or cookie from screenshots.

### B.5 Overview → list → read (MAN‑04)

From ChatGPT, have the assistant call `project_overview`, then `list_files` and
`read_files` on a fixture file. **Proof:** the returned `root` and file content
match the real fixture (not merely "Inspector worked"); the read hash is
`sha256:<64 hex>`.

### B.6 Preview → apply → test → rollback (MAN‑05)

1. Assistant calls `preview_changes` for a small edit, then `apply_changes`.
2. Assistant starts a test job; approve the **exec** action from the terminal:

```bash
juju pending             # ACTION approvals (exec/job) — distinct from `juju auth pending`
juju approve <requestId> # approve the exact recipe; client retries the same call
```

3. Inspect diff + test result; then roll back via the history/rollback tool.

**Proof:** changeset id + job receipt, test **exit code**, and before/after file
hashes. `juju approve` covers the command approval; `juju auth approve` covers
OAuth only — keep them distinct in evidence.

### B.7 Grant isolation: revoke / reconnect / restart / root‑switch (MAN‑06)

```bash
# REVOKE — access + refresh tokens die with the grant.
juju auth list
juju auth revoke <grantId>
#   Expect: the next ChatGPT tool call fails (401 / invalid_token); reconnect needs fresh consent.

# RECONNECT — re-auth from ChatGPT, approve again.
juju auth pending && juju auth approve <id>
#   Expect: tools work again under a NEW grant id.

# RESTART — grant persists; stale epoch is rejected until the client re-reads overview.
juju stop && juju start
#   Expect: same token still authenticates; calls carrying the old workspaceEpoch return
#   STALE_WORKSPACE until the assistant re-runs project_overview; jobs mid‑flight report
#   interrupted_on_restart with no automatic rerun.

# ROOT SWITCH — point a second server at a DIFFERENT project root.
cd /path/to/other-fixture && juju trust --mode edit && juju start
#   Expect: the first grant/token does NOT read the second root (WORKSPACE_MISMATCH); a second
#   workspace needs its own URL, registration, and consent — grants never leak across projects.
```

**Proof:** post‑revoke denial observed through the *actual* ChatGPT client, and a
cross‑root call rejected with `WORKSPACE_MISMATCH`.

### B.8 Transport mode through Cloudflare (MAN‑07)

Record whether streamed (SSE) or JSON‑only responses are used through the named
tunnel and any timeout behavior. A **Quick Tunnel is not equivalent** and must not
be recorded as this gate (§17.1).

### B.9 Catalog refresh after a schema change (MAN‑08)

Change a tool schema on the server, rebuild/restart, and document what the client
does — some clients cache the tool catalog and need an app **recreate** to pick up
new schemas. Record the observed refresh/recreate behavior; do not assume a server
bug (§17.2).

---

## MAN‑01..MAN‑08 status table

All rows are **`MANUAL_NOT_RUN`** here. Redact tokens, codes, cookies, and tunnel
credentials from every artifact; record the callback URL and registration mode.

| ID | Manual step | Required proof (redacted) | Status |
|---|---|---|---|
| MAN‑01 | Account exposes custom MCP / developer mode | Screenshot/text of feature availability, no personal/secret data; plan tier + menu path recorded | `MANUAL_NOT_RUN` |
| MAN‑02 | User‑managed named tunnel forwards the full hostname | HTTPS `/healthz` + both `.well-known` metadata checks pass; no tunnel credentials in report | `MANUAL_NOT_RUN` |
| MAN‑03 | Real ChatGPT OAuth registration + consent | Flow succeeded; callback URL + registration mode (static / CIMD) recorded; browser phrase == `juju auth pending`; tokens redacted | `MANUAL_NOT_RUN` |
| MAN‑04 | overview / list / read the synthetic project via ChatGPT | Returned `root` + fixture content match reality; read hash `sha256:…`; not just Inspector success | `MANUAL_NOT_RUN` |
| MAN‑05 | preview / apply / test / rollback via ChatGPT | Changeset id + job receipt, test exit code, before/after hashes | `MANUAL_NOT_RUN` |
| MAN‑06 | revoke / reconnect / restart / root switch | Post‑revoke denial via real client; `WORKSPACE_MISMATCH` on cross‑root; grant binding verified | `MANUAL_NOT_RUN` |
| MAN‑07 | Stream or JSON mode through Cloudflare | Transport mode + timeout behavior recorded; Quick Tunnel not treated as equivalent | `MANUAL_NOT_RUN` |
| MAN‑08 | App tool‑catalog refresh after schema change | Client refresh/recreate behavior documented | `MANUAL_NOT_RUN` |

---

## What the developer could NOT do, and why

The developer building JUJU is an AI with **no authorized ChatGPT account that
exposes custom MCP / developer mode and no domain/tunnel it is permitted to
operate.** Running MAN‑01..MAN‑08 would require signing into a personal account and
standing up public infrastructure — actions outside the developer's authority.
Therefore these gates are **`MANUAL_NOT_RUN`**, not failures: nothing was attempted
and refused; the environment to attempt them was simply never the developer's to
use. Marking them `MANUAL_PASS` without the owner's own evidence would be false.

**What the automated suite already proves on real code (no mocks)** — record these
as the `AUTOMATED_PASS` backing for the equivalent logic, never as a substitute for
the ChatGPT + tunnel gate:
- Real OAuth authorization‑code + PKCE against the actual provider and SQLite, with
  workspace‑bound grants — `tests/integration/m0-transport-oauth.test.ts`, `tests/security/auth.test.ts`.
- Both MCP generations (modern StreamableHTTP client and 2025‑era stateless) plus a
  deterministic 24‑tool catalog — `tests/compatibility/mcp.test.ts`, `m0-transport-oauth.test.ts`.
- Tool dispatch and workspace enforcement (`WORKSPACE_MISMATCH` / `STALE_WORKSPACE`)
  and metadata discovery / audience checks — same integration and security suites.

Local Inspector or proxy‑fixture success (Runbook A) confirms the protocol path, not
ChatGPT + a user tunnel (Runbook B). Keep the two separate in any report.

---

## Troubleshooting notes

- **Tool catalog looks stale after a schema change (MAN‑08).** Some clients cache
  the catalog. Refreshing the connection — or **recreating the app/connector** — is
  expected client behavior, not necessarily a server bug. Document what you saw.
- **Quick Tunnel is not a substitute.** Cloudflare Quick Tunnels differ in SSE
  handling and hostname persistence; they may serve as an extra JSON‑only diagnostic
  but must not be recorded as the named‑tunnel OAuth acceptance (§17.1).
- **Route ALL paths, not just `/mcp`.** If sign‑in fails at discovery/consent,
  confirm the tunnel forwards `/.well-known/*`, `/auth`, `/token`, and `/interaction`
  (see `examples/cloudflared.yml`).
- **Redirect URI mismatch** is the most common consent failure — it must match the
  ChatGPT‑shown callback character‑for‑character (`juju auth clients` to compare).
- **Server starts LOCKED.** With no public URL, `/mcp` returns 401 `setup_required`
  and no tools are served — run `juju init --public-url …` first (`juju status` shows `LOCKED`).
- **Cloudflare Access ≠ MCP OAuth.** An Access browser‑login page does not satisfy
  MCP OAuth by itself, and service tokens need headers the client may not send; do not "fix" sign‑in by disabling JUJU auth (§17.1).
- **Never disable auth or widen the workspace root to make a manual step pass.** A
  step that cannot be completed honestly stays `MANUAL_NOT_RUN`.

## Runbook C — local stdio client (Claude Code / Cursor / Codex CLI)

This path needs **no** domain, tunnel or OAuth, and the protocol round-trip is
already `AUTOMATED_PASS` (`tests/integration/stdio.test.ts` drives `juju stdio`
with the real `@modelcontextprotocol/client` stdio transport). What remains
manual is only the client's own UI:

```bash
npm install -g juju-mcp
cd /path/to/project
juju trust --mode trusted --yes
claude mcp add juju -- juju stdio --root "$PWD"      # Claude Code
# Cursor / Claude Desktop / Codex: add {"command":"juju","args":["stdio","--root","/path/to/project"]} to mcpServers
```

| ID | Manual step | Required proof | Status |
|---|---|---|---|
| MAN-09 | client lists JUJU tools after adding the stdio server | screenshot/text of the tool list (43 tools) | MANUAL_NOT_RUN |
| MAN-10 | ask the client to run tests, fix a failure with `edit_file`, re-run with `run_commands` | job receipts + diff in `juju audit` | MANUAL_NOT_RUN |
| MAN-11 | `juju audit` shows the calls; `rollback_changes` undoes one edit | audit lines + restored file bytes | MANUAL_NOT_RUN |


## 0.2 installation identity and local configuration

Local browser fixture: **MANUAL_PASS** (2026-09-09): opened the token-fragment
URL in Codex in-app browser, verified root and inspect mode, granted read to a
fixture client, saw saved confirmation, reloaded and verified the checkbox
persisted. No production credentials or user workspace were changed.

External client acceptance: **MANUAL_NOT_RUN**:

1. Upgrade an existing 0.1 installation, start a project with `juju`, and open
   the private config URL. Verify MCP 21730, config 21731, saved root/mode.
2. Log in from ChatGPT once to acquire a v2 identity grant. Confirm callback
   completion and a successful project_overview.
3. Stop A; cd B; run juju with the same public origin. Confirm B rejects the
   client before local consent. In private config grant that client read only.
4. Reconnect ChatGPT without a new OAuth consent and refresh overview. Confirm
   root B, old workspace context refused, writes refused, reading B succeeds.
5. Remove all B scopes; the next MCP request must fail. Return to A and check
   previously granted A scopes still work. Revoke identity grant via CLI and
   verify both paths reject its token.
6. Run --allow --all and --bypass separately; check displayed effective mode,
   unchanged saved mode, and OAuth/ACL still required. Restart without flags
   and verify saved mode restored. These flags do not change client prompts.
7. Never route a tunnel to 21731. Attempt foreign Host/Origin/proxy headers
   against config: refusal expected. Without the private token no config data
   may be read or changed, including from local requests.
8. Stop cleanly; check both listeners close. Occupy config port intentionally
   in a fixture, start JUJU: expect failure without killing the existing holder.

## 0.3 Local Config dashboard and runtime workspace switch

Automated coverage: `tests/security/workspaceSwitch.test.ts` (WS-01..09) and
`tests/security/localConfig.test.ts` (boundary, CSP, static host).

Browser fixture (AI-driven, isolated config dir and two throwaway projects,
no user workspace touched): **MANUAL_PASS** (2026-09-09) — see the 0.3 delivery
notes for what was exercised (dark/light/system toggle persisted across reload,
desktop and 375 px layouts, copy-button error handling (clipboard writes were
denied by the embedded browser), permissions save with override
messaging, client scope save and revoke with confirmation dialog, real switch
A→B and back with the project_overview hint and refusal states).

External acceptance: **MANUAL_NOT_RUN**:

1. With a real client connected (ChatGPT or Claude.ai) and a job running,
   press "เปลี่ยนโปรเจกต์": expect a 409 refusal naming the running job; the
   job continues; cancel it; switch again: expect success, the client's next
   call refused until you allow the client on the new path, then
   `project_overview` returns the new root.
2. Start a second `juju stdio --root B` for the target directory, then try to
   switch the HTTP server to B: expect "another JUJU process already serves
   that directory"; stop it; switch succeeds.
3. From the new directory run `juju status` / `juju auth pending`: the CLI
   must find the server (IPC socket followed the workspace). From the old
   directory `juju status` must report no running server.
4. Switch back to the original project: saved trust and client scopes of that
   project reappear unchanged; its change history is intact; the other
   project's history is not visible.
5. Kill -9 the server mid-switch (rare): on the next start the target root
   must bootstrap normally and the journal reconcile must not touch the other
   root's changesets.

Legacy clients configured to launch no-args JUJU must change to explicit stdio.
Token expiry/revocation can still require reauthentication; the feature removes
per-project OAuth consent, not normal OAuth lifetime rules.

### 0.3.1 — stale Local Config tabs

Local browser fixture: **MANUAL_PASS**, 2026-09-09. Opened two tabs with the
same private config URL, left an unsaved client permission on project A in one,
switched to B in the other, then submitted the stale A form. The UI reported
`Workspace changed or is switching. Refresh this page before saving.` and did
not silently retry. No production config or client grants were changed.

Repeat after an upgrade: open the new Private config URL, repeat the two-tab
sequence, then reload the old tab and check its root before saving again. A
dialog opened before a switch must also retain its original workspace context
and be rejected if confirmed afterward. Automated WS-07 additionally checks
A→B→A with an old epoch; WS-08 checks disconnect during a live handler; WS-09
checks old HTTP sockets and token revocation across restart.

Live ChatGPT/Claude session after this patch, including reconnect and tool
dispatch on the new root: **MANUAL_NOT_RUN**. Local fixture results do not
establish those external results.

## 0.4 desktop capture/control

Native fixture (2026-09-09, macOS 26.3.1 arm64): helper compilation, selected
window capture and OCR `JUJU FIXTURE 42` passed. Accessibility was false, so the
control permission-refusal path passed; positive click/type/key/drag/scroll/focus
and AX reading remain **MANUAL_NOT_RUN**. See `scripts/test-desktop-native.mjs`.

Local Config browser fixture: Desktop access displayed the timed app grant;
clicking the stop button changed the state to off and showed the success notice.
No live user workspace, OAuth client grant or OS permission was changed.

Owner runbook for positive desktop acceptance:

1. Build/install the 0.4.0 tarball; run `juju desktop setup --request-permissions`
   locally and grant the macOS permissions yourself. Check `juju desktop status`
   from a running workspace; both native permissions should be true.
2. Run `npm run test:desktop:native -- --control`: only the disposable fixture
   window may receive input; OCR must observe `Clicks: 1`. Record exit code.
3. In a disposable project, allow TextEdit control for 5 minutes, connect a
   real MCP client with juju:exec and explicit local exec consent, and exercise
   windows → capture → focus → capture → type Thai/emoji → capture, key chords,
   click, scroll and an in-window drag. Verify each result visually. Some apps
   may ignore Unicode keyboard events; record the app and actual behavior.
4. Move/resize the target or focus another app after capture. Input must refuse
   until a fresh target capture/focus sequence. Test a covered pointer target.
5. Disable via CLI and via Local Config; subsequent desktop calls must refuse.
   For temporary grants, restart/switch project and verify the old grant and
   snapshots are unusable. For explicit `--persist` grants (0.4.1), restart or
   return to that same path and verify the saved app/mode is restored; another
   path remains off, and ALL old snapshots/approvals remain unusable. Disable
   while stopped and confirm the next start stays off. Native input through
   the saved grant remains MANUAL_NOT_RUN until this scenario is performed.
6. Drop the response after an action; retry identical key/arguments: no duplicate
   input. If outcome is uncertain, capture/inspect and never auto-repeat.

All positive live-client scenarios above are MANUAL_NOT_RUN in this release
record until performed. No Windows/Linux desktop backend is claimed.

## 0.4.2 — real GPT session after owner kill (MANUAL_NOT_RUN)

1. With the existing ChatGPT OAuth connector and same public URL, call
   project_overview and record the workspace ID/epoch (never copy raw tokens).
2. From a terminal outside that project run `juju kill`. Expect only JUJU and
   its jobs to stop; existing Terminal/GPT/tunnel processes remain. During the
   outage the endpoint cannot answer tools.
3. Start `juju` again from the same project with the same state/config and
   public URL. Use the existing connector and call project_overview again.
   Expect no new client registration and a new epoch; record whether this
   specific client retries or needs its existing connection retried manually.
4. Exercise refresh after access-token expiry through the client's normal flow.
   A valid refresh grant should continue; revoked/expired refresh grants must
   fail and require authorization. Do not reset state to make this pass.
5. Start a different already-authorized project. Confirm the same identity can
   use only its configured client/path scopes; an unauthorized path remains
   denied. Never silently expand permissions to simulate a seamless switch.

The local automated test uses real OAuth access and refresh tokens through
kill/restart. It does not mark the live ChatGPT/tunnel UI steps above passed.

## 0.4.3 large inputs with external AI clients — MANUAL_NOT_RUN

1. Install the 0.4.3 tarball, run `juju limits --profile large`, restart JUJU or
   the STDIO subprocess. Keep the existing OAuth/URL/config.
2. Ask the client to refresh its tools and call project_overview: expect the
   new epoch and active commandBytes/readFileBytes/requestBodyBytes budgets.
3. Ask it to write HTML/CSS/JS via write_file/edit_file and run a build/test
   with a short command. Expect hashes and real command receipts, not truncated
   source or an unsupported claim that CSS was security-blocked.
4. Exercise a long command and >2 MiB text through the real tunnel/AI client.
   Expect success only within both JUJU and client/proxy budgets. Record actual
   client versions and errors; reducing a call is distinct from granting access.
5. Scope/approval refusal must still block actions. Existing OAuth credentials
   remain subject to expiry/revocation and workspace ACL.

Status: MANUAL_NOT_RUN for ChatGPT/Claude UI, real tunnel limits and Linux/Intel
large-command execution. Local SDK HTTP/STDIO fixtures are recorded separately
in TEST_REPORT; they do not establish external-client input limits.

## 0.6.0 direct MCP use and schedules — MANUAL_NOT_RUN (external clients)

1. Install the local juju-mcp-0.6.0.tgz and restart the selected JUJU process.
   Retain its OAuth/state/config and public URL; do not delete the database.
2. Refresh the real ChatGPT/Claude/Codex client's MCP tool catalog. Expect 49
   tools and no usage_request/status/end or chat/usageId inputs. Remove any
   manually pasted instructions that still require those obsolete fields.
3. Through an already-authorized HTTP connection, call project_overview directly.
   Verify the root. Read/search a fixture, then edit/run according to saved trust.
   No JUJU per-chat allow/phrase step should appear. The AI app may still ask
   its own tool confirmations; record its behavior separately.
4. Repeat with STDIO. Verify CWD/--root and direct overview; inspect mode must
   still require local approval before edits/exec. Desktop must stay disabled
   unless the owner enabled it separately.
5. Start a new conversation on the same connection: access follows the same
   OAuth/path/trust policy. There is no conversation isolation to test or claim.
6. Switch to an unauthorized workspace: expect WORKSPACE_ACCESS_REQUIRED, not
   an OAuth relink solely because path access is missing. Grant local path ACL,
   call overview for fresh context and retry. Stale epoch calls must fail.
7. Remove client path scopes or revoke its OAuth grant; verify denied tools.
   Do not use removed chat/reset commands for revocation.
8. In private Local Config verify Workspace, Permissions, Client access, Desktop
   and Scheduled commands still work, with no chat allowlist/usage form.
9. Create a harmless short-lived cron schedule and inspect/approve it locally.
   It must remain pending before approval, including in trusted/bypass mode.
   Keep HTTP JUJU running without a chat and verify one due job/receipt; revoke
   to stop future runs. Do not use production tasks for this manual test.
10. Sleep/wake and restart across due times: missed slots are skipped and
    uncertain runs are not repeated. Multi-day/DST behavior remains manual.

Local HTTP/STDIO/OAuth fixtures and packaging results are recorded separately
in TEST_REPORT. They do not establish real AI-client UI or tunnel compatibility.

## 0.6.1 real-client folder list — MANUAL_NOT_RUN

1. Upgrade/restart JUJU and reload Private Config. Verify Client access shows
   the current path and only clients that have permission there.
2. Switch to a new folder with no ACL: expect zero cards. Open the add-client
   picker and select the ID used by your existing ChatGPT connection; save
   only the scopes intended for this folder. OAuth re-registration is not needed
   solely because of this UI change.
3. Verify MCP can access only that folder and its configured scopes. Revoke its
   local scopes: card disappears and tool access is denied for this folder.
4. Switch back to the old folder: its client permissions remain. Open the picker
   in one tab, switch root in another, then try saving: expect stale-workspace
   refusal or a reset form, never an unintended grant to the new folder.

Local fixtures cover server/API behavior and browser rendering separately.

## 0.6.2 real-client deletion — MANUAL_NOT_RUN

Use an expendable test connection, not the primary ChatGPT connection. After
installing/restarting JUJU, expand Client access > ล้าง client ที่ไม่ใช้. Verify
IDs and affected-folder counts; cancel once and confirm nothing changed. Select
only the test connection, review the global impact and delete. Its access and
refresh flow must fail; a separate connection must still work. A deleted client
needs fresh registration/authorization to reconnect. Existing jobs may finish.
No real user client was deleted to produce the automated/browser fixture report.

## 0.7.2 hybrid surface — live ChatGPT acceptance: MANUAL_PASS (2026-09-10 evening)

Performed by the owner through the real ChatGPT UI against
`juju start --tools hybrid` (v0.7.2) over the owner's tunnel, after deleting
and recreating the MCP app (a plain Refresh did NOT clear ChatGPT's cached
0.6.3 catalog; the stale cache had earlier produced 18 misleading
"Tool not found" results whose names were exactly the classic-minus-hybrid
set). Post-rescan results, cross-checked against the server audit log:

- ChatGPT ingested the hybrid catalog and exercised ALL 74 capabilities:
  69 full success paths, 5 correct precondition/config refusals
  (approval_status/run_task/git_commit NOT_FOUND per spec;
  media_transcribe NOT_SUPPORTED — no Whisper model installed;
  fetch_url NOT_SUPPORTED — allowWebFetch=false), 0 broken tools.
- Audit log confirms 93 gateway dispatches across all 17 gateways plus 44
  juju_discover calls, and ~70 distinct target operations returning ok,
  including image_view/desktop_capture/browser_action/game_step/workflow_run/
  speech_synthesize/media_extract/preview_refactor through gateways.
- Real behaviors verified by the client, not just ok envelopes: preview→
  apply→rollback round-trip, job_input into a live process, job_cancel,
  desktop capture/accessibility/focus, isolated-browser DOM change, workflow
  step matchedAfter, synthetic MP4 → probe → frame extract → subtitles →
  transcript search → close.
- Cleanup verified server-side: 0 running jobs; one pending schedule proposal
  remains (never executable without local owner approval; expires the same
  evening); handoff/workflow self-test records remain by design (no delete
  operation on this surface).

## 0.7.2 hybrid option

If ChatGPT truncates near ~50 tools and direct tools are preferred, run the
same acceptance below with `juju start --tools hybrid` and expect 49 tools;
verify the FIRST 19 are the compact core and that one direct write_file plus
one gateway call (e.g. juju_media multimodal_status) both work.

## 0.7.1 compact surface — ChatGPT acceptance

Status: **MANUAL_NOT_RUN** (requires the owner's ChatGPT account and running
tunnel). After `npm install -g juju-mcp@0.7.1` and restarting `juju`:

1. In ChatGPT, refresh or recreate the JUJU MCP app and rescan tools.
2. Record how many tools ChatGPT displays; expect the compact 19 (compare with
   the server's `[juju] mcp tool surface | transport=http | surface=compact |
   tools=19` log line — if ChatGPT shows fewer, that difference is
   client-side and now provable).
3. New chat: create a fixture file through `juju_write` (operation
   write_file), read it back with `juju_read`, edit it via edit_file with the
   expectedHash from the read, read again, then delete it.
4. Ask for `juju_discover` with query "transcribe local video" and confirm it
   returns juju_media operations.
5. If Desktop consent exists, call `juju_desktop_view` (operation
   desktop_status) only; do not trigger desktop_action.
6. Record pass/fail per step here with the date.
