Storage Security
Access control and threat model
Storage Backends — Security Model
Three concerns drive the storage abstraction's security design:
1. Credentials must never live on disk in config. 2. Path traversal must be impossible — agents writing through the adapter cannot escape the configured root. 3. Concurrent writes must not corrupt files.
1. Credentials are env-var-only
`.aiwg/storage.config` is checked into git on most projects. It would be wrong to put `secret` or `apiKey` there — even one accidental commit pushes the credential into history.
The schema (`https://aiwg.io/schemas/storage.config.v1.json`) actively rejects credential-named properties at every nesting depth. The forbidden property names are:
token, password, secret, apiKey, api_key,
accessKey, accessKeyId, secretAccessKey
Loading a `storage.config` containing any of these throws an error pointing at the offending property path. This catches both the obvious case (`{ "backends": { "memory": { "token": "…" } } }`) and the sneaky case (`{ "backends": { "memory": { "extras": [{ "apiKey": "…" }] } } }`).
`aiwg doctor` runs the same recursive walk on the loaded config as defense-in-depth — schema validation alone can be bypassed by hand-rolled config edits.
Where credentials do come from
Each backend documents its env-var (and optional OS-keychain) requirements:
| Backend | Env var(s) |
|---|---|
| `fs` | none |
| `obsidian` | none (file-system access governed by OS permissions) |
| `logseq` | `LOGSEQ_API_TOKEN` (only when `useApi: true`) |
| `notion` | `NOTION_API_TOKEN` (planned, #959) |
| `anythingllm` | `ANYTHINGLLM_API_KEY` (planned, #960) |
| `fortemi` | governed by AIWG MCP server config, not by `storage.config`; legacy storage only, not Fortemi Core index/search |
| `s3` | AWS default credential chain (planned, #962) |
| `webdav` | `AIWG_WEBDAV_USER` + `AIWG_WEBDAV_PASSWORD` or `AIWG_WEBDAV_TOKEN` (planned, #963) |
`aiwg storage list-backends` reports each backend's status. `aiwg doctor` does a reachability probe and reports clearly when a required env var is unset.
2. Path traversal rejection
Every adapter validates subsystem-relative paths at the boundary. The same five rules apply across `fs`, `obsidian`, and `logseq`:
| Rejected pattern | Example | Why |
|---|---|---|
| `..` segments | `../etc/passwd` | Escapes subsystem root |
| Leading `/` | `/etc/passwd` | Absolute path |
| Leading `~` | `~/secrets.md` | Home expansion |
| Backslashes | `a\\b.md` | Windows separator (POSIX bug) |
| Empty | `""` | Ambiguous |
Additionally, file-shaped backends refuse to touch the host application's config directory:
- `obsidian` rejects any path resolving into `<vault>/.obsidian/` — for read, write, delete, AND list (skipped during walk).
- `logseq` rejects any path resolving into `<graph>/logseq/`.
These rejections are tested per-adapter; see `test/unit/storage/{fs,obsidian,logseq}.test.ts`.
3. Atomic writes
Three places use atomic write-then-rename (or `O_APPEND`) semantics:
- `fs` adapter `append()` — uses Node's `fs.appendFile` which opens with `O_APPEND`. The kernel guarantees atomicity for writes ≤ `PIPE_BUF` (4096 bytes on Linux), which means concurrent appenders interleave at line granularity rather than racing read-then-write. (#976)
- `aiwg activity-log append` — prefers `adapter.append` when available; falls back to read-then-write only on backends that don't expose append.
- `sandbox-registry` identity store — writes to `<path>.tmp.<pid>` first, then renames onto the live path. SIGINT during save can no longer leave a half-written JSON file. (#969)
Async backends (Notion, AnythingLLM, Fortemi) have their own concurrency models and don't expose `append()`. The read-then-write fallback is harmless on them since they'd serialize requests at the API layer anyway.
Defense in depth — `aiwg doctor`
`aiwg doctor` runs four checks on `.aiwg/storage.config`:
1. Schema validation — JSON Schema v1 conformance. 2. Credential walk — recursively rejects any property named in the forbidden list above. 3. Reachability probe — best-effort ping per backend (e.g., `obsidian --version`, `GET /api/v1/system` for AnythingLLM, vault existence for `obsidian`). 4. Roots existence — checks each `roots.<subsystem>` path actually exists for `fs` backends.
Failures print the property path, the issue, and a remediation hint. Run `aiwg doctor` after every `storage.config` edit and at the start of long sessions.
What this does not protect against
- An attacker with write access to your `storage.config` can redirect AIWG writes anywhere on the filesystem (within the configured root rules). Treat `storage.config` like any other build-config file — review changes in PRs.
- A misconfigured external backend (wrong vault path, wrong workspace ID) writes to the wrong place. AIWG can't validate semantic correctness — only structural. Run `aiwg storage test <subsystem>` after any config change.
- Backend-side authentication misconfiguration (e.g., an Obsidian sync client also writing to the same vault folder) is outside AIWG's scope.
Reporting issues
Security concerns about the storage system: file an issue at https://git.integrolabs.net/roctinam/aiwg/issues with the `security` label, or follow the project's responsible-disclosure process.