Security · Layer 7
Activation store isolation.
The set of active elements determines what the Gatekeeper allows, so that state is isolated per session and validated on load. The source below is quoted verbatim from the public mcp-server repository.
← Back to the security overview
The threat
Active elements are the inputs the Gatekeeper resolves policy from. If one session's activation state could leak into another, a permissive element activated in a throwaway session would reshape the permission surface of a different one. The state is stored in a file on disk, so a corrupt or crafted activation file must never be trusted blindly or allowed to crash the load.
Activation state lifecycle
A session is named, the name is validated, and that name alone determines which file is read. Everything loaded from that file is re-normalized and type-checked before it becomes live state.
flowchart TD
ENV[DOLLHOUSE_SESSION_ID resolved] --> VAL{Matches SESSION_ID_PATTERN?}
VAL -- no --> DEF[Fall back to a safe default identity]
VAL -- yes --> PATH[Derive per-session path: activations-SESSIONID.json]
DEF --> PATH
PATH --> LOAD{File present?}
LOAD -- "no (ENOENT)" --> FRESH[Start fresh, no event]
LOAD -- yes --> PARSE{Parse and shape valid?}
PARSE -- "no (corruption)" --> SAFE[Start fresh + log MEDIUM security event]
PARSE -- yes --> NORM[Normalize identifiers, apply type allowlist, dedupe]
NORM --> STATE[In-memory per-session state]
FRESH --> STATE
SAFE --> STATE
STATE --> GK[Gatekeeper resolves policy from active elements]
classDef deny fill:#b91c1c,stroke:#7f1d1d,color:#fff;
classDef allow fill:#15803d,stroke:#14532d,color:#fff;
class SAFE deny;
class STATE allow;
The session identifier is a strict allowlist pattern
The session name comes from the environment and is constrained to a tight character class with a bounded length. It cannot contain a path separator or traversal sequence, so it cannot redirect the activation file outside the state directory.
From
src/services/sessionIdentity.ts
export const SESSION_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
The validated identity is the sole determinant of which file is read; the path is derived from it, never from caller-supplied input:
From
src/services/ActivationStore.ts
const identity = resolveSessionIdentity();
this.sessionId = identity.sessionId;
this.runtimeSessionId = identity.runtimeSessionId;
this.enabled = isPersistenceEnabled();
this.stateDir = stateDir ?? path.join(os.homedir(), '.dollhouse', 'state');
this.persistPath = path.join(this.stateDir, `activations-${this.sessionId}.json`);
Everything loaded is re-normalized and type-checked
The on-disk file is untrusted input. Element identifiers are re-run through the same Unicode normalization the rest of the platform uses, and element types are collapsed to a known canonical set, so a tampered file cannot smuggle a look-alike identifier or an unknown element type into live state.
function normalizeType(elementType: string): string {
const lower = elementType.toLowerCase();
return PLURAL_TO_SINGULAR[lower] ?? lower;
}
function normalizeActivationIdentifier(value: string): string {
return UnicodeValidator.normalize(value).normalizedContent.trim();
}
The loader fails safe and records why
A missing file is normal: a fresh session starts empty. A file that exists but will not parse is treated as possible tampering or corruption. The session starts fresh rather than crashing or trusting invalid data, and the condition is recorded as a security event rather than swallowed silently.
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
logger.debug(`[ActivationStore] No activation file found for session '${this.sessionId}', starting fresh`);
} else {
logger.warn(`[ActivationStore] Failed to load activation file for session '${this.sessionId}', starting fresh`, { error });
SecurityMonitor.logSecurityEvent({
type: 'ELEMENT_ACTIVATED',
severity: 'MEDIUM',
source: 'ActivationStore.initialize',
details: `Failed to load activation file for session '${this.sessionId}' — starting fresh (possible data corruption)`,
additionalData: { error: String(error), sessionId: this.sessionId },
});
}
}
Persistence is opt-out: a session can run fully ephemeral, with no activation file written or read.
The session name is the only key, and it is validated before it can name anything. Fail-safe is the default: on any uncertainty, the store starts empty and logs the reason rather than trusting a file it cannot verify.
Position in the security stack
This layer feeds the Gatekeeper. The permission decision on every operation depends on which elements are active, and that set is what the activation store isolates per session and re-validates on load. Content validation sanitized those elements on the way in; the activation store ensures that exactly the correct elements are live for this session.
flowchart LR CV[Content validation: clean element on activation] --> AS[Activation store: per-session, validated state] AS --> GK[Gatekeeper: resolves policy from active elements] AS -. "isolated per DOLLHOUSE_SESSION_ID" .-> AS2[Other sessions: separate files, no cross-talk] GK --> DEC[Allow / confirm / deny decision]
Related
-
Security overview
The full eight-layer model and how activation store isolation fits into it.
-
The Gatekeeper
The permission engine that resolves policy from exactly the active-element set this layer isolates.
-
Content validation
The normalization this layer reuses when it re-validates identifiers loaded from disk.