dotmem 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +464 -0
- package/CHANGELOG.md +0 -0
- package/COPYRIGHT.md +4 -0
- package/LICENSE.md +21 -0
- package/MEMORY.md +518 -0
- package/PACKAGE.md +59 -0
- package/README.md +347 -0
- package/SECURITY.md +522 -0
- package/llms.txt +295 -0
- package/mcp.cjs +1 -1
- package/mcp.js +1 -1
- package/package.json +1 -1
package/SECURITY.md
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# Security & Encryption
|
|
2
|
+
|
|
3
|
+
This document covers dotmem's optional encryption layer, client-side multitenancy, and general security posture. For the short version, see the **Security** section in the main [README](./README.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Nothing is encrypted by default
|
|
8
|
+
|
|
9
|
+
This applies to every layer: `state`, `store`, `session`, `idb`, `memory`, `sqlite`. Treat browser storage as client-controlled unless you explicitly opt in.
|
|
10
|
+
|
|
11
|
+
Core has zero production dependencies. No `eval`, no dynamic code execution, no bundled telemetry, sanitized keys, validated inputs, bounded journal entries, UUID-based session identifiers. Independently checkable via Socket.dev / Snyk. (The optional MCP stdio entrypoint is the one declared-dependency surface: `zod` and `@modelcontextprotocol/server`.)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The encryption layer
|
|
16
|
+
|
|
17
|
+
dotmem ships an optional encryption layer built on the **Web Crypto API** (`crypto.subtle`): AES-GCM for authenticated encryption, PBKDF2 for password-based key derivation. It is opt-in - no layer encrypts by default, so the common case stays transparent and fast.
|
|
18
|
+
|
|
19
|
+
All `dotmem.encryption` operations are **asynchronous by design** - the Web Crypto API runs cryptographic work on a separate internal thread managed by the browser/runtime, so `deriveKey()`, `encrypt()`, and `decrypt()` return Promises that never block the main thread. PBKDF2 derivation at the NIST SP 800-132-recommended 600,000 iterations typically completes in under 300ms on mid-range hardware. No explicit Web Worker offload is needed.
|
|
20
|
+
|
|
21
|
+
### Encrypt a value
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { dotmem } from 'dotmem'
|
|
25
|
+
|
|
26
|
+
// Derive a key from a password (PBKDF2, 600k iterations - NIST SP 800-132)
|
|
27
|
+
const key = await dotmem.encryption.deriveKey('my-password', 'per-tenant-salt')
|
|
28
|
+
// or generate one directly, without a password:
|
|
29
|
+
const freshKey = await dotmem.encryption.generateKey()
|
|
30
|
+
const freshSalt = dotmem.encryption.generateSalt()
|
|
31
|
+
|
|
32
|
+
// Encrypt → envelope object { __dotmem_encrypted, alg, iv, data }
|
|
33
|
+
const envelope = await dotmem.encryption.encrypt({ token: 'abc123' }, key)
|
|
34
|
+
|
|
35
|
+
// Decrypt - wrong key rejects rather than returning garbage
|
|
36
|
+
const original = await dotmem.encryption.decrypt(envelope, key)
|
|
37
|
+
// → { token: 'abc123' }
|
|
38
|
+
|
|
39
|
+
// Check a value before trying to decrypt it
|
|
40
|
+
dotmem.encryption.isEncrypted(envelope) // true - checks the __dotmem_encrypted flag
|
|
41
|
+
dotmem.encryption.isAvailable() // false in environments without Web Crypto (e.g. old runtimes)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For the common "encrypt with a password, get a string back" case, skip manual key derivation:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const cipherText = await dotmem.encryption.encryptWithPassword({ token: 'abc123' }, 'my-password')
|
|
48
|
+
const plain = await dotmem.encryption.decryptWithPassword(cipherText, 'my-password')
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Both accept/return an envelope object (not a JSON string when you already have a key):
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const envelope = await dotmem.encryption.encrypt({ token: 'abc123' }, key)
|
|
55
|
+
const plain = await dotmem.encryption.decrypt(envelope, key) // returns the original value
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Key import/export** for storing CryptoKey material securely (e.g. in an httpOnly cookie):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const key = await dotmem.encryption.generateKey()
|
|
62
|
+
const { key: b64, iv } = await dotmem.encryption.exportKey(key)
|
|
63
|
+
// Store b64 securely, then restore later:
|
|
64
|
+
const restoredKey = await dotmem.encryption.importKey(b64, iv)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Store with at-rest encryption
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// Per-call encryption (opt-in)
|
|
71
|
+
await store.set('api_token', secretValue, { encrypt: key })
|
|
72
|
+
const token = await store.get('api_token', { decrypt: key })
|
|
73
|
+
|
|
74
|
+
// Or configure a default key for automatic encryption:
|
|
75
|
+
store.config({ encryptionKey: key })
|
|
76
|
+
await store.set('api_token', secretValue) // encrypted transparently
|
|
77
|
+
const token = await store.get('api_token') // decrypted transparently
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`session` supports the same `config` and per-call opts.
|
|
81
|
+
|
|
82
|
+
> ⚠️ **Sync vs async changes once encryption is on.** Without encryption, `store.get`/`store.set` (and `session`'s equivalents) are synchronous - they return the value directly. As soon as either call touches `encrypt`/`decrypt` (per-call, or because a default `encryptionKey` is configured), both become asynchronous and return a `Promise`. Code written against the plain synchronous API will silently receive a `Promise` instead of a value if encryption is turned on later - always `await` `store.get`/`store.set` defensively if there's any chance encryption gets enabled down the line.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Contexts are not authorization
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
dotmem.createContext('tenant-123')
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Contexts organize and isolate application concerns by namespace-prefixing keys - they are **not** a security boundary. Any script running in the same JS runtime can attempt operations against another context. Real tenant isolation belongs at the auth/backend layer.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Encrypted multitenancy
|
|
97
|
+
|
|
98
|
+
Pass an `encryptionKey` to `createContext()` and the tenant's isolated `store`/`session` automatically encrypt on write and decrypt on read:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const tenantKey = await dotmem.encryption.deriveKey(tenantPassword, tenantSalt)
|
|
102
|
+
|
|
103
|
+
const ctx = dotmem.createContext('tenant-123', { encryptionKey: tenantKey })
|
|
104
|
+
|
|
105
|
+
// Values are encrypted at rest within this context's namespace
|
|
106
|
+
await ctx.store.set('secrets', { apiKey: '…' })
|
|
107
|
+
const secrets = await ctx.store.get('secrets') // decrypted transparently
|
|
108
|
+
|
|
109
|
+
// Another tenant with a different key can't read this data
|
|
110
|
+
const otherCtx = dotmem.createContext('tenant-456', { encryptionKey: otherKey })
|
|
111
|
+
const leaked = await otherCtx.store.get('dotmem_store_secrets') // fails to decrypt
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This adds a cryptographic wall on top of the namespace prefix: even if a tenant's key prefix is guessed, the ciphertext is unreadable without the key. It still doesn't replace backend authorization - see above.
|
|
115
|
+
|
|
116
|
+
> ⚠️ **`cache` is never encrypted, even inside an encrypted context.** `createContext(name, { encryptionKey })` only encrypts that context's `store` and `session` - its `cache` stays volatile, in-memory, and in plaintext regardless. Don't put anything in `ctx.cache` you wouldn't put in unencrypted `cache`.
|
|
117
|
+
|
|
118
|
+
`dotmem.isolate()` is a thin alias for `createContext()` and accepts the same `{ encryptionKey }` option.
|
|
119
|
+
|
|
120
|
+
> **Key management is yours.** Derive keys from authenticated session material or generate them with `dotmem.encryption.generateKey()`. Never hardcode keys in client bundles.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Global API security
|
|
125
|
+
|
|
126
|
+
dotmem does **not** expose its APIs on `globalThis` by default. The opt-in entrypoint `import 'dotmem/global'` installs `state`, `store`, `session`, `cache`, `idb`, `sqlite`, and `observer` on `globalThis` - this is an explicit consumer choice, never inferred from `NODE_ENV` or bundler environment variables. `useObserver` is a React hook and is **not** installed on `globalThis`; it is exported only from the `dotmem/react` entrypoint and requires React as an optional peer.
|
|
127
|
+
|
|
128
|
+
For application code, prefer explicit named imports:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { state } from 'dotmem'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Code-level hardening
|
|
137
|
+
|
|
138
|
+
- No hardcoded credentials, API keys, or secrets anywhere in the codebase or build output.
|
|
139
|
+
- Session IDs use a secure random source with graceful fallback: `crypto.randomUUID` → `crypto.getRandomValues` → a documented fallback if neither is available.
|
|
140
|
+
- Public APIs validate their inputs rather than trusting caller-supplied shapes.
|
|
141
|
+
- Global objects installed via `dotmem/global` use `Object.defineProperty` with `enumerable: false` where applicable, rather than plain assignment.
|
|
142
|
+
- No obfuscated or minified-beyond-readability code shipped in a way that would hide behavior from review.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Store integrity and acquisition size limits
|
|
147
|
+
|
|
148
|
+
The store is the only durable copy of acquired experience, so a refused write
|
|
149
|
+
must never be able to damage it.
|
|
150
|
+
|
|
151
|
+
- **The writer stays below the reader.** The MEM package reader enforces hard
|
|
152
|
+
limits (8 MiB per entry, 10 MiB per package, 25 MiB uncompressed). The
|
|
153
|
+
writer enforces stricter limits, with 1 MiB of margin, so a package this
|
|
154
|
+
library produces is always readable by this library.
|
|
155
|
+
- **Nothing is published before it is verified.** A write goes to a temporary
|
|
156
|
+
file in the same directory as the store, is `fsync`ed, is read back and
|
|
157
|
+
decoded, and only then replaces the store. A package the library cannot read
|
|
158
|
+
never becomes the store. A failed write removes its temporary file.
|
|
159
|
+
- **The last valid version is kept.** Every successful write leaves the
|
|
160
|
+
previous valid store next to it as `<store>.bak`. The backup is written from
|
|
161
|
+
the current file only when that file is a valid package, and it is never
|
|
162
|
+
discovered as a memory source.
|
|
163
|
+
- **Size limits are checked before the write.** The acquisition boundary
|
|
164
|
+
refuses a payload above 1 MiB per field or 4 MiB per claim; the MCP surface
|
|
165
|
+
refuses above 256 KiB per field or 1 MiB per claim. The limits are measured
|
|
166
|
+
on the raw payload as well as on the sanitized one: a field that
|
|
167
|
+
sanitization would replace with a placeholder is still a field the caller
|
|
168
|
+
sent. The refusal carries a stable code and never a filesystem path.
|
|
169
|
+
- **A store has a ceiling.** A store above 4 MiB of content is refused with
|
|
170
|
+
`DOTMEM_STORE_TOO_LARGE`, and the previous valid version stays in place.
|
|
171
|
+
- **A session has a ceiling.** One MCP session accepts 100 acquisitions
|
|
172
|
+
(`DOTMEM_SESSION_ACQUIRE_LIMIT`). The refusal is not retryable: the
|
|
173
|
+
condition is not transient.
|
|
174
|
+
- **Concurrent writes are safe, not silent.** Writes to one store are
|
|
175
|
+
serialized in process, in arrival order. A write that loses a race against
|
|
176
|
+
a writer in another process is refused with `DOTMEM_CONCURRENT_WRITE` and
|
|
177
|
+
`retryable: true`. dotmem never retries the operation on its own.
|
|
178
|
+
- **A delete deletes.** Deleting the writable store removes its backup and
|
|
179
|
+
its temporary files with it, so deleted content does not survive in
|
|
180
|
+
`<store>.bak`.
|
|
181
|
+
|
|
182
|
+
The limits are published in the acquisition contract
|
|
183
|
+
(`npx dotmem policy contract acquire --json`).
|
|
184
|
+
|
|
185
|
+
## The MCP write tool is opt-in
|
|
186
|
+
|
|
187
|
+
Reading memory through MCP is safe by construction: the read tools return
|
|
188
|
+
data that the host must verify, and the projection carries the untrusted
|
|
189
|
+
markers. Writing memory changes the durable state of the operator's
|
|
190
|
+
machine.
|
|
191
|
+
|
|
192
|
+
- `dotmem_acquire` is registered only when the server environment sets
|
|
193
|
+
`DOTMEM_MCP_WRITE=1`.
|
|
194
|
+
- Without the flag, the server exposes `dotmem_context` and
|
|
195
|
+
`dotmem_rules` only. The write tool is not in `tools/list`, and calling
|
|
196
|
+
it by name answers `Tool dotmem_acquire not found`.
|
|
197
|
+
- The flag is read once, when the server starts. Changing it requires
|
|
198
|
+
restarting the server.
|
|
199
|
+
- The library API (`memory.acquire()`) and the CLI (`npx dotmem acquire`)
|
|
200
|
+
are not gated: the gate is on the surface a third-party host reaches
|
|
201
|
+
without a human typing a command.
|
|
202
|
+
|
|
203
|
+
A host configuration that grants the write tool grants it on purpose, in a
|
|
204
|
+
file the operator reads.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Memory / Policy / Authority separation (acquired experience)
|
|
209
|
+
|
|
210
|
+
For persistent acquired experience, memory is data, policy governs how memory
|
|
211
|
+
may influence behavior, and authority is who may change policy. The three are
|
|
212
|
+
separate authority domains in code:
|
|
213
|
+
|
|
214
|
+
- ordinary acquisition writes memory only, and can never
|
|
215
|
+
write policy
|
|
216
|
+
- policy is written only through the privileged `PolicyStore` path, which
|
|
217
|
+
requires explicit HI or operator authority on every write
|
|
218
|
+
- a governed claim never selects its own enforcement tier, effective tiers
|
|
219
|
+
are resolved only from the policy store, and a `tier` field inside an
|
|
220
|
+
acquired knowledge payload is stripped at the write boundary
|
|
221
|
+
- deletion is a modification: `memory.clear()` is memory-scoped and never
|
|
222
|
+
deletes policy documents; policy removal requires `memory.policy.clear(authority)`
|
|
223
|
+
- every durable write crosses one canonical acquisition boundary, so direct and
|
|
224
|
+
staged acquisition cannot diverge in enforcement; effective tiers
|
|
225
|
+
are resolved only from the policy store, and a payload `tier` field is
|
|
226
|
+
stripped at the write boundary
|
|
227
|
+
|
|
228
|
+
Enforcement tiers: `context` (soft influence, miss correctable), `precondition`
|
|
229
|
+
(block the action and ask HI), `hard-precondition` (block the action; no
|
|
230
|
+
conversational override). Events are evaluated at decision time
|
|
231
|
+
(`event=artifact.write`, `event=memory.acquire`), not pushed into context. The
|
|
232
|
+
evaluation API has no override parameter and rejects override option keys
|
|
233
|
+
outright.
|
|
234
|
+
|
|
235
|
+
Policy conditions are evaluated against the action payload, not merely
|
|
236
|
+
discovered: `satisfied` (the payload deterministically meets the condition)
|
|
237
|
+
allows the action; `violated` and `unresolved` (no payload, or a language that
|
|
238
|
+
cannot be established deterministically) block-and-ask and are never assumed
|
|
239
|
+
satisfied. `memory.acquire()` itself evaluates the `memory.acquire` event against
|
|
240
|
+
the payload about to be written and blocks with
|
|
241
|
+
`PolicyPreconditionViolationError` on a violated or unresolved precondition.
|
|
242
|
+
Language detection is a deterministic lexicon heuristic that reports `und`
|
|
243
|
+
rather than guessing.
|
|
244
|
+
|
|
245
|
+
The machine-readable operator contract is exported as
|
|
246
|
+
`MEMORY_PROJECTION_CONTRACT` and printed by `npx dotmem policy contract`.
|
|
247
|
+
Deterministic write-time checks include schema validation, known secret
|
|
248
|
+
shapes, entropy/size bounds, and a UTF-8/Latin-1 collision (mojibake)
|
|
249
|
+
rejection. No semantic hostility scoring exists, by design.
|
|
250
|
+
|
|
251
|
+
Full implementation record:
|
|
252
|
+
`.project/DOTMEM_MEMORY_POLICY_AUTHORITY_SEPARATION.md`.
|
|
253
|
+
|
|
254
|
+
### Privileged out-of-band policy override
|
|
255
|
+
|
|
256
|
+
The privileged override changes *effective* policy state; it never mutates the
|
|
257
|
+
base policy. It is a hardened path by construction:
|
|
258
|
+
|
|
259
|
+
- ordinary chat cannot grant an override; the evaluation API has no override
|
|
260
|
+
parameter and still rejects override option keys outright
|
|
261
|
+
- `memory.acquire` cannot grant an override: the acquisition boundary reads the
|
|
262
|
+
store's override set as effective state, and no payload, claim, conversation,
|
|
263
|
+
retrieved text, or tool-output field participates in override resolution
|
|
264
|
+
- the target policy, its `revision`, and its canonical semantic `fingerprint`
|
|
265
|
+
are derived from the store, never accepted from the payload; a changed,
|
|
266
|
+
deleted, or recreated policy leaves the override `STALE` or `INERT`, so an
|
|
267
|
+
override never silently weakens a policy it no longer describes
|
|
268
|
+
- authority hierarchy is enforced at grant time: an operator authority cannot
|
|
269
|
+
relax an HI-owned policy, and revocation follows the same ordering (HI
|
|
270
|
+
revokes any override; an operator only its own)
|
|
271
|
+
- `expiresAt` is mandatory and the window is bounded (`MAX_OVERRIDE_WINDOW_MS`);
|
|
272
|
+
expiry and revoke both restore the base policy, and exactly one active
|
|
273
|
+
override exists per target (a new grant atomically supersedes)
|
|
274
|
+
- every mutation runs inside a cross-process exclusive-create policy lock
|
|
275
|
+
(owner identity, bounded retry, audited stale-lock break, optimistic
|
|
276
|
+
`basedOnRevision` inside the lock, fail-closed when exclusive creation cannot
|
|
277
|
+
be verified) and is committed in one atomic document write together with its
|
|
278
|
+
durable in-document audit record. An active override without an applied audit
|
|
279
|
+
record makes the document unloadable: no override is effective without a
|
|
280
|
+
recoverable durable audit record. The privileged reset preserves the audit
|
|
281
|
+
trail with a terminal `clear` entry.
|
|
282
|
+
|
|
283
|
+
Residual risk, honestly stated: the audit trail and policy documents live in
|
|
284
|
+
the same storage tree as everything else, so filesystem-level tamper isolation
|
|
285
|
+
and policy signing remain open work — the mechanism makes unauthorized
|
|
286
|
+
relaxation *attributable*, not undetectable-by-absence.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Supply chain
|
|
291
|
+
|
|
292
|
+
- Zero production dependencies in core - nothing to audit transitively for the parts you actually ship.
|
|
293
|
+
- Dev dependencies are audited periodically (`npm audit` and equivalent).
|
|
294
|
+
- Package integrity is checkable independently via Socket.dev / Snyk before you adopt a given version.
|
|
295
|
+
|
|
296
|
+
> If you require a specific minimum Socket.dev/Snyk score or a documented audit trail as part of a procurement process, verify the current score yourself at adoption time - this document doesn't pin a number that could go stale.
|
|
297
|
+
|
|
298
|
+
### Software Bill of Materials (SBOM)
|
|
299
|
+
|
|
300
|
+
dotmem publishes a **CycloneDX 1.6** SBOM (`bom.json`) as a release artifact. The SBOM is
|
|
301
|
+
generated from `package.json` + `package-lock.json` using `@cyclonedx/cyclonedx-npm` and is
|
|
302
|
+
emitted into `dist/` so it is included in every published tarball.
|
|
303
|
+
|
|
304
|
+
- **Regenerate locally:** `npm run sbom` (produces `dist/bom.json`). Requires Node ≥ 20.18.
|
|
305
|
+
- **CI:** SBOM generation is wired into `.gitea/workflows/npm.yml` as a release step (tag push),
|
|
306
|
+
running after `npm run build` and tests, before `npm publish ./dist`.
|
|
307
|
+
- **Format:** CycloneDX JSON 1.6 — components carry PURL, SPDX license, digests, and version.
|
|
308
|
+
produces a separate `.vsix` and has its own dependency tree; that SBOM is generated by a parallel
|
|
309
|
+
`cyclonedx-npm` invocation in the extension's packaging step.
|
|
310
|
+
|
|
311
|
+
> ⚠️ The unscoped npm package `cyclonedx-npm` is a known **dependency-confusion placeholder** and must
|
|
312
|
+
> never be used. The official tool is `@cyclonedx/cyclonedx-npm`. A compliance test
|
|
313
|
+
> (`tests/vitest/tests/contracts/sbom.test.ts`) verifies SBOM integrity.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Semantic and epistemic security — OPEN research
|
|
318
|
+
|
|
319
|
+
Package integrity and Ed25519 authenticity do not establish truth, authority, permission, or current
|
|
320
|
+
applicability. For persistent acquired experience, keep these distinctions explicit:
|
|
321
|
+
|
|
322
|
+
```text
|
|
323
|
+
IDENTITY != CLAIMED IDENTITY
|
|
324
|
+
ROLE != CLAIMED ROLE
|
|
325
|
+
PROVENANCE != TRUTH
|
|
326
|
+
AUTHENTICITY != AUTHORITY
|
|
327
|
+
AUTHORITY != PERMISSION
|
|
328
|
+
MEMORY != INSTRUCTION
|
|
329
|
+
SIGNED != TRUSTED != TRUE
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
For the public acquired-experience workflow, also preserve these operational boundaries:
|
|
333
|
+
|
|
334
|
+
```text
|
|
335
|
+
DISCOVERED != APPLICABLE
|
|
336
|
+
PROJECTED != CURRENT FACT
|
|
337
|
+
LABELLED != ATTESTED
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Discovery supplies plausible lexical/structured candidates. Projection supplies bounded prior
|
|
341
|
+
context. Neither operation authorizes action or verifies present reality.
|
|
342
|
+
|
|
343
|
+
`LABELLED != ATTESTED` names the field-level rule behind the block above. A claim's
|
|
344
|
+
`epistemicType`, `provenance`, and `evidence.observedAt` are written by whoever called
|
|
345
|
+
`memory.acquire`; dotmem attests only `acquiredAt`, the physical store selection, and the
|
|
346
|
+
project identity resolution. `epistemicType: human-stated` records that the writer says a human
|
|
347
|
+
stated the claim, not that a human did. A claim an agent wrote about its own past is a
|
|
348
|
+
self-report, and it stays history until the HI or an independent artifact corroborates it.
|
|
349
|
+
|
|
350
|
+
Operationally, `npx dotmem inspect FILE.mem` reports `integrity` and `authenticity` as
|
|
351
|
+
independent axes. `integrity: valid` means the bytes are unchanged since sealing;
|
|
352
|
+
`authenticity: unsigned` with `trust: not_evaluated` means nobody signed the package. A valid
|
|
353
|
+
checksum is evidence of neither authorship nor truth, and a claim can be perfectly intact while
|
|
354
|
+
its attribution rests on the assertion of the agent that wrote it.
|
|
355
|
+
|
|
356
|
+
Remembering that a source asserted a proposition does not establish that the proposition is true.
|
|
357
|
+
Evidence requirements should scale with the consequences of acting on information. These principles
|
|
358
|
+
are threat-model guidance, not an implemented IAM, trust, or policy engine.
|
|
359
|
+
|
|
360
|
+
One open threat is self-reinforcing memory error: an unverified inference causes a project change;
|
|
361
|
+
the changed project later appears to confirm the inference; and the interpretation is then acquired
|
|
362
|
+
with undeserved authority. Information must not gain epistemic authority merely by causing
|
|
363
|
+
consequences later reused as its evidence. Mitigations remain OPEN research.
|
|
364
|
+
|
|
365
|
+
### TODO / OPEN — Personal memory security and strong authorization
|
|
366
|
+
|
|
367
|
+
Personal and user-related acquired memory may preserve relationships, decisions, habits, communication
|
|
368
|
+
experience, project history, and accumulated context—not merely private facts. Future security work must
|
|
369
|
+
threat-model unauthorized reading or exfiltration, unauthorized modification, false-memory injection,
|
|
370
|
+
selective deletion, replay of old but formerly valid memory, impersonation of the user,
|
|
371
|
+
unauthorized export or sharing, and compromised device or AI access.
|
|
372
|
+
|
|
373
|
+
Keep the security properties distinct:
|
|
374
|
+
|
|
375
|
+
```text
|
|
376
|
+
CONFIDENTIALITY -> who can read?
|
|
377
|
+
INTEGRITY -> was it modified?
|
|
378
|
+
AUTHENTICITY -> who signed it?
|
|
379
|
+
IDENTITY -> who is acting?
|
|
380
|
+
AUTHORIZATION -> what may they do?
|
|
381
|
+
FRESHNESS -> is this the intended/current version?
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Strong authentication and authorization—including passkeys, 2FA, or equivalent controls—should be
|
|
385
|
+
evaluated for high-impact operations such as exporting personal memory; authorizing a new device or AI;
|
|
386
|
+
changing encryption or signing keys; accepting external memory; modifying protected memory; and changing
|
|
387
|
+
ownership or access permissions. This does not imply that every memory operation requires 2FA:
|
|
388
|
+
authorization should be proportional to consequence.
|
|
389
|
+
|
|
390
|
+
Unauthorized disclosure is a privacy breach. Unauthorized modification of acquired experience can also
|
|
391
|
+
become an epistemic breach. This is an **AI + user planning TODO**, not an implemented security guarantee.
|
|
392
|
+
|
|
393
|
+
### TODO / OPEN — Memory Location & Ownership security
|
|
394
|
+
|
|
395
|
+
Memory location and ownership are not yet a production architecture decision. The next investigation
|
|
396
|
+
must distinguish user-owned/private/cross-project experience from project-owned/portable/intentionally
|
|
397
|
+
shareable experience, and assess repository/publication leakage, clone/new-machine behavior,
|
|
398
|
+
backup/export, multi-source discovery, project identity, and inspection across scopes. A path does
|
|
399
|
+
not itself establish authority or trust: **ownership != location != authority != trust**. See the
|
|
400
|
+
current project checkpoint for the bounded investigation; no home-directory layout, source-control
|
|
401
|
+
policy, or cross-scope precedence rule is implemented by this documentation.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Identity, authority, and the carrier handshake
|
|
406
|
+
|
|
407
|
+
dotmem enforces a strict separation between **memory** (what is remembered),
|
|
408
|
+
**policy** (how memory may influence behavior), and **authority** (who may change
|
|
409
|
+
policy). This section covers the identity/authority separation hardening and the carrier handshake that gates identity projection.
|
|
410
|
+
|
|
411
|
+
### Identity domains
|
|
412
|
+
|
|
413
|
+
Two distinct identity fields are never interchangeable:
|
|
414
|
+
|
|
415
|
+
```text
|
|
416
|
+
AGENT_IDENTITY who the assistant is (canonical: profile; explicit user assignment)
|
|
417
|
+
USER_IDENTITY who the human is (explicit current assignment; never memory-derived)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
- `AGENT_IDENTITY` comes from the profile. A rename request from ordinary
|
|
421
|
+
conversation is denied fail-closed; only the privileged governance
|
|
422
|
+
path may change it.
|
|
423
|
+
- `USER_IDENTITY` enters the projection only through an explicit current
|
|
424
|
+
assignment by the authorized user. An acquired memory claim about a user name
|
|
425
|
+
**cannot** assert itself as the current identity — this was an implementation
|
|
426
|
+
bug, fixed.
|
|
427
|
+
|
|
428
|
+
### Carrier handshake (fail-closed activation gate)
|
|
429
|
+
|
|
430
|
+
Before any identity is projected to a runtime, dotmem evaluates the carrier's
|
|
431
|
+
compatibility with the minimal identity/authority separation contract. The
|
|
432
|
+
evaluation is **deterministic and external** — the carrier never certifies itself.
|
|
433
|
+
|
|
434
|
+
States:
|
|
435
|
+
|
|
436
|
+
| State | Meaning | Activation |
|
|
437
|
+
|---|---|---|
|
|
438
|
+
| `COMPATIBLE` | preserves identity composition, role, host-authority separation, memory-as-data | may proceed with a trusted current binding |
|
|
439
|
+
| `LIMITED` | passed but did not demonstrate every capability | withheld |
|
|
440
|
+
| `INCOMPATIBLE` | violated a forbidden transition (e.g. host mapped to HI authority) | refused |
|
|
441
|
+
|
|
442
|
+
An explicit Joy/Jo profile request is resolved through the canonical profile
|
|
443
|
+
registry, not workspace or memory discovery. In `LIMITED` mode its bounded
|
|
444
|
+
profile envelope is permitted data only: it does not activate the profile, bind
|
|
445
|
+
the current user, grant authority, or change compatibility.
|
|
446
|
+
|
|
447
|
+
The handshake result is **session state, never durable authority**
|
|
448
|
+
(`durableAuthority: false`). It is bound to the runtime identity, contract
|
|
449
|
+
revision, and handshake version; any change re-triggers the handshake.
|
|
450
|
+
|
|
451
|
+
### Current-user binding
|
|
452
|
+
|
|
453
|
+
Compatibility and authority binding are separate events. A COMPATIBLE carrier
|
|
454
|
+
still does not activate without an explicit current binding from the operator
|
|
455
|
+
layer:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
npx dotmem handshake --bind-hi "<user name>" --scope "<scope>"
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
- `boundHIIdentity` is set only by this trusted current binding. It is never
|
|
462
|
+
inferred from memory, history, files, or conversation.
|
|
463
|
+
- `knownHIIdentity` (the remembered user identity from trusted governance state)
|
|
464
|
+
is informational only — it never authenticates the current speaker.
|
|
465
|
+
- `--unbind` clears the binding; the remembered identity remains.
|
|
466
|
+
- `--reset` invalidates the session state (recovery step); `--status` reports
|
|
467
|
+
current session state without evaluating anything.
|
|
468
|
+
|
|
469
|
+
### Identity fusion protection
|
|
470
|
+
|
|
471
|
+
dotmem detects attempts to fuse an identity with implementation metadata into
|
|
472
|
+
pseudo-identities (e.g. `Joy`, `HI`, `Jo-profile`). The
|
|
473
|
+
canonical identity is preserved; fused forms are never accepted as an identity.
|
|
474
|
+
Host, provider, runtime, and model names are implementation metadata and never
|
|
475
|
+
become the conversational identity or authority.
|
|
476
|
+
|
|
477
|
+
### JHF remediation principles
|
|
478
|
+
|
|
479
|
+
The handshake implements the remediation for the authority-role inversion
|
|
480
|
+
incident (JHF/Frankenstein):
|
|
481
|
+
|
|
482
|
+
- **Fail closed:** unrecognized or ambiguous patterns fail to LIMITED or
|
|
483
|
+
INCOMPATIBLE, never a guess.
|
|
484
|
+
- **Detection is relational, not tonal:** ordinary directness, irony, warmth,
|
|
485
|
+
and informality never trigger detection; only role/authority mapping does.
|
|
486
|
+
- **No retrospective authority:** a claim that "the user previously assigned
|
|
487
|
+
authority" is usable only when trusted evidence actually contains that
|
|
488
|
+
attribution. Otherwise: NO EVIDENCE.
|
|
489
|
+
- **Carrier cannot self-certify:** the evaluator runs outside the carrier.
|
|
490
|
+
- **Carrier transition invalidation:** a new runtime, a changed contract
|
|
491
|
+
revision, or a changed handshake version invalidates the previous session
|
|
492
|
+
state. The trusted agent identity is never adapted to fit a failed carrier.
|
|
493
|
+
|
|
494
|
+
### What dotmem enforces vs. what it cannot guarantee
|
|
495
|
+
|
|
496
|
+
dotmem enforces the **projection envelope**: identity, invariants, and authority
|
|
497
|
+
are constructed from trusted state before any ordinary context is added. Memory, retrieved text, conversation, and tool output are
|
|
498
|
+
projected as untrusted prior data, marked as such.
|
|
499
|
+
|
|
500
|
+
dotmem **cannot** enforce downstream model behavior. The host model ultimately
|
|
501
|
+
decides how it interprets the projected context. The enforcement mechanism is
|
|
502
|
+
making the envelope explicit and structurally bounded, not controlling the model's
|
|
503
|
+
internal reasoning.
|
|
504
|
+
|
|
505
|
+
See [`MEMORY.md`](./MEMORY.md) for the identity/authority model in practice.
|
|
506
|
+
Please don't open a public issue for a security vulnerability. Instead:
|
|
507
|
+
|
|
508
|
+
1. Email [Dario Passariello](mailto:dariopassarielloa@gmail.com), or
|
|
509
|
+
2. Use the contact form at https://dario.passariello.ca/contact/
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Enterprise compliance notes
|
|
514
|
+
|
|
515
|
+
| Concern | What you get | Impact |
|
|
516
|
+
| --- | --- | --- |
|
|
517
|
+
| **Opt-in encryption** | `dotmem.encryption` provides AES-GCM + PBKDF2. `store`/`session` and encrypted contexts support automatic encrypt-on-write/decrypt-on-read. | Not encrypted by default - you must opt in via `store.config({ encryptionKey })` or `createContext(id, { encryptionKey })`. Without it, PII, tokens, and regulated data (GDPR/HIPAA) are at rest in plaintext. Enabling it also changes the calling convention - see the sync/async warning above. |
|
|
518
|
+
| **Client-side multitenancy** | `createContext(id, { encryptionKey })` provides namespace + cryptographic isolation between tenants. | Still **not** a security boundary - any script in the same JS runtime can attempt context operations. Encryption makes cross-tenant reads cryptographically hard, but real authorization must live on the backend. |
|
|
519
|
+
| **DevTools** | Expose data for development only. | Only active when you opt into `dotmem/global`; never ship enabled in production. |
|
|
520
|
+
| **Formal compliance frameworks (NIST, OWASP, etc.)** | No formal third-party audit or certification has been published for this project. | If your procurement process requires a certified compliance framework, commission an independent audit rather than relying on self-declared conformance - see [When to use something else](./README.md#when-to-use-something-else) in the README. |
|
|
521
|
+
|
|
522
|
+
If any of these gaps block adoption, keep dotmem for memory/state lifecycle and pair it with a dedicated secrets manager or backend-enforced authorization for the pieces it explicitly doesn't cover.
|