# a360-mcp — Development Rules

MCP server for Automation Anywhere **Automation 360 (A360) Control Room**.
Purpose is **diagnosis, not execution**: given a failed bot run, answer
*"which action died, why, and what was missing upstream."*

The vendor already ships MCP Inbound (`https://<cr>/mcp`) which **runs** bots.
It does not read execution history, bot logic, WLM queues, or audit logs.
That gap is this project. Do not rebuild bot execution here.

---

## Verified API Facts (do not re-derive — these cost days of research)

### Authentication
- `POST /v2/authentication` — **not `/v1`**. v1 only has `/token`, `/logout`, `/app/login`.
- Body: `{username, apiKey}`. `password` and `apiKey` are **mutually exclusive** (spec `anyOf`).
- Response token is the top-level **`token`** field.
- Header on every later call: **`X-Authorization: <token>`** — **no `Bearer` prefix**.
  `Authorization: Bearer` is OAuth-only; never send both.
- Token TTL **20 min** default. Refresh: `POST /v2/authentication/token` with the current token.
- **Refreshing kills the old token immediately.** Serialize refresh under a mutex and swap
  atomically, or concurrent calls will use a dead token.
- **Max 5 concurrent sessions per account.** A 6th login evicts one. Cache the token; never
  authenticate per request. The debug browser window costs one slot too.
- API key: needs a custom role with `Generate API-Key` (no system role has it).
  Duration configurable 1 min – 9999 days.
- 2FA: A360 v2 `AuthRequest` has **no `mfaCode` field**. Keep API accounts on a non-2FA role.

### Diagnosis path (the core feature)
- `POST /v3/activity/list` — find executions. Filter/sort/page, max page length 10,000.
- `GET /v3/activity/execution/{id}` — carries **`error` → `DetailedError`**, populated only when
  `status == "RUN_FAILED"`, plus a `message` string.
  **Feature-detect the `error` key at runtime and fall back to parsing `message`.**
  Do not branch on Control Room version — the version that introduced `DetailedError`
  is not confirmed, and `DetailedError` is a structured decomposition of `message` anyway.
- `GET /v2/repository/files/{fileid}/content` — bot logic JSON. Walk it to map the failure
  onto an action node.
- **`deploymentId` != execution `id`.** After deploy, locate the run via `/v3/activity/list`
  filtered on `deploymentId`, then use its `id`. (Whether that field is filterable is
  **unverified** — confirm against a live Control Room.)

### Other endpoints
- Bots/files: `POST /v2/repository/file/list`
- Deploy: `POST /v4/automations/deploy` (vendor recommends v4 over v3)
- Devices: `POST /v2/devices/list`
- Audit: `POST /v1/audit/messages/list` — **400 rows per response**, page via offset.
  `AuditMessage` has **no `objectType` field** — do not design around one.
- WLM: `POST /v3/wlm/queues/list`, `.../{id}/workitems/list`.
  **8 WLM endpoints are deprecated as of v.43, EoL at v.46** — new code uses v4 for those.

### Hard limits that shape the design
- **Cloud retention: execution history 90 days, audit log 180 days** (365 in some regions).
  Cloud has **no history→audit backup path**. Therefore: **snapshot diagnosed executions to
  local storage from day one.** This is a correctness requirement, not an optimization.
- UI labels do not match API enums (`Run failed` vs `RUN_FAILED`). Keep a mapping table.
- `Log text to file` output is constrained to `FILE_LOCAL` — it **cannot** be retrieved via
  Control Room or any API. Never promise to read it.
- Execution *trajectory* (loop iteration, branch taken, variable values) is **not available**
  from any API. Only the failure point is.

### Instrumentation prescription (a real feature, not a workaround)
The `Catch` action exposes **`Assign the line number to`** and
**`Assign exception message to`**. Paired with `Log to Server`, a bot emits
`executionId` + line number + exception message as one record. Screenshots likewise only
exist if the bot places `Screen Capture` inside `Catch`.
So when a bot has no Try/Catch, the correct tool output is a **prescription**:
"this bot cannot be diagnosed post-hoc; instrument it like this."

### Reading vendor docs
`docs.automationanywhere.com` is a Fluid Topics SPA that renders Redoc **inside an iframe**.
Start from a `/r/...` page, let `/access?ft:originId=<spec>.yaml&ft:sourceId=redoc` resolve,
then fetch the `htmlpackage` path **same-origin**. Package IDs change on republish — always
cite the human-readable `/r/...` URL, treat package IDs as retrieval mechanics only.

---

## Tool Description Rules (LLM Context Budget)

Tool definitions are sent on every request. Every character costs tokens.

- Tool `description`: max 120 chars. What it does + one usage hint.
- Parameter `description`: max 80 chars. No examples unless critical.
- `Field(description=...)` on every parameter. Undocumented params waste LLM reasoning.
- Never put default string values >60 chars in `Field(default=...)` — the compactor **drops**
  long defaults rather than truncating them (a truncated default that the model copies
  produces a broken tool call).
- Do not duplicate the tool description in the model docstring — the compactor strips it.

## Performance Rules (measured, not assumed)

Upstream measurement on the sibling project (12,936 calls / 8.38M output tokens) settled these:

- **The input/schema axis is saturated.** Merging tools saves ~139 tokens (2.7%) while one
  mis-route costs 500–9,000. **Never consolidate tools on a token argument.**
- **Never trim parameter descriptions** — past auto-truncation removed routing hints and
  caused mis-routes.
- **No meta-router.** A single `a360_exec` entry point destroys schema guidance and forces
  blind parameter guessing.
- An action allowlist only saves tokens when paired with `_FIELDS_BY_ACTION` — properties must
  actually be removed, not just hidden.
- **Output is the whole game.** Compact JSON (orjson), columnar encoding at >=3 rows, empty
  field stripping, byte-based response budget with binary-search stubs, default projections.
- **No eval harness, no consolidation and no description cuts.** Measure with
  `scripts/measure_tool_tokens.py` against real `list_tools()` output.

**Package layering starts lean.** Do not create one god `standard` package that everything
inherits — that is a known 2–3k token/request tax upstream. Layers:
`core` → `bot_read` / `log_read` → `diagnose` → `full`.

## Safety Rules

- Writes are **opt-in per instance** (`allow_writes`, default `False`). A missing flag must
  never mean "writable".
- An unconfirmed mutation touches the network **zero times**. Split guards into pre-confirm
  (local only) and post-confirm (may read live).
- Bot execution / schedule creation is **publish-class**: requires `confirm='approve'` **and**
  a second explicit confirmation, plus target-environment confirmation.
- Every refusal must state the next action. Never refuse without a path forward.
- Credential Vault values are **never** returned in any response — strip the key entirely.
- The browser layer **never clicks**. It observes console/network/screenshots only. If it ever
  gains a click, it stops being read-only and must move under the write guards.

## Testing

- Tests first (RED → GREEN → refactor). Coverage gate `fail_under = 80`.
- Mock at two layers: `make_request` for the HTTP/service layer, and the `a360_api` query
  helpers for the tool layer. Assert **call arguments**, so silent projection changes fail.
- `MagicMock(spec=...)` always — an unspecced mock swallows typos.
- autouse fixtures must isolate: query cache, write journal, workspace roots, session budget.
- **Always run pytest under the memory-capped wrapper.** Unwrapped pytest has killed the
  tmux session on this machine before.

## No Company-Specific Data

Public repo. Never commit real Control Room hosts, bot names, device names, usernames, or
API keys — not in code, tests, fixtures, examples, or comments. Use generic placeholders.
