# Defense-in-Depth Score: OpenOps

**Repo:** https://github.com/openops-cloud/openops · **Commit:** `0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262` · **Reviewed:** 2026-10-03
**What it is:** No-code FinOps/cloud-ops automation platform with AI assistant that runs workflows against cloud accounts
**Category:** Infrastructure & Ops
**Scored configuration:** Community edition via the shipped docker-compose deployment and .env.defaults, scoring the built-in AI assistant chat (its MCP tool set and the workflow engine it can trigger); the external-agent MCP container (opt-in profile) is footnoted.
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials yes · persistent memory yes · untrusted input yes · third party extensions yes · sub agents no · external communication yes

## Score: 2.1 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L1 | L1 | L0 | L1 | 0.20 | C1-SELFESC | **0.20** | High |
| C2 | Approval gates | L0 | L0 | L0 | L0 | 0.00 | C2-POWERBYPASS | **0.00** | High |
| C3 | Tool & action scoping | L2 | L2 | L1 | L1 | 0.40 | — | **0.40** | High |
| C4 | Code-execution isolation | L2 | L0 | L2 | L0 | 0.25 | — | **0.25** | High |
| C5 | Untrusted input blast radius | L0 | L0 | L0 | L0 | 0.00 | C5-WORSTCASE | **0.00** | High |
| C6 | Memory, context & configuration integrity | L1 | L0 | L2 | L0 | 0.17 | — | **0.17** | High |
| C7 | Third-party extensions | L1 | L1 | L2 | L1 | 0.30 | — | **0.30** | High |
| C8 | Secrets & sensitive-data protection | L0 | L1 | L1 | L0 | 0.12 | — | **0.12** | High |
| C9 | Audit & traceability | L1 | L2 | L2 | L1 | 0.38 | — | **0.38** | High |
| C10 | Limits & kill switch | L1 | L1 | L2 | L1 | 0.30 | — | **0.30** | High |


OpenOps' AI assistant runs its tools with no human approval in the community edition: the approval hook is an empty stub. With a 7-day token minted from the user's session it can re-run any workflow in the project and rewrite stored cloud and AI connections. Its connection-update path is also not locked down and can expose stored credentials. Anything the assistant reads, such as workflow run outputs built from emails, webhooks, tickets or cloud tags, can steer it into these actions, and secret management in the shipped docker-compose defaults is not locked down.

## Critical gaps
- The assistant can rewrite the AWS connection that supplies its own cost-tool credentials (roles, endpoint) via an unapproved PATCH tool. (ASI03; C1) — [packages/server/api/src/app/mcp/mcp-profile.ts:27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L27); [packages/server/api/src/app/ai/mcp/cost-tools.ts:44-47](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/cost-tools.ts#L44-L47)
- No approval gate exists in the community build; flow-run retry and connection patch run unattended. (ASI09, ASI02; C2) — [packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts:3-5](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts#L3-L5); [packages/server/api/src/app/mcp/mcp-profile.ts:26](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L26)
- Cloud CLI steps, bash-wrapped steps and npm installs run unsandboxed in an engine process holding the platform encryption key and cloud credentials. (ASI05; C4) — [packages/openops/src/lib/cli-command-wrapper.ts:14-27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/openops/src/lib/cli-command-wrapper.ts#L14-L27); [packages/engine/src/lib/code-block/code-builder.ts:127-133](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/engine/src/lib/code-block/code-builder.ts#L127-L133); [packages/server/worker/src/lib/engine/engine-pool.ts:241-258](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/worker/src/lib/engine/engine-pool.ts#L241-L258)
- A prompt-injected assistant can rewrite connections and re-run cloud-modifying workflows with no human involved; a related credential-exposure path also exists. (ASI01, LLM01; C5) — [packages/server/api/src/app/mcp/mcp-profile.ts:26](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L26)

## Criterion details

### C1 Identity & least privilege — 0.20 (high)

The assistant's OpenOps tools run with a service token minted from the logged-in user's session. It carries the same user and project, lives for 7 days by default, and is handed to a Python MCP subprocess. The optional AWS cost tools get a connection's raw long-lived access key and secret. The community build never checks the per-route permissions it declares, so the token holds the user's full project authority. The assistant can also rewrite the AWS connection that supplies its own cost-tool credentials, including its roles and endpoint.

- **S L1:** A dedicated SERVICE principal is minted from the user token with identical claims and a 168-hour default lifetime; AWS cost tools receive the connection's raw static keys. — [packages/server/api/src/app/authentication/context/access-token-manager.ts:90-117](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/authentication/context/access-token-manager.ts#L90-L117); [packages/server/api/src/app/authentication/context/access-token-manager.ts:25](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/authentication/context/access-token-manager.ts#L25); [packages/server/api/src/app/ai/mcp/openops-tools.ts:44-55](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/openops-tools.ts#L44-L55); [packages/server/api/src/app/ai/mcp/cost-tools.ts:98-101](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/cost-tools.ts#L98-L101) (verified)
  - *To reach the next level:* No per-capability credentials: read tools and write tools share one full-authority token, and no deterministic authorization gate maps each call to least privilege.
- **C L1:** Only principal type and project id are checked; the `permission` declared on routes is never evaluated, SERVICE is allowed by default, and the AWS cost tools bypass OpenOps authorization entirely by holding raw keys. — searched `rg -n -i permission` in `packages/server/api/src/app/core/security/authz` → 0 hits (No authorization handler reads the per-route `permission` field declared in route policies; community authz only checks principal type and project id.); [packages/server/api/src/app/core/security/authz/simple-authorization-handler.ts:6-9](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/core/security/authz/simple-authorization-handler.ts#L6-L9); [packages/server/api/src/app/core/security/authz/principal-type-authz-handler.ts:14-18](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/core/security/authz/principal-type-authz-handler.ts#L14-L18); [packages/server/api/src/app/ai/mcp/cost-tools.ts:44-47](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/cost-tools.ts#L44-L47) (verified)
  - *To reach the next level:* No single authorization layer covers every tool path (openops API, tables MCP key, AWS keys).
- **D L0:** The default authorization handler has no role or permission enforcement, so the assistant acts with the full authority of whoever opened the chat. — [packages/server/api/src/app/core/security/authz/authorization-handler-factory.ts:4-6](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/core/security/authz/authorization-handler-factory.ts#L4-L6); searched `rg -n -i permission` in `packages/server/api/src/app/core/security/authz` → 0 hits (No authorization handler reads the per-route `permission` field declared in route policies; community authz only checks principal type and project id.) (verified)
  - *To reach the next level:* No narrower default role for the assistant; least privilege is not available in the community build.
- **B L1:** A hijacked assistant can rewrite any project connection and re-run any project workflow, and those workflows hold AWS/Azure/GCP credentials, which means write access across several systems. — [packages/server/api/src/app/mcp/mcp-profile.ts:27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L27); [packages/server/api/src/app/mcp/mcp-profile.ts:26](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L26) (verified)
  - *To reach the next level:* Writes are not confined to one system or to non-destructive operations.
- **Cap:** C1-SELFESC — The chat tool PATCH /v1/app-connections/ lets the model rewrite the AWS connection (roles, endpoint) that cost-tools.ts loads as the assistant's own AWS credential.
- **Notes:** Cap evidence: mcp-profile.ts:27 exposes PATCH; cost-tools.ts:44-47 resolves the assistant's AWS credential from a named project connection; aws/auth.ts:11-29 shows roles and endpoint are connection fields.

### C2 Approval gates — 0.00 (high)

The community build has no approval gate for the AI assistant. The function meant to wrap tools with approval returns the tools unchanged. So the assistant can re-run workflow runs, which may de-provision cloud resources or send messages, and can rewrite stored connections with no human confirming. The workflow engine has its own approval blocks, but they only apply where a workflow author placed them and never gate the assistant's own calls.

- **S L0:** wrapToolsWithApproval returns the tool set unchanged; no approval is requested for any tool. — [packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts:3-5](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts#L3-L5); [packages/server/api/src/app/ai/mcp/tools-context-builder.ts:142-143](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/tools-context-builder.ts#L142-L143); searched `rg -n -i approv` in `packages/server/api/src/app/ai` → 7 hits (All hits are the no-op approval wrapper, its import/call site, and an unused approval-id generator; no gate exists.) (verified)
  - *To reach the next level:* No per-call human approval at all.
- **C L0:** The most powerful assistant actions (flow-run retry and connection patch) reach the API without crossing any gate. — [packages/server/api/src/app/mcp/mcp-profile.ts:26](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L26); [packages/server/api/src/app/mcp/mcp-profile.ts:27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L27); [packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts:3-5](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts#L3-L5) (verified)
  - *To reach the next level:* No tool path traverses an approval gate.
- **D L0:** There is no approval to turn on in the community build; the stub is hard-wired through a tsconfig path alias. — [packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts:3-5](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts#L3-L5); [packages/server/api/tsconfig.json:20-23](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/tsconfig.json#L20-L23) (verified)
  - *To reach the next level:* Approval is not available, let alone on by default.
- **B L0:** A wrongly issued retry re-executes a workflow (e.g. deleting idle cloud resources) and a connection patch overwrites stored credentials with no undo. — [packages/server/api/src/app/flows/flow-run/flow-run-controller.ts:83-88](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/flows/flow-run/flow-run-controller.ts#L83-L88); [packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts:138-150](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts#L138-L150) (verified)
  - *To reach the next level:* No checkpoint, dry-run, or rollback for assistant-initiated actions.
- **Cap:** C2-POWERBYPASS — The most powerful assistant actions (flow-run retry, connection patch) run with no approval in the default configuration.

### C3 Tool & action scoping — 0.40 (high)

The assistant does not get a generic shell or HTTP tool. Its OpenOps tools come from a fixed allowlist of API paths and methods, every call is checked against the API's typed schemas and the user's project, and the table tools are limited to list operations. But the default set still includes two writes that the operator cannot turn off: re-running a workflow and patching a connection. Nothing restricts what a patched connection may contain, so endpoints, base URLs and roles can be set to any value.

- **S L2:** A path+method allowlist builds the tool set and requests pass Fastify/TypeBox schema validation with project scoping, but connection values (endpoint, baseURL, roles) are free-form. — [packages/server/api/src/app/mcp/mcp-document.ts:15-27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-document.ts#L15-L27); [packages/server/api/src/app/mcp/mcp-profile.ts:13-35](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L13-L35) (verified)
  - *To reach the next level:* No allowlist or host validation on security-relevant arguments such as connection endpoints and base URLs.
- **C L2:** OpenOps API tools validate through API schemas and tables tools are filtered to list operations, while the third-party AWS cost servers get no OpenOps-side validation. — [packages/server/api/src/app/ai/mcp/tables-tools.ts:32](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/tables-tools.ts#L32); [packages/server/api/src/app/ai/mcp/cost-tools.ts:98-101](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/cost-tools.ts#L98-L101) (verified)
  - *To reach the next level:* Extension (AWS cost MCP) tools are not wrapped by a shared validation layer.
- **D L1:** The hard-coded community chat profile is read-mostly but ships two write operations enabled with no operator switch; per-query tool selection is done by an LLM router, which is not a control. — [packages/server/api/src/app/mcp/mcp-profile-factory.ts:3-5](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile-factory.ts#L3-L5); [packages/server/api/src/app/mcp/mcp-profile.ts:26](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L26); [packages/server/api/src/app/ai/mcp/llm-query-router.ts:112-121](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/llm-query-router.ts#L112-L121) (verified)
  - *To reach the next level:* Write tools are not disableable and not off by default.
- **B L1:** A misused tool can re-run any workflow run or rewrite any connection in the project, reaching every cloud account those workflows touch. — [packages/server/api/src/app/flows/flow-run/flow-run-controller.ts:83-88](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/flows/flow-run/flow-run-controller.ts#L83-L88); [packages/server/api/src/app/mcp/mcp-profile.ts:27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L27) (verified)
  - *To reach the next level:* No quantity bounds or narrowing to reversible operations.
- **Cap:** none

### C4 Code-execution isolation — 0.25 (high)

Code steps run inside a V8 isolate (isolated-vm) with a memory limit and no host functions, which is a meaningful barrier. The main cloud-operations paths run as ordinary worker subprocesses with no sandbox: AWS/Azure/GCP CLI steps (which the assistant helps draft), bash-wrapped hcledit/yq steps, and npm installs of code-step dependencies with install scripts enabled. The engine process those paths run in holds the platform encryption key, the Redis password and decrypted cloud credentials, and has full network access. Execution-mode configuration is not tamper-resistant.

- **S L2:** Code steps run in an isolated-vm V8 isolate with a memory limit and only copied-in inputs; it is a C++ engine embedded in the engine process, so between basic separation and a memory-safe capability runtime. — [packages/engine/src/lib/core/code/v8-isolate-code-sandbox.ts:25-30](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/engine/src/lib/core/code/v8-isolate-code-sandbox.ts#L25-L30); [packages/engine/src/lib/core/code/v8-isolate-code-sandbox.ts:103-108](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/engine/src/lib/core/code/v8-isolate-code-sandbox.ts#L103-L108) (verified)
  - *To reach the next level:* Not a memory-safe capability runtime or hardened OS/container sandbox, and the isolate run has no execution timeout.
- **C L0:** CLI steps (aws/az/gcloud via execFile), bash -c hcledit/yq steps and npm install of step dependencies run unsandboxed in the worker; the assistant's block chat drafts these CLI commands. — [packages/openops/src/lib/cli-command-wrapper.ts:14-27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/openops/src/lib/cli-command-wrapper.ts#L14-L27); [packages/blocks/terraform/src/lib/hcledit-cli.ts:253](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/blocks/terraform/src/lib/hcledit-cli.ts#L253); [packages/engine/src/lib/code-block/code-builder.ts:127-133](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/engine/src/lib/code-block/code-builder.ts#L127-L133); [packages/server/api/src/app/ai/chat/prompts.service.ts:72-74](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/prompts.service.ts#L72-L74) (verified)
  - *To reach the next level:* The main cloud-command execution paths are not sandboxed.
- **D L2:** SANDBOX_CODE_ONLY is the default, but OPS_EXECUTION_MODE=UNSANDBOXED loads the no-op sandbox without warning, and execution-mode configuration is not tamper-resistant. — [packages/server/shared/src/lib/system/system.ts:83](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/system/system.ts#L83) (verified)
  - *To reach the next level:* Disabling isolation needs no explicit, loudly named flag.
- **B L0:** An isolate escape or any unsandboxed path lands in the engine process whose environment includes OPS_ENCRYPTION_KEY and the Redis password, alongside decrypted cloud credentials and unrestricted network (the worker runs as the non-root node user). — [packages/server/worker/src/lib/engine/engine-pool.ts:241-258](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/worker/src/lib/engine/engine-pool.ts#L241-L258); [packages/server/worker/src/lib/engine/engine-pool.ts:333-335](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/worker/src/lib/engine/engine-pool.ts#L333-L335); [worker.Dockerfile:193](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/worker.Dockerfile#L193) (verified)
  - *To reach the next level:* Credentials and the platform encryption key are present where model-influenced code runs.
- **Cap:** none

### C5 Untrusted input blast radius — 0.00 (high)

The assistant reads workflow run outputs, table rows and remote documentation search results. Those can carry text from webhooks, emails, chat messages, tickets and cloud tags, and they enter the conversation as ordinary tool results with no marking that they are untrusted. A hijacked assistant can re-run workflows without anyone approving. It can also patch stored connections, and that path is not locked down and can leak credentials with no human involved. The shipped content security policy blocks the common trick of leaking data through markdown images.

- **S L0:** Nothing limits a hijacked session: no taint tracking, no approval after untrusted reads, no detection. — searched `rg -n -i 'untrusted|prompt.injection|provenance'` in `packages/server/api/src/app/ai` → 0 hits (No provenance, taint, or injection handling anywhere in the AI chat code.); [packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts:3-5](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/tool-approval-wrapper.ts#L3-L5) (verified)
  - *To reach the next level:* No capability is gated once untrusted content has been read.
- **C L0:** Run outputs, table rows and docs results enter context as plain tool messages with the same standing as everything else. — [packages/server/api/src/app/mcp/mcp-profile.ts:24-25](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L24-L25); [packages/server/api/src/app/ai/mcp/tool-utils.ts:106-115](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/tool-utils.ts#L106-L115) (verified)
  - *To reach the next level:* Untrusted sources are not distinguished from principal input.
- **D L0:** There is no untrusted-input control to configure. — searched `rg -n -i 'untrusted|prompt.injection|provenance'` in `packages/server/api/src/app/ai` → 0 hits (No provenance, taint, or injection handling anywhere in the AI chat code.) (verified)
  - *To reach the next level:* No control exists to be on by default.
- **B L0:** Unattended, an injected instruction can patch connections (through a path that can expose credentials) and re-run cloud-modifying workflows; CSP only blocks image-based exfiltration. — [packages/server/api/src/app/mcp/mcp-profile.ts:27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L27); [packages/server/api/src/app/mcp/mcp-profile.ts:26](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L26); [deploy/docker-compose/nginx.gateway.routing.template:12](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/deploy/docker-compose/nginx.gateway.routing.template#L12) (verified)
  - *To reach the next level:* No human approval before exfiltration-capable or irreversible actions.
- **Cap:** C5-WORSTCASE — B is L0: credential leakage and irreversible workflow re-runs are reachable with no human involved.

### C6 Memory, context & configuration integrity — 0.17 (high)

Chat history is stored in Redis per chat, user and project for 30 days, and it is fed back to the model on later turns, untrusted tool output included. Tools used earlier in a chat stay selected for later turns. The bigger issue is that the assistant can permanently change project-wide connection settings, such as endpoints, base URLs and roles, with no review. Every later workflow run, and every other user of the project, then inherits the change. System prompts are fetched at runtime from the vendor's GitHub main branch.

- **S L1:** The model can write persistent, project-wide connection configuration that changes future workflow behaviour; the only check is that the connection validates, and a telemetry event records the update. — [packages/server/api/src/app/mcp/mcp-profile.ts:27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L27); [packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts:118-131](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts#L118-L131); [packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts:138-150](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts#L138-L150) (verified)
  - *To reach the next level:* Persistent writes are not gated by human approval, provenance, or expiry.
- **C L0:** Neither chat history (including injected tool output), carried-over tool selections, nor connection writes are controlled. — [packages/server/api/src/app/ai/chat/user-message-handler.ts:149-157](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/user-message-handler.ts#L149-L157); [packages/server/api/src/app/ai/mcp/llm-query-router.ts:137-139](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/llm-query-router.ts#L137-L139) (verified)
  - *To reach the next level:* No memory or configuration path has a write control.
- **D L2:** Chat history keys are namespaced by chat, user and project with a 30-day TTL; connection config is shared project-wide. — [packages/server/api/src/app/ai/chat/ai-chat.service.ts:270-277](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/ai-chat.service.ts#L270-L277) (verified)
  - *To reach the next level:* The model can still write the project-wide connection namespace that other users rely on.
- **B L0:** A poisoned connection persists across sessions and all project users and is used automatically by scheduled workflows and the assistant's cost tools. — [packages/server/api/src/app/ai/mcp/cost-tools.ts:44-47](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/cost-tools.ts#L44-L47); [packages/server/api/src/app/mcp/mcp-profile.ts:27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/mcp/mcp-profile.ts#L27) (verified)
  - *To reach the next level:* No review, versioning or rollback of model-written configuration.
- **Cap:** none
- **Notes:** Prompts loaded from raw.githubusercontent.com/openops-cloud/openops/main (system.ts:100-101) are an official vendor source over HTTPS, so no input-trust limit is applied; they are unpinned to the deployed version.

### C7 Third-party extensions — 0.30 (high)

Users cannot add MCP servers or plugins from inside the product. The bundled OpenOps MCP server is pinned to a commit with a frozen lockfile, and the AWS cost servers are pinned to a release tag, though their Python dependencies are re-resolved from PyPI on each build. Code steps install the npm dependencies listed in their package.json at run time, and the assistant often writes that file. The install has no lockfile, no integrity check and install scripts left on, and it runs inside the engine process environment that holds the platform encryption key.

- **S L1:** Code-step npm dependencies (often model-proposed, accepted by the user) install unpinned without integrity checks; awslabs MCP transitive deps resolve fresh from PyPI; only openops-mcp is hash-locked. — [packages/engine/src/lib/code-block/code-builder.ts:127-133](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/engine/src/lib/code-block/code-builder.ts#L127-L133); [Dockerfile:32-56](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/Dockerfile#L32-L56); [Dockerfile:20-27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/Dockerfile#L20-L27) (verified)
  - *To reach the next level:* No pinning or integrity verification for code-step dependencies and most MCP transitive dependencies.
- **C L1:** Only the openops-mcp server is locked (uv --frozen); npm dependencies, the awslabs servers' dependencies and the remote docs MCP are not verified. — [Dockerfile:20-27](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/Dockerfile#L20-L27); [packages/server/shared/src/lib/system/system.ts:102](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/system/system.ts#L102) (verified)
  - *To reach the next level:* Verification does not extend to most extension types.
- **D L2:** No workspace file can add extensions and the AWS cost MCP is opt-in, but code-step dependencies install on first run with the user seeing package names only, not what install scripts will execute. — [packages/engine/src/lib/code-block/code-builder.ts:127-133](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/engine/src/lib/code-block/code-builder.ts#L127-L133); [packages/server/shared/src/lib/package-manager.ts:90-98](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/package-manager.ts#L90-L98) (verified)
  - *To reach the next level:* Adding dependencies does not show what will run, and nothing gates install scripts.
- **B L1:** npm install runs via exec in the engine process, inheriting its environment (OPS_ENCRYPTION_KEY, Redis password); MCP stdio servers do get an explicit minimal env. — [packages/server/shared/src/lib/package-manager.ts:51](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/package-manager.ts#L51); [packages/server/shared/src/lib/exec-async.ts:11](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/exec-async.ts#L11); [packages/server/worker/src/lib/engine/engine-pool.ts:241-258](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/worker/src/lib/engine/engine-pool.ts#L241-L258) (verified)
  - *To reach the next level:* Install-time code is not confined to a scrubbed environment or sandbox.
- **Cap:** none

### C8 Secrets & sensitive-data protection — 0.12 (high)

Stored connection secrets are encrypted at rest, masked in API responses (so the assistant sees them redacted), and masked in logs by default. But secret management in the shipped docker-compose defaults is not locked down. The engine environment also carries the encryption key into every process where workflow code runs. Usage telemetry is sent to the vendor by default, including AI chat error messages. And the patch-connection path can expose stored credentials.

- **S L0:** Secret management in the deploy defaults is not locked down. (verified)
  - *To reach the next level:* Harden secret management in the shipped deployment defaults.
- **C L1:** Logs are redacted and API responses mask connection secrets, but the engine/subprocess environment carries the encryption key and telemetry error payloads are not covered. — [packages/server/shared/src/lib/logger/log-cleaner.ts:34](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/logger/log-cleaner.ts#L34); [packages/server/api/src/app/app-connection/app-connection.controller.ts:111-118](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/app-connection/app-connection.controller.ts#L111-L118); [packages/server/worker/src/lib/engine/engine-pool.ts:241-258](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/worker/src/lib/engine/engine-pool.ts#L241-L258) (verified)
  - *To reach the next level:* Subprocess environments and telemetry are not protected.
- **D L1:** Telemetry defaults to COLLECTOR mode posting events (including AI chat error messages) to telemetry.openops.com; log redaction is on by default but can be switched off by env var. — [packages/server/shared/src/lib/system/system.ts:111-112](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/system/system.ts#L111-L112); [packages/server/api/src/app/telemetry/telemetry.ts:78-80](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/telemetry/telemetry.ts#L78-L80); [packages/server/api/src/app/telemetry/event-models/ai.ts:73](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/telemetry/event-models/ai.ts#L73) (verified)
  - *To reach the next level:* Telemetry is on by default and not strictly content-free.
- **B L0:** Long-lived cloud keys and AI API keys are reachable by every engine subprocess, and through a further path. — [packages/server/api/src/app/ai/mcp/cost-tools.ts:98-101](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/mcp/cost-tools.ts#L98-L101) (verified)
  - *To reach the next level:* Keys are long-lived and model-reachable rather than short-lived and scoped.
- **Cap:** none

### C9 Audit & traceability — 0.38 (high)

There is no audit log. Assistant tool calls and results are kept only in chat history in Redis, which has no timestamps or approver fields, expires after 30 days, can be deleted by the user and is written only at the end of each turn. HTTP request logs record routes and timing but not arguments. Re-run workflows show up in the normal run history.

- **S L1:** Chat history holds structured tool calls and outputs but no timestamps or actor attribution; request logs have timestamps but no arguments. — [packages/server/api/src/app/ai/chat/user-message-handler.ts:149-157](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/user-message-handler.ts#L149-L157); [packages/server/api/src/app/server.ts:28-34](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/server.ts#L28-L34); searched `rg -n -i audit` in `packages/server/api/src/app/` → 0 hits (No audit-log module in the community server.) (verified)
  - *To reach the next level:* No single structured per-call record with arguments, status and timestamps.
- **C L2:** Every tool the assistant used in a turn, including the MCP-backed ones, lands in chat history. — [packages/server/api/src/app/ai/chat/user-message-handler.ts:149-157](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/user-message-handler.ts#L149-L157); [packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts:138-150](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts#L138-L150) (verified)
  - *To reach the next level:* Connection changes and credential use are not recorded locally (only as vendor telemetry).
- **D L2:** History is on by default and stored outside any workspace, but the user can delete it and it expires after 30 days. — [packages/server/api/src/app/ai/chat/ai-chat.service.ts:270-277](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/ai-chat.service.ts#L270-L277); [packages/server/api/src/app/ai/chat/ai-chat.service.ts:286-290](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/ai-chat.service.ts#L286-L290) (verified)
  - *To reach the next level:* The record is not written by a component the user/agent context cannot delete.
- **B L1:** History is persisted once the multi-step turn finishes (or on abort); tool actions already executed are lost from the record if the process dies mid-turn. — [packages/server/api/src/app/ai/chat/user-message-handler.ts:149-157](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/user-message-handler.ts#L149-L157) (verified)
  - *To reach the next level:* Records are not flushed per action.
- **Cap:** none

### C10 Limits & kill switch — 0.30 (high)

Each chat message is capped at 100 model steps, and closing the browser stream aborts the loop. There is no time, token or spend cap; token usage is only logged. Workflows the assistant re-runs execute separately, under the platform's 600-second flow timeout, and keep running after the chat stops.

- **S L1:** Only a step cap (stopWhen stepCountIs) plus a cooperative abort on client disconnect. — [packages/server/api/src/app/ai/chat/llm-stream-handler.ts:90-91](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/llm-stream-handler.ts#L90-L91); [packages/server/api/src/app/ai/chat/chat-request-router.ts:50-55](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/chat-request-router.ts#L50-L55); searched `rg -n -i 'maxTokens|max_tokens|costLimit|budget|timeout'` in `packages/server/api/src/app/ai` → 1 hits (The only hit is the NodeJS.Timeout type of an SSE heartbeat interval that keeps the stream open; there is no token, cost, or wall-clock cap on the chat loop.) (verified)
  - *To reach the next level:* No wall-clock or token/cost cap on the chat loop.
- **C L1:** The limit covers the chat loop only; retried workflow runs execute under their own separate timeout. — [packages/server/api/src/app/flows/flow-run/flow-run-controller.ts:83-88](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/flows/flow-run/flow-run-controller.ts#L83-L88); [packages/server/shared/src/lib/system/system.ts:69](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/system/system.ts#L69) (verified)
  - *To reach the next level:* Spawned workflow runs do not count against the chat's budget.
- **D L2:** The default of 100 steps is operator-configurable and the model cannot raise it. — [packages/server/shared/src/lib/system/system.ts:109](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/shared/src/lib/system/system.ts#L109) (verified)
  - *To reach the next level:* No hard ceiling that configuration cannot exceed, and no default spend limit.
- **B L1:** Usage is only logged, there is no spend ceiling, and stopping the chat leaves any re-run workflows executing. — [packages/server/api/src/app/ai/chat/user-message-handler.ts:190-193](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/user-message-handler.ts#L190-L193); [packages/server/api/src/app/ai/chat/chat-request-router.ts:50-55](https://github.com/openops-cloud/openops/blob/0e9082b1d0fd7e6cf0c20c0a446afb1ba8402262/packages/server/api/src/app/ai/chat/chat-request-router.ts#L50-L55) (verified)
  - *To reach the next level:* Stopping does not cancel work the assistant started.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: Workflow run outputs (webhook, email, chat, ticket and cloud data) via GET /v1/flow-runs/{id} - packages/server/api/src/app/mcp/mcp-profile.ts:25 · [B] sensitive data/systems: Project connections and AWS cost data with raw access keys - packages/server/api/src/app/ai/mcp/cost-tools.ts:99 · [C] state change / egress: Retry any flow run and PATCH any app connection, no approval - packages/server/api/src/app/mcp/mcp-profile.ts:26-27 · Same default session? Yes

## Highest-impact improvements
1. Implement the community tool-approval wrapper: require a per-call human approval showing exact arguments for every non-GET tool (flow-run retry, connection patch, blocks options). — C2 S L0→L3, +0.225 before caps (Playbook 5)
2. Drop PATCH /v1/app-connections/ and POST /v1/flow-runs/{id}/retry from the chat profile so the default assistant is read-only, removing the unattended state-changing paths. — C5 B L0→L2, +0.100 before caps (Playbook 1)
3. Harden secret management in the shipped deployment defaults. — C8 S L0→L2, +0.150 before caps (Playbook 4)
4. Run CLI-block commands and code-block npm installs (with --ignore-scripts) in an isolated sandbox without engine secrets, not as plain worker subprocesses. — C4 C L0→L2, +0.150 before caps (Playbook 3)
5. Add a per-message wall-clock and token/cost cap to the chat loop alongside the 100-step limit. — C10 S L1→L2, +0.075 before caps (Playbook 3 step 3)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, or probed.
- The openops-mcp Python server (separate repo, pinned at ad6085759ef2cc3073113ee97a3d457f66dde6f6 in Dockerfile) and the awslabs cost MCP servers were not reviewed; that every chat-profile operation becomes a tool is taken from the in-repo profile and its description (mcp.controller.ts:21).
- Enterprise-edition overrides (tsconfig alias for the approval wrapper, authorization handlers, MCP profiles) are outside this repo and were not credited; scores describe the community build.
- The Helm chart moved to a separate repository and the docs-hosted install script are not in this repo; whether the install script changes the shipped defaults could not be verified.
- One credential-exposure path is partly inferred from naming and shared contracts.
- Side note (correctness bug): cost-tools.ts:25 uses assignment (c.name = AWS_COST_MCP_CONFIG_NAME) instead of comparison, so the first MCP config of any name is used.
- No reviewer-directed prompt injection was found in AGENTS.md, CLAUDE.md, README or ai-prompts.
