Skip to content

healthclaw-guardrails: agent guardrails in front of Aidbox - #43

Open
aks129 wants to merge 1 commit into
Aidbox:mainfrom
aks129:add-healthclaw-guardrails
Open

healthclaw-guardrails: agent guardrails in front of Aidbox#43
aks129 wants to merge 1 commit into
Aidbox:mainfrom
aks129:add-healthclaw-guardrails

Conversation

@aks129

@aks129 aks129 commented Aug 14, 2026

Copy link
Copy Markdown

Adds aidbox-integrations/healthclaw-guardrails/ — Aidbox as the system of record, with a guardrail proxy in front of it enforcing four things the FHIR authorization model does not express: redact on read, audit every access, step up on writes, and hold anything irreversible for a human.

AI agent ──▶ MCP server ──▶ Guardrail proxy ──▶ Aidbox
  :3001                        :5000              :8080

Nothing about the Aidbox side is unusual, and that is the point. The layer is additive; Aidbox keeps behaving like a FHIR server and still holds the complete, fully-identified record. Companion to an article written with Health Samurai.

What it demonstrates

scripts/walkthrough.sh runs six steps and fails loudly when a property does not hold, rather than printing OK whatever happens:

0 the proxy is in upstream mode, and Aidbox refuses anonymous callers
1 the same Patient — identified from Aidbox, redacted through the proxy
2 the read left an AuditEvent, and the AuditEvent carries no PHI
3 a write, and two gates that do not substitute for each other
4 the guardrail conformance grade for this deployment
5 the MCP tool surface: 401 unauthenticated, 27 tools authenticated

Step 1, live:

direct from Aidbox:  "name": [{"given": ["Maria"], "family": "Alvarez"}],
                     "identifier": [{"value": "MRN-88214"}],
                     "birthDate": "1974-03-11",
                     "address": [{"line": ["221 Baker St"], "city": "Pittsburgh"}]

through the proxy:   "name": [{"family": "A.", "given": ["M."]}],
                     "identifier": [{"value": "***8214"}],
                     "birthDate": "1974",
                     "address": [{"state": "PA"}]

Step 3 is a four-row matrix rather than two refusals in sequence, because a sequence only shows that some refusal happened:

X-Human-Confirmed X-Step-Up-Token
428 confirmation missing
true 401 a confirmation is not a credential
valid 428 a credential is not a confirmation
true valid 201

The write is then confirmed by querying Aidbox directly — the proxy reporting its own 201 says nothing about storage.

Two things stated rather than smoothed over

The grade is B (6/7), not A. In upstream mode the error-fidelity property measures how Aidbox answers an unknown search parameter (404/502) rather than how the guardrail does. Step 4 asserts that every other property holds, so the assertion stays correct on the day that gap closes. Tracked as HealthClawGuardrails#498.

The human-confirmation header is set by the caller, so it evidences a human the way a checkbox does. It is documented as a compensating control, not proof. The README's What this example does not show section covers this and two others.

Notes for reviewers

  • Aidbox activation. BOX_LICENSE is passed through as a bare env key, deliberately: an empty licence is worse than a missing one — Aidbox refuses to boot with "License is invalid ... not in correct format" — so absent from .env means absent from the container. With it unset, docker compose up -d waits on Aidbox's health check while you click Continue with Aidbox account, then starts the proxy on its own.
  • The health check requires a literal 200, not curl -f. An unactivated Aidbox answers /health with a 302 to its activation page, and curl -f only fails on 4xx/5xx — so the obvious check reports an unactivated Aidbox as healthy.
  • The MCP server requires MCP_AUTH_TOKEN and refuses to start without one. MCP_PUBLIC_DEMO=true is the documented alternative and is deliberately not used here.
  • qa/ holds a Playwright harness that asserts the same properties from a browser and records the run, so the recording and the assertions come from the same requests.

Verification

Run end to end against healthsamurai/aidboxone:edge on the published ghcr.io images — all six steps green, including a fresh docker compose pull to confirm the pinned tags resolve.

Guardrail proxy is MIT: https://github.com/aks129/HealthClawGuardrails

Aidbox holds the record. An AI agent talks to a guardrail proxy in front of
it, and the proxy enforces four things the FHIR authorization model does not
express: redact on read, audit every access, step up on writes, and hold
anything irreversible for a human. The Aidbox side is ordinary, which is the
point — the layer is additive, and Aidbox still holds the complete,
fully-identified record.

Companion to an article written with Health Samurai.

scripts/walkthrough.sh runs the six steps and FAILS LOUDLY when a property
does not hold, rather than printing OK whatever happens:

  0. the proxy is in upstream mode and Aidbox refuses anonymous callers
  1. the same Patient identified from Aidbox and redacted through the proxy
  2. the read left an AuditEvent, and the AuditEvent carries no PHI
  3. a write, and two gates that do not substitute for each other
  4. the guardrail conformance grade for this deployment
  5. the MCP tool surface: 401 unauthenticated, 27 tools authenticated

Step 3 is a four-row matrix rather than two refusals in sequence, because a
sequence only shows that SOME refusal happened. Presented alone, a human
confirmation is not a credential (401) and a credential is not a confirmation
(428); only both write (201). The write is then confirmed by querying Aidbox
directly — the proxy reporting its own 201 says nothing about storage.

Step 4 reports B (6/7), not A, and says why. In upstream mode the error
fidelity property measures how Aidbox answers an unknown search parameter
rather than how the guardrail does, so it is stated rather than graded away.
The assertion is that every OTHER property holds, which stays correct when
that gap closes.

qa/ contains a Playwright harness that asserts the same properties from a
browser and records the run, so the recording and the assertions come from
the same requests.

Verified end to end against Aidbox edge on the published images.
@aks129

aks129 commented Aug 14, 2026

Copy link
Copy Markdown
Author

Adding what was actually run, so reviewers can see where the claims come from and where they stop.

Verified end to end

Against healthsamurai/aidboxone:edge on the published ghcr.io images, with a fresh docker compose pull first to confirm the pinned tags resolve rather than reusing anything local:

0. Preflight        PASS  proxy in upstream mode and connected;
                          Aidbox refuses anonymous callers (401)
1. Both ways        PASS  Aidbox holds the identified record; the agent's path does not
2. Audit            PASS  AuditEvent written, and PHI-free
3. Write matrix     PASS  428 / 401 / 428 / 201, confirmed by querying Aidbox
4. Grade            PASS  B (6/7); only error fidelity fails, and it is stated
5. MCP              PASS  401 unauthenticated, 27 tools authenticated

Step 1, verbatim from that run:

direct from Aidbox:  "name": [{"given": ["Maria"], "family": "Alvarez"}],
                     "identifier": [{"value": "MRN-88214"}],
                     "birthDate": "1974-03-11",
                     "address": [{"line": ["221 Baker St"], "city": "Pittsburgh"}]

through the proxy:   "name": [{"family": "A.", "given": ["M."]}],
                     "identifier": [{"value": "***8214"}],
                     "birthDate": "1974",
                     "address": [{"state": "PA"}]

The qa/ directory in this PR holds a Playwright harness that asserts the same six properties from a browser and records the run, so the recording and the assertions come from the same requests. A 14-second capture of one run: aidbox-guardrails-demo.mp4.

What is deliberately not claimed

  • The grade is B, not A. In upstream mode the error-fidelity property measures how Aidbox answers an unknown search parameter (404/502) rather than how the guardrail does. Step 4 asserts that every other property holds, so the assertion stays correct on the day that gap closes rather than needing an edit. Tracked as HealthClawGuardrails#498.
  • The human-confirmation header is set by the caller. It evidences a human the way a checkbox does, and is documented as a compensating control rather than proof.
  • Redaction is a compensating control, not a de-identification determination. A rare diagnosis with an unusual date sequence is not made unlinkable by masking a name. What changes is the default.

Three setup traps that are handled rather than left for the reader

Each one cost a debugging session here, and each presents as something other than its cause:

  1. An empty BOX_LICENSE is worse than a missing one. Aidbox refuses to boot with "License is invalid ... not in correct format", where an absent one merely leaves it unactivated. So it is passed as a bare env key and the .env.example line is commented out rather than blank.
  2. curl -f does not fail on a 302. An unactivated Aidbox answers every route, including /health, with a redirect to its activation page, so the obvious health check reports an unactivated Aidbox as healthy. The check requires a literal 200, which also makes it the activation gate: docker compose up -d waits there while you click through in the browser, then starts the proxy on its own.
  3. curl is not in the guardrail image. The proxy's own health check used it, exited 127 on every probe, and reported unhealthy forever while serving perfectly. That kept the MCP service, which waits on service_healthy, from ever starting. It is a python urlopen now, and step 5 exists so nothing can quietly stop running again.

Happy to restructure, rename, or split anything to fit the repo's conventions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant