dotmem 0.0.4 → 0.0.5

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/SECURITY.md DELETED
@@ -1,524 +0,0 @@
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
- and `../../ADR/documents/adr/picla.npm.dotmem/023-identity-behavior-integrity.md` for the design record.
507
-
508
- Please don't open a public issue for a security vulnerability. Instead:
509
-
510
- 1. Email [Dario Passariello](mailto:dariopassarielloa@gmail.com), or
511
- 2. Use the contact form at https://dario.passariello.ca/contact/
512
-
513
- ---
514
-
515
- ## Enterprise compliance notes
516
-
517
- | Concern | What you get | Impact |
518
- | --- | --- | --- |
519
- | **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. |
520
- | **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. |
521
- | **DevTools** | Expose data for development only. | Only active when you opt into `dotmem/global`; never ship enabled in production. |
522
- | **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. |
523
-
524
- 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.