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:

BackendEnv 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 patternExampleWhy
`..` 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.