* feat(workspace): layered memory with sensitivity-based privacy redirect
Introduce MemoryLayer type for named memory layers with sensitivity
levels and write permissions. Layers map to synthetic user_id values
in workspace tables, enabling shared/private memory isolation.
- Add MemoryLayer, LayerSensitivity types with default_for_user()
- Add layer-aware write methods (write_to_layer, append_to_layer)
- Add PatternPrivacyClassifier to guard shared layer writes
- Add optional 'layer' parameter to memory_write tool and HTTP API
- Add 'redirected' and 'actual_layer' fields to write response
- Add MEMORY_LAYERS env var (JSON) for layer configuration
- Workspace user_id now derived from GATEWAY_USER_ID (was hardcoded "default")
- 10 integration tests for layered memory operations
Addresses prerequisite for Issue #59 (multi-tenancy).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add explicit default to memory_write layer schema
Add "default": "private" to the layer parameter's JSON schema so
LLM tool consumers can see the default without reading code.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: extract resolve_layer_target to deduplicate layer writes
Consolidate shared layer-lookup, writable check, and privacy
classification logic from write_to_layer and append_to_layer into a
single resolve_layer_target helper.
Flagged on #349 review — the duplication originates in this PR.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review feedback on layered memory PR
- Fix email regex pipe bug in TLD character class (privacy.rs)
- Add append support to web memory_write handler via `append` field
- Validate MemoryLayer name/scope: reject empty, check duplicates
- Remove hardcoded 'private' default from tool schema; omit layer
fields from output when no layer specified
- Document scope isolation risk for multi-tenant (Issue #59)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address adversarial review findings
- CRITICAL: fix identity file protection bypass via trailing slash
(normalize target path before protection checks)
- HIGH: check private layer is writable before privacy redirect
- HIGH: map LayerNotFound/ReadOnly to proper 4xx HTTP status codes
- HIGH: honor `append` field in non-layer HTTP write path
- MEDIUM: remove redundant DB fetch in append_to_layer (narrower
TOCTOU window)
- MEDIUM: remove dead memory_write_handler from handlers/memory.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: opt-in privacy classifier, force override, confidence scoring
Address review feedback from @zmanian:
- Privacy classifier is now opt-in via with_privacy_classifier() instead
of always-on. Default hardcoded patterns (doctor, therapy, email, phone)
had unacceptable false positive rates in household contexts. LLM chooses
the correct layer via system prompt; regex can't improve on that.
- Add ConfigurablePrivacyClassifier for operator-supplied patterns.
- PatternPrivacyClassifier defaults narrowed to hard PII only (SSN,
credit card, credentials).
- Add force param to write_to_layer/append_to_layer to skip classifier.
- PrivacyClassifier trait returns SensitivityResult { is_sensitive,
confidence } instead of bool, ready for probabilistic classifiers.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove redundant heartbeat match arm in memory_write
The heartbeat arm was identical to the catch-all — resolved_path
already points to paths::HEARTBEAT when target is "heartbeat".
Addresses review feedback from gemini-code-assist on #1112.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: return Result from PatternPrivacyClassifier::new()
Replace .expect() with proper error propagation per project
no-panics policy. Remove Default impl (unused in production).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: move memory_layers from GatewayConfig to WorkspaceConfig
Resolve merge conflicts between HEAD (transcription, search, env helpers)
and the workspace config branch. GatewayConfig no longer owns memory_layers;
WorkspaceConfig::resolve() handles parsing, validation (name length >64,
character set, empty scope, duplicates), and fallback defaults.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* test: strengthen privacy classifier and layer isolation coverage
Add 8 privacy classifier edge case tests (format variants, keywords,
longer documents, empty/partial inputs) and 5 layer write isolation
integration tests (cross-scope invisibility, overwrite, empty path,
sensitive-to-private no-redirect).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: tautological test assertion and add WorkspaceConfig validation tests
Replace always-true `is_ok() || is_err()` in write_empty_path_to_layer
with actual behavior assertion (write succeeds with normalized empty path).
Add 8 unit tests for WorkspaceConfig::resolve() covering valid JSON parsing,
invalid JSON, empty/long/invalid-char layer names, empty scopes, duplicates,
and default fallback behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: cargo fmt after staging merge
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: [email protected] <[email protected]>