Files: 72. Audited: 72 / 72. Refactored: 0 / 72.
This is the 5a portion of Layer 5. Layer 5b (UI primitives at
eliza/packages/ui/src/, ~180 files) is deferred to a separate audit run
because it's larger and stylistic-heavy.
Two packages, one layer because they sit at the same dependency depth:
@elizaos/vault(16 files) — secrets/config store with two parallel backends (file + PGlite), three master-key paths (keychain / passphrase / in-memory), one inventory/profiles/routing meta-layer, and three external password-manager adapters (1Password / Bitwarden / Proton Pass).@elizaos/shared(56 files) — browser-safe commons consumed byapp-core,agent, and the renderer. Owns config types, contracts, the onboarding provider catalog, theme tokens, env-resolution, i18n keyword matching, and small leaf utilities.
- The vault is the canonical secrets store after the Phase 1 PGlite migration (MASTER.md §3 Phase 1). Layer 6 (agent runtime) and Layer 4 (api server) both consume it — until 5a is mapped, dedup work in 4 and 6 may move logic into vault that should live elsewhere.
@elizaos/sharedis the lowest non-vault dependency in the graph forapp-core,agent, and the renderer. Nothing above can be canonical unless shared's types are.- Phase 2 task 16 ("Derive
SECRET_SALTfrom master key") explicitly needs a verdict on whether the master-key entropy is sufficient — that question can only be answered by readingmaster-key.ts+crypto.tsin this layer.
- Vault backend coexistence —
vault.ts(file) vspglite-vault.ts(PGlite). What duplicates between them? What's genuinely backend-specific? - Master-key resolution paths — every code path that can produce
MasterKeyUnavailableError. AGENTS.md commandment 8: no?? 0fallbacks. - External-CLI shell-out surface —
external-credentials.ts,password-managers.ts,install.ts,manager.ts. Any unsafe argv composition? Command-injection risk? - Shared package boundary policing — anything not used by 2+
packages doesn't belong in
shared. Pull it down to its single owner. - Type duplication between shared and agent / app-core / ui.
- Onboarding provider catalog as source of truth — vs any parallel.
- [!]
eliza/packages/vault/src/index.ts— 153 LOC barrel. Clean per-export. Note: re-exportsemptyStoreonly via./vault.js—pglite-vault.tsline 443 also re-exports it via the same hop. No dead exports — every named export is consumed somewhere in app-core or agent. - [!]
eliza/packages/vault/src/types.ts— 64 LOC. Clean.StoredEntryis the canonical discriminated union for the file-backed store; the PGlite backend translates rows back into the same shape only insideinsertLegacyEntry. Note:PasswordManagerReference["source"]is"1password" | "protonpass"— does NOT include"bitwarden". Bitwarden goes throughexternal-credentials.tsonly, never as a stored reference. That's intentional (noop://analog for bw items) but worth documenting. - [!]
eliza/packages/vault/src/vault.ts— 411 LOC. Dual responsibility:createVault()is a backend selector (env-flag dispatch toVaultImplorPgliteVaultImpl) ANDVaultImplis the file-backed implementation in the same module. dedup:assertKeyis duplicated verbatim withpglite-vault.ts:429. dedup:optsCalleris duplicated verbatim withpglite-vault.ts:438. dedup:cachedKey+loadMasterKey()is duplicated (vault.ts:305 vs pglite-vault.ts:308). dedup:setReferencesource/path validation duplicated (vault.ts:184 vs pglite-vault.ts:134). dedup:describe()source-mapping duplicated. dedup:stats()aggregation duplicated. dead:MILADY_VAULT_BACKEND=file/ELIZA_VAULT_BACKEND=fileopt-out is documented as "legacy file-backed VaultImpl" — once Phase 1 cutover is complete and one release has shipped (per the migration safety-net comment at lines 116-120 + pglite-vault.ts:339-340),VaultImpland theacquireFsLock/withStoreMutationLock/PROCESS_STORE_LOCKSmachinery (lines 359-411) become dead code. types:clean, noany. - [!]
eliza/packages/vault/src/pglite-vault.ts— 443 LOC. MirrorsVaultImploperation-for-operation against a single-table SQL schema. dedup:seevault.tsfinding above — every per-op handler has a parallel here. legacy:maybeMigrateFromFile(lines 341-386) +insertLegacyEntry(lines 389-415) are the only PGlite-only paths that should disappear once migration is complete. errors:readValue(lines 273-306) throwsError("vault: corrupt entry...")directly — three distinct corrupt-row checks. These are correctly NOT swallowed. Surprise: PGlite's data dir is<stateDir>/.vault-pglite/— separate from the runtime DB (.elizadb/). The doc comment (lines 32-39) explains the choice well. boundaries:maybeMigrateFromFilereaches into./store.ts(readStore,StoreData) — that's correct because the migration is intentionally a one-way file→PGlite read. -
eliza/packages/vault/src/store.ts— 161 LOC. File-backedvault.jsonreader/writer with atomic temp+rename. Three tinytry/catchblocks: each filters bycode === "ENOENT"or rethrows; correct. Per-pid + 8-random-bytes tmp filename for concurrent-process collision avoidance is sensible. legacy:becomes dead (modulo migration import) onceVaultImplis removed. -
eliza/packages/vault/src/audit.ts— 31 LOC. Append-only JSONL audit log. Singletry/catchthat logs and continues — the comment says "audit failure must not block the caller" which is the correct call here (the caller is doing the actual mutation; an audit-write failure to disk shouldn't poison the operation). Clean.
-
eliza/packages/vault/src/crypto.ts— 83 LOC. AES-256-GCM with key-as-AAD. Wire formatv1:<nonce_b64>:<tag_b64>:<ct_b64>. Singletry/catchonly arounddecipher.final()to wrap asCryptoError. Clean.KEY_BYTES = 32(256-bit key) — confirms Phase 2 task 16 feasibility (HKDF over 32 strong bytes is well-studied). - [!]
eliza/packages/vault/src/master-key.ts— 350 LOC. Three resolvers (osKeychainMasterKey,passphraseMasterKey,inMemoryMasterKey) + a chaineddefaultMasterKey()that walks 1→2 with diagnostic error composition. TheisKeychainUnsafe()heuristic at lines 194-201 (Linux + no D-Bus signal) is defensive against@napi-rs/keyringsegfaulting at the C level — that's a legitimate "refuse before invoking" pattern, not error-swallowing. errors:describe()at lines 262-275 callspassphraseMasterKeyFromEnv(...)which readsprocess.env.ELIZA_VAULT_PASSPHRASEon every describe call — minor, but means describe is not a pure function. dedup:passphrase scrypt parameters (N=2^15, r=8, p=1, maxmem=64MB) are reasonable defaults but undocumented in the env-var registry; CLAUDE.md doesn't listELIZA_VAULT_PASSPHRASEorELIZA_VAULT_DISABLE_KEYCHAIN. types:clean. Phase 2 task 16 feasibility — see Summary. - [!]
eliza/packages/vault/src/password-managers.ts— 73 LOC. Resolvesop://references viaop read(1Password). Proton Pass throws "scaffolded; vendor CLI not stable yet" — correctly fails loud, doesn't return a sentinel. Usespromisify(execFile)with argv array — no shell interpolation. The user-controlledpathis concatenated into aop://URI then passed as a single argv element toop read, which is safe. Dead path:resolveProtonPassbody never returns — could be a one-linerthrow new PasswordManagerError(...)without the unused_pathparam.
- [!]
eliza/packages/vault/src/external-credentials.ts— 495 LOC. 1Password (op) + Bitwarden (bw) CLI adapters with injectedExecFnfor testability. Argv-array-only invocations everywhere — no shell. dedup:1Password account-list / desktop-active probe at lines 348-405 is duplicated almost verbatim inmanager.ts:579-625; the two implementations diverge slightly —manager.tsusespromisify(execFile)directly while this file uses the injectedExecFn. They should converge. legacy:OnePasswordListItem.additional_informationenrichment (lines 116-122) is explicitly to dodge a per-itemop item get -enrichment round-trip; well-documented. types:OnePasswordListItem.urlsand.fieldsareReadonlyArray<{...optional}>— correct narrowing pattern. errors:safeListExternalwrapper inmanager.tsis the correct boundary for treating per-CLI failures as "show what worked, surface the rest as failures"; this file throws and lets the manager wrap. boundaries:defaultExecFn()at lines 465-495 lazy-importsnode:child_process— the doc comment says "so the test environment doesn't accidentally run real subprocesses if a test forgets to inject a stub" butimport("node:child_process")is always available in Node — the lazy import doesn't actually prevent that. The defense-in-depth is in tests injecting a stub at the call site, not in lazy importing. - [!]
eliza/packages/vault/src/manager.ts— 738 LOC. Largest file in the package. Owns four backends (in-house,1password,bitwarden,protonpass), preferences storage, unified saved-login listing across backends, and per-backend detection probes. dedup:readDefaultOpAccountat lines 579-602 is a duplicate ofexternal-credentials.ts:361-386(both probeop account list --format=jsonand pick the first shorthand) — they should share. dedup:isOnePasswordDesktopActiveat lines 611-625 vsexternal-credentials.ts:388-405— same probe (op vault list --account=<sh>), one uses rawexecand one usesExecFn. dedup:isCommandAvailableat lines 726-738 vsinstall.ts:163-170(isCommandRunnable) — samewhich/where.exeprobe with different timeouts (3s vs 5s). One helper. errors:getPreferences()at lines 215-226 catchesVaultMissErrorto returnDEFAULT_PREFERENCES— correct (a missing preferences key is a legitimate "first run" condition, not a failure). errors:detectOnePassword/detectBitwardenuse baretry { ... } catch { return {...status: false} }blocks — these are appropriate boundary catches because the detection probes must always return aBackendStatuseven when the CLI is missing or hung. legacy:protonpassis wired through every detection/preference path but the actual write path throws — the four-backend abstraction has a dead arm. boundaries:createManager()is the only entry; clean. types:clean.
- [!]
eliza/packages/vault/src/inventory.ts— 446 LOC. Meta-layer over Vault:_meta.<key>non-sensitive JSON blobs hold category/label/profiles/activeProfile per stored key;_routing.configholds cross-key routing rules. Reserved-prefix discipline (_meta.,_manager.,_routing.) is correctly enforced inlistVaultInventoryand re-enforced inmanager.list(manager.ts:279-285) — two filters for the same concept. The two filters should be one (the inventory-internal filter is the source of truth). dead:PROVIDER_KEY_PATTERNS(lines 156-169) andPROVIDER_EXACT_KEYS(lines 152-153) andPROVIDER_KEY_TO_ID(lines 134-149) overlap —PROVIDER_EXACT_KEYSis aSetof the keys ofPROVIDER_KEY_TO_ID;PROVIDER_KEY_PATTERNSis a parallel regex list of mostly the same keys. One source of truth. dedup:Z_AI_API_KEYandZAI_API_KEYboth map to"zai";MOONSHOT_API_KEYandKIMI_API_KEYboth map to"moonshot"— same env-alias pattern as Layer 1'scli/run-main.ts(Z_AI_API_KEY → ZAI_API_KEY,KIMI_API_KEY → MOONSHOT_API_KEY). Three places encode the same alias map. types:clean. errors:parseMetaRecordat lines 378-435 throws on non-object JSON — caller catches via the implicitJSON.parsefailure path. Could be tighter. - [!]
eliza/packages/vault/src/profiles.ts— 252 LOC. Per-key activeProfile + per-context routing rules (agent/app/skill scoped). Pure read/write/normalize layer over Vault. Clean per-function. legacy:pickRuleat line 134 walks an unindexedrules[]looking forkeyPattern === key && matchesScope— for users with hundreds of routing rules this is O(n) pergetActive()call. Won't matter in practice (rule counts will be ≤ ~10) but worth noting. boundaries:correctly stays a thin overlay;vault.get/hasis the only read path. - [!]
eliza/packages/vault/src/credentials.ts— 223 LOC. Saved-login storage atcreds.<domain>.<account>with autoallow flag atcreds.<domain>.:autoallow.parseLoginKeyat line 187 splits on the last dot — correct because domains contain dots (github.com.<account>). dedup:URL-encoded account segment (encodeURIComponentat line 52) shares semantics with the autoallow sentinel collision-avoidance comment at lines 24-27 — the sentinel:autoallowsurvivesencodeURIComponentas%3Aautoallowso a literal user:autoallowcannot collide. Smart. dead:failures: string[]array at line 128 is initialized, only ever read viaif (failures.length > 0)at line 143, but never written — that whole branch is dead. errors:parseLoginat line 203 throws on malformed JSON — correct (a corrupt credential entry should NOT silently degrade to "missing"). - [!]
eliza/packages/vault/src/install.ts— 218 LOC. Per-OS install specs (brew/npm/manual) for1password-cli,bitwarden-cli. Pure data + small detection helper. dedup:isCommandRunnableat lines 163-170 (which/where.exeprobe with--version) overlapsmanager.ts:726-738(isCommandAvailablewith barewhich/where.exe) — same idea, one sayscmd --versionand the other sayswhich cmd. Consolidate. types:clean. legacy:protonpassinstall spec is "manual; closed beta" on every platform — the entry exists for typing completeness, no real install path. -
eliza/packages/vault/src/testing.ts— 92 LOC.createTestVault()factory usinginMemoryMasterKey(generateMasterKey())+ auto-cleanup tmpdir. Clean; this is the canonical test fixture. Note: only ever creates the file backend (it callscreateVault({...})without settingMILADY_VAULT_BACKEND— but the default is now PGlite pervault.ts:124). So tests callingcreateTestVault()actually exercise the PGlite path now. Worth a header note.
- [!]
eliza/packages/shared/src/index.ts— 195 LOC barrel. dedup:lines 13-162 are an inlineexport type { ... } from "./config/types.js"enumeration spanning 130+ named types — instead ofexport * from. The reason at line 92-94 is the documentedInboxAutoReplyConfig/InboxTriageRulescollision withcontracts/inbox, which forces the aliasInboxAutoReplyConfig as AgentDefaultsInboxAutoReplyConfig. Two type families with the same name — the canonical owner is the contract; the config shape is "what's stored in eliza.json". Either rename one or scope through subpath (@elizaos/shared/config). boundaries:line 173-177 explicitly excludeseliza-core-roles.tsfrom the barrel because importing it pulls@elizaos/core(and therefore plugin-sql / transformers / onnxruntime) into every consumer of@elizaos/shared. That's a legitimate boundary marker — leave the explanation in place. - [-]
eliza/packages/shared/src/types.ts— 27 LOC. DEAD FILE. DefinesStylePresetwith optionalvoicePresetId/greetingAnimation/topics. The canonicalStylePreset(with these fields required) lives incontracts/onboarding.ts:31. The barrelindex.tsdoes NOT re-export./types.js, and no file in the workspace imports@elizaos/shared/typesorshared/src/types(verified — zero hits). Slated for deletion. - [!]
eliza/packages/shared/src/validation-keywords.ts— 1 LOC re-export to./i18n/validation-keywords.js. dedup:./i18n/validation-keywords.jsis itself a 22-LOC re-export-only barrel that re-exports from./keyword-matching.js. Triple indirection:validation-keywords.ts→i18n/validation-keywords.ts→i18n/keyword-matching.ts. The middle hop adds nothing (no aliasing, no narrowing). Delete the top-levelvalidation-keywords.tsshim, or merge thei18n/validation-keywords.tsbarrel intokeyword-matching.ts. - [!]
eliza/packages/shared/src/connector-cred-types.ts— 72 LOC. Three parallel maps (CONNECTOR_CRED_TYPES,CRED_TYPE_TO_PROVIDER,PROVIDER_LABELS) with hand-maintained alignment. dedup:gmailOAuth2andgmailOAuth2Apiboth map to"gmail"etc. — would be cleaner as a single source-of-truth array with derived maps. Keep — the file is small and the doc warns about alignment. - [!]
eliza/packages/shared/src/connectors.ts— 118 LOC.CONNECTOR_SOURCE_ALIASES+ a runtime-mutable_registeredAliasesregistry with a_rebuildRawToCanonical()rebuild on every register. Module-level mutable singleton (RAW_TO_CANONICAL: Map) that gets rebuilt on registration — same anti-pattern asapp-shell-components.ts:95(Layer 1 finding). For a process-wide registry of canonical names this is OK; for cross-bundle-boundary registration it's a smell. Note dead:lines 99-103 the_getMergedAliases(canonical).length > 0 ? _getMergedAliases(canonical) : [canonical]ternary calls the same getter twice and falls through to[canonical]only when both maps are empty for the canonical — butnormalizeConnectorSourcealready returns""for unknown sources. So the[canonical]branch is unreachable when the input was a known canonical name. Either dead or a cheap defense. - [!]
eliza/packages/shared/src/format-error.ts— 19 LOC.formatError+formatErrorWithStack. Good. Note: same logic appears inline in many call-sites (err instanceof Error ? err.message : String(err)) — Layer 6+ audits should flag those for migration. - [!]
eliza/packages/shared/src/type-guards.ts— 51 LOC.asRecord/asRecordOrUndefined/asObjectArray/asNonEmptyString. Solid. Actively used (e.g.eliza-core-roles.ts:11andservice-routing.ts:1). Status: clean. -
eliza/packages/shared/src/spoken-text.ts— 65 LOC.sanitizeSpeechTextfor TTS pre-processing. Pure function, no env reads. Clean. -
eliza/packages/shared/src/recent-messages-state.ts— 14 LOC. One thin getter forstate.data.providers.RECENT_MESSAGES.data.recentMessages. The doc comment explains why the helper exists (canonical access path). Clean. -
eliza/packages/shared/src/restart.ts— 38 LOC. Browser-safesetRestartHandler/requestRestart— host (CLI / desktop / dev-server) registers the real implementation.RESTART_EXIT_CODE = 75documented as "must stay in sync with run-node.mjs". Simple and correct. Clean. - [!]
eliza/packages/shared/src/self-edit.ts— 146 LOC.isSelfEditEnabled(env gate) +isSelfEditPathDenied(denylist). The denylist explicitly includespackages/shared/src/restart.tsandpackages/shared/src/self-edit.ts— defense in depth so a self-editing agent can't disable its own gate. Good. Status: clean. Surprise: the env vars (MILADY_ENABLE_SELF_EDIT,MILADY_DEV_MODE) are not in the CLAUDE.md env registry. - [!]
eliza/packages/shared/src/settings-debug.ts— 119 LOC.isElizaSettingsDebugEnabledreads three sources (Viteimport.meta.env, an explicit env arg,process.env) — sensible for a logger that runs in both Vite browser bundles and Node. Status: clean. boundaries:SENSITIVE_KEY_REregex at line 9-10 partially overlaps withawareness/registry.ts:21-30'sSANITIZE_PATTERNS— different concerns (key-name pattern vs value-content pattern) so no consolidation. - [!]
eliza/packages/shared/src/eliza-core-roles.ts— 967 LOC. Vendored elizaOS roles helpers (file comment line 13-14). Imports from@elizaos/coreso it's intentionally NOT in the barrel (index.ts:173-177). Used by@elizaos/shared/eliza-core-rolessubpath consumers (e.g.plugins/app-phone,plugins/app-wifi). Boundaries:correct — keep. Out-of-layer review (the 967 LOC body is roles-domain logic; deeper sweep belongs in Layer 6).
- [!]
eliza/packages/shared/src/env-utils.ts— 5 LOC re-export shim around./env-utils.impl.js. dedup:the impl is 8 LOC; merging removes one indirection. The split exists because./env-utils.impl.jsis also imported directly bycontracts/onboarding.ts:5andself-edit.ts:26andruntime-env.ts:1andsettings-debug.ts:6— they all want to bypass the public barrel. The pattern works but two-files-for-one-helper smells. -
eliza/packages/shared/src/env-utils.impl.ts— 8 LOC.isTruthyEnvValuewith a fixed truthy set. Clean. - [!]
eliza/packages/shared/src/runtime-env.ts— 391 LOC. Large, well-organized port + API-security resolver. Three port families (serverOnly,desktopApi,desktopUi), three security knobs (bindHost,apiToken,disableAutoApiToken), CSVallowedOrigins/allowedHosts, loopback/wildcard host classification. dedup:resolveServerOnlyPort,resolveSingleProcessPort,resolveUiPort,resolveDesktopUiPort,resolveAllowedOrigins,resolveApiAllowedOrigins,resolveAllowedHosts,resolveApiAllowedHosts,isNullOriginAllowed,resolveAllowNullOrigin— at least 5 alias-pair functions that re-export the same value through differently-named wrappers. Looks like incremental rename without callsite cleanup. Boundary:resolveDesktopApiPortPreferencereturns{port, sourceLabel, changeLabel, winningKey}— thesourceLabel/changeLabelstrings are user-facing dev-banner copy embedded in the resolver. UI copy in a runtime resolver is a boundary smell; either move strings to the printer (dev-settings-table) or accept that the resolver's diagnostic output is a first-class part of its contract. - [!]
eliza/packages/shared/src/awareness/index.ts— 1 LOC re-export to./registry.js. Same triple-indirection issue asvalidation-keywords.ts. - [!]
eliza/packages/shared/src/awareness/registry.ts— 220 LOC.AwarenessRegistry— Self-Awareness System orchestration. errors:per-contributortry { ... } catch { line = "[id: unavailable]" }is a deliberate "registry MUST NOT throw" contract (file comment line 8-10). That's a defensible exception to AGENTS.md "no swallow" — but thecatch { ... }swallows the error type entirely; switching tocatch (err) { logger.warn(...); line = ... }preserves the contract while making failures observable. dedup:two near-identical detail paths —composeAllDetails(line 197) and the single-contributor branch ingetDetail(line 122) — same try/catch + sanitize logic. types:clean. Module-level singleton_globalRegistry(line 45) — same anti-pattern as Layer 1app-shell-components.ts:95andconnectors.ts:43.
- [!]
eliza/packages/shared/src/onboarding-presets.ts— 268 LOC. Style-preset resolution + character catalog. Three lookup maps (CHARACTER_DEFINITION_BY_ID,BY_NAME,BY_AVATAR_INDEX) built once at module load — fine. legacy:STYLE_PRESETS(line 142) is an unparameterized export, kept "for back-compat" alongsidegetStylePresets(language). Old callers can migrate. boundaries:buildElizaCharacterCatalog()at line 237 returns a structured catalog — used byapps/app/src/character-catalog.ts(Layer 1 audit noted the cast smell there). - [!]
eliza/packages/shared/src/onboarding-presets.shared.ts— 8 LOC.SHARED_STYLE_RULESconst tuple. Inline candidate — only ever imported fromonboarding-presets.ts:10; merging removes one file. - [!]
eliza/packages/shared/src/onboarding-presets.characters.ts— 2648 LOC. The actual character definitions data file. Out-of-layer review — file is data, not logic. Note:wc -lreports it twice in the listing (likely deduped at filesystem level), so 56 files reflect the actual unique count.
-
eliza/packages/shared/src/dev-settings-banner-style.ts— 53 LOC. ANSI styling helpers for orchestrator/Vite/API/Electrobun startup banners. Clean; pure functions, NO_COLOR / FORCE_COLOR / TTY-aware. -
eliza/packages/shared/src/dev-settings-figlet-heading.ts— 86 LOC. Figlet-rendered subsystem headings with a graceful no-figlet fallback. boundary:lazy-requiresfigletviacreateRequireso the package stays browser-safe — well-handled. Clean. - [!]
eliza/packages/shared/src/dev-settings-table.ts— 235 LOC. Dev settings table rendering (Unicode box, narrow / wide layouts). Pure rendering. Status: clean, modulo the boundary note inruntime-env.tsaboutsourceLabel/changeLabelstrings being baked into the resolver.
- [!]
eliza/packages/shared/src/app-hero-art.ts— 419 LOC. Per-app hero-art metadata (background colors, gradients, accents) keyed by app slug. Pure data + small lookup. Out-of-layer review (data).
-
eliza/packages/shared/src/themes/index.ts— 28 LOC barrel. Re-exports theme types + presets fromcontracts/theme.tsandpresets.ts. Clean. - [!]
eliza/packages/shared/src/themes/presets.ts— 807 LOC. Built-in theme definitions (BSC_GOLD_THEME,COMIC_POP_THEME,HACKER_TERMINAL_THEME,NEON_CYBER_THEME,RETRO_90S_THEME). Out-of-layer review (data + design tokens). The shape consumesThemeColorSetfromcontracts/theme.ts.
-
eliza/packages/shared/src/contracts/index.ts— 14 LOC barrel. Re-exports every contract module excepttheme.ts(themes barrel handles theme). Clean. - [!]
eliza/packages/shared/src/contracts/apps.ts— 561 LOC. App manager DTOs (RegistryAppInfo, AppSessionState, AppRunHealth, etc.). ImportsIAgentRuntimefrom@elizaos/core— same tree-shake risk aseliza-core-roles.tsBUT this one IS in the public barrel viacontracts/index.ts. boundaries:every consumer of@elizaos/sharednow drags@elizaos/corebecause of this one type import. Either drop theIAgentRuntimeuse here (it's only referenced for a callback signature, replaceable with a structural type) or split this file. - [!]
eliza/packages/shared/src/contracts/awareness.ts— 56 LOC. Awareness contributor contract. ImportsIAgentRuntimefrom@elizaos/core— same boundary issue asapps.ts. TheIAgentRuntimeis used insummary/detailcallback signatures — a structural alias would untangle. -
eliza/packages/shared/src/contracts/cloud-topology.ts— 126 LOC. Topology resolver derived fromonboarding.tshelpers. Clean. - [!]
eliza/packages/shared/src/contracts/config.ts— 184 LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/contracts/content-pack.ts— 248 LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/contracts/drop.ts— small, clean. - [!]
eliza/packages/shared/src/contracts/inbox.ts— 28 LOC. DefinesInboxAutoReplyConfigandInboxTriageRules— the two names that collide withconfig/types.agent-defaults.ts. The collision is the reason theindex.tsbarrel can'texport * from "./config/types"cleanly. Pick one as canonical and rename the other (the contract version should win — it's the runtime DTO; the agent-defaults one is "what's stored in eliza.json defaults"). Status: findings flagged. - [!]
eliza/packages/shared/src/contracts/lifeops.ts— 3752 LOC. Largest file in the repo by far for this layer. Out-of-layer review — domain-specific to LifeOps. - [!]
eliza/packages/shared/src/contracts/lifeops-connector-degradation.ts— 50ish LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/contracts/lifeops-extensions.ts— 450 LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/contracts/onboarding.ts— 1619 LOC. The onboarding source of truth.ONBOARDING_PROVIDER_CATALOG(line 210) is the catalog. dedup:no parallel catalog in app-core (verified: app-core's 4 references atproviders/index.ts,state/startup-phase-restore.ts,components/settings/ProviderSwitcher.tsxall import this catalog as the source). Good. dead:OnboardingProviderFamilyandOnboardingProviderIdare union types of fixed strings BUT include(string & {})to keep the union open — this defeats type narrowing on every consumer. The catalog declares concrete ids; the union should be the literal union of those ids only. legacy:migrateLegacyRuntimeConfig(line 1061) +pruneLegacyCloudRoutingFields(line 1029) +inferCompatibilityOnboardingConnection(line 1454) +resolveLegacyServiceRoutingInConfig(line 944) +resolveLegacyDeploymentTargetInConfig(line 909) — substantial legacy-shape migration code. Phase 2 task 14 ("collapse reset cascade") may benefit from these eventually being removed once the legacy-config window has passed. types:OnboardingProviderAuthModeandOnboardingProviderGroupalso use the(string & {})open-union trick. Same fix. boundaries:lots ofRecord<string, unknown>walking viaasConfigRecord+readConfigString— the right approach for parsing untrusted JSON, but it pushes type discipline into every reader. -
eliza/packages/shared/src/contracts/permissions.ts— 58 LOC. System permission contracts. Clean discriminated union over OS permission states. - [!]
eliza/packages/shared/src/contracts/scratchpad.ts— 145 LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/contracts/service-routing.ts— 693 LOC. Linked-account + service-routing canonical types + normalizers. legacy:normalizeLinkedAccountConfigandnormalizeLinkedAccountsConfigat lines 374-380 are explicit@deprecatedre-exports ofnormalizeLinkedAccountFlagConfig/normalizeLinkedAccountFlagsConfig"during the WS1→WS3 migration; will be removed once all callers move to the flag-typed helpers (still WS3)". These should be deletable. dedup:normalizeServiceRouteConfigat lines 552-637 manually destructures and re-emits 16 fields with the spread-truthy pattern (...(field ? { field } : {})) — for 16 fields this is 32 lines of mechanical code. Helper would help. types:ServiceRouteConfig.accountIdis back-compat shorthand foraccountIds: [accountId]— the comment at lines 119-122 documents the runtime treats both equivalently. Two ways to say one thing. legacy. - [!]
eliza/packages/shared/src/contracts/theme.ts— 300 LOC. ThemeDefinition + CSS-var-name maps. Out-of-layer review. - [!]
eliza/packages/shared/src/contracts/verification.ts— small. Out-of-layer review. - [!]
eliza/packages/shared/src/contracts/wallet.ts— 766 LOC. Wallet API contracts (balances, NFTs, EVM/Solana). Out-of-layer review.
-
eliza/packages/shared/src/config/types.ts— 8 LOC barrel re-exporting the seven sub-modules. Clean. - [!]
eliza/packages/shared/src/config/types.eliza.ts— 891 LOC. The big eliza.json shape. Out-of-layer review; the InboxAutoReplyConfig/InboxTriageRules collision (withcontracts/inbox.ts) is documented here. - [!]
eliza/packages/shared/src/config/types.agents.ts— 116 LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/config/types.agent-defaults.ts— 401 LOC. Source of theInboxAutoReplyConfig/InboxTriageRulesre-export collision; defines those names with subtly different shapes thancontracts/inbox.ts. Out-of-layer review. - [!]
eliza/packages/shared/src/config/types.gateway.ts— 243 LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/config/types.hooks.ts— 124 LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/config/types.messages.ts— 201 LOC. Out-of-layer review. - [!]
eliza/packages/shared/src/config/types.tools.ts— 416 LOC. Out-of-layer review.
-
eliza/packages/shared/src/i18n/keyword-matching.ts— 159 LOC. Keyword matching + per-locale lookup. Pure functions; ASCII word-boundary detection + Unicode normalization. Clean. - [-]
eliza/packages/shared/src/i18n/validation-keywords.ts— 22 LOC. Pure re-export barrel from./keyword-matching.js. Triple-indirection middle hop. Delete (or merge with keyword-matching.ts). -
eliza/packages/shared/src/i18n/generated/validation-keyword-data.ts— 1107 LOC. Auto-generated fromkeywords/*.keywords.jsonper the file header. Locale coverage: en, zh-CN, ko, es, pt, vi, tl. Clean structure.
The VaultImpl (vault.ts) and PgliteVaultImpl (pglite-vault.ts) implementations are almost line-for-line parallel, with only the storage layer differing. Concrete dedup map:
| Concern | VaultImpl location |
PgliteVaultImpl location |
Resolution |
|---|---|---|---|
assertKey(key) |
vault.ts:343 | pglite-vault.ts:429 | Move to vault-shared.ts (or top of types.ts); both backends import. |
optsCaller(opts) |
vault.ts:352 | pglite-vault.ts:438 | Same file, same deal. |
| `cachedKey: Buffer | null+loadMasterKey()` |
vault.ts:139 + 305 | pglite-vault.ts:82 + 308 |
setReference() source/path validation (3 lines) |
vault.ts:184-189 | pglite-vault.ts:134-139 | Validation lives in a pure helper validateReference(ref) taking PasswordManagerReference. |
set() sensitive-vs-value branching |
vault.ts:152-177 | pglite-vault.ts:90-127 | Split encryptIfSensitive(masterKey, value, key, sensitive) returning { kind, ...payload }; backend persists the resulting record. |
describe() source-mapping |
vault.ts:240-267 | pglite-vault.ts:209-239 | Pure helper descriptorFromEntry(key, kind, source, lastModified) mapping to VaultDescriptor. |
stats() aggregation |
vault.ts:269-285 | pglite-vault.ts:241-261 | Pure reducer tallyEntries(rows) taking Iterable<{kind: string}>. |
readValue() decrypt-or-resolve |
vault.ts:289-299 | pglite-vault.ts:273-306 | After per-backend row fetch, both call the same materializeEntry(masterKey, entry, key) helper that handles secret/value/reference dispatch. |
audit.record() wiring |
vault.ts every method | pglite-vault.ts every method | Already uses shared AuditLog class — no duplication, just call-site noise. |
emptyStore re-export |
vault.ts:357 | pglite-vault.ts:443 | Both re-export from ./store.js. Consumers should import from ./store.js directly; remove both re-exports. |
After consolidation, VaultImpl and PgliteVaultImpl should each be ~150-200 LOC of strictly storage-specific code (file IO + locking, vs SQL + connection management). The shared base + helpers absorb the rest.
One genuine difference that should NOT be abstracted: file-backed VaultImpl needs withStoreMutationLock + acquireFsLock + PROCESS_STORE_LOCKS for its "read whole file → mutate → write whole file" model. PGlite handles concurrency at the connection level. Don't unify the locking primitives; they solve different problems.
Once the Phase 1 cutover is complete and MILADY_VAULT_BACKEND=file is removed (the safety-net release window per vault.ts:116-120):
- Delete
VaultImpland thevault.tsfile-store dispatcher branch. - Delete
withStoreMutationLock,acquireFsLock,PROCESS_STORE_LOCKS. - Delete
store.ts(modulo theStoreDatashape used bymaybeMigrateFromFile's read). - Delete
maybeMigrateFromFile+insertLegacyEntryfrompglite-vault.ts. - Net delete: ~250 LOC across
vault.ts,pglite-vault.ts,store.ts.
Verdict: feasible. Recommend implementing.
Master-key entropy is sufficient:
- Keychain mode: 32 bytes from
crypto.randomBytes(32)(crypto.ts:25generateMasterKey, used atmaster-key.ts:334when no entry exists). Cryptographically uniform. - Passphrase mode: scrypt with N=2^15 (32768), r=8, p=1, 32-byte output (
master-key.ts:124-130). Strong KDF — within an order of magnitude of 1Password's documented master-password derivation, well above PBKDF2 norms. - In-memory mode: 32-byte buffer the caller provides; tests only.
Current SECRET_SALT lifecycle (verified via grep):
- Generated at runtime boot in
eliza/packages/agent/src/runtime/eliza.ts:2921-2924:if (!process.env.SECRET_SALT) { process.env.SECRET_SALT = crypto.randomBytes(32).toString("hex"); }
- Consumed by
eliza/packages/core/src/settings.ts:80(getEnv("SECRET_SALT", "secretsalt")) and the production-mode validator atcore/src/settings.ts:97-106which throws when the salt is the default literal"secretsalt". - Wallet plugins (
plugins/plugin-tee/,plugins/plugin-wallet/) consumeWALLET_SECRET_SALTandSOLANA_SECRET_SALTindependently — those are separate env vars that the Phase 2 task should NOT conflate.
Recommended implementation:
// eliza/packages/vault/src/secret-salt.ts (new)
import { hkdfSync } from "node:crypto";
const SECRET_SALT_INFO = "elizaos.secret-salt.v1";
const SECRET_SALT_BYTES = 32;
/**
* Derive a deterministic SECRET_SALT from the vault master key via HKDF
* (RFC 5869). Bound to the install via the master key's randomness.
*/
export function deriveSecretSalt(masterKey: Buffer): string {
const derived = hkdfSync(
"sha256",
masterKey,
Buffer.alloc(0),
Buffer.from(SECRET_SALT_INFO, "utf8"),
SECRET_SALT_BYTES,
);
return Buffer.from(derived).toString("hex");
}Wire-up: runtime/eliza.ts:2921-2924 becomes process.env.SECRET_SALT ??= deriveSecretSalt(await masterKey.load()). The salt is now stable across restarts (current code regenerates a fresh random salt every boot — meaning any agent-side encryption keyed off SECRET_SALT is already broken across restarts unless something else is persisting it. Worth confirming during implementation).
Migration cost:
- If any consumer of
SECRET_SALTwrites durable encrypted data keyed off it, that data must be re-encrypted with the new derived salt OR the operator's existing salt must be migrated into the vault as_secret_salt.v1and read from there in preference. The fact that the current code regenerates randomly every boot suggests no consumer relies on cross-restart stability — so migration may be a no-op. Verify before shipping.
Caveat: wallet plugins use WALLET_SECRET_SALT and SOLANA_SECRET_SALT (plugin-tee/src/index.ts:41, plugin-wallet/src/chains/solana/environment.ts). These are independent env vars, not the same SECRET_SALT. Phase 2 task 16 should explicitly NOT touch those — they have user-supplied semantics tied to wallet derivation.
| Type / shape | Shared definition | Other definition | Action |
|---|---|---|---|
StylePreset |
contracts/onboarding.ts:31 (canonical, fields required) |
shared/src/types.ts:1 (stale, fields optional) |
Delete shared/src/types.ts (file is dead — verified zero imports). |
InboxAutoReplyConfig |
contracts/inbox.ts:1 (canonical, runtime DTO) |
config/types.agent-defaults.ts (re-aliased as AgentDefaultsInboxAutoReplyConfig in barrel due to collision) |
Pick one as canonical. The contract is the runtime DTO; the agent-defaults entry is "what the user set in eliza.json defaults". Rename the agent-defaults one. |
InboxTriageRules |
contracts/inbox.ts:9 |
config/types.agent-defaults.ts (same alias dance) |
Same. |
PasswordManagerReference["source"] |
vault/src/types.ts:14 (`"1password" |
"protonpass"`) | manager.ts BackendId (`"in-house" |
RuntimeMode / DesktopRuntimeMode |
(none in shared) | electrobun/src/api-base.ts:10 defines `DesktopRuntimeMode = "local" |
"external" |
BackendStatus.authMode |
vault/manager.ts:71 (`"desktop-app" |
"session-token" | null`) |
OnboardingProviderId (open union via (string & {})) |
contracts/onboarding.ts:74 |
(no duplicate, but defeats narrowing for every consumer) | Tighten to literal union; remove the open-union escape. |
Ranked by deletion safety (high to low confidence):
shared/src/types.ts— 27 LOC. StaleStylePresetdefinition, NOT in barrel, zero imports anywhere in the workspace. Safe delete. (grepverified: nofrom "@elizaos/shared/types"and nofrom ".../shared/src/types"matches in the entire repo.)shared/src/validation-keywords.ts(the top-level shim) — 1 LOC re-export to./i18n/validation-keywords.js. Triple-indirection middle hop. Either delete and have callers import from the i18n subpath, or move the keyword-matching content up and delete the i18n folder shim.shared/src/i18n/validation-keywords.ts— 22 LOC. Pure re-export barrel from./keyword-matching.js. Same triple-indirection. Pick one of #2 or #3 to delete (depending on which import shape callers actually use).shared/src/awareness/index.ts— 1 LOC barrel. Delete; callers import./registry.jsdirectly.shared/src/onboarding-presets.shared.ts— 8 LOC.SHARED_STYLE_RULESconst tuple. Single consumer (onboarding-presets.ts:10). Inline candidate.shared/src/env-utils.ts— 5 LOC re-export shim. Most internal callers already bypass to./env-utils.impl.js. Delete the shim, renameenv-utils.impl.tstoenv-utils.ts.- Deprecated
normalizeLinkedAccountConfig/normalizeLinkedAccountsConfigre-exports incontracts/service-routing.ts:374-380— explicitly marked@deprecated"will be removed once all callers move to the flag-typed helpers (still WS3)". Find call-sites, migrate, delete. PROVIDER_KEY_PATTERNS+PROVIDER_EXACT_KEYSinvault/inventory.ts:151-169— both are derived from the third mapPROVIDER_KEY_TO_ID. Three sources of truth for the same concept. KeepPROVIDER_KEY_TO_ID, derive the other two on demand or delete them.Z_AI_API_KEY → ZAI_API_KEYandKIMI_API_KEY → MOONSHOT_API_KEYaliases — appear in three places:vault/inventory.ts:140-143,app-core/cli/run-main.ts:44-53(Layer 1 finding), andapps/app/src/brand-env.ts. Pick one source of truth (shared/src/provider-env-aliases.tswould be a new home that's correctly cross-package); delete the two duplicates.shared/src/index.tslines 13-162 — the 130+ explicitexport type {...}enumeration. If theInboxAutoReplyConfigcollision is fixed by renaming the agent-defaults version, the whole block collapses toexport * from "./config/types.js".
Risk level: LOW. Audit-positive finding.
Every CLI invocation in the vault package goes through one of:
node:child_process.execFile(argv array) — used inexternal-credentials.ts:defaultExecFn,password-managers.ts:38,manager.ts:22,install.ts:13-16.- An injected
ExecFnthat the production default (defaultExecFn) wires toexecFilewith the same argv-array contract.
There is no shell between the caller's strings and the OS process. The user-controllable surfaces are:
| Surface | User input | Sink | Safety |
|---|---|---|---|
password-managers.ts:37 |
path (user-supplied 1Password reference path) |
exec("op", ["read", op://${path}]) — single argv element |
Safe; argv element is not interpreted by a shell. The op:// prefix concat is a URL-scheme string, not shell-special. |
external-credentials.ts:151 |
externalId (1Password item id) |
exec("op", [...sessionArgs, "item", "get", externalId, "--format=json"]) |
Safe; argv element. |
external-credentials.ts:281 |
externalId (Bitwarden item id) |
exec("bw", ["get", "item", externalId]) |
Safe; argv element. |
external-credentials.ts:281 BW_SESSION env |
session token from vault | env: { ...process.env, BW_SESSION: session } |
Safe; passed via env, not argv. |
manager.ts:556 |
session token from vault | exec("op", [..., --session=${session}]) |
Safe; argv element with token concatenated into a single --session= flag value. The = is parsed by op itself, not a shell. |
install.ts:165 |
command name ("brew", "npm") |
exec(cmd, ["--version"]) |
Safe; argv. |
install.ts:208-217 (buildInstallCommand) |
install method package name (from BACKEND_INSTALL_SPECS, hardcoded) | {command, args} returned to caller |
Safe; the spec is hardcoded data, no user injection. |
manager.ts:728-733 (isCommandAvailable) |
command name | exec("which", [cmd]) / exec("where.exe", [cmd]) |
Safe; argv. |
One nuance worth flagging: external-credentials.ts:476-487 (defaultExecFn) sets maxBuffer: 16 * 1024 * 1024 (16 MB). A vault containing 1000+ Login items in 1Password could in principle return more than 16 MB of JSON for op item list --format=json — the call would then fail with MAXBUFFER rather than degrade. That's the right failure mode (no silent truncation), but the limit could be documented or made configurable.
No user-supplied content reaches a shell metacharacter context. No child_process.exec (with shell), no template-string interpolation into a bash -c argument, no popen-style pipe construction. The vault package is shell-injection-clean.
- 5 alias-pair functions in
runtime-env.ts(resolveServerOnlyPort↔resolveSingleProcessPort,resolveDesktopUiPort↔resolveUiPort,resolveAllowedOrigins↔resolveApiAllowedOrigins,resolveAllowedHosts↔resolveApiAllowedHosts,isNullOriginAllowed↔resolveAllowNullOrigin) — incremental rename without callsite cleanup. Pick canonical names and migrate. - Module-level mutable singletons in shared:
_globalRegistryinawareness/registry.ts:45,_registeredAliases+RAW_TO_CANONICALinconnectors.ts:17,43,_registeredProviderOptionsincontracts/onboarding.ts:1588,_packageManagerCacheinvault/install.ts:142. Same anti-pattern as Layer 1app-shell-components.ts:95(Symbol-keyed singleton). Most are defensible for process-wide registries; a sweep should consolidate the registry idiom. @elizaos/coreimport surface in shared —eliza-core-roles.ts(intentionally excluded from barrel) ANDcontracts/apps.tsANDcontracts/awareness.ts(both in the public barrel). The latter two drag@elizaos/coreinto every consumer. Either replaceIAgentRuntimewith structural callback signatures or split those types into ashared/src/contracts-with-runtime/subpath that's NOT in the public barrel.- CLAUDE.md env-var registry gaps — none of
ELIZA_VAULT_PASSPHRASE,ELIZA_VAULT_DISABLE_KEYCHAIN,MILADY_VAULT_BACKEND/ELIZA_VAULT_BACKEND,MILADY_ENABLE_SELF_EDIT,MILADY_DEV_MODE,ELIZA_SETTINGS_DEBUG/VITE_ELIZA_SETTINGS_DEBUG,ELIZA_VAULT_BACKENDare in the env-var section. Should be added during the Phase 2 cleanup.
- Extract a vault
BaseVaultmixin / set of shared helpers (assertKey,optsCaller,cachedKey/loadMasterKey,descriptorFromEntry,tallyEntries,materializeEntry). Net: ~150 LOC removed, second backend implementations become ~200 LOC each. Aligns with Phase 1 already-shipped state. - Implement Phase 2 task 16:
deriveSecretSalt(masterKey)in vault, called fromruntime/eliza.ts. Removes the boot-timecrypto.randomBytes(32)SECRET_SALT generation; the vault becomes the single source of secret-salt entropy. Verify no consumer relies on the (currently broken) cross-restart instability before shipping. - Delete
shared/src/types.ts. Verified dead. Net: -27 LOC, removes the conflictingStylePresetshape. - Tighten
OnboardingProviderId/OnboardingProviderFamily/OnboardingProviderAuthMode/OnboardingProviderGroup— drop the(string & {})open-union trick. Every consumer regains exhaustiveness checking. The catalog is the source of truth; the type union should reflect it. - Resolve
InboxAutoReplyConfig+InboxTriageRulescollision betweencontracts/inbox.tsandconfig/types.agent-defaults.ts. Pick the contract version as canonical; rename the agent-defaults one toAgentDefaultsInboxAutoReplyOverrides(or similar). Removes the 130-line explicitexport type {...}enumeration inshared/src/index.ts.
| File | Violating concern | Belongs in |
|---|---|---|
shared/src/contracts/apps.ts |
IAgentRuntime import drags @elizaos/core into every shared consumer |
Either replace with structural type, or move to contracts-with-runtime/ (not in public barrel) |
shared/src/contracts/awareness.ts |
Same IAgentRuntime issue |
Same |
shared/src/runtime-env.ts |
Embedded user-facing dev-banner copy strings (sourceLabel, changeLabel) inside the resolver |
Move strings to dev-settings-table.ts printer; resolver returns structural data only |
vault/src/manager.ts |
Duplicates op CLI account/desktop probes from external-credentials.ts (lines 579-625) |
Both files share a op-cli-helpers.ts module |
vault/src/install.ts |
isCommandRunnable duplicates manager.ts:isCommandAvailable |
Single helper, both files import |
vault/src/inventory.ts + vault/src/manager.ts |
Reserved-prefix filtering applied in two places (inventory's listVaultInventory AND manager's list) |
Inventory is the source of truth; manager.list defers |
The default test fixture in vault/src/testing.ts:48 actually exercises the PGlite backend now — not the file backend. After Phase 1 flipped the default in vault.ts:121-125 to PGlite, createTestVault() calls createVault({...}) without setting MILADY_VAULT_BACKEND, so every test that previously asserted file-backend semantics is now asserting PGlite-backend semantics. This is probably what you want (test what production runs), but it's worth noting in the file header AND verifying that the 17 parity tests MASTER.md references actually run on both backends rather than just the new default.