# Permissions and data

dsh plugins run unsandboxed with the user's permissions. This page lists
every network call, every secret, and every filesystem path MedSeek or dsh
persists.

## Network

Only these hosts are reachable through `src/lib/net.ts`. Anything else
throws before `fetch`.

| Host | Tools | What is sent |
|---|---|---|
| `eutils.ncbi.nlm.nih.gov` | `pubmed_evidence_search` | search term, optional API key, PMIDs |
| `rxnav.nlm.nih.gov` | `drug_label_lookup` | drug name |
| `api.fda.gov` | `drug_label_lookup` | normalized drug name, optional API key |
| `dailymed.nlm.nih.gov` | none (link only) | no request; the result contains a DailyMed URL |
| `clinicaltables.nlm.nih.gov` | `patient_education_lookup` | diagnosis or medication term |
| `connect.medlineplus.gov` | `patient_education_lookup` | ICD-10-CM or RxCUI code, language |
| `clinicaltrials.gov` | `trial_search` | condition term, status filter, page size |

NLM logs full request URLs and prohibits PHI in those requests. The PHI
guard (`dsh-medseek/guard`) screens network-tool arguments for high-confidence
identifier patterns and, in the default `block` mode, denies the call.

Local tools (`deidentify_text`, the draft tools, `note_completeness_check`)
make no network calls.

`web_search` and `web_fetch` are also on the guard's default screen list so
a deployment that still has dsh web search enabled does not send identifiers
through it either.

## Secrets

| Config key | Env-style equivalent | Used for |
|---|---|---|
| `ncbiApiKey` | none built-in; set in the plugin row | NCBI E-utilities rate limit (10 req/s vs 3) |
| `openFdaApiKey` | same | openFDA daily cap |

Neither key unlocks patient data. Still, do not commit them. Put them in
the profile `cordis.patch.yml` that is not in git, or in dsh settings.

## Disk and logs

The audit row writes two paths inside the dsh home - the same root dsh
itself resolves (explicit override > `$DSH_HOME` > `~/.dsh`, via upstream's
home-paths helper; never the process working directory):

- `medseek-audit.jsonl` - one receipt per tool execution storing ONLY:
  sequence number and the previous receipt's hash (chain link), timestamp,
  tool name, call id, error flag, and SHA-256 digests of the arguments and
  of the result content. No note text, no query string, no result body -
  a compliance trail without a second copy of the chart.
- `medseek-audit.jsonl.lock` - upstream's bounded cross-process writer
  lock, present only for milliseconds around each append. A crashed
  writer can leave it behind; the plugin never deletes a lock it did not
  create, so removing an orphan is an operator action.

Editing or deleting any line breaks every hash after it, which makes
silent rewriting detectable. `verifyChain` in `dsh-medseek/audit`
recomputes the chain offline; the recomputation order is documented
beside it. Per append, only the file's tail is read (no whole-file scan),
and the tail line must re-verify against its own hash before anything is
appended.

Precise limitation, stated plainly: **a dropped receipt is not visible in
the file.** Numbering continues from the last successful line, so the
file alone shows only successes and always looks intact. Drops happen on
lock timeout (2 s), an unreadable or tampered tail (fail closed - the
plugin never restarts a GENESIS chain into a damaged file), or a failed
append. The observable signal is exactly one content-free operational
warning per drop on the deployment's log channel (`medseek-audit:` on
stderr); route stderr to your log infra if drop visibility matters.
To start a new chain deliberately, move the damaged file aside first.

Disable with `id: medseek-audit, config: { mode: off }`.

The `medseek-preset-sync` row also writes inside the dsh home: it provisions
the deployment-owned clinical persona into
`$DSH_HOME/.agent-presets/medseek-clinical/` (`agent.cordis.yml` +
`preset.yml`, replaced atomically through a random-suffix sibling). The
content is composition YAML from this package - no patient data, no
secrets, and no file outside that one preset directory is ever read or
written. Identical content is left alone; a locally edited copy of the
managed id is restored on boot with a warning naming the files.

These two rows are the only MedSeek code paths that open files for writing;
nothing else in the plugin reads or writes the filesystem beyond its own
module and asset loading.

dsh persists every `tool/call` argument and `tool/result` body in the
session jsonl under the same home's `sessions/`. Compression is zstd, not
encryption. There is no deletion API. Treat both directories as PHI
storage when clinicians paste identified notes - the session log carries
content; the audit file deliberately does not.

Keep session telemetry off (`DSH_TELEMETRY_MODE` unset or `DISABLED`). The
shipped dsh exporter, if enabled, uploads message text and tool arguments
with no redaction.

## Model endpoint

The only intended PHI egress besides the operator's own disk is the LLM
the operator configured in dsh. Use local weights or a BAA-covered
endpoint. MedSeek cannot see or constrain that choice; the README and the
persona say so because the operator must.

## What the plugin never does

- No analytics, crash reporting, or update ping.
- No EHR read or write.
- No request to a host outside the table above.
