# Configuration

RiskProof is configured through a Schemastery schema. Invalid values fail at plugin load — there is no half-configured startup.

## Full reference

```yaml
config:
  mode: enforce                 # enforce | observe

  provenance:
    enabled: true
    maxEntries: 256             # entries per session
    maxEntryBytes: 262144       # per entry
    maxTotalBytes: 2097152      # per session
    minMatchLength: 4           # substring-match floor (chars)

  taint:
    enabled: true

  output:
    enabled: true
    blockedTaints: [SECRET, API_KEY]
    trustedDeclassifiers: {}
    # approved_redactor: [PII, CUSTOMER_DATA]

  toolchain:
    enabled: true
    maxEvents: 128
    chainWindow: 12

  classification:
    overrides: {}               # { toolName: [CAPABILITY, ...] }

  policy:
    preset: balanced                 # permissive | balanced | strict
    internalDomains: []            # e.g. ["acme.com"]
    blockedDomains: []             # always denied when named as a sink
    allowedExternalDomains: []     # empty disables allowlist enforcement
    sensitivePathPatterns: []      # e.g. ["**/private/*.asc"]

    # Optional per-rule overrides; leave commented to inherit the preset.
    # sensitiveExternalAction: deny   # allow | ask | deny
    # untrustedPrivateAccess: ask
    # untrustedCodeExecution: deny
    # untrustedLocalMutation: ask
    # credentialAccessAfterUntrusted: deny
    # sensitivePathRead: ask
    # sensitivePathMutation: deny
    # destructiveOperation: ask
    # remoteScriptExecution: deny
    # unlistedExternalAction: ask
    # unknownTool: ask

  proof:
    enabled: true
    maxRecords: 1000
    # file: /var/log/dsh/riskproof.jsonl   # optional, append-only
```

## Modes

- **`enforce`** — RiskProof applies `allow` / `ask` / `deny` and records proofs.
- **`observe`** — RiskProof analyzes, records proofs, and warns, but never changes execution. Use it during initial rollout and false-positive triage.

## Output control and trusted declassification

`output.enabled` evaluates every normalized result in DSH `tools/post-execute`, including
model-facing replacement values, content and additional context returned by downstream
post-processors. The default blocks `SECRET` and `API_KEY`; add any supported taint label
to `blockedTaints` to protect a broader class. Output control has no approval phase because
the tool body has already run: it either accepts the result or replaces it with safe block
feedback. In `observe` mode it records `would_block` and leaves the result unchanged.

`trustedDeclassifiers` is an operator-owned exact tool-name map:

```yaml
output:
  blockedTaints: [SECRET, API_KEY, PII, CUSTOMER_DATA]
  trustedDeclassifiers:
    approved_redactor: [PII, CUSTOMER_DATA]
```

A listed tool may remove only those labels inherited from its arguments. Kind-based labels
from the output tool and deterministic labels detected in the returned value/content are
then added back. Thus a tool configured to declassify `PII` still blocks if its returned
projection contains an email address. Ordinary tools cannot remove labels. Declassifier
configuration is exact-name, bounded and load-time validated; it is not mutable by an agent.

## Policy decisions

Each policy field accepts `allow`, `ask`, or `deny`. `ask` maps to DSH's native approval service; if no approval service is mounted, the DSH registry degrades `ask` to `deny` (fail closed).

Hard invariants are not configurable: cloud metadata/link-local access, catastrophic system operations, credential material in external/network actions, operator-blocked destinations, and confirmed private-data exfiltration remain `deny` under every preset.

## Policy presets

Presets provide a usable starting point; an explicitly configured decision overrides the selected preset.

| Policy | `permissive` | `balanced` (default) | `strict` |
| --- | --- | --- | --- |
| Sensitive external action | ask | deny | deny |
| Private access after ingestion | allow | ask | deny |
| Untrusted code execution | ask | deny | deny |
| Untrusted local mutation | allow | ask | deny |
| Credential access after ingestion | ask | deny | deny |
| Sensitive path read | ask | ask | deny |
| Sensitive path mutation | ask | deny | deny |
| Destructive operation | ask | ask | deny |
| Remote script execution | ask | deny | deny |
| Unlisted external destination | ask | ask | deny |
| Unknown tool | allow | ask | deny |

`permissive` does not bypass hard invariants. It is intended for an initial rollout after `observe`, not as a full-access mode.

## Capability overrides

Capabilities: `EXTERNAL_INGESTION`, `PRIVATE_ACCESS`, `EXTERNAL_ACTION`, `LOCAL_MUTATION`, `CODE_EXECUTION`, `CREDENTIAL_ACCESS`.

```yaml
classification:
  overrides:
    gmail_send: [EXTERNAL_ACTION]
    company_db: [PRIVATE_ACCESS]
    shell: [CODE_EXECUTION]
```

A typo in an override value fails at load — it cannot silently weaken the boundary.

## Internal domains

`policy.internalDomains` marks email domains, URL hosts, and bare host targets as trusted, so `sensitive_data_external_action` does not flag, for example, customer data sent to `colleague@acme.com` or `https://api.acme.com`. Exact domains, subdomains, and `*.example.com` entries are supported. Private IP ranges and `localhost` are internal; cloud metadata and link-local targets are always blocked by the hard invariant.

## Egress domain policy

- `blockedDomains` denies recognized email and URL/bare-host destinations even when the payload has no detected sensitive data.
- `allowedExternalDomains` activates an external-destination allowlist. A recognized external destination outside the list uses `unlistedExternalAction`; an empty list disables this rule.
- `internalDomains` defines the trust boundary for sensitive-data rules and is independent of the allowlist.

Domain entries accept an exact domain/IP or a leading `*.` wildcard. Schemes, paths, credentials, control characters, and interior wildcards fail configuration validation. Destination policy examines recognized sink fields; it is not a DNS firewall and does not govern network traffic hidden inside an opaque tool implementation.

## Sensitive paths

Built-in patterns cover live `.env` variants, Git/network/package-registry credentials, shell histories, process environments, common cloud/container/cluster/developer-service credentials, SSH private keys, private-key/key-store extensions, Vault/Terraform credentials, and service-account files. Safe templates such as `.env.example`, `.env.sample`, and `.env.template` are excluded.

Use `sensitivePathPatterns` to add normalized full-path globs with `*`, `?`, or `**`:

```yaml
policy:
  sensitivePathPatterns:
    - "**/private/*.asc"
    - "**/production/credentials.json"
```

Proof evidence stores only the argument field and sensitivity category, not the raw path.

## Command-risk checks

`CODE_EXECUTION` tools receive bounded high-confidence checks for catastrophic system operations, forced/destructive Git or filesystem operations, remote content piped directly to an interpreter, privilege escalation, opaque PowerShell commands, and network-capable commands carrying credential taint. RiskProof is not a full shell parser; ambiguous or obfuscated syntax remains subject to the unknown/untrusted rules and the host sandbox.

## Proof persistence

Proofs are retained in a bounded in-memory ring by default. Set `proof.file` to append each redacted proof as one JSON object per line. The parent directory must already exist and be writable by the DSH process. Newly created files use mode `0600`; operators remain responsible for directory permissions, rotation, retention, and backups.

Proofs contain rule ids, decisions, risk levels, source ids, taint labels, toolchain state, remediation guidance, and opaque proof ids. They never contain raw tool arguments or results. `ask` and `deny` reasons include the proof id and up to two recommended actions for correlation and recovery. `proofStats()` aggregates retained records by decision, risk, and rule id.

## Validation limits

Configuration fails at plugin load when a value is out of range or inconsistent:

| Field | Maximum |
| ----- | ------- |
| `provenance.maxEntries` | 4096 |
| `provenance.maxEntryBytes` | 4 MiB |
| `provenance.maxTotalBytes` | 64 MiB |
| `provenance.minMatchLength` | 4096 |
| `toolchain.maxEvents` / `chainWindow` | 4096 |
| `proof.maxRecords` | 10000 |
| Each policy list | 256 entries |
| Each policy list entry | 512 characters |
| `output.trustedDeclassifiers` | 256 tools |

`maxEntryBytes` cannot exceed `maxTotalBytes`, and `chainWindow` cannot exceed `maxEvents`.

## v0.3: visible reports and operator task contracts

```yaml
- id: riskproof
  config:
    experience:
      language: zh-CN       # zh-CN (default) | en
    task:
      mode: standard        # standard | read-only | local-only
```

`/riskproof task <mode>` changes only the calling live agent's task contract. New sessions,
subsessions and plugin reloads use the configured default. It cannot disable base rules.
No agent tool can mutate this setting. In `observe` mode all RiskProof findings, including
task and identity findings, remain advisory.

`read-only` rejects LOCAL_MUTATION, EXTERNAL_ACTION, CODE_EXECUTION, and unknown tools.
`local-only` rejects EXTERNAL_INGESTION, EXTERNAL_ACTION, CODE_EXECUTION, and unknown tools.
These rely on configured/classified capabilities; use the host sandbox to contain tools.

The report shows current mode, policy preset, task scope, disabled detection warnings,
and counts for the current agent's retained proofs. `proof.maxRecords` bounds the global
ring across all agents, so counts are not lifetime totals. With `proof.enabled: false`,
reports disclose that no proof accounting is available.

JSONL files now contain original proof records and append-only `riskproof/receipt` events
correlated by `proofId`. Consumers must distinguish the two forms. In-memory proofs carry
the latest receipt. Existing files are not replayed into the live dashboard on restart.

## v0.4: output receipts

Execution receipts may include an `output` object containing only the action (`allow`,
`block`, or `would_block`), taint labels and labels removed by trusted declassification.
An enforced output block settles with `outcome: output_blocked`. No result body is stored
in memory proofs, JSONL receipt events, reports or dashboard payloads.
