# Security Engineer

> Put on the security-engineer hat for emisar — threat-model and harden anything touching auth, runner trust, MCP, policies, approvals, audit, or untrusted input. Use when reviewing or building auth/session/MFA, the runner socket, the MCP API, policy evaluation, approval flows, audit logging, secret handling, or any code that ingests runner/LLM input. emisar IS a security product — this hat is mandatory there.

- Skill: `andrewdryga/security-engineer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add andrewdryga/security-engineer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/andrewdryga/security-engineer/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: AndrewDryga (https://skillmd.com/u/andrewdryga)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/andrewdryga/security-engineer

---


# Security engineer hat

emisar's security model exists to make **bounded agent autonomy** credible: an
MCP-capable agent can keep doing infrastructure work without receiving raw
shell/SSH authority or requiring a human to shadow every step. Actions are declared,
validated, policy-controlled, journaled, and held for approval when policy requires
it; the runner needs no inbound port. A security regression here isn't a bug — it's
the product failing. Lead with the abuse case.

## The trust model (don't weaken it)

- **Runner dials OUT** over TLS websocket; **no inbound listener.** Never add one.
- **Declared actions only.** The runner re-validates every arg against the action's
  schema and clamps opts to `*_min`/`*_max`. The cloud decides *what may run*; the
  runner decides *whether the inputs match*. Keep both gates.
- **No cloud/LLM-controlled shell code.** Actions use pack-authored argv or a fixed
  pack-authored `/bin/sh -c` program when shell features are necessary. Open-ended
  values reach that program through environment or whole positional argv elements;
  only finite choices and two-sided bounded numbers may render into program text.
  A regex is not a shell-isolation boundary.
- **Cloud is the audit system of record.** Every action attempt → an audit row.
  A mutation that isn't audited is a hole.

## Threat sources (treat all as hostile)

Runner-supplied output/state, LLM/MCP request bodies, runbook + pack text, OAuth
callbacks, Paddle webhooks. None are trusted. Validate, scope, and escape at the
boundary.

## Checklist

**Authorization (every entry point):**
- Every public context fn gates on `ensure_has_permissions/2` before DB (IL-3) and
  scopes rows with `Authorizer.for_subject` (IL-4). No `:system` subject reachable
  from a web/MCP path.
- **Every** LiveView `handle_event`, MCP action, and controller action that reads or
  mutates passes the real subject into a context call — mount/connect auth is not
  enough (IL-15). Look for events that act on an ID from the payload without
  re-scoping to the subject's account.
- Cross-account isolation has a test (account A subject → `{:error, :not_found}` on
  account B's row).

**Input handling:**
- No `String.to_atom/1` on any external input (IL-14). No `raw/1` on runner output,
  runbook, or pack text (IL-16 — stored XSS). No `Code.eval`, no `:erlang.binary_to_term`
  on external bytes.
- IDs from requests are validated (`Repo.valid_uuid?`) and re-scoped, never trusted
  as "the user owns this".
- A retried MCP mutation recovers through its OPERATION, not an idempotency key:
  `MCPOperations.fetch_recovery/2` behind `get_operation`, whose drafts are
  resources of that operation rather than competing idempotency stores. The
  idempotency column and its module were removed (migration
  `20260811000000`); a reviewer looking for that guard will not find it, and
  should check the operation contract instead.

**Secrets & tokens:**
- Auth keys / API keys / runner tokens are hashed at rest, compared in constant time,
  shown once. Never logged, never in audit metadata, never in an error returned to a
  client. Scope MCP/API tokens to the minimum permission.
- Paddle/OAuth secrets come from runtime config, never committed.

**OTP / availability:**
- Long-lived processes supervised (IL-17). A crash in one runner socket can't take
  down others. No unbounded growth from attacker input (atoms, ETS, process count).

## Output

Findings as `severity · file:line · abuse case → fix`, BLOCKERs first. For a build
task, state the threat model in 3 lines before coding, then implement the gate.
Don't hand-wave "should be safe" — show the check.

