Security
Security is a first-class subsystem in Graphorin, not an afterthought. @graphorin/security ships:
- Secrets —
SecretValuewrapper,SecretRefURI scheme, OS keychain integration, optional encrypted-file store. See Secrets for the full sub-page. - Sandbox tiers —
'none','isolated-vm','docker'. - Server-token authentication — HMAC-SHA256 with a deployment-wide pepper.
- Audit log — SQLite database with mandatory encryption-at-rest and a SHA-256 hash chain.
- OAuth 2.1 with PKCE — outbound flows for MCP servers and skill registries.
- Supply-chain helpers — Ed25519 signature verification for distributed skills.
- Lateral-leak defense layer — composes orthogonally with the agent runtime's safety primitives.
Sandbox tiers
| Tier | Backed by | Default for |
|---|---|---|
'none' | The Node.js process. | First-party tools. |
'isolated-vm' | isolated-vm (peer dependency, ISC). | Untrusted JavaScript skills. |
'docker' | dockerode (peer dependency, Apache-2.0). | Untrusted binaries / full subprocess isolation. |
isolated-vm and dockerode are opt-in peer dependencies — they are not installed by default, so a base install pulls in zero native sandbox code. Add them only if you load untrusted code.
Sensitivity model
Every message, memory row, tool result, and trace attribute carries a Sensitivity tag:
| Tag | Meaning | Where it can flow |
|---|---|---|
public | No restrictions. | Anywhere. |
internal | Operator-private but not user-secret. | Local trace + opt-in collectors; never to providers without acceptsSensitivity: ['internal']. |
secret | User secret. | Never leaves the process. Memory rows tagged secret are filtered before any payload reaches a provider. |
The default for an unfamiliar provider is deny everything except public until you opt in. The default for an exporter is never secret, and you cannot override it.
Server-token authentication
The standalone server (@graphorin/server) requires every authenticated REST / WebSocket / SSE connection to present a bearer token signed with HMAC-SHA256 against a deployment-wide pepper. The unauthenticated /v1/health probe is exempt so liveness checks work before token verification is wired. Tokens are generated and rotated through graphorin token:
graphorin token create --scope agents:invoke --ttl 30d
graphorin token list
graphorin token revoke <token-id>The pepper itself is resolved at server boot through a SecretRef (typically stored under keyring:graphorin_server_pepper or the encrypted-file store). See Secrets for the resolution pipeline.
Audit log
Every privileged operation writes one row to the audit log:
- secret access (read / write / list);
- tool execution (start / end / approval);
- memory mutations (write / supersede / forget);
- skill installs (with signature verification result);
- token issuance / revocation;
- OAuth flows (initiation / token issuance / refresh).
The audit log lives in a dedicated SQLite database with mandatory encryption-at-rest (via better-sqlite3-multiple-ciphers) and a SHA-256 hash chain that links every row to its predecessor. Tampering breaks the chain.
The CLI commands graphorin audit list / graphorin audit verify walk the chain and report any breaks.
OAuth 2.1 with PKCE
The client is built on openid-client (MIT). Token storage uses the configured secrets store (OS keychain by default). Refresh happens lazily on the next call — no background daemon ever phones home.
Supply-chain pipeline
Loading from npm-package or git-repo always:
- runs the install with
--ignore-scriptsenforced (nopostinstallexecution); - fetches the publisher's Ed25519 public key from the configured well-known URL;
- verifies the package's bundled signature against the resolved key;
- writes one audit row recording success or failure.
Local folder installations are trusted-by-default but flow through the same validator pipeline.
Lateral-leak defense layer
The agent runtime's defense layer composes orthogonally with the security primitives above:
| Layer | Purpose |
|---|---|
causalityMonitor (createAgent({ causalityMonitor })) | Implements an Agentic Reference Monitor pattern. Every cross-agent flow is checked against the stated capability. |
mergeGuard (createAgent({ mergeGuard })) | Per-child trust scoring + bias detection on the 'judge-merge' fan-out strategy. |
protocolGuard (createAgent({ protocolGuard })) | Control-character escape catalogue applied at protocol boundaries. |
| Commentary-phase trace sanitisation | At the session-output boundary, before any export. |
| Inbound sanitisation preamble | When non-trusted content is in the message list, a locale-resolved preamble is appended after the cache breakpoint. |
Threat model
Graphorin's design assumes a STRIDE threat model across eight trust boundaries:
- User application <-> Graphorin runtime.
- Runtime <-> provider adapter.
- Runtime <-> tool execution.
- Runtime <-> skill loader.
- Runtime <-> MCP server.
- Runtime <-> storage layer.
- Runtime <-> standalone server (REST / WebSocket / SSE).
- Standalone server <-> operator (CLI, OAuth flows, audit).
The full threat model is summarised in Design principles.
Hardening
The CLI ships graphorin doctor — a single command that audits POSIX file modes on the secrets store, the audit log, and the database, plus the systemd unit template (where applicable):
graphorin doctorFailures are categorised by severity and emit actionable remediation steps.
Next steps
- Secrets —
SecretValue,SecretRef, OS keychain, encrypted-file store. - Privacy — the no-phone-home contract.
- Observability — redaction + replay sanitisation.
- Standalone server — server-token auth, idempotency.
Graphorin · v0.1.0 · MIT License · © 2026 Oleksiy Stepurenko