Layer Permissions
SOMA enforces strict write permissions across its four knowledge layers. Every writeToLayer call is checked against a static permission matrix, and every entity is validated for layer-specific required fields before being persisted. This page is the authoritative reference for what is allowed, what is required, and what happens when rules are violated.
Permission Matrix
Seven workers, four layers. Each cell indicates the access level.
| Worker | L1 Archive | L2 Working Memory | L3 Emerging Knowledge | L4 Canon |
|---|---|---|---|---|
| Harvester | WRITE | — | — | — |
| Reconciler | WRITE | — | — | — |
| Synthesizer | — | — | WRITE | — |
| Cartographer | — | — | WRITE | — |
| Team-Context | — | WRITE | — | — |
| Governance | — | — | — | WRITE |
| Policy Bridge | READ | READ | READ | READ |
Key constraints:
- Workers can only write to their designated layer. A Harvester attempting to write to L3 is rejected.
- Policy Bridge has read-only access to all layers. It never creates or modifies entities.
- Governance is the sole writer to L4. There is no automated path to canon.
- L1 has two writers (Harvester and Reconciler) because the Reconciler auto-fixes structural issues in archived entries.
- L3 has two writers (Synthesizer and Cartographer) because both generate machine proposals — the Synthesizer from content patterns and the Cartographer from relationship and contradiction detection.
Required Fields per Layer
Each layer requires specific fields beyond the common entity fields. writeToLayer calls validateLayerFields before persisting.
L1 — Execution Archive
| Field | Type | Constraint |
|---|---|---|
layer | 'archive' | Must be exactly 'archive' |
source_worker | WorkerName | Must be 'harvester' or 'reconciler' |
L1 entries must not have a decay_at field. L1 is permanent storage — raw traces and decayed entries live here indefinitely.
L2 — Team Working Memory
| Field | Type | Constraint |
|---|---|---|
layer | 'working' | Must be exactly 'working' |
source_worker | WorkerName | Must be 'team-context' |
team_id | string | Must be non-empty. Scopes the entry to a specific team. |
decay_at | string (ISO 8601) | Required. Default: 14 days from creation. Configurable per team via DecayConfig.teamDecayDays. |
L3 — Emerging Knowledge
| Field | Type | Constraint |
|---|---|---|
layer | 'emerging' | Must be exactly 'emerging' |
source_worker | WorkerName | Must be 'synthesizer' or 'cartographer' |
confidence_score | number | Required. Must be between 0.0 and 1.0 inclusive. |
evidence_links | string[] | Required. Array of L1 entity IDs that support this proposal. |
decay_at | string (ISO 8601) | Required. Default: 90 days from creation. Configurable via DecayConfig.l3DefaultDays. |
Confidence scoring rules:
- Cross-agent corroboration (5+ agents): >= 0.8
- Single-agent patterns: capped at 0.5
- Evidence count contributes incrementally (+0.02 per trace, +0.15 per agent)
L4 — Institutional Canon
| Field | Type | Constraint |
|---|---|---|
layer | 'canon' | Must be exactly 'canon' |
source_worker | WorkerName | Must be 'governance' |
ratified_by | string | Required. Reviewer ID who approved the promotion. |
ratified_at | string (ISO 8601) | Required. Timestamp of the promotion action. |
origin_l3_id | string | Required. ID of the source L3 entry. Creates traceability: L4 -> L3 -> L1 evidence chain. |
L4 entries must not have a decay_at field. Canon is permanent — ratified organizational truth does not expire.
Validation Rules
These rules are enforced by validateLayerFields and the vault's update() method.
Confidence Score
The confidence_score field on L3 entries must be a number between 0.0 and 1.0 inclusive. Values outside this range cause a validation error. This is enforced at write time, not just at synthesis time, so manually created L3 entries are also validated.
confidence_score: -0.1 -> validation error
confidence_score: 0.0 -> valid
confidence_score: 0.85 -> valid
confidence_score: 1.0 -> valid
confidence_score: 1.5 -> validation error
Decay Prohibition on L1 and L4
L1 (Archive) and L4 (Canon) entries must not have a decay_at field. Archive is permanent historical storage; canon is ratified truth. If a decay_at field is present on an L1 or L4 entity, validateLayerFields rejects the write.
layer: 'archive', decay_at: '2026-06-01' -> validation error
layer: 'canon', decay_at: '2026-06-01' -> validation error
layer: 'working', decay_at: '2026-06-01' -> valid (required)
layer: 'emerging', decay_at: '2026-06-01' -> valid (required)
Team ID Non-Empty
The team_id field on L2 entries must be a non-empty string. An empty string or missing value causes a validation error. This ensures every working memory entry is properly scoped to a team.
layer: 'working', team_id: 'backend-team' -> valid
layer: 'working', team_id: '' -> validation error
layer: 'working', team_id: undefined -> validation error
Layer Immutability
The vault.update() method rejects any attempt to change the layer field on an existing entity. Entities cannot move between layers via direct update. The only supported layer transitions are:
- Governance promotion:
promote()creates a new L4 entity from an L3 source. The original L3 entry's status changes topromoted, but its layer staysemerging. - Decay: The decay processor creates a new L1 entity and removes the expired L2/L3 entry. The L1 entry gets a
decayed_fromfield indicating origin.
Both transitions create new entities rather than modifying the layer field in place.
Error Types
LayerPermissionError
Thrown when a worker attempts to write to a layer outside its permission scope.
class LayerPermissionError extends Error {
worker: WorkerName;
layer: KnowledgeLayer;
}
Example triggers:
// Harvester trying to write to L3
writeToLayer(vault, 'emerging', 'harvester', entity);
// -> LayerPermissionError: Worker 'harvester' cannot write to layer 'emerging'
// Synthesizer trying to write to L4
writeToLayer(vault, 'canon', 'synthesizer', entity);
// -> LayerPermissionError: Worker 'synthesizer' cannot write to layer 'canon'
// Any worker trying to write to L4 except governance
writeToLayer(vault, 'canon', 'cartographer', entity);
// -> LayerPermissionError: Worker 'cartographer' cannot write to layer 'canon'
Validation Errors
Thrown by validateLayerFields when required fields are missing or invalid.
// Missing confidence_score on L3 entry
writeToLayer(vault, 'emerging', 'synthesizer', {
type: 'insight',
name: 'retry-pattern',
evidence_links: ['trace-1'],
decay_at: '2026-06-19T00:00:00Z',
});
// -> Error: L3 entry requires confidence_score
// Missing team_id on L2 entry
writeToLayer(vault, 'working', 'team-context', {
type: 'insight',
name: 'sprint-context',
decay_at: '2026-04-04T00:00:00Z',
});
// -> Error: L2 entry requires team_id
// decay_at on L4 entry
writeToLayer(vault, 'canon', 'governance', {
type: 'policy',
name: 'retry-limit',
ratified_by: 'alice',
ratified_at: '2026-03-21T00:00:00Z',
origin_l3_id: 'proposal-123',
decay_at: '2026-12-01T00:00:00Z',
});
// -> Error: L4 entries must not have decay_at
Layer Change Rejection
Thrown by vault.update() when the layer field is included in the update payload.
await vault.update('entity-123', { layer: 'canon' });
// -> Error: Layer field cannot be changed via update
This prevents circumventing the governance pipeline. The only path from L3 to L4 is through promote().
writeToLayer Flow
The complete execution flow when writeToLayer is called:
-
Permission check —
enforceWritePermission(worker, layer)verifies the worker appears in the permission matrix for the target layer. ThrowsLayerPermissionErroron failure. -
Field injection — The
layerandsource_workerfields are set on the entity object automatically, overriding any values the caller may have provided. -
Field validation —
validateLayerFields(layer, entity)checks all layer-specific required fields:- L1:
layer+source_workerpresent; nodecay_at - L2: above +
team_id(non-empty) +decay_at - L3: above +
confidence_score(0-1) +evidence_links+decay_at - L4: above +
ratified_by+ratified_at+origin_l3_id; nodecay_at
- L1:
-
Disk space check —
statfsSyncverifies at least 10MB of free space. Rejects with a clear error if insufficient. -
Lock acquisition — Acquires
_vault.lockwithO_EXCLflag. Times out after 5 seconds with 50ms retry intervals. Stale locks (PID not running) are auto-removed. -
Entity creation — Writes the YAML frontmatter + Markdown body to a
.mdfile in the appropriate type subdirectory. -
Index update — Adds the entity to
_index.jsonwithlayerincluded for fast filtering. -
Mutation log — Appends a create record to
_mutations.jsonlwith timestamp and affected fields. -
Lock release — Removes
_vault.lock. -
Return — Returns the fully populated
Entityobject with all generated fields (id, created, updated).
If any step after lock acquisition fails, the lock is released in a finally block and any temp files are cleaned up. No partial writes are left on disk.