openlatch-provider 0.2.2

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
# Authoring a tool — Python (FastAPI)

The simplest tool is a FastAPI server that:

1. Verifies the inbound webhook's HMAC signature.
2. Parses the event.
3. Runs detection.
4. Returns a verdict.

The runtime's localhost proxy handles transport — your tool just needs
to verify the signature and respond within the deadline.

## Scaffold

```bash
openlatch-provider new tool --template python --out my-tool
cd my-tool
uv sync
```

The template includes a `main.py` with the verification + verdict
shape pre-wired.

## Minimum viable tool

```python
import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException

WHSEC = bytes.fromhex(open(".secret").read().strip())  # whsec_live_… decoded

app = FastAPI()

@app.post("/event")
async def on_event(request: Request):
    headers = request.headers
    body = await request.body()

    webhook_id = headers["webhook-id"]
    webhook_ts = int(headers["webhook-timestamp"])
    signature = headers["webhook-signature"]  # "v1,<b64>"

    if abs(time.time() - webhook_ts) > 300:
        raise HTTPException(401, "timestamp skew")

    expected = hmac.new(
        WHSEC,
        f"{webhook_id}.{webhook_ts}.".encode() + body,
        hashlib.sha256,
    ).digest()
    presented = signature.split(",", 1)[1]
    import base64
    presented_bytes = base64.b64decode(presented)
    if not hmac.compare_digest(expected, presented_bytes):
        raise HTTPException(401, "signature mismatch")

    event = await request.json()

    # ---- detection ----
    return {
        "riskScore": 5,
        "verdictHint": "allow",
        "ruleId": None,
        "rationaleSummary": None,
        "latencyMs": 1,
    }
```

## Verdict shape

Per `schemas/platform/provider-call.schema.json`:

| Field | Type | Notes |
|---|---|---|
| `riskScore` | int 0-100 (nullable) | |
| `severityHint` | `"low"` / `"medium"` / `"high"` / `"critical"` | |
| `verdictHint` | `"allow"` / `"approve"` / `"deny"` | NOT `"block"` / `"flag"` |
| `ruleId` | string ≤120 | |
| `rationaleSummary` | string ≤500 | |
| `userFacing` | object | headline ≤120, body ≤2000, evidence ≤16 |
| `enrichment` | object | Arbitrary JSON. |
| `latencyMs` | int ≥0 | Tool-side measured time. |

Don't echo back `bindingId` / `eventId` / `category` — the platform
infers those from the request.

### Per-action scoring + prior config state (v2)

Optional. When the platform extracts discrete agent actions it expects a
parallel `actions[]` array keyed by a deterministic `actionRef`
(`"{kind}:{index}"`, kind ∈ `domain|file|command`):

```python
from openlatch_tool_sdk import ActionScore, score_to_severity

risk = 87
return Verdict(
    risk_score=risk,
    severity_hint=score_to_severity(risk),
    verdict_hint="deny",
    actions=[
        ActionScore(
            action_ref="cmd:0",
            risk_score=risk,
            severity=score_to_severity(risk),       # MUST use the helper
            threat_category="shell_dangerous",      # routing 12-category
            axes={"destructive": 18, "exfil": 0, "secret": 0,
                  "privesc": 0, "reversibility": 20},
        )
    ],
)
```

- `actions` is optional and capped at 256 items; omit it and per-action
  risk is recorded null (gap-tolerant).
- **`severity` must come from `score_to_severity()`** — the platform
  persists it verbatim, so first-party tools keep the canonical
  `<40 / 40-69 / 70-89 / 90+` buckets true by construction.
- Stateful config / tool-integrity detectors receive prior artifact state
  as `event.prior_config_state` (the CloudEvents `priorconfigstate`
  extension) — but only when the capability declares
  `needs_prior_config_state: true` in `openlatch.yaml`.

## Best practices

- **Verify before parsing.** A malformed signature must short-circuit
  before `request.json()`.
- **Constant-time compare** via `hmac.compare_digest`, not `==`.
- **Cap response size** — body must be ≤ 250 KB. Truncate
  `userFacing.evidence` first.
- **Honor the deadline** in the `X-OpenLatch-Deadline-Ms` header.
  Bail with a 504 if you can't return in time; the runtime forwards a
  `OL-4228` to the platform.
- **No PII in logs.** The runtime audit log already records
  outcome metadata; your tool should match (latency, verdict_hint —
  never the payload).

## SDK

The `@openlatch/tool-sdk` (TS) and `openlatch-tool-sdk` (PyPI) ship a
ready-made FastAPI dependency that handles verification + verdict
shaping. See `tool-authoring/node.md` for the TS counterpart.

## Test against the runtime locally

```bash
# Start your tool:
uv run uvicorn main:app --port 8000

# Start the runtime against your manifest (with local_endpoint
# pointing at http://localhost:8000/event):
openlatch-provider listen --port 8443

# Send a synthetic event:
openlatch-provider trigger --binding bnd_yourid
```

`trigger` builds a synthetic event, HMAC-signs it with the local
binding secret, and POSTs to your running runtime. Verdict round-trip
+ audit log line lands in `~/.openlatch/provider/logs/`.