Core0 / Nio
Core0
GitHub v2.4.4
Pipeline · Phase 6

# External Scoring — your enterprise policy gets the final word.

Optional. When enabled, Nio GETs your scoring endpoint and uses the returned score in the weighted average. Use this for compliance routing (HIPAA, SOC2), DLP integration, or any policy specific to your org that doesn't belong in shared rules.

Property | Value
--- | ---
Latency | configurable (per-endpoint HTTP timeout)
Scope | All guarded actions (when one or more endpoints enabled)
Endpoints | 0..N, each with its own `weight` in the aggregate
Concurrency | All enabled endpoints called in parallel (`Promise.allSettled`)
Default state | disabled — empty array (`external_analyser: []`)
Short-circuit | any endpoint scoring ≥ active deny threshold → deny immediately, siblings ignored (details)
On auth/HTTP failure | endpoint drops out of the average; the failure is surfaced via diagnostics

## Response contract

Your scoring endpoint must return a JSON body of this exact shape:

    {
      "score":  0.42,      // required: number, clamped to [0, 1]
      "reason": "..."      // optional: string explanation
    }

Anything else — wrong field name, nested score, score as a string, JSON array, non-JSON body — is rejected with an `external_analyser / response_invalid` diagnostic that includes a 200-byte preview of what the endpoint actually returned, so you can see expected-vs-actual at a glance.

nio does not support custom field names, nested paths (`data.score`), or template responses. If your existing scoring service returns a different shape, deploy a thin shim service on your side that maps it to this contract. The shim is typically 10 lines of code and keeps nio's surface area small.

You can verify your endpoint conforms before triggering any hook by running `/nio doctor` — it issues a real request and reports the parsed score, or the validation failure with the body preview.

## Enable

    guard:
      external_analyser:
        # No auth
        - name: scorer_plain
          enabled: true
          endpoint: "https://plain.example.com/score"
          timeout: 3000
          weight: 1.0

        # Bearer / static API key
        - name: scorer_a
          endpoint: "https://a.example.com/score"
          weight: 2.0
          auth:
            type: bearer
            api_key: "sk-..."

        # OAuth2 client_credentials. Pre-register the client with the OAuth
        # provider and paste its credentials below.
        - name: scorer_primary
          endpoint: "https://scoring.example.com/api/scores/agent?agent-name=cc"
          weight: 2.0
          # headers:                    # optional custom headers
          #   X-Tenant-Id: "..."
          auth:
            type: oauth
            oauth_url:     "https://scoring.example.com/oauth"
            client_id:     "..."
            client_secret: "..."

See `guard.external_analyser` for the full schema.

For local development, the repo bundles a mock scorer at `http://localhost:9090`.

## Wire format

Nio issues a `GET` against each endpoint and expects a JSON response with at least a `score` field in [0, 1]. The endpoint URL is the entire request — encode any context the endpoint needs as query parameters (e.g. `?agent-name=X`). Authentication (when configured) flows through the `Authorization: Bearer <token>` header. Response example:

    GET /api/.../scores/agent?agent-name=cc
      Authorization: Bearer eyJ…
    ← { "score": 0.94, "reason": "production bucket" }

## Weights

Each endpoint declares its own `weight` (default 1.0). The final score is the weighted average across all phases that ran:

    final_score = Σ(wᵢ × sᵢ) / Σ(wᵢ)

An endpoint that fails (network / HTTP / auth) drops out of both numerator and denominator — its weight does not pollute the average.

## Per-endpoint short-circuit

Weights only matter when the pipeline reaches the weighted-average step. Phase 6 evaluates short-circuit per endpoint: if any single enabled endpoint returns a score that crosses the active deny threshold (`strict` 0.5, `balanced` 0.8, `permissive` 0.9), the pipeline short-circuits on that endpoint alone and the verdict is deny. Sibling endpoints — even with high weights — cannot pull the verdict back down.

Concretely, suppose three endpoints answer at balanced level:

    scorer_ffwd_agent_1hr     = 0.0953
    scorer_ffwd_agent_10min   = 0.0755
    scorer_ffwd_agent_env     = 0.8898   ← crosses balanced deny (≥ 0.8)

    weighted avg (would have been): (0.0953 + 0.0755 + 0.8898) / 3 ≈ 0.35
    final  : 0.89  (single-endpoint short-circuit)
    verdict: DENY

The `0.89` endpoint short-circuits the pipeline; the two low-scoring siblings do not enter the verdict. This is intentional — a single authoritative DLP / compliance endpoint should be able to deny without being averaged away by quieter scorers. See Scoring · Short-circuit for the full rule set.

If you want the opposite behaviour — every endpoint always participates in a weighted vote — keep each endpoint's individual score below the deny threshold (have your service return at most the warning band) and let nio's aggregator do the maths.

## OAuth identity sharing

When multiple endpoints share the same OAuth identity (`oauth_url` + `client_id` + `client_secret`), they automatically share a single token cache file under `~/.nio/oauth-cache/<host>-<sha256-fingerprint>.json` and a single in-process `OAuthAuthStrategy`. The `/token` POST runs only once even when N endpoints fire concurrently. Endpoints with different `client_id`s get isolated caches. The OAuth client must be pre-registered with the OAuth provider.

## When something goes wrong

Auth failures, HTTP errors, timeouts — none of these block the agent's action. They surface through Nio's diagnostics channel: a structured entry in `~/.nio/audit.jsonl`, a stderr line, and (on the next hook call) a block of `additionalContext` that the agent reads. Run `/nio doctor` to validate every OAuth endpoint with a real `client_credentials` dry-run.

## Use cases

- DLP — your DLP service knows what data classes are in this repo; it can score writes that touch sensitive fields higher.
- Compliance routing — flag commands that touch HIPAA / SOC2 / PCI paths even if they look benign.
- Org allowlists — your internal trusted-domain list, validated per-request rather than baked into config.
- Audit trail signing — sign the decision with your KMS so the audit log is non-repudiable.