# BUG-01 Diagnosis: Secret Exposure Guard Semantic Check Deterministic-Block

**Diagnosed:** 2026-04-11
**Plan:** Phase 1.5 Plan 1 (Governance Runtime Bugfix)
**Symptom:** POST /api/guard returns `decision: "block"` with reason `"Secret Exposure Guard: Semantic check failed (fallback: block)"` for all commands, regardless of content.

---

## 1. Semantic Classifier Location

**File:** `app/lib/guard.js`
**Lines:** 425–449 (case `semantic_check` in `evaluatePolicy`)

```javascript
case 'semantic_check': {
  const instruction = rules.instruction;
  if (!instruction) return null;

  const globalFallback = process.env.DASHCLAW_GUARD_FALLBACK || 'allow';
  const fallback = rules.fallback || globalFallback;
  const model = rules.model || 'gpt-4o-mini';

  const result = await checkSemanticGuardrail(context, instruction, model);

  if (!result) {
    // LLM check failed or was skipped (no key)
    if (fallback === 'block') {
      return { action: 'block', reason: 'Semantic check failed (fallback: block)' };
    }
    return null; // pass-through
  }
  // ...
}
```

The actual semantic classification function is in `app/lib/llm.js` at line 222 (`checkSemanticGuardrail`).

---

## 2. LLM Provider and Model

**Function:** `checkSemanticGuardrail` in `app/lib/llm.js` (line 222–293)
**Provider:** OpenAI (exclusively in this function — does NOT use the multi-provider `tryLLMComplete`)
**Model:** `gpt-4o-mini` (default via `rules.model` in the policy config, or `getDefaultProviderModel('openai', 'semantic_guard')`)
**API Base:** `process.env.GUARD_LLM_BASE_URL || 'https://api.openai.com/v1'`

---

## 3. Environment Variables Required

The `checkSemanticGuardrail` function (llm.js:227) checks:

```javascript
const apiKey = process.env.GUARD_LLM_KEY || process.env.OPENAI_API_KEY;
```

**Neither `GUARD_LLM_KEY` nor `OPENAI_API_KEY` appears in `.env.example` or `.env`.**

This means the function always hits the early-exit guard:

```javascript
if (!apiKey) {
  console.warn('[Guard] No GUARD_LLM_KEY or OPENAI_API_KEY configured. Skipping semantic check.');
  return null;
}
```

---

## 4. Actual Error Reproduced Locally

The function returns `null` (not an error — it's a clean skip). Back in `evaluatePolicy` (guard.js:437):

```javascript
const result = await checkSemanticGuardrail(context, instruction, model);

if (!result) {
  if (fallback === 'block') {
    return { action: 'block', reason: 'Semantic check failed (fallback: block)' };
  }
  return null;
}
```

The policy `gp_178772c27d0f40e69240f82f` ("Secret Exposure Guard") has `rules.fallback = 'block'`.

So the evaluation path is:
1. `checkSemanticGuardrail` → `null` (no API key)
2. `result === null` → check fallback
3. `fallback === 'block'` (from policy rules) → return block decision
4. `applyResult` prepends the policy name → `"Secret Exposure Guard: Semantic check failed (fallback: block)"`

**Actual error:** No real LLM error — it's a missing-API-key skip path that returns `null`, combined with a policy that specifies `fallback: 'block'`. This is a configuration bug, not a code bug in the calling path.

---

## 5. Root Cause Hypothesis

**Root cause: Missing API key (`GUARD_LLM_KEY` / `OPENAI_API_KEY`) combined with an aggressive policy-level fallback (`rules.fallback: 'block'`).**

The policy `gp_178772c27d0f40e69240f82f` was created with `fallback: 'block'` — intentionally fail-closed. But the hosted Vercel instance has no `OPENAI_API_KEY` or `GUARD_LLM_KEY` set in production env vars, so every semantic check silently skips and falls back to block.

**This is NOT a regression from `cd9dbaf5`** (the lazy-openai commit). The `cd9dbaf5` commit only affected `app/lib/embeddings.js` and example files. `app/lib/llm.js` and `app/lib/guard.js` were untouched. The bug existed before and after that commit — the only trigger was (1) enabling the Secret Exposure Guard policy without setting an LLM API key.

---

## 6. Two-Pronged Fix Required

### Fix A (code): Change the global fallback default in `evaluatePolicy`

The `DASHCLAW_GUARD_FALLBACK` env var defaults to `'allow'` via:
```javascript
const globalFallback = process.env.DASHCLAW_GUARD_FALLBACK || 'allow';
```

But the policy overrides this with `rules.fallback = 'block'`. The policy-level fallback should still be respected when an LLM key IS configured and the API returns an error. However, when the skip is specifically because NO KEY IS CONFIGURED, failing closed is wrong — the policy can never work, and the user hasn't intentionally decided to fail-closed; they simply haven't set up the LLM yet.

**Fix:** In the `semantic_check` case, detect the "no key" skip specifically and always fall through to `require_approval` (middle path) rather than honoring `rules.fallback = 'block'`. The existing log line in `checkSemanticGuardrail` identifies this case.

### Fix B (env): Document the required env vars

`GUARD_LLM_KEY`, `OPENAI_API_KEY`, and `DASHCLAW_GUARD_FALLBACK` must be added to `.env.example` with comments so operators know to set them before enabling semantic_check policies.

---

## 7. Scope of Files to Change

- `app/lib/guard.js` — semantic_check case: change fallback logic so "no key" path returns `require_approval`, not `block`
- `.env.example` — add `GUARD_LLM_KEY`, `OPENAI_API_KEY` (alternative), `DASHCLAW_GUARD_FALLBACK` entries

**No `cd9dbaf5` revert needed.** The commit is unrelated to this bug.
