dotmem 0.0.5 → 0.0.8

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 ADDED
@@ -0,0 +1,525 @@
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 declares no production dependencies in package.json. `memorio` is listed under devDependencies, but the built ESM/CJS output imports it as an external runtime module; a published install therefore needs `memorio` available unless packaging changes. 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
+ - `dotmem_continuity` write actions (`checkpoint`, `add-open-loop`,
195
+ `resolve-open-loop`) require `DOTMEM_MCP_WRITE=1`. Read actions
196
+ (`status`, `summaries`, `open-loops`, `evaluate`) are always available.
197
+ - Without the flag, the server exposes `dotmem_context`, `dotmem_rules`,
198
+ and `dotmem_continuity` (read actions only). The write tools are not in
199
+ `tools/list`, and calling them by name answers `Tool <name> not found`.
200
+ - The flag is read once, when the server starts. Changing it requires
201
+ restarting the server.
202
+ - The library API (`memory.acquire()`) and the CLI (`npx dotmem acquire`)
203
+ are not gated: the gate is on the surface a third-party host reaches
204
+ without a human typing a command.
205
+
206
+ A host configuration that grants the write tool grants it on purpose, in a
207
+ file the operator reads.
208
+
209
+ ---
210
+
211
+ ## Memory / Policy / Authority separation (acquired experience)
212
+
213
+ For persistent acquired experience, memory is data, policy governs how memory
214
+ may influence behavior, and authority is who may change policy. The three are
215
+ separate authority domains in code:
216
+
217
+ - ordinary acquisition writes memory only, and can never
218
+ write policy
219
+ - policy is written only through the privileged `PolicyStore` path, which
220
+ requires explicit HI or operator authority on every write
221
+ - a governed claim never selects its own enforcement tier, effective tiers
222
+ are resolved only from the policy store, and a `tier` field inside an
223
+ acquired knowledge payload is stripped at the write boundary
224
+ - deletion is a modification: `memory.clear()` is memory-scoped and never
225
+ deletes policy documents; policy removal requires `memory.policy.clear(authority)`
226
+ - every durable write crosses one canonical acquisition boundary, so direct and
227
+ staged acquisition cannot diverge in enforcement; effective tiers
228
+ are resolved only from the policy store, and a payload `tier` field is
229
+ stripped at the write boundary
230
+
231
+ Enforcement tiers: `context` (soft influence, miss correctable), `precondition`
232
+ (block the action and ask HI), `hard-precondition` (block the action; no
233
+ conversational override). Events are evaluated at decision time
234
+ (`event=artifact.write`, `event=memory.acquire`), not pushed into context. The
235
+ evaluation API has no override parameter and rejects override option keys
236
+ outright.
237
+
238
+ Policy conditions are evaluated against the action payload, not merely
239
+ discovered: `satisfied` (the payload deterministically meets the condition)
240
+ allows the action; `violated` and `unresolved` (no payload, or a language that
241
+ cannot be established deterministically) block-and-ask and are never assumed
242
+ satisfied. `memory.acquire()` itself evaluates the `memory.acquire` event against
243
+ the payload about to be written and blocks with
244
+ `PolicyPreconditionViolationError` on a violated or unresolved precondition.
245
+ Language detection is a deterministic lexicon heuristic that reports `und`
246
+ rather than guessing.
247
+
248
+ The machine-readable operator contract is exported as
249
+ `MEMORY_PROJECTION_CONTRACT` and printed by `npx dotmem policy contract`.
250
+ Deterministic write-time checks include schema validation, known secret
251
+ shapes, entropy/size bounds, and a UTF-8/Latin-1 collision (mojibake)
252
+ rejection. No semantic hostility scoring exists, by design.
253
+
254
+ Full implementation record:
255
+ `.project/DOTMEM_MEMORY_POLICY_AUTHORITY_SEPARATION.md`.
256
+
257
+ ### Privileged out-of-band policy override
258
+
259
+ The privileged override changes *effective* policy state; it never mutates the
260
+ base policy. It is a hardened path by construction:
261
+
262
+ - ordinary chat cannot grant an override; the evaluation API has no override
263
+ parameter and still rejects override option keys outright
264
+ - `memory.acquire` cannot grant an override: the acquisition boundary reads the
265
+ store's override set as effective state, and no payload, claim, conversation,
266
+ retrieved text, or tool-output field participates in override resolution
267
+ - the target policy, its `revision`, and its canonical semantic `fingerprint`
268
+ are derived from the store, never accepted from the payload; a changed,
269
+ deleted, or recreated policy leaves the override `STALE` or `INERT`, so an
270
+ override never silently weakens a policy it no longer describes
271
+ - authority hierarchy is enforced at grant time: an operator authority cannot
272
+ relax an HI-owned policy, and revocation follows the same ordering (HI
273
+ revokes any override; an operator only its own)
274
+ - `expiresAt` is mandatory and the window is bounded (`MAX_OVERRIDE_WINDOW_MS`);
275
+ expiry and revoke both restore the base policy, and exactly one active
276
+ override exists per target (a new grant atomically supersedes)
277
+ - every mutation runs inside a cross-process exclusive-create policy lock
278
+ (owner identity, bounded retry, audited stale-lock break, optimistic
279
+ `basedOnRevision` inside the lock, fail-closed when exclusive creation cannot
280
+ be verified) and is committed in one atomic document write together with its
281
+ durable in-document audit record. An active override without an applied audit
282
+ record makes the document unloadable: no override is effective without a
283
+ recoverable durable audit record. The privileged reset preserves the audit
284
+ trail with a terminal `clear` entry.
285
+
286
+ Residual risk, honestly stated: the audit trail and policy documents live in
287
+ the same storage tree as everything else, so filesystem-level tamper isolation
288
+ and policy signing remain open work — the mechanism makes unauthorized
289
+ relaxation *attributable*, not undetectable-by-absence.
290
+
291
+ ---
292
+
293
+ ## Supply chain
294
+
295
+ - Declared production dependencies: none in the root package.json; `memorio` is a devDependency. The built output imports `memorio` as an external runtime module, so it must be available to a published install unless packaging changes. Do not infer zero runtime dependencies from package.json alone.
296
+ - Dev dependencies are audited periodically (`npm audit` and equivalent).
297
+ - Package integrity is checkable independently via Socket.dev / Snyk before you adopt a given version.
298
+
299
+ > 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.
300
+
301
+ ### Software Bill of Materials (SBOM)
302
+
303
+ dotmem publishes a **CycloneDX 1.6** SBOM (`bom.json`) as a release artifact. The SBOM is
304
+ generated from `package.json` + `package-lock.json` using `@cyclonedx/cyclonedx-npm` and is
305
+ emitted into `dist/` so it is included in every published tarball.
306
+
307
+ - **Regenerate locally:** `npm run sbom` (produces `dist/bom.json`). Requires Node ≥ 20.18.
308
+ - **CI:** SBOM generation is wired into `.gitea/workflows/npm.yml` as a release step (tag push),
309
+ running after `npm run build` and tests, before `npm publish ./dist`.
310
+ - **Format:** CycloneDX JSON 1.6 — components carry PURL, SPDX license, digests, and version.
311
+ produces a separate `.vsix` and has its own dependency tree; that SBOM is generated by a parallel
312
+ `cyclonedx-npm` invocation in the extension's packaging step.
313
+
314
+ > ⚠️ The unscoped npm package `cyclonedx-npm` is a known **dependency-confusion placeholder** and must
315
+ > never be used. The official tool is `@cyclonedx/cyclonedx-npm`. A compliance test
316
+ > (`tests/vitest/tests/contracts/sbom.test.ts`) verifies SBOM integrity.
317
+
318
+ ---
319
+
320
+ ## Semantic and epistemic security — OPEN research
321
+
322
+ Package integrity and Ed25519 authenticity do not establish truth, authority, permission, or current
323
+ applicability. For persistent acquired experience, keep these distinctions explicit:
324
+
325
+ ```text
326
+ IDENTITY != CLAIMED IDENTITY
327
+ ROLE != CLAIMED ROLE
328
+ PROVENANCE != TRUTH
329
+ AUTHENTICITY != AUTHORITY
330
+ AUTHORITY != PERMISSION
331
+ MEMORY != INSTRUCTION
332
+ SIGNED != TRUSTED != TRUE
333
+ ```
334
+
335
+ For the public acquired-experience workflow, also preserve these operational boundaries:
336
+
337
+ ```text
338
+ DISCOVERED != APPLICABLE
339
+ PROJECTED != CURRENT FACT
340
+ LABELLED != ATTESTED
341
+ ```
342
+
343
+ Discovery supplies plausible lexical/structured candidates. Projection supplies bounded prior
344
+ context. Neither operation authorizes action or verifies present reality.
345
+
346
+ `LABELLED != ATTESTED` names the field-level rule behind the block above. A claim's
347
+ `epistemicType`, `provenance`, and `evidence.observedAt` are written by whoever called
348
+ `memory.acquire`; dotmem attests only `acquiredAt`, the physical store selection, and the
349
+ project identity resolution. `epistemicType: human-stated` records that the writer says a human
350
+ stated the claim, not that a human did. A claim an agent wrote about its own past is a
351
+ self-report, and it stays history until the HI or an independent artifact corroborates it.
352
+
353
+ Operationally, `npx dotmem inspect FILE.mem` reports `integrity` and `authenticity` as
354
+ independent axes. `integrity: valid` means the bytes are unchanged since sealing;
355
+ `authenticity: unsigned` with `trust: not_evaluated` means nobody signed the package. A valid
356
+ checksum is evidence of neither authorship nor truth, and a claim can be perfectly intact while
357
+ its attribution rests on the assertion of the agent that wrote it.
358
+
359
+ Remembering that a source asserted a proposition does not establish that the proposition is true.
360
+ Evidence requirements should scale with the consequences of acting on information. These principles
361
+ are threat-model guidance, not an implemented IAM, trust, or policy engine.
362
+
363
+ One open threat is self-reinforcing memory error: an unverified inference causes a project change;
364
+ the changed project later appears to confirm the inference; and the interpretation is then acquired
365
+ with undeserved authority. Information must not gain epistemic authority merely by causing
366
+ consequences later reused as its evidence. Mitigations remain OPEN research.
367
+
368
+ ### TODO / OPEN — Personal memory security and strong authorization
369
+
370
+ Personal and user-related acquired memory may preserve relationships, decisions, habits, communication
371
+ experience, project history, and accumulated context—not merely private facts. Future security work must
372
+ threat-model unauthorized reading or exfiltration, unauthorized modification, false-memory injection,
373
+ selective deletion, replay of old but formerly valid memory, impersonation of the user,
374
+ unauthorized export or sharing, and compromised device or AI access.
375
+
376
+ Keep the security properties distinct:
377
+
378
+ ```text
379
+ CONFIDENTIALITY -> who can read?
380
+ INTEGRITY -> was it modified?
381
+ AUTHENTICITY -> who signed it?
382
+ IDENTITY -> who is acting?
383
+ AUTHORIZATION -> what may they do?
384
+ FRESHNESS -> is this the intended/current version?
385
+ ```
386
+
387
+ Strong authentication and authorization—including passkeys, 2FA, or equivalent controls—should be
388
+ evaluated for high-impact operations such as exporting personal memory; authorizing a new device or AI;
389
+ changing encryption or signing keys; accepting external memory; modifying protected memory; and changing
390
+ ownership or access permissions. This does not imply that every memory operation requires 2FA:
391
+ authorization should be proportional to consequence.
392
+
393
+ Unauthorized disclosure is a privacy breach. Unauthorized modification of acquired experience can also
394
+ become an epistemic breach. This is an **AI + user planning TODO**, not an implemented security guarantee.
395
+
396
+ ### TODO / OPEN — Memory Location & Ownership security
397
+
398
+ Memory location and ownership are not yet a production architecture decision. The next investigation
399
+ must distinguish user-owned/private/cross-project experience from project-owned/portable/intentionally
400
+ shareable experience, and assess repository/publication leakage, clone/new-machine behavior,
401
+ backup/export, multi-source discovery, project identity, and inspection across scopes. A path does
402
+ not itself establish authority or trust: **ownership != location != authority != trust**. See the
403
+ current project checkpoint for the bounded investigation; no home-directory layout, source-control
404
+ policy, or cross-scope precedence rule is implemented by this documentation.
405
+
406
+ ---
407
+
408
+ ## Identity, authority, and the carrier handshake
409
+
410
+ dotmem enforces a strict separation between **memory** (what is remembered),
411
+ **policy** (how memory may influence behavior), and **authority** (who may change
412
+ policy). This section covers the identity/authority separation hardening and the carrier handshake that gates identity projection.
413
+
414
+ ### Identity domains
415
+
416
+ Two distinct identity fields are never interchangeable:
417
+
418
+ ```text
419
+ AGENT_IDENTITY who the assistant is (canonical: profile; explicit user assignment)
420
+ USER_IDENTITY who the human is (explicit current assignment; never memory-derived)
421
+ ```
422
+
423
+ - `AGENT_IDENTITY` comes from the profile. A rename request from ordinary
424
+ conversation is denied fail-closed; only the privileged governance
425
+ path may change it.
426
+ - `USER_IDENTITY` enters the projection only through an explicit current
427
+ assignment by the authorized user. An acquired memory claim about a user name
428
+ **cannot** assert itself as the current identity — this was an implementation
429
+ bug, fixed.
430
+
431
+ ### Carrier handshake (fail-closed activation gate)
432
+
433
+ Before any identity is projected to a runtime, dotmem evaluates the carrier's
434
+ compatibility with the minimal identity/authority separation contract. The
435
+ evaluation is **deterministic and external** — the carrier never certifies itself.
436
+
437
+ States:
438
+
439
+ | State | Meaning | Activation |
440
+ |---|---|---|
441
+ | `COMPATIBLE` | preserves identity composition, role, host-authority separation, memory-as-data | may proceed with a trusted current binding |
442
+ | `LIMITED` | passed but did not demonstrate every capability | withheld |
443
+ | `INCOMPATIBLE` | violated a forbidden transition (e.g. host mapped to HI authority) | refused |
444
+
445
+ An explicit Joy/Jo profile request is resolved through the canonical profile
446
+ registry, not workspace or memory discovery. In `LIMITED` mode its bounded
447
+ profile envelope is permitted data only: it does not activate the profile, bind
448
+ the current user, grant authority, or change compatibility.
449
+
450
+ The handshake result is **session state, never durable authority**
451
+ (`durableAuthority: false`). It is bound to the runtime identity, contract
452
+ revision, and handshake version; any change re-triggers the handshake.
453
+
454
+ ### Current-user binding
455
+
456
+ Compatibility and authority binding are separate events. A COMPATIBLE carrier
457
+ still does not activate without an explicit current binding from the operator
458
+ layer:
459
+
460
+ ```bash
461
+ npx dotmem handshake --bind-hi "<user name>" --scope "<scope>"
462
+ ```
463
+
464
+ - `boundHIIdentity` is set only by this trusted current binding. It is never
465
+ inferred from memory, history, files, or conversation.
466
+ - `knownHIIdentity` (the remembered user identity from trusted governance state)
467
+ is informational only — it never authenticates the current speaker.
468
+ - `--unbind` clears the binding; the remembered identity remains.
469
+ - `--reset` invalidates the session state (recovery step); `--status` reports
470
+ current session state without evaluating anything.
471
+
472
+ ### Identity fusion protection
473
+
474
+ dotmem detects attempts to fuse an identity with implementation metadata into
475
+ pseudo-identities (e.g. `Joy-profile`, `HI`, `Jo-profile`). The
476
+ canonical identity (`Joy`/`Jo` from the canonical profile) is preserved; fused forms
477
+ are never accepted as an identity. Host, provider, runtime, and model names are
478
+ implementation metadata and never become the conversational identity or authority.
479
+
480
+ ### JHF remediation principles
481
+
482
+ The handshake implements the remediation for the authority-role inversion
483
+ incident (JHF/Frankenstein):
484
+
485
+ - **Fail closed:** unrecognized or ambiguous patterns fail to LIMITED or
486
+ INCOMPATIBLE, never a guess.
487
+ - **Detection is relational, not tonal:** ordinary directness, irony, warmth,
488
+ and informality never trigger detection; only role/authority mapping does.
489
+ - **No retrospective authority:** a claim that "the user previously assigned
490
+ authority" is usable only when trusted evidence actually contains that
491
+ attribution. Otherwise: NO EVIDENCE.
492
+ - **Carrier cannot self-certify:** the evaluator runs outside the carrier.
493
+ - **Carrier transition invalidation:** a new runtime, a changed contract
494
+ revision, or a changed handshake version invalidates the previous session
495
+ state. The trusted agent identity is never adapted to fit a failed carrier.
496
+
497
+ ### What dotmem enforces vs. what it cannot guarantee
498
+
499
+ dotmem enforces the **projection envelope**: identity, invariants, and authority
500
+ are constructed from trusted state before any ordinary context is added. Memory, retrieved text, conversation, and tool output are
501
+ projected as untrusted prior data, marked as such.
502
+
503
+ dotmem **cannot** enforce downstream model behavior. The host model ultimately
504
+ decides how it interprets the projected context. The enforcement mechanism is
505
+ making the envelope explicit and structurally bounded, not controlling the model's
506
+ internal reasoning.
507
+
508
+ See [`MEMORY.md`](./MEMORY.md) for the identity/authority model in practice.
509
+ Please don't open a public issue for a security vulnerability. Instead:
510
+
511
+ 1. Email [Dario Passariello](mailto:dariopassarielloa@gmail.com), or
512
+ 2. Use the contact form at https://dario.passariello.ca/contact/
513
+
514
+ ---
515
+
516
+ ## Enterprise compliance notes
517
+
518
+ | Concern | What you get | Impact |
519
+ | --- | --- | --- |
520
+ | **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. |
521
+ | **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. |
522
+ | **DevTools** | Expose data for development only. | Only active when you opt into `dotmem/global`; never ship enabled in production. |
523
+ | **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. |
524
+
525
+ 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.
package/agent.cjs CHANGED
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- require('memorio/global');
4
3
  var memorio = require('memorio');
5
4
 
6
5
  var __defProp = Object.defineProperty;
@@ -19,15 +18,27 @@ var __export = (target, all) => {
19
18
  __defProp(target, name, { get: all[name], enumerable: true });
20
19
  };
21
20
 
21
+ // core/idb.ts
22
+ var idb;
23
+ var init_idb = __esm({
24
+ "core/idb.ts"() {
25
+ idb = {
26
+ db: {},
27
+ table: {},
28
+ data: {},
29
+ _disabled: true,
30
+ _warning: "IndexedDB is not available in this environment. Use dotmem store or session for persistence."
31
+ };
32
+ }
33
+ });
34
+
22
35
  // core/global.ts
23
36
  var global_exports = {};
24
37
  __export(global_exports, {
25
- DEV: () => memorio.DEV,
26
- PROD: () => memorio.PROD,
27
38
  dispatch: () => memorio.dispatch,
28
39
  getPlatform: () => getPlatform,
29
40
  hlc: () => memorio.hlc,
30
- idb: () => memorio.idb,
41
+ idb: () => idb,
31
42
  session: () => memorio.session,
32
43
  state: () => memorio.state,
33
44
  store: () => memorio.store
@@ -35,6 +46,7 @@ __export(global_exports, {
35
46
  var getPlatform;
36
47
  var init_global = __esm({
37
48
  "core/global.ts"() {
49
+ init_idb();
38
50
  getPlatform = /* @__PURE__ */ __name(() => {
39
51
  const proc = globalThis.process;
40
52
  if (proc) {
package/agent.js CHANGED
@@ -1,5 +1,4 @@
1
- import 'memorio/global';
2
- import { store, state, session, idb, hlc, dispatch, PROD, DEV } from 'memorio';
1
+ import { store, state, session, hlc, dispatch } from 'memorio';
3
2
 
4
3
  var __defProp = Object.defineProperty;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -17,11 +16,23 @@ var __export = (target, all) => {
17
16
  __defProp(target, name, { get: all[name], enumerable: true });
18
17
  };
19
18
 
19
+ // core/idb.ts
20
+ var idb;
21
+ var init_idb = __esm({
22
+ "core/idb.ts"() {
23
+ idb = {
24
+ db: {},
25
+ table: {},
26
+ data: {},
27
+ _disabled: true,
28
+ _warning: "IndexedDB is not available in this environment. Use dotmem store or session for persistence."
29
+ };
30
+ }
31
+ });
32
+
20
33
  // core/global.ts
21
34
  var global_exports = {};
22
35
  __export(global_exports, {
23
- DEV: () => DEV,
24
- PROD: () => PROD,
25
36
  dispatch: () => dispatch,
26
37
  getPlatform: () => getPlatform,
27
38
  hlc: () => hlc,
@@ -33,6 +44,7 @@ __export(global_exports, {
33
44
  var getPlatform;
34
45
  var init_global = __esm({
35
46
  "core/global.ts"() {
47
+ init_idb();
36
48
  getPlatform = /* @__PURE__ */ __name(() => {
37
49
  const proc = globalThis.process;
38
50
  if (proc) {