Repository navigation
[Security / Storage] Insecure plaintext storage and broken Windows file permissions for Telegram bot token in mavis local-runtime #141
Description
Security Disclosure: Plaintext Storage of Telegram Bot Tokens in Mavis Local-Runtime
Researcher: fotedev
Contact: via GitHub @fotedev
Date: 2026-09-12
Vendor: MiniMax (Mavis desktop product)
Affected component: @mavis/local-runtime (bundled in MiniMax Code desktop app, shipped as app.asar)
Affected versions observed: @mavis/local-runtime@0.2.1 (bundled with MiniMax Code, build date 2026-09-09)
Summary
The Mavis desktop runtime (@mavis/local-runtime) stores Telegram bot tokens, Feishu app secrets, WeChat iLink credentials, and OAuth bearer tokens as plaintext files inside the user's data directory. The runtime attempts to lock them down with fs.chmod(0o600) after writing, but that call is silently ignored on Windows NTFS, so any other interactive user on the same machine can read the bot token. The legacy credential files imported by the one-time legacy-im-migration are also never deleted after migration, leaving an additional plaintext copy behind.
This is not a remote-execution flaw. It is a local information disclosure with material blast radius: any local account (including low-privilege ones) can read an arbitrary user's bound Telegram bot token and impersonate that bot to every chat it has been added to.
Vulnerability classification
- CWE-256 — Plaintext Storage of a Password
- CWE-732 — Incorrect Permission Assignment for Critical Resource
- CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
Estimated CVSS v3.1: 5.5 (Medium) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N. Local, low-privilege, no user interaction; high confidentiality impact on the bound bot token.
Affected files and code paths
All paths below are inside the bundled Electron archive at:
<MINIMAX_INSTALL_DIR>/resources/app.asar
└── node_modules/@mavis/local-runtime/
├── src/channels/telegram.ts # active store
├── src/channels/legacy-im-migration.ts # one-time forward migration
└── src/channels/adapters/telegram/ # consumer
└── node_modules/@mavis/oauth-core/
└── dist/credential-store/file-store.js # sibling OAuth store
Active store — writes the bot token in plaintext
LocalTelegramChannelStore.save() in src/channels/telegram.ts:
private async save(): Promise<void> {
const payload: LocalTelegramChannelFile = {
schemaVersion: 1,
bindings: Object.fromEntries(
[...this.bindings.entries()].sort(([left], [right]) => left.localeCompare(right)),
),
};
await mkdir(dirname(this.filePath), { recursive: true });
await writeFile(this.filePath, yaml.dump(payload, { lineWidth: 120, noRefs: true }), {
encoding: 'utf8',
mode: 0o600,
});
// Explicit chmod — on some filesystems (e.g. FAT32 mounts inside CI) the
// `mode` option is silently ignored.
await chmod(this.filePath, 0o600);
}
private get filePath(): string {
return join(this.dataDir(), 'telegram-channel.yaml');
}The mode: 0o600 and the follow-up chmod(0o600) are no-ops on Windows NTFS. NTFS ignores POSIX mode bits; fs.chmod on Windows can only toggle the read-only attribute. The file inherits the parent directory's DACL, which on most installations grants Read to the local Users group (every interactive account on the host).
Resulting on-disk files (verified on a test install):
$MINIMAX_DATA_DIR/telegram-channel.yaml # active store, botToken plaintext
$MINIMAX_DATA_DIR/credentials/mavis/telegram.json # legacy copy, botToken plaintext
$MINIMAX_DATA_DIR/auth.json # OAuth bearer + refresh, FileStore path
Legacy copy is never removed after migration
src/channels/legacy-im-migration.ts reads the legacy JSON and migrates into the new YAML store:
async function migrateTelegram(options, agent, result): Promise<void> {
const cred = await readLegacyCredential(options.dataDir, agent, 'telegram');
if (!cred) return;
const botToken = readFirstString(cred, ['botToken', 'bot_token', 'token']);
if (!botToken) { result.skipped += 1; return; }
if (await options.telegramStore.get(agent)) {
result.skipped += 1;
} else {
await options.telegramStore.bind({ agentName: agent, botToken, /* ... */ suppressFamilyMutation: true });
result.migrated += 1;
}
await ensureRoute(options, 'telegram', agent, telegramClientId(agent), result);
}There is no unlink of the legacy file. It remains on disk with its original permissions.
Sibling OAuth store has the same flaw, explicitly
@mavis/oauth-core/dist/credential-store/file-store.js:
async assertPrivatePermissions() {
if (process.platform === 'win32')
return; // ← silent skip on Windows
try {
const [directory, file] = await Promise.all([stat(this.authHome), stat(this.path)]);
if ((directory.mode & 0o077) !== 0 || (file.mode & 0o077) !== 0) {
throw new CredentialStorePermissionError();
}
} catch (error) { ... }
}The win32 early return makes the permission guarantee a fiction on the most common desktop OS.
Impact
- Token theft. Any local user (including a low-privilege service account) on the host can
cat $MINIMAX_DATA_DIR/telegram-channel.yamland obtain the bot token. The same is true for the OAuth bearer/refresh token inauth.json. - Bot impersonation. A stolen Telegram token gives full access to the bot's conversations, files, and inline keyboards. If the bot has been added to group chats, the attacker can read and post in them until the token is revoked.
- Lateral exposure. If the same host is used by multiple people (shared workstation, RDP host, kiosk mode), every user with a local account is implicitly in scope.
- Persistence. The legacy
credentials/<agent>/<platform>.jsonfile is never deleted after migration, doubling the on-disk footprint and remaining readable by other local users.
Steps to reproduce (anonymised)
On any host with Mavis / MiniMax Code installed and a Telegram bot bound via the channel UI:
# 1. Locate the user data dir (default on Windows: %USERPROFILE%\\.minimax)
ls "$USERPROFILE/.minimax/"
# 2. Confirm a world-readable copy of the bot token exists
ls -l "$USERPROFILE/.minimax/telegram-channel.yaml"
# expected (MSYS): -rw-r--r-- # mode bits 0644, inherited Users-group DACL
# 3. Confirm the legacy copy is still present after migration
ls -l "$USERPROFILE/.minimax/credentials/<agent>/telegram.json"
# expected (MSYS): -rw-r--r--
# 4. Grep for a Telegram bot token format (digit:base64)
grep -E '"botToken"\\s*:\\s*"[0-9]+:[A-Za-z0-9_-]+"' \\
"$USERPROFILE/.minimax/telegram-channel.yaml" \\
"$USERPROFILE/.minimax/credentials/<agent>/telegram.json"No secret values are reproduced in this report.
Recommended remediation
A complete fix touches the Mavis monorepo (@mavis/local-runtime, @mavis/oauth-core). Suggested order:
- Move secrets out of plaintext files. Default to an OS keyring (Windows Credential Manager / macOS Keychain / libsecret) via a thin cross-platform wrapper (
@napi-rs/keyringis a known-good choice). Make the plaintext file path a fallback that emits a one-shot startup warning. - Persist a reference, not the secret.
LocalTelegramBindingRecordcarries abotTokenRefinstead ofbotToken; the per-call sender factory resolves the reference through the keyring. - Fix
FileStore.assertPrivatePermissions()on Windows. Drop theprocess.platform === 'win32'early return. Apply an explicit NTFS DACL viaicaclsor a Node native addon (e.g.@napi-rs/win32-acl), and fail startup when the resulting ACL still grants read access to non-owners. - Shred the legacy file on successful migration. After
legacy-im-migration.tsupserts into the new store, overwrite the source JSON with zeros thenfs.unlinkit. Add alegacy-shred-atmarker so re-runs are no-ops. - Run the migration once on existing installs. A
cleanupTokenMigrationV1flag, set when the user next opens the desktop app, decrypts the on-disk YAML in memory, moves each token into the keyring, and rewrites the YAML withbotTokenRefonly. - Audit other channels. The same pattern applies to
LocalFeishuChannelStoreandLocalWeChatChannelStore, and to@mavis/oauth-core's OAuth credential file. Verify each one before declaring the fix complete.
A sketch diff (illustrative, not drop-in) for the active store:
--- a/src/channels/telegram.ts
+++ b/src/channels/telegram.ts
@@ class LocalTelegramChannelStore
- botToken: input.botToken.trim(),
+ // Persist an opaque reference; the real token lives in the OS keyring.
+ botTokenRef: await this.credentialRef.put({
+ agentName,
+ service: 'telegram',
+ account: clientId,
+ secret: input.botToken.trim(),
+ }),And for the legacy shred step:
--- a/src/channels/legacy-im-migration.ts
+++ b/src/channels/legacy-im-migration.ts
@@ async function migrateTelegram(...)
await options.telegramStore.bind({ ... });
+ await shred(join(options.dataDir, 'credentials', agent, 'telegram.json'));
result.migrated += 1;Suggested coordinated disclosure timeline
- T+0 (now): report sent to GitHub via this issue.
- T+30: suggested public disclosure date if a fix is in progress, or sooner if you prefer.
- T+90: hard deadline for public disclosure if no fix has shipped.
I am happy to coordinate timing, validate candidate fixes against a throwaway install, and to keep the technical details confidential until a release window is confirmed.
No real bot token values are included; on-disk file paths shown are intentionally parameterised.
Activity
hetaoBackend commentedon Sep 21, 2026
Thank you for the responsible, detailed disclosure. Acknowledged on both counts:
Forwarded to security review. For future sensitive findings, feel free to note in the issue that details are available privately and we'll provide a channel.
Ralle1976 commentedon Sep 23, 2026
Credential-write audit as requested in the triage ("coverage should be audited across all credential files") — every row read at the cited path:line against current
main. Data only; the Windows DPAPI-vs-ACL design call stays yours.Two helpers exist and cover part of the surface:
packages/config/src/private-config-file.ts:16writePrivateConfigFileSync(+restrictConfigFileSync:7, which returns early on win32 at :8) — used byconfig.ts,byok-config.ts,cu-backend-io.ts, and the migration backups.packages/oauth-core/src/fs/atomic-write.ts:9atomicWritePrivateFile(+ensurePrivateDirectory:4— the only helper that does dir 0o700) — used byoauth-corefile-store.ts:80,119andstate-store.ts:140.Everything else re-implements its own temp+chmod dance (or does nothing). Inventory of secret-bearing writes:
channels/telegram.ts:234-241channels/feishu.ts:285-290channels/wechat.ts:303-308channels/owner-store.ts:269-274channels/durable-yaml.ts:65,72,81(→channel-routes.yaml,channel-bindings.yaml)channels/access-control-store.ts:382-383config/local-runtime-auth-context.ts:76-92tui/src/auth/storage.ts:272-286,359-368tui/src/auth/identity-storage.ts:143-150local-runtime-v2/.../miniapp/workspace-storage.ts:395local-runtime-v2/src/infra/runtime-owner/identity.ts:88-90tui/src/auth/region-preference.ts:40-53Already test-covered (
packages/config/test/config-file-permissions.test.ts, note:describe.skipIf(process.platform === "win32")): the fiveconfig.yamlwriters (config.ts:1655,1851-1919,byok-config.ts:247,495-513,cu-backend-io.ts:54-55,local-model-provider-write.ts:230-268,tui-status-line-write.ts:17-43,local-runtime/config/update.ts:82-305). Non-secret writers with only cosmetic gaps:model-cache.ts:154-161,review-link/store.ts:177-181.Legacy-migration leftovers (as the report states):
channels/legacy-im-migration.tsreadscredentials/<agent>/{feishu,telegram,wechat}.json(:166, :186) and its imports arereaddir/readFileonly — no unlink anywhere in the file, so migrated plaintext copies survive and are re-scanned every repair pass (listLegacyImCredentialCandidates:116-133).persistence/migration/agent-name-conflict-migration-backup.ts:114-122cpSync-copiescredentials/<agent>into the backup dir — a second plaintext copy that is likewise never deleted.Windows note (confirming the disclosure): every 0o600 above is either explicitly skipped on win32 (
private-config-file.ts:8,atomic-write.ts:6,23) or an unconditional no-op; there is no DPAPI or ACL code anywhere inpackages/(grep for chmod/ACL/DPAPI), so all of these files inherit the parent directory ACL with no owner-only restriction.Summary counts: 12 secret-bearing write sites — 5 covered (POSIX) via the two helpers, 5 pattern-covered but untested and missing dir-0700 or chmod, 2 with partial protection, 1 (
access-control-store.ts) with none. Not found in the public source: legacycredentials/<agent>/*.jsonwriters (only readers/migrators exist here).Ralle1976 commentedon Sep 26, 2026
Follow-up research to the write-site audit comment above (2026-09-23): a full storage & credential write-site inventory — channel stores, legacy migration, auth storage, and the composition layer — with file:line evidence for ≈145 write sites plus 10 consistency findings (F1–F10) proposed as follow-ups. Read-only analysis of the public source at
ci/windows-package-acceptance@b75ff66; no repository files were changed (9/9 spot-checks re-verified verbatim before posting).One note on the draft below: its "publication target to be decided" line refers to the choice between this thread and a new data-safety issue — resolved here in favor of this thread so the #141 audit line stays in one place. F1–F10 are tabled at the end with suggested handling; happy to split them into dedicated issues if you prefer them triaged that way.
Storage & Credential Write-Site Inventory — channel stores, legacy migration, auth
Read-only analysis of
MiniMax-AI/minimax-codepublic source. Provenance: branchci/windows-package-acceptance, HEADb75ff66, analyzed 2026-09-26 (Windows 11 x64 26200, Node 24, no Developer Mode). Every row is file:line evidence; 9/9 spot-check samples were re-verified verbatim against the source before publishing this table. No repository files were changed by this analysis.Scope (one inventory across four areas): (A) channel store write sites (telegram/feishu/wechat/owner/route + adjacent stores), (B) legacy migration startup + receipts incl. a deletion-safety check ("confirm no deletion of legacy files"), (C) oauth/tui auth storage + local-runtime-v2 credential writes (+ v1 parallels), (D)
host-channel-composition.ts/channel-context.ts. Prepared as research material in the #141-audit tradition; publication target to be decided (follow-up research comment or a new data-safety issue).Key facts (TL;DR)
packages/local-runtime/src/channels/— 5 named stores (feishu, telegram, wechat, owner, route) + 4 adjacent (bindings, im-connections, access-control, outbound-audit).agent-modules/agent-core/mcode-tools-hostcontain no store code.rename, v2-derivative cache cleanup, destructive SQL confined to the v2 DB copy). See Part B for the full exception table.auth.json(atomic 0o600), TUIcli-auth/*matrix + shared projection,config.yamlBYOK keys (plaintext, 0o600), and v1 channel YAMLs (botToken/appSecret plaintext, 0o600).auth-state.jsonis token-free by an explicit regex guard.host-channel-composition.tsis pure wiring (zero store writes);channel-context.tsis a pure 22-line mapper. All persistence in the composition layer goes through session/conversation-DB lifecycle ports, not channel YAML.writeFilerewrites while 3 sibling stores use temp+fsync+rename; 3 stores still have the async load race feishu already fixed; credential-store mutators have no write lane; access-control.yaml is saved without0o600;channel-routes.yamlis rewritten on every boot.Part A — Channel store write inventory
Base path
B = packages/local-runtime/src/. Store state keys: feishu per bareagentName(feishu.ts:823-825), telegram pertelegram:<agent>(telegram.ts:973-975), wechat perwechat:<agent>(wechat.ts:996-998), owner perclientName, route global (schemaVersion:2).A.0 Store overview
LocalFeishuChannelStoreB/channels/feishu.ts:75webhook|websocket|mock, timestamps<dataDir>/feishu-channel.yaml(:293-295)save():278-291plainwriteFile:286+chmod 0o600:290LocalTelegramChannelStoreB/channels/telegram.ts:66mock|sdk<dataDir>/telegram-channel.yaml(:244-246)save():227-242plainwriteFile:235+ chmod:241LocalWeChatChannelStoreB/channels/wechat.ts:86pending:<session>, ilinkBotId, baseUrl, webhookToken, botName, bindSessionId, qrcodeToken/qrcodeUrl, getUpdatesBuf (long-poll cursor), connected, enabled, modemock|polling<dataDir>/wechat-channel.yaml(:311-313)save():296-309plainwriteFile:304+ chmod:308LocalChannelOwnerStoreB/channels/owner-store.ts:85{ownerSenderId, ownerName?, bootstrappedAt}(first-DM owner gate)<dataDir>/channel-owner.yaml(:277-279)save():262-275plainwriteFile:270+ chmod:274LocalChannelRouteStoreB/channels/route-api.ts:199rules[](id/platform/match/target/enabled/priority/requireMention) +defaultRouteper platform (:55-59)<dataDir>/channel-routes.yaml(:70, filePath:212)mutate():532-548→mutateDurableYaml(atomic)LocalChannelBindingStoreB/channels/infra.ts:265-745bindings+messageFilters<dataDir>/channel-bindings.yaml(:742-744)mutate():686-698→mutateDurableYaml(atomic)LocalImConnectionStoreB/channels/im-connection-store.ts:68connections,physicalBindings(cursor =updatedAtgeneration),legacyImMigrationTombstoneschannel-bindings.yaml(:188)mutateDurableYaml(atomic, per-file lane)LocalAccessControlStoreB/channels/access-control-store.ts:153${platform}:${clientName}ACL policy<dataDir>/access-control.yaml(:386-388)save():373-384plainwriteFile:383, no chmodLocalChannelOutboundStoreB/channels/runner.ts:66-146<dataDir>/channel-outbound-messages.yaml(:143-145)appendScratch attachments:
<dataDir>/tmp/im-attachments/<scope>/<ts>-<file>(wechat.ts:1413-1426). Legacy seed source for imports:<dataDir>/credentials/<agent>/{feishu,telegram,wechat}.json(legacy-im-migration.ts:186).A.1
LocalFeishuChannelStorewrite sitesfeishu.ts:146-147bindings.set+save()bind()feishu.ts:163-164bindings.delete+save()unbind()feishu.ts:205-206bindings.set+save()cloneBindingForAgent()feishu.ts:221-222bindings.set+save()enabled+updatedAtsetEnabled()feishu.ts:278-291bindingsmapsave()feishu.ts:505store.bindLocalFeishuChannelApi.bindBodyfeishu.ts:554store.unbindLocalFeishuChannelApi.unbindBodyadapters/feishu/feishu-adapter.ts:407store.bindFeishuPlatformAdapter.bindadapters/feishu/feishu-adapter.ts:444store.unbindFeishuPlatformAdapter.unbindprimary-agent-family-platforms.ts:78cloneBindingForAgentenabled)port.cloneCredential(feishu)primary-agent-family-platforms.ts:80setEnabledport.setEnabled(feishu)primary-agent-family-platforms.ts:82unbind({suppressFamilyMutation:true})port.deleteCredential(feishu)legacy-im-migration.ts:268bind({…, suppressFamilyMutation:true})migrateFeishuIndirect drivers:
channel-bind-routes.ts:80(bindFeishuConnect),:146(bindFeishu),:128(disconnectChannel),:193(unbindFeishu);feishu-onboard.ts:373(bindBot);primary-agent-family-reconcile-runner.ts:145-161,338,391(port ops);primary-agent-family-unbind-cleanup.ts:101.A.2
LocalTelegramChannelStorewrite sitesInternal mutators:
bindtelegram.ts:116-117,unbind:131-132,cloneBindingForAgent:169-170,setEnabled:185-186,save():227-242.telegram.ts:580store.bindgetMetoken verify:537-578)LocalTelegramChannelApi.bindtelegram.ts:611store.unbindLocalTelegramChannelApi.unbindadapters/telegram/telegram-adapter.ts:236store.bindTelegramPlatformAdapter.bindadapters/telegram/telegram-adapter.ts:342store.unbindTelegramPlatformAdapter.unbindprimary-agent-family-platforms.ts:98cloneBindingForAgentport.cloneCredential(telegram)primary-agent-family-platforms.ts:100setEnabledport.setEnabled(telegram)primary-agent-family-platforms.ts:102unbind({suppressFamilyMutation:true})port.deleteCredential(telegram)legacy-im-migration.ts:305bind({…, suppressFamilyMutation:true})getMe)migrateTelegramIndirect drivers:
infra.ts:2032/:2036(legacy HTTP bind/unbind),channel-bind-routes.ts:170/:114,adapters/telegram/telegram-adapter-routes.ts:175/:209, reconcile-runner port ops.A.3
LocalWeChatChannelStorewrite sitesInternal mutators:
cloneBindingForAgentwechat.ts:134-135,setEnabled:150-151,bind:200-201,unbind:219-220,setGetUpdatesBuffield-delete:255/ set:256/ save:257,save():296-309.wechat.ts:780store.bindconnected:true)LocalWeChatChannelApi.bindBodywechat.ts:805store.unbindLocalWeChatChannelApi.unbindadapters/wechat/wechat-onboard.ts:142store.bindpending:<session>placeholder + qrcodeToken/qrcodeUrl (connected:false)startBindReal(QR issuance)adapters/wechat/wechat-onboard.ts:292store.bindconnected:true)finaliseBinding(frompollBindStatusReal:248)adapters/wechat/wechat-adapter.ts:169store.bindLocalWeChatChannelAdapter.bindadapters/wechat/wechat-adapter.ts:356store.unbindLocalWeChatChannelAdapter.unbindadapters/wechat/wechat-monitor-wire.ts:295setGetUpdatesBuf.catch)disconnectedListenerapi/host-channels.ts:1926setEnabled(agentName,false)onBindFinalisedhookprimary-agent-family-platforms.ts:118/120/122legacy-im-migration.ts:338bind({…, suppressFamilyMutation:true})migrateWeChatIndirect drivers:
channel-bind-routes.ts:214/:215/:230/:119/:253,infra.ts:2044, reconcile-runner port ops.A.4
LocalChannelOwnerStorewrite sitesInternal mutators:
checkAndBootstrapinsert+saveowner-store.ts:138-139,cleardelete+save:166-167,rekeyPrimaryFamilymove/drop+save:212-217,save():262-275.access.ts:115checkAndBootstrapcheckChannelAccess(owner gate)infra.ts:1007checkAndBootstrappreflightInbound(AC-pipeline path)feishu.ts:564,telegram.ts:621,wechat.ts:813,adapters/telegram/telegram-adapter.ts:347,primary-agent-family-unbind-cleanup.ts:58clearOwnerAllConventions(owner-store.ts:71-83→clear:80)primary-agent-family-state.ts:89rekeyPrimaryFamily:90-97)migratePrimaryFamilyStatemode'apply'A.5
LocalChannelRouteStorewrite sites (channel-routes.yaml)All mutators funnel through
mutate()route-api.ts:532-548→mutateDurableYaml:addRulepush:308,updateRulereplace:344,deleteRulesplice:353,updateDefaults:385,rekeyPrimaryFamilyrewrite/drop:513-523,migrateToV2target rewrite:258-289.route-api.ts:143updateRulerouteLocalChannelRouteApi(PUTrules/:id)route-api.ts:149deleteRulerules/:idroute-api.ts:164addRulerulesroute-api.ts:175updateDefaultsdefaultRouteper platformdefaultslegacy-im-migration.ts:399addRulelegacy-<platform>-<agent>ensureRoute(legacy import, only when no rule exists:377)rootless-startup-migration.ts:179migrateToV2migrateRootlessChannelState(startup, every boot — see F6)primary-agent-family-state.ts:72rekeyPrimaryFamilyclientName/agentId(+ defaults:517-523)migratePrimaryFamilyStateprimary-agent-family-unbind-cleanup.ts:55deleteRule(loop)match.clientName === clientIdcleanupPrimaryFamilyUnboundA.6 Adjacent stores — write sites (condensed)
LocalChannelBindingStore(channel-bindings.yamlbindings/messageFilters, viamutate()infra.ts:686-698):channel-route-resolver.ts:94upsertresolveWithLegacyFallback(legacy route creation)rootless-route-resolver.ts:236-237advanceProjectMain/compareAndSetbindExplicitrootless-route-resolver.ts:261attachProjectMainpublish(project-main attach)rootless-route-resolver.ts:328-329advanceProjectMain/compareAndSetpublish(new cursor)infra.ts:1313deleteByAgentrouteAgentIm(DELETEagent/:name/im)infra.ts:1984setMessageFiltermessage-filter/:clientIdinfra.ts:2080upsertbindingsinfra.ts:2090deleteByKeybindings/:keyapi/host.ts:1297deleteBySessiondeleteChannelBindingsForSession←local-runtime-v2/src/compat/v1/session.ts:201(session deletion)default-channel-binding.ts:50deleteByKey(loop)deleteAgentPlatformChannelBindingsfeishu-onboard.ts:408upsertbindDefaultSession(legacy no-connection path)rootless-startup-migration.ts:230migrateToV2primary-agent-family-state.ts:79rekeyPrimaryFamilyprimary-agent-family-unbind-cleanup.ts:57deleteByAgentPlatform(
pininfra.ts:449,pinAllForAgent:463,migratePinnedSession(ForAgents):488/:497have no production callers — documented legacy compat API:484-487; see F7.)LocalImConnectionStore(channel-bindings.yamlconnections/physicalBindings/legacyImMigrationTombstones, all viamutateDurableYaml):prepareImConnectionim-connection-store-connection-operations.ts:67-157;clearImPendingLegacyMigrationim-connection-store-operations.ts:133-146;bindImConnectionim-connection-store-binding-operations.ts:260-325;deleteImBinding:355-372;moveImBindingSessionIfCurrent:132-180;advanceImBindingim-connection-store-conversations.ts:69-89;ensureImBindingSession:125-139;deleteImConnection(incl. tombstone write:269-280,deleteMessageFilter:278)im-connection-store-connection-operations.ts:255-282;repairTrustedImLegacyRoutesim-connection-store-legacy-operations.ts:173-218;migrateExistingImBindingConversationState:305-361. External drivers:channel-connection-binding.ts:266/:163/:170/:191/:209/:279/:294/:196/:143;channel-bind-routes.ts:77/:143/:167/:208/:270/:260/:267;feishu-onboard.ts:133/:183/:395/:211/:477/:480;channel-binding-target-route.ts:87;api/host-channels.ts:934(migrateLegacy); cursor advance viachannel-route-resolver.ts:293/:310.LocalAccessControlStore(access-control.yaml):setaccess-control-store.ts:193-208,delete:210-217,ensureDefaultPolicy:237-246(never overwrites:241). Callers:access-control-routes.ts:124(PUT),:149(DELETE);default-channel-binding.ts:31,api/host-channel-restore.ts:141,feishu-onboard.ts:419(seeds);primary-agent-family-state.ts:142/:146-147(rekey set+delete);primary-agent-family-unbind-cleanup.ts:64.LocalChannelOutboundStore(append-only audit):appendatfeishu-outbound-client.ts:85,telegram.ts:320(recordOutbound, called:300/:302),adapters/wechat/wechat-adapter.ts:390/:467/:482/:571/:672;clearrunner.ts:116-122(no production caller found).A.7 Shared write helpers
mutateDurableYaml+writeFileDurably(same-dir temp + file fsync + atomic replace + best-effort dir fsync; per-filemutationLaneserialisesdurable-yaml.ts:8)channels/durable-yaml.ts:25-44,:59-92,:46-56route-api.ts:537,infra.ts:689, all IM-store mutatorsclearOwnerAllConventions(+imGatewayInboundClientName)channels/owner-store.ts:71-83,:52-57mergeChannelBindingsIntoDocument+validChannelBindingRawKeyschannels/channel-binding-codec.ts:109-151,:93-107schemaVersion ≥2)infra.ts:690/:694writeBindingData/readBindingData/writeLegacyImMigrationTombstones/deleteMessageFilterim-connection-store-data-codec.ts:37-61,im-connection-store-data.ts:68-92,:219-225schemaVersion ≥3data-codec.ts:41)writeConversationDatadata.ts:115is dead — F7)attachProjectMainBinding/advanceProjectMainBindingschannel-binding-project-main.ts:11-37,:39-97infra.ts:321-330,:338-347PrimaryAgentPlatformPort(cloneCredential/setEnabled/deleteCredential→ store mutators)primary-agent-family-platforms.ts:56-125primary-agent-family-reconcile-runner.ts:145-391,primary-agent-family-unbind-cleanup.ts:101ChannelFamilyMutationHookwrite seam (isPrimaryFamilyAgentpre-save /afterMutationpost-save)channel-family-mutation.ts:32-50; implprimary-agent-family-reconciler.ts:131-255; installedapi/host-channels.ts:733-744setFamilyMutationHookfeishu.ts:93,telegram.ts:77,wechat.ts:102)bind/unbindA.8 Notable behavior (Part A)
save()is a whole-file rewrite (sorted map → YAML): last-writer-wins per file.bind()is a full-record upsert that rotates credentials in place and preservescreatedAt(feishu.ts:143,telegram.ts:113,wechat.ts:197).startBindclobbering a confirmed binding back topending:) — guard documented atwechat-onboard.ts:105-129.unbind()row deletes (3 stores), ownerclear()on unbind (both key conventions),deleteRuleloop on unbind cleanup,deleteByAgentPlatform, and IMdeleteImConnection(which deletes connection+binding and writes a migration tombstone so stale legacy rows cannot recreate it,im-connection-store-connection-operations.ts:269-280).wechatStore.setEnabled(false)when the post-scan monitor fails to start (host-channels.ts:1926) before surfacing 503.channel-bindings.yaml(BindingStore + ImConnectionStore) but both funnel throughmutateDurableYaml's per-file lane; section merges preserve foreign sections andschemaVersionis max-preserved (channel-binding-codec.ts:142-148,im-connection-store-data-codec.ts:41).channel-routes.yamlis written only byLocalChannelRouteStore; the IM store merely readsimConversationsfrom it once (im-connection-store-legacy-operations.ts:304).PrimaryAgentFamilyReconcilerholds an in-process lock percanonical:platform(primary-agent-family-reconciler.ts:94/:311-323), but the four stores it migrates cannot be written atomically — mitigated only by inspect-then-apply two-phase (primary-agent-family-state.ts:49-56).Part B — Legacy migration startup, receipts, deletion safety
"Legacy" here = v1 data:
<dataDir>/local-runtime/**,<dataDir>/sqlite.db,agents/,credentials/, channel YAMLs,~/.mavis, opencode DBs. The v2 DBv2/sqlite/runtime-state.sqlitecounts as target (a copy), not legacy source.B.1 Startup flow (entry points & trigger order)
~/.mavis→~/.minimaxpackages/config/src/data-dir.ts:384(wrappers:395/:399)packages/config/src/config.ts:1921-1923→byok-config.ts:178config.ts:1906-1910)config.json(:184), backup copy (:513), provider-list rewritev2/)packages/local-runtime-v2/src/runtime.ts:174-179→local-runtime/src/persistence/migration/v2-migration.ts:152-160; second callerlocal-runtime/src/api/host.ts:503runtime.ts:171-173), idempotentruntime.ts:191-197→local-runtime-v2/src/infra/db/initialize.ts:60-147readAppliedVersions:202-223):76-80) →disableLegacyCronsWithoutFutureRun(:83-86) → migrations <7 →recoverProjectPreferencesBeforeBackfill(:88-91) →recoverLegacySessionRecordsBeforeBackfill(:92-95) → migrations <15 →recoverSessionProjectMetadataBeforeRepair(:97-100) →recoverPublishedCronQueryCollapseCollision(:101-105) → all migrations (:106) → schema check (:109)migrations/cron/migration-0002-copy-legacy-cron-data.ts:7-128,migration-0004-copy-legacy-cron-run-history.ts:5-9local_runtime_crons/_cron_session_history/ purpose sessions intolocal_runtime_v2_scheduler_jobs/_cron_definitions/_cron_runsruntime.ts:208-227→local-runtime-v2/src/service/agent/composition.ts:182-324; lockcompat/v1/runtime.ts:125-201→agent-name-conflict-migration.ts:191-241(proper-lockfile on dataDir)target_preflight(composition.ts:232-247, fail-open:242/:484-496) →runGatedLegacyBootstrap(:248-257,:453-476) → builtin-name-conflict repair (:266-282) →owner_create(:284-299) →ensureBuiltinRows()(:302→agent.service.ts:876-911, identity detach_migration-legacy-identity-detach.ts:110-128) →materializeLegacyCustomAgents()(:308→legacy-custom-materialization.ts:26-72) →completed(:315)runtime.ts:321-333, 382-410→api/host-channels.ts:759-800:769-778→legacy-im-migration.ts(b)migrateRootlessChannelState(c)migrateLegacyImConnectionState.mdimportlocal-runtime/src/cron/legacy-md-migration.ts:92-121<dataDir>/agents/*/crons/<name>.md(:27-32)service/session-system/legacy-migration/importer.ts:105, wiredsession-system/owner.ts:459-497sessions/query/query-service.ts:70, message readsmessages/query/query-service.ts:58etc.,ensureExecutionReadyimporter.ts:122)local-runtime/src/memory/local-memory-store-fs.ts:405-413renameof one legacy memory file (§B.3)packages/tui/packages/agent-runtimehave no legacy-migration startup hooks ("receipt" hits there are turn receipts / update-install receipts).B.2 Receipts
<dataDir>/v2/migration/manifests/<yyyy>/<mm>/<dd>/current-local-runtime-<stamp>.json(v2-migration.ts:951-961; pathsshared/local-runtime-paths.ts:110-112); brand/carrierv2-migration.ts:70,:207-217,:76-78,runtime.ts:53-55,:244-253V2MigrationManifest(v2-migration.ts:27-55): schemaVersion, assetRefMigrationVersion + DB identity (dev/ino/birthtimeMs), migrationId, status, timestamps,copied[]/skipped[],assetRefsUpdated,errors[],rootSessionsDeduped[]:112/:127); only the asset-ref scan is skipped whenisAssetRefMigrationCurrent(:340-355)…/manifests/legacy-agent-bootstrap-import-v1.json(legacy-agent-bootstrap-receipt.ts:6,68-70){"version":1,"kind":"legacy-agent-bootstrap-import"}(:7)isCompleted()(:40-47) exact compare, fail-open (composition.ts:478-496); written viapublishFileIfAbsent(:49-65, hard-link, never overwrite); completed ⇒ collision scan + legacy import wholesale-skipped (skipCode:'bootstrap_receipt_completed':503-516)…/manifests/legacy-custom-agent-identity-reconcile-v1.json(legacy-custom-identity-receipt.ts:6,37-39){"version":1,"kind":"legacy-custom-agent-identity-reconcile"}agent.repository.ts:451(allowLegacyIdentityRecovery); written after a clean materialization pass (legacy-custom-materialization.ts:104-113)…/manifests/builtin-agent-name-conflicts-v1.json(v2-builtin-agent-name-conflict-migration.ts:25,94){schemaVersion:1, migrationId, status:'prepared'|'in_progress'|'completed'|'failed', timestamps, backupDir, mappings[]}(:31-39):97-98); completed + new collisions → receipt discarded and re-run (:99-100)…/manifests/agent-name-conflicts.json(agent-name-conflict-migration.ts:37,266-280)AgentNameConflictManifest(…-manifest.ts:52-71) incl. pinnedmappings(:9-13, retries reuse the same suffix:381-384)getCompletedMigrationResult(:351-372):completed→ no re-applylegacy_migrations(infra/db/schema/legacy-session.ts; repolegacy-migration/repo/migration-repository.ts:16-79)LegacyMigrationRecord(infra/legacy-db/model.ts:195-220): ids,status:'discovered'|'metadata'|'migrated'|'failed'|'deleted', checksums, counts, report, readiness timestamps, warningsisDisplayReady/isHistoryReady(importer.ts:906-922, converter-version gate:918-922) short-circuit re-import;status:'deleted'is a tombstone (importer.ts:132,363)file_history_migrations(legacy-migration/repo/history-checkpoint.ts:21-46){sessionId, source:'ledger-snapshot'|'sqlite-rows'|'sqlite-blob'|'empty', migratedAtMs, messageCount, targetRevision}legacy-im-credentials-migrated(service/channel-system/legacy-credential-receipt.ts:4-14)run(action):true→ return (:11); else run + set (:12-13).mdimport markerlocal_runtime_cron_legacy_imports(cron/legacy-import-store.ts:36-39)(agent_name, cron_name, imported_at_ms),INSERT OR IGNORElegacy-md-migration.ts:34-48, explicitly non-destructive)copied[]/skipped[]*:source-missing,*:target-current,*:target-created-racefs.existsSyncchecks (v2-migration.ts:740-754, 920-943)First start vs. every start: L0–L5 run every start as idempotent checks; genuinely additive/destructive work happens once (R2, R3, R4, R5, R8, R9-gated). L6 is fully lazy per session (R6/R7).
B.3 DELETION CHECK — "confirm no deletion of legacy files"
Verdict: CONFIRMED for legacy files in the normal migration path — the import/copy paths are strictly copy-not-move and delete no legacy files. Evidence:
VACUUM INTOcopy (v2-migration.ts:760-768);copyDirIfMissingcopies only missing targets (:932-949); legacy DB readers arereadonly:true, fileMustExist:true(infra/legacy-db/agent-source.ts:185,readonly-source.ts:94/249,legacy-preferences.ts:31); cron.mdimport leaves sources untouched (legacy-md-migration.ts:35-48);importLegacyAgentshas "no marker, overwrite, upsert, or dual-write" contract (_migration-legacy-agents.ts:63-71).NOT confirmed as an absolute "no deletion of legacy data" — five documented destructive exceptions exist:
local-runtime-v2/src/infra/legacy-db/cleanup.ts:37DELETE FROM session_messages WHERE session_id = ?<dataDir>/sqlite.dbimporter.ts:139-159→owner.ts:480-485→initialize.ts:200); scope contract "never scans or prunes unrelated rows" (cleanup.ts:11-14); errors only logged (owner.ts:468-472)local-runtime/src/persistence/migration/agent-name-conflict-migration-runtime.ts:288,318;…-backup.ts:300-305,319-339,365-368;…-files.ts:46,60,130fs.rmSyncof v2 snapshot caches;UPDATErenames in<dataDir>/sqlite.db; atomic rewrite of legacy YAML/JSON files (channel-bindings.yaml,feishu/telegram/wechat-channel.yaml,channel-routes.yaml,channel-owner.yaml,access-control.yaml,memory/tracking/*.json,plans/*/plan.json);renameSyncof<dataDir>/agents/<from>,<dataDir>/credentials/<from>(…-backup.ts:295-308)explore/worker/verifier,manifest.ts:7,172-195) in the 1× bootstrap (composition.ts:463-469); mandatory full backup first tov2/migration/backups/<date>/<migrationId>/(…-backup.ts:43-170)local-runtime/src/memory/local-memory-store-fs.ts:413rename(legacy, location.mainPath):410)agent-name-conflict-migration-runtime.tssnapshot deletes;assets/store.ts:343-345,392-394,480-481(safeRmSync):473-474)migrations/plugin/migration-0005-adopt-plugin-state.ts:23,runtime/migration-0026-add-cron-execution-fields.ts:128,session/migration-0007-backfill-session-storage.ts:882,925,935,…-frozen.ts:254,310,session/migration-0008-normalize-turn-ingress.ts:542,813,session/migration-0010-converge-session-queue.ts:63,396DROP/DELETEdestructive SQLv2/sqlite/runtime-state.sqlitelocal-runtime/local-runtime.sqlite+sqlite.dbuntouched; reconcile is additiveINSERT OR IGNOREv2-migration.ts:810-813)Additional deletion sites outside migration proper (runtime session lifecycle, listed for completeness):
history-checkpoint.ts:47-80andmessages/repo/drizzle.ts:546-568delete/blank converted legacy rows in the v2 DB on session delete;queue/repo/drizzle.ts:550,:562-565delete legacy queue rows after replace / on session delete. Config data-dir migration removes only links or a verified-empty dir (data-dir.ts:100,165-172,197,310-331), and its EXDEV fallback is copy-then-remove (:227-240).B.4 Migration write sites (condensed)
v2-migration.ts:762,769→858VACUUM INTO+renameSyncv2/sqlite/runtime-state.sqlitev2-migration.ts:810-813INSERT OR IGNORE … SELECTv2-migration.ts:506-509,561-563UPDATE … SET jsonColumnlocal_runtime_message_rows.data_json,local_runtime_queue_items.data_json,local_runtime_messagesdisplay/pi-historyv2-migration.ts:652-661→assets/store.ts:352-395writeFileSync(…asset.json)+assets.jsonlappendv2/assets/<yyyy>/<mm>/<dd>/<time>-<assetId>-<name>v2-migration.ts:944copyFileSync(missing targets only)v2/chats/ledgers/**,v2/chats/snapshots/**v2-migration.ts:960writeFileSyncv2/migration/manifests/<date>/current-local-runtime-*.jsonv2-migration.ts:326→v2-root-session-dedup.ts:165,182,201UPDATE … record_jsoninitialize.tspath:legacy-cron-recovery.ts:22-55;legacy-session-recovery.ts:117-168UPDATE … config_json/record_jsonmigrations/cron/migration-0002-copy-legacy-cron-data.ts:103-124,305-327INSERTlocal_runtime_v2_scheduler_jobs/_cron_definitions/_cron_runssession/migration-0007…:882-935CREATE/INSERT/DROP/RENAME,DELETE FROM sqlite_sequencelocal_runtime_projects(v2 DB rebuild)_migration-legacy-agents.ts:117-119tx.insert(agents)agentsagent-name-conflict-migration-backup.ts:300-305,319-339,365-368;…-files.ts:46,60,130;…-runtime.ts:72-157UPDATE/ atomic rewrite /renameSyncagent-name-conflict-migration.ts:413-494→manifest.ts:129-135writeJsonAtomicv2/migration/manifests/agent-name-conflicts.jsonagent-name-conflict-migration-backup.ts:50-168,247-257VACUUM INTO/copyFileSync/cpSyncv2/migration/backups/<date>/agent-name-conflicts-<ts>/**v2-builtin-agent-name-conflict-migration.ts:114-135,242-245writeJsonAtomic+ renames +UPDATE definition_jsonagents/dirs + v2 DB + YAML/plan fileslegacy-agent-bootstrap-receipt.ts:49-65;legacy-custom-identity-receipt.ts:22-35publishFileIfAbsent(hard-link, never overwrite —infra/file/jsonl.ts:252-319)_migration-legacy-identity-detach.ts:161,168,269-270;…-backup.ts:201-208agents/<name>/+ v2 DB:181-219)agent.repository.ts:782-808(writeMaterializedCustomConfig),materializeLegacyAvatar<dataDir>/agents/<name>/importer.ts:336,371-383,632,421,447-455,504-551;display-import.ts:93,234-241;history-checkpoint.ts:31-46;migration-repository.ts:54-78markDeletedtombstone:54-78)legacy-im-migration.ts:268,305,338,399store.bind×3 +routeStore.addRulechannel-routes.yamlsuppressFamilyMutation:true)legacy-credential-receipt.ts:13;legacy-import-store.ts:36-39INSERT OR IGNOREbyok-config.ts:513+ rewriteconfig.json+ provider rewrite<dataDir>/config.jsondata-dir.ts:86,147,211,259,227symlinkSync/renameSync/cpSync~/.mavis↔~/.minimaxPart C — Credential & auth write sites
C.1 oauth-core + oauth-lease-protocol (+ lease broker)
oauth-core/src/credential-store/file-store.ts:67-71(disk write:119inwritePayload:117-120)put(atomic whole-file rewrite)StoredCredential: accessToken + refreshToken + tokenType/scopes/audience/expiresAtMs/generation/subject/accountId/loginEpoch (types.ts:15-28)<namespaceHome>/auth.json(path:59)FileStore.put— all secrets land hereoauth-core/src/auth-core.ts:510credentialStore.putcommitLoginoauth-core/src/auth-core.ts:522-525credentialStore.putcommitLogincatch pathoauth-core/src/auth-core.ts:527credentialStore.deletecommitLogincatch pathoauth-core/src/auth-core.ts:715credentialStore.putrefreshCredential(viagetAccessToken:268,handleUnauthorized:298,runLogin:455)oauth-core/src/auth-core.ts:349credentialStore.deletelogout({revoke})oauth-core/src/auth-core.ts:695credentialStore.deleteinvalid_grantrefreshCredentialinvalidation pathoauth-core/src/state-store.ts:138-141AuthStateStore.writeSENSITIVE_KEY_PATTERN:41-52)<namespaceHome>/auth-state.jsoncommitLogin:513,refreshCredential:677/685/702/716,logout:320/335/350,publishAuthorizationProgress:588,releaseAuthorizationLease:609,recoverInterruptedCredentialCommit:649,runLogin:426oauth-core/src/namespace-migration.ts:19,32stateStore.writeerror(no secrets)auth/auth-state.jsonmigrateLegacyAuthNamespace/retireMatchingLegacyRootStateoauth-core/src/cross-process-lock.ts:27open(lockPath,'a')<namespaceHome>/auth.lockCrossProcessAuthLockmcode-tools-host/src/lease-broker.ts:145(helperatomicWriteCapability:246-266)randomBytes(32):46)<dataDir>/run/mcode-auth-lease-v1.cap(oauth-lease-protocol/src/endpoints.ts:29/37)startMcodeToolsAuthLeaseBrokeroauth-lease-protocolpersists nothing itself (server writes only socket/endpointnode-server.ts:47-49/87; client only reads the capabilitynode-client.ts:232-244).C.2 TUI auth storage
packages/config/src/local-runtime-auth-context.ts:64-95(write:83)writeLocalRuntimeAuthContext<dataDir>/local-runtime.auth.json(name:5, path:46-48)tui/auth/storage.tstui/src/auth/storage.ts:298(writePrivateCliAuthContext:293-304)<dataDir>/cli-auth/<buildEnv>/<region>/local-runtime.auth.jsonwriteCliAuthContext:103-126,importSharedCliAuthContext:178,migrateLegacyCliAuthContext:323tui/src/auth/storage.ts:120<dataDir>/local-runtime.auth.jsonwriteCliAuthContexttui/src/auth/storage.ts:199<dataDir>/local-runtime.auth.jsonsyncCliRuntimeAuthProjectiontui/src/auth/storage.ts:281(writeCliSharedProjection:266-287)<dataDir>/cli-auth/shared-projection.json(:231-233)writeCliAuthContext:123,importSharedCliAuthContext:179tui/src/auth/storage.ts:363(writeCliAuthScope:357-369)<dataDir>/cli-auth/<env>/<region>/cli-auth.scope.jsonwritePrivateCliAuthContext:301tui/src/auth/identity-storage.ts:145(writeCliAccountIdentity:136-151):153-155) + user identity + verified/rejected<dataDir>/cli-auth/<env>/<region>/account-identity.json(:36-38)writeVerifiedCliAccountIdentity:66-88/writeRejectedCliAccountIdentity:90-103; callersruntime/lifecycle.ts:717,767,825tui/src/auth/region-preference.ts:49(writeTuiRegionPreference:38-55)<dataDir>/preferences/mcode-region.jsonauth/application.ts:149tui/runtime/auth-session.ts:23-39createCredentialStore(oauth-core)auth.json(C.1)McodeAuthApplication.loginauth/application.ts:88/.logout:123cli/provider-command.ts:147/104/85-89→provider/application.ts:80-90→runtime/adapter.ts:480-481→adapters/product-access.ts:197-203→local-runtime-v2 cli-service.ts:740-750→process-local-application.ts:145config.yaml(C.3)runMcodeProviderCommandTUI legacy-matrix token renewal persists only via
writeRefreshedAuthContext(runtime/lifecycle.ts:781-793, call-sites:703,:716,:753,:766).C.3 local-runtime-v2 credential writes
service/model-system/management/service-minimax-operations.ts:255-265(key set:261)updateByokConfigdraftminimax_api.apiKey<dataDir>/config.yaml→minimax_api.apiKeyupsertMiniMaxApiKeyservice-custom-provider-operations.ts:78-107(createUserProvider,options.apiKey:91):94)config.yaml→custom_provider.<key>.options.apiKeycreateUserProviderservice-custom-provider-operations.ts:197-198(updatedProviderOptions:190-201)options.apiKey:116-117)config.yamlupdateUserProviderservice-custom-provider-operations.ts:242-245structuredClonecopiesoptions.apiKeyconfig.yamlduplicateUserProviderservice-candidate-operations.ts:191-202(persistCandidate)config.yamlsaveUserModelProviderCandidateservice/model-system/management/service-context.ts:93-94(candidateOptions:83-97)persistCandidateresolveUserProviderCandidate:242-271service/model-system/codex-oauth.ts:134(storage.set, defaultauthStorageFactory:104-138)AuthStorage)<dataDir>/codex-auth.json(:395, confirmedconfig.ts:502)CodexOAuthManager.startLogin→finishLogin:360-375service/model-system/codex-oauth.ts:455-463+configureCodexOAuthProvider:483-498authMode:'oauth', removesoptions.apiKey:517— OAuth keys never land in config)config.yamlrefreshProviderCatalogpackages/config/src/local-model-provider-write.ts:96-135(updateLocalByokConfig, BYOK roots:114-125) →withLockedConfig:221-257(write:242) →atomicWriteFile:259-277(mode 0o600:17)minimax_api/custom_provider/minimaxModelSource/defaultModelconfig.yamlservice/model-system/initialize.ts:44C.4 local-runtime (v1) parallel stores
local-runtime/src/config/update.ts:226-290(updateLocalByokConfig, write:282;atomicWriteFile:292-310)minimax_api.apiKey/custom_provider.*.options.apiKey<dataDir>/config.yamllocal-runtime/src/channels/telegram.ts:91-119(bind,botToken:108, save:113→:235)telegram-channel.yaml(:245)LocalTelegramChannelStore.bindlocal-runtime/src/channels/wechat.ts:155-199(bind,botToken:174,webhookToken:189,qrcodeToken:192→ save:304)wechat-channel.yaml(:312)LocalWeChatChannelStore.bindlocal-runtime/src/channels/feishu.ts:127-144(bind,appId:129,appSecret:130,verificationToken:131-133,encryptKey:134→ save:286)feishu-channel.yaml(:294)LocalFeishuChannelStore.bindlocal-runtime/src/channels/durable-yaml.ts:22-42+:51-83im-connection-store-*-operations.tsExplicitly excluded as non-credential: miniapp
workspace-storage.ts:395(transient init-marker UUID, deleted on commit:406),agent/storage/identity-codec.ts(AES with hard-coded key:6, but only displayName/description/avatar — see F8),runtime-owner/identity.ts:89(PID lease),review-link/store.ts(PR URLs only).C.5 Shared credential write helpers
atomicWritePrivateFile+ensurePrivateDirectory(0o600/0o700, tmp+rename)oauth-core/src/fs/atomic-write.ts:4-29file-store.ts:119,state-store.ts:140FileStore.put/.deleteoauth-core/src/credential-store/file-store.ts:67-77auth-core.ts:510,522,527,715/:349,527,695AuthStateStore.writeoauth-core/src/state-store.ts:138-141auth-core(8 sites),namespace-migration.ts:19/32writeLocalRuntimeAuthContext/clearLocalRuntimeAuthContextFilepackages/config/src/local-runtime-auth-context.ts:64-104tui/auth/storage.ts:120,199,298/:156,190,324writeCliAuthContext(storage.ts:103),writePrivateCliAuthContext(:293),writeCliSharedProjection(:266),writeCliAuthScope(:357),writeCliAccountIdentity(identity-storage.ts:136),writeTuiRegionPreference(region-preference.ts:38)runtime/lifecycle.ts:717/767/792/825,auth/application.ts:149updateLocalByokConfig→withLockedConfig→atomicWriteFile(lockfile + 0o600)packages/config/src/local-model-provider-write.ts:96/:221/:259initialize.ts:44)atomicWriteCapabilitymcode-tools-host/src/lease-broker.ts:246-266:145(only broker-secret write)C.6 On-disk layout at runtime
~/.minimaxor~/.minimax-<profile>(packages/config/src/data-dir.ts:5,40-42); legacy~/.mavismigrated + compat symlink (:83-90,384-393). Env overrides:MINIMAX_DATA_DIR>MAVIS_DATA_DIR(config.ts:1318;tui/runtime/data-dir.ts:31-37), plus--data-dir,DATA_DIR,PROFILE(config.ts:1341-1356).<dataDir>/auth/<buildEnv>/<region>/mcode-public/auth.json+auth-state.json+auth.lock; legacy locationsauth/{auth-state.json,auth.lock,credentials.enc}(namespace.ts:22-41). buildEnv ∈ dev/test/staging/prod, region ∈ cn/en.<dataDir>/local-runtime.auth.json,<dataDir>/cli-auth/<buildEnv>/<region>/{local-runtime.auth.json,cli-auth.scope.json,account-identity.json},cli-auth/shared-projection.json,preferences/mcode-region.json.<dataDir>/config.yaml(getConfigPath()config.ts:1511-1513) —minimax_api.apiKey+custom_provider.<key>.options.apiKeyplaintext, 0o600.<dataDir>/codex-auth.json.<dataDir>/run/mcode-auth-lease-v1.cap+.sock(unix); Windows named pipe\\.\pipe\mcode-auth-lease-<sha256(dataDir)[:16]>(endpoints.ts:6-39).<dataDir>/{telegram,wechat,feishu}-channel.yaml(secrets plaintext, 0o600).C.7 Read-back, delete, rotate, logout
FileStore.get(file-store.ts:62-65, permission check:104-115— unix requires 0o700/0o600, win32 skipped);AuthStateStore.read(state-store.ts:122-136);readLocalRuntimeAuthContext(local-runtime-auth-context.ts:50-62); TUIreadCliAuthContextfamily (storage.ts:55-101,306-327incl. legacy migration:314-327);readVerifiedCliAccountIdentity(identity-storage.ts:40-56); CodexgetCredentials(codex-oauth.ts:108-121); broker capability read (node-client.ts:232-244).MCodeOAuthCore.logout(auth-core.ts:307-362—revokeToken:333, credential delete:349,logout_pendingstate on revoke failure:335-345);invalid_grantdeletes the credential after state publish (:685-696). TUI:clearCliAuthContext(storage.ts:128-135),invalidateCliAuthContext(:155-158),clearCliRuntimeAuthProjectionIfMatching(:183-193),clearLocalRuntimeAuthContextFile(local-runtime-auth-context.ts:97-104),clearCliAccountIdentity(identity-storage.ts:105-107),clearCliSharedProjection(storage.ts:289-291),clearCliAuthScope(:371-373); rejected-token pathinvalidateRejectedAuthContext(lifecycle.ts:817-832).deleteUserProvider(service-custom-provider-operations.ts:250-283) incl.removeProviderCredentials:259-275(initialize.ts:90→CodexOAuthManager.removeCredentialscodex-oauth.ts:345-358→storage.logout:123); key clear viaupdateUserProvider(kind==='clear':198); capability removallease-broker.ts:163,184.refreshCredential(auth-core.ts:673-720) replaces tokens at constantloginEpoch(:709-715, generation+1); TUI matrix renewal writes back viawriteRefreshedAuthContext(lifecycle.ts:781-793).Part D —
host-channel-composition.ts/channel-context.tspackages/local-runtime/src/api/host-channel-composition.ts(370 lines)wireHostChannelSubsystem(:62-357) delegates towireChannelSubsystem(api/host-channels.ts:273); passes store instances/overrides through (ownerStoreOverride:296,feishuChannelStoreOverride:299-301,telegramChannelStoreOverride:302-304,wechatChannelStoreOverride:305-307,wechatRuntimeSdkOverride:308) plus family-state ports (primaryFamilyRootSessions:182-232,primaryFamilySessionModels:233-248). Its only persistence effects are session/conversation-DB lifecycle calls:lifecycle.updateSession:104-109(revealMigratedSession),createSession/replaceRootSession/deleteSession:167-173,replaceRootSession:202-208,createRootSession:213-224,updateSession:244-247,ingress.submit:260-275,ingress.abort:285,maintenance.compact:287-294packages/local-runtime/src/api/channel-context.ts(22 lines)channelContextFromMessageContext:4-17(message context →LocalChannelContext) + platform validator:19-22Findings (proposed follow-ups — nothing changed by this analysis)
writeFile:feishu.ts:286,telegram.ts:235,wechat.ts:304,owner-store.ts:270,access-control-store.ts:383vsdurable-yaml.ts:59-92writeFileDurably/mutateDurableYamlloadedflipped before the async read completes → second caller can observe a half-populated map); feishu already fixed via sharedloadPromisetelegram.ts:213-215,wechat.ts:282-284,owner-store.ts:244-246vsfeishu.ts:76-83,239-259loadPromisepatternrekeyPrimaryFamily; whole-map last-writer-wins. WeChatsetGetUpdatesBufis fire-and-forget on every monitor disconnect and can race bind/unbindwechat-monitor-wire.ts:289-301;owner-store.ts:187-219(mutex only coverscheckAndBootstrap/clear:231-242)mutationLanepattern)access-control.yamlsaved without0o600(every other channel store sets it)access-control-store.ts:373-384feishu.ts:290auth.json+ sensitive-key guard, while channel secrets (botToken/appSecret/verificationToken/encryptKey/webhookToken/qrcodeToken) and BYOK API keys sit in YAML/plaintextfeishu.ts:127-144,telegram.ts:91-119,wechat.ts:155-199,config.yamlwriters vsstate-store.ts:41-52channel-routes.yamlis rewritten on every boot:migrateToV2returnschanged:trueunconditionallyroute-api.ts:288,rootless-startup-migration.ts:179changedonly when targets actually changedpin/pinAllForAgent/migratePinnedSession(ForAgents)(infra.ts:449-497, documented compat),writeConversationData(im-connection-store-data.ts:115), v1 BYOK writerlocal-runtime/src/config/update.ts:226(no in-repo caller),LocalChannelOutboundStore.clear(runner.ts:116-122)identity-codec.ts:6uses a hard-coded AES key (currently only cosmetic agent fields: displayName/description/avatar)agent/storage/identity-codec.ts:6, usedagent.repository.ts:504-506<dataDir>/sqlite.dband deletessession_messagesrowscleanup.ts:37(contract:11-14)v2/migration/backups/…)agent-name-conflict-migration-backup.ts:43-170,…-files.ts:46-130,…-backup.ts:295-339Method & verification
git status --porcelainempty before and after).feishu.ts:286writeFile+:290chmod 0o600;durable-yaml.ts:58-59temp+fsync+atomic replace;cleanup.ts:37DELETE FROM session_messages;auth-core.ts:510/522/527credentialStore.put/delete;service-minimax-operations.ts:261draft.minimax_api.apiKeywrite;local-memory-store-fs.ts:413rename(move case);owner-store.ts:270/274writeFile+chmod;host-channel-composition.tsgrep forwriteFile|.save(|mutateDurableYaml|.bind(→ no matches (confirms D-table verdict); HEADb75ff66.pnpm verify(full, this machine): reachedtest:capabilities, then FAIL —Verification failed at: test:capabilities, exit 1. Result inside that gate: 15 failed / 4598 passed / 61 skipped (4674 tests, 174 files, 468s). Failure breakdown: 12×EPERM: operation not permitted, symlink(this machine has no Developer Mode), 2× POSIX-vs-Windows path assertions (observability.test.ts:86expects/data/v2/…, gets\data\v2\…;:210stringContaining/v2/observability/mcode/), 1×wslpathconversion (wsl-attachment-paths.test.ts:159). All 15 map to the documented [Bug]: Windows local verification: 30 test failures + 1 gate failure in 4 root causes — fixes on 4 prepared branches (+3 findings) #322 Windows classes and to the prepared fix branchestest/symlink-privilege-gated-skip+test/symlink-privilege-warm-scan(EPERM incl. the warm-scan case intest/history-processing.test.ts:1573) andtest/windows-observability-followup(observability paths + WSL case). The failures are pre-existing onci/windows-package-acceptance@b75ff66(which does not contain those branches) and are unrelated to this analysis; the previously published 14/14-green proof was measured on the combined integration state with all prepared branches applied.ci/windows-package-acceptance@b75ff66.hetaoBackend commentedon Oct 19, 2026
Closing as part of a backlog cleanup: this issue was filed more than a month ago against an older release, and the codebase has changed substantially since then. If it still reproduces on the latest version, please open a new issue with the current version and reproduction steps. Thank you for the report!
批量清理积压:该 issue 创建已超过一个月,针对的是较旧版本,代码已有较大变化。如果在最新版本上仍能复现,欢迎附上当前版本号和复现步骤重新提交 issue。感谢反馈!