mixdog 1.3.4 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +15 -0
- package/README.md +11 -0
- package/package.json +2 -1
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/test.mjs +5 -0
- package/src/defaults/shipped-history.json +8 -0
- package/src/defaults/skills/browser-use/SKILL.md +21 -9
- package/src/defaults/skills/setup/SKILL.md +7 -2
- package/src/defaults/skills/setup/references/actions.md +82 -27
- package/src/defaults/skills/setup/references/surfaces.md +67 -34
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +109 -14
- package/src/runtime/channels/lib/schedules-db.mjs +49 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
- package/src/runtime/channels/lib/worker-main.mjs +9 -0
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/config.mjs +49 -22
- package/src/runtime/shared/connector-catalog.d.mts +4 -2
- package/src/runtime/shared/connector-catalog.mjs +16 -8
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/provider-accounts.mjs +224 -58
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/routine-trigger.mjs +42 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/session-runtime/agent-home-delivery.mjs +57 -13
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +292 -79
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +2 -0
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
- package/src/session-runtime/mixy-heartbeat.mjs +807 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/owned-automation-run.mjs +33 -12
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/channel-admin.mjs +66 -18
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -4
- package/src/session-runtime/settings-system-api.mjs +16 -1
- package/src/session-runtime/setup-tool/executor.mjs +49 -14
- package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
- package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/daemon-session-control.mjs +88 -20
- package/src/standalone/daemon.mjs +8 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +8 -5
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/dist/index.mjs +24 -2
- package/src/tui/session/notice-surface.mjs +17 -1
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/settings.mjs +14 -2
- package/src/tui/session/turn-stream.mjs +17 -1
- package/src/tui/session/turn.mjs +22 -2
|
@@ -8,7 +8,7 @@ or requests a setting with no mutation action.
|
|
|
8
8
|
| Target | Destination |
|
|
9
9
|
|---|---|
|
|
10
10
|
| `settings` | Settings home |
|
|
11
|
-
| `providers` | Provider authentication
|
|
11
|
+
| `providers` | Provider authentication |
|
|
12
12
|
| `model` | Main model selection |
|
|
13
13
|
| `websearch` | Web Search route |
|
|
14
14
|
| `workflow` | Workflow packs |
|
|
@@ -17,51 +17,77 @@ or requests a setting with no mutation action.
|
|
|
17
17
|
| `theme` | Theme surface where supported |
|
|
18
18
|
| `profile` | User profile |
|
|
19
19
|
| `autoclear` | Session lifecycle controls |
|
|
20
|
-
| `memory` | Memory capability surface |
|
|
21
|
-
| `mcp` | Extensions →
|
|
22
|
-
| `skills` |
|
|
23
|
-
| `plugins` | Extensions →
|
|
20
|
+
| `memory` | Memory capability surface (Built-in card on the Extensions tab) |
|
|
21
|
+
| `mcp` | Extensions → Connectors tab, including OAuth Connect/Reconnect/Disconnect |
|
|
22
|
+
| `skills` | Skills (activity rail destination) |
|
|
23
|
+
| `plugins` | Extensions → Extensions tab (plugins that bring MCP servers: Connectors → Custom connectors) |
|
|
24
24
|
| `update` | Update surface |
|
|
25
|
-
| `usage` |
|
|
25
|
+
| `usage` | The /usage surface (provider usage); no sign-in |
|
|
26
26
|
| `doctor` | Runtime diagnostics |
|
|
27
27
|
| `context` | Context and compaction |
|
|
28
28
|
| `developer` | Developer options (TUI: Developer picker) |
|
|
29
|
-
| `voice` | Voice
|
|
29
|
+
| `voice` | Voice Built-in card on the Extensions tab (TUI: Settings hub) |
|
|
30
30
|
| `connection` | Web-app pairing; Desktop only, the TUI shows guidance |
|
|
31
31
|
|
|
32
32
|
An attached Desktop or TUI may navigate and return `opened:true`.
|
|
33
33
|
|
|
34
34
|
## Desktop map
|
|
35
35
|
|
|
36
|
-
- Settings → General: profile
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
- Settings → General: profile; Features (Regular check-ins, Reactions, Web
|
|
37
|
+
search, import conversations from other apps (UI-only), Load instruction
|
|
38
|
+
files, Auto reasoning base, link preview); notifications; worktree
|
|
39
|
+
defaults; display language, time format, side panels and their position;
|
|
40
|
+
markdown default view.
|
|
41
|
+
- Settings → Context: Let AI rename sessions, auto-compact (also on model
|
|
42
|
+
change, summary model) and auto-clear.
|
|
43
|
+
- Settings → Model management: "Show in /model" and favorites per model.
|
|
39
44
|
- Settings → Output style: output style selection, plus the desktop-only
|
|
40
45
|
"Expand tool activity" choice (collapsed / commands and edits / everything),
|
|
41
46
|
saved per device; Ctrl+O toggles everything open for the current window.
|
|
42
|
-
- Settings → Theme (one page): System / Dark / Light or a
|
|
43
|
-
(plate, accent, transparency), imported JSON color
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Light / skin choice.
|
|
47
|
+
- Settings → Theme (one page): System / Dark / Light (`theme: "white"`) or a
|
|
48
|
+
skin picture (plate, accent, photo, transparency), imported JSON color
|
|
49
|
+
themes (about 26 color roles; import/export/remove are UI-only), installed
|
|
50
|
+
fonts, font sizes and chat text size. Saved on this device only. First-run
|
|
51
|
+
setup has a Theme step with the same System / Dark / Light / skin choice.
|
|
47
52
|
- Settings → Bots (desktop only): the representative bot overlay, a small
|
|
48
53
|
window with the representative bot that opens a quick message to it. On
|
|
49
|
-
by default; options
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
by default; options: enable, Position (4-way: pinned above the message box
|
|
55
|
+
(default), pinned here, free in the app window, free on screen), keep above
|
|
56
|
+
other windows (default off), collapse bubble, auto-advance long replies,
|
|
57
|
+
close replies automatically, size, and reset position (UI-only). The
|
|
58
|
+
overlay's right-click menu has the same options. Saved on this PC.
|
|
59
|
+
- Settings → Providers: API-key and OAuth authentication, custom provider
|
|
60
|
+
registration, and Usage limits (resume automatically). A custom provider's
|
|
61
|
+
model list waits up to 60 s, can be skipped, and then takes model ids typed
|
|
62
|
+
by hand. Local models are managed in the Local Provider Built-in detail.
|
|
63
|
+
- Settings → Connection (desktop only): web-app pairing, this PC's name and
|
|
64
|
+
the remote-access switch, connected devices (rename, Disconnect all), "PCs
|
|
65
|
+
I opened", SSH servers (per-server settings, server logins, cleanup), and
|
|
66
|
+
"Reopen server windows". Only `revoke_linked_device` has a tool action.
|
|
67
|
+
SSH connect: the address dialog's collapsed "Advanced options" take a login
|
|
68
|
+
key file (typed or browsed) and a default server folder; the key file is
|
|
69
|
+
used for every later connection to that server (reconnects included), and a
|
|
70
|
+
server is saved only after a connection with those settings worked. A server
|
|
71
|
+
that is connected already refuses a changed key or folder: disconnect first.
|
|
72
|
+
Per-server "Model login" is "This PC's accounts" (the default; models run
|
|
73
|
+
through this PC, only while it is connected) or "Server accounts" (models run
|
|
74
|
+
with the logins signed in on the server). "Manage server logins" (Server
|
|
75
|
+
accounts, connected server) logs a provider in or out on the server; an API
|
|
76
|
+
key saved there is kept in a private file (owner-only) when the server has
|
|
77
|
+
no keychain, as on Linux. Changing the model login restarts the server's
|
|
78
|
+
Mixdog after one confirmation and stops its running sessions.
|
|
58
79
|
- Settings → System: update, keep-awake, run in background after the window
|
|
59
80
|
closes, and Doctor.
|
|
60
|
-
- Settings → Developer options: risk-gated
|
|
81
|
+
- Settings → Developer options: data-driven sections (approvals, risk-gated
|
|
82
|
+
providers); read them with `status developer`.
|
|
61
83
|
- Projects: Project registration, name, Project Memories, and Common Memory.
|
|
62
84
|
- Workflows: workflow packs, Main and Web Search defaults, and agent editing.
|
|
63
|
-
- Extensions →
|
|
64
|
-
|
|
85
|
+
- Extensions → Extensions tab: Built-in cards (including Git & GitHub, Media
|
|
86
|
+
generation and its Default generator) and plugins.
|
|
87
|
+
- Extensions → Connectors tab: MCP servers and catalog connectors (Gmail
|
|
88
|
+
connects only with an app password; Calendar and Drive offer "Use my
|
|
89
|
+
Google key"), plus Custom connectors (including plugin MCP servers).
|
|
90
|
+
- Skills (activity rail): skills.
|
|
65
91
|
|
|
66
92
|
Provider authentication is intentionally hidden from the remote web app; guide
|
|
67
93
|
the user to Desktop.
|
|
@@ -75,8 +101,8 @@ install or toggles run through the attached Desktop; the receipt identifies
|
|
|
75
101
|
|
|
76
102
|
Computer observe-only and background Memory cycles (`set_recap_enabled`)
|
|
77
103
|
have no settings control in either app; setup is their only supported route.
|
|
78
|
-
Usage pin
|
|
79
|
-
on the activity rail.
|
|
104
|
+
Usage pin, the short + long rail bars, expanded accounts and rail slots live
|
|
105
|
+
in the usage card's flyout menu on the activity rail.
|
|
80
106
|
|
|
81
107
|
Schedules/webhooks and workflow/agent/skill definitions use their runtime
|
|
82
108
|
actions directly. These do not require clicking their Desktop rails.
|
|
@@ -85,16 +111,23 @@ actions directly. These do not require clicking their Desktop rails.
|
|
|
85
111
|
|
|
86
112
|
The setup tool does not mutate:
|
|
87
113
|
|
|
88
|
-
- API keys, OAuth,
|
|
114
|
+
- API keys, OAuth, and GitHub CLI connection;
|
|
115
|
+
- turning on the developer option "Allow all actions" (only the person turns
|
|
116
|
+
it on; `set_developer_option` can turn it off);
|
|
117
|
+
- custom provider registration and edits (non-secret fields included), group
|
|
118
|
+
room options, bot persona and avatar, import conversations, reset overlay
|
|
119
|
+
position, SSH and remote-access settings, sidebar/list ordering and other
|
|
120
|
+
per-device view state;
|
|
89
121
|
- MCP server OAuth sign-in and sign-out (Desktop MCP list/editor, TUI
|
|
90
122
|
`/mcp login` and `/mcp logout`; remote windows paste the redirect address);
|
|
91
123
|
- notification permission and subscription on the receiving device;
|
|
92
124
|
- web-app pairing credentials and OS permission dialogs;
|
|
93
125
|
- TUI-local theme palettes (use `/theme`; Desktop themes are separate);
|
|
94
|
-
- importing or
|
|
95
|
-
skin picture, and the Lead's pixel face
|
|
96
|
-
edited from the Desktop or a paired
|
|
97
|
-
|
|
126
|
+
- importing, exporting or removing a JSON color theme file, choosing a skin or
|
|
127
|
+
custom skin picture (plate, accent, photo), and the Lead's pixel face
|
|
128
|
+
(stored on the host as `leadAvatar`, edited from the Desktop or a paired
|
|
129
|
+
client, and shared by all of them; bots and Mixy have per-bot avatars in
|
|
130
|
+
the bot profile editor; only the Workflows agent editor has none);
|
|
98
131
|
- approval grants ("항상 허용" is per feature and per bot, never for payment,
|
|
99
132
|
permanent delete, Lead, workflow, Mixy link, group rooms or schedules) and
|
|
100
133
|
browser action confirmations (`MIXDOG_BROWSER_CONFIRM_ACTIONS`,
|
|
@@ -67,9 +67,9 @@ connector links an outside service so bots and agents can read and act in it,
|
|
|
67
67
|
and anything that sends or posts asks first through an approval card. Then
|
|
68
68
|
suggest by what the user does, only these: Gmail (with a Google app password;
|
|
69
69
|
it needs 2-Step Verification on the Google account, and the connect window
|
|
70
|
-
opens the app-password page in the browser),
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
opens the app-password page in the browser), Google Calendar, Google Drive
|
|
71
|
+
(both with the Google sign-in), Notion, Linear or Jira. GitHub is
|
|
72
|
+
not a connector; it is the signed-in GitHub CLI of Git & GitHub. Connect one with
|
|
73
73
|
`connect_connector`; it only leaves a card, so tell the user to press it and
|
|
74
74
|
confirm with `status mcp`.
|
|
75
75
|
|
package/src/lib/keychain-cjs.cjs
CHANGED
|
@@ -341,12 +341,81 @@ function keychainDebug(what, detail) {
|
|
|
341
341
|
if (process.env.MIXDOG_KEYCHAIN_DEBUG) console.error(`[keychain:debug] ${what}: ${String(detail).slice(0, 500)}`);
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
// A keychain failure that POSITIVELY means "this host has no usable provider"
|
|
345
|
+
// (secret-tool or libsecret missing, no Secret Service on the bus) as opposed to
|
|
346
|
+
// a keyring that exists but is locked, or any failure nobody recognises. Only
|
|
347
|
+
// the former is served by the private secrets file (see "Linux fallback file");
|
|
348
|
+
// a locked keyring must be unlocked, and an unknown error fails closed.
|
|
349
|
+
const KEYRING_UNAVAILABLE = 'KEYRING_UNAVAILABLE';
|
|
350
|
+
|
|
351
|
+
function keyringUnavailable(message) {
|
|
352
|
+
return Object.assign(new Error(message), { code: KEYRING_UNAVAILABLE });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// What secret-tool/libsecret/D-Bus print when there is no provider at all:
|
|
356
|
+
// libsecret missing from the loader, no D-Bus session bus to talk to, or no
|
|
357
|
+
// service owning org.freedesktop.secrets. Anything else is NOT matched.
|
|
358
|
+
const NO_PROVIDER_STDERR = new RegExp(
|
|
359
|
+
[
|
|
360
|
+
'error while loading shared libraries: libsecret',
|
|
361
|
+
'libsecret-1\\.so[^\\n]*cannot open shared object',
|
|
362
|
+
'org\\.freedesktop\\.DBus\\.Error\\.ServiceUnknown',
|
|
363
|
+
'org\\.freedesktop\\.DBus\\.Error\\.NameHasNoOwner',
|
|
364
|
+
'org\\.freedesktop\\.DBus\\.Error\\.NoServer',
|
|
365
|
+
'org\\.freedesktop\\.secrets was not provided by any \\.service files',
|
|
366
|
+
'name org\\.freedesktop\\.secrets (?:is not|has no owner)',
|
|
367
|
+
'Cannot autolaunch D-Bus without X11',
|
|
368
|
+
'Unable to autolaunch a dbus-daemon without a \\$DISPLAY',
|
|
369
|
+
'Failed to execute child process .{0,6}dbus-launch',
|
|
370
|
+
'Could not connect: (?:No such file or directory|Connection refused)',
|
|
371
|
+
'Failed to connect to (?:socket|bus)[^\\n]*(?:No such file or directory|Connection refused)',
|
|
372
|
+
'Error (?:connecting|spawning command line)[^\\n]*dbus',
|
|
373
|
+
'Cannot spawn a message bus',
|
|
374
|
+
].join('|'),
|
|
375
|
+
'i'
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const SECRET_TOOL_MISSING_MESSAGE =
|
|
379
|
+
'[keychain] secret-tool is not installed — install libsecret-tools (sudo apt install libsecret-tools), ' +
|
|
380
|
+
'or set the relevant MIXDOG_*/PROVIDER_API_KEY environment variable instead';
|
|
381
|
+
|
|
382
|
+
// Bounded, non-prompting availability probe: `secret-tool --version` never
|
|
383
|
+
// touches the keyring. Resolves true only when the spawn itself reports ENOENT;
|
|
384
|
+
// an existing tool, a timeout or any other failure is "not confirmed missing".
|
|
385
|
+
function secretToolMissing() {
|
|
386
|
+
return new Promise((resolve) => {
|
|
387
|
+
let child;
|
|
388
|
+
try {
|
|
389
|
+
child = spawn('secret-tool', ['--version'], {
|
|
390
|
+
env: secretToolEnv(),
|
|
391
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
392
|
+
shell: false,
|
|
393
|
+
windowsHide: true,
|
|
394
|
+
});
|
|
395
|
+
} catch (error) {
|
|
396
|
+
resolve(error?.code === 'ENOENT');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const timer = setTimeout(() => {
|
|
400
|
+
try {
|
|
401
|
+
child.kill('SIGKILL');
|
|
402
|
+
} catch {}
|
|
403
|
+
resolve(false);
|
|
404
|
+
}, 2000);
|
|
405
|
+
child.once('error', (error) => {
|
|
406
|
+
clearTimeout(timer);
|
|
407
|
+
resolve(error?.code === 'ENOENT');
|
|
408
|
+
});
|
|
409
|
+
child.once('close', () => {
|
|
410
|
+
clearTimeout(timer);
|
|
411
|
+
resolve(false);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
344
416
|
function secretToolCheck(r) {
|
|
345
417
|
if (r.error?.code === 'ENOENT') {
|
|
346
|
-
throw
|
|
347
|
-
'[keychain] secret-tool is not installed — install libsecret-tools (sudo apt install libsecret-tools), ' +
|
|
348
|
-
'or set the relevant MIXDOG_*/PROVIDER_API_KEY environment variable instead'
|
|
349
|
-
);
|
|
418
|
+
throw keyringUnavailable(SECRET_TOOL_MISSING_MESSAGE);
|
|
350
419
|
}
|
|
351
420
|
if (r.error?.code === 'ETIMEDOUT') {
|
|
352
421
|
throw new Error(
|
|
@@ -355,6 +424,8 @@ function secretToolCheck(r) {
|
|
|
355
424
|
);
|
|
356
425
|
}
|
|
357
426
|
if (r.error) {
|
|
427
|
+
// Not ENOENT: an unknown start failure (EACCES, resource limits) is not
|
|
428
|
+
// proof that no provider exists, so it fails closed instead of falling back.
|
|
358
429
|
keychainDebug('secret-tool failed to start', r.error.message);
|
|
359
430
|
throw new Error(
|
|
360
431
|
'[keychain] secret-tool could not be started — check that libsecret-tools is installed and a Secret Service is running'
|
|
@@ -439,13 +510,16 @@ function secretToolFailure(action, r) {
|
|
|
439
510
|
const detail = (r.stderr || '').trim() || `exit ${r.status}`;
|
|
440
511
|
// User-facing text is fixed; the raw tool output only goes to the debug log.
|
|
441
512
|
keychainDebug(`secret-tool ${action} failed`, detail);
|
|
442
|
-
if (/locked|denied|dismissed|cancel|not authori[sz]ed|IsLocked/i.test(detail)) {
|
|
513
|
+
if (/locked|unlock|denied|dismissed|cancel|not authori[sz]ed|IsLocked/i.test(detail)) {
|
|
443
514
|
return new Error(
|
|
444
515
|
`[keychain] secret-tool ${action} failed: the keyring is locked or the unlock prompt was declined. ` +
|
|
445
516
|
'Unlock your keyring and retry.'
|
|
446
517
|
);
|
|
447
518
|
}
|
|
448
|
-
|
|
519
|
+
const message = `[keychain] secret-tool ${action} failed — ${secretToolHint()}`;
|
|
520
|
+
// Only a recognised "no provider" report may fall back to the secrets file.
|
|
521
|
+
// Anything else (storage error, unknown output) fails closed with this error.
|
|
522
|
+
return NO_PROVIDER_STDERR.test(detail) ? keyringUnavailable(message) : new Error(message);
|
|
449
523
|
}
|
|
450
524
|
|
|
451
525
|
function linuxGetBlocking(account) {
|
|
@@ -604,12 +678,32 @@ function _noteKeychainFailure(error) {
|
|
|
604
678
|
if (error?.code === 'KEYRING_UNREACHABLE') state = 'unreachable';
|
|
605
679
|
else if (/not installed/.test(message)) state = 'missing';
|
|
606
680
|
else if (/locked|timed out/.test(message)) state = 'locked';
|
|
607
|
-
|
|
681
|
+
// Only a confirmed missing provider counts. A failed passive probe
|
|
682
|
+
// (KEYRING_UNREACHABLE: gdbus missing, unparsable reply) proves nothing, so
|
|
683
|
+
// it keeps its warning and recovery action.
|
|
684
|
+
_keychainStatus = { state, message, unavailable: error?.code === KEYRING_UNAVAILABLE };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Sync entry points feed the same health the async ones do (Linux only).
|
|
688
|
+
function _noteSyncOutcome(error) {
|
|
689
|
+
if (platform() !== 'linux') return;
|
|
690
|
+
if (error) _noteKeychainFailure(error);
|
|
691
|
+
else _noteKeychainOk();
|
|
608
692
|
}
|
|
609
693
|
|
|
610
|
-
/** Linux keyring health for the UI: ok | locked | missing |
|
|
694
|
+
/** Linux keyring health for the UI: ok | locked | missing | unreachable | error |
|
|
695
|
+
* unknown. `fallback` is true only when a keychain write/read confirmed that
|
|
696
|
+
* this host has no provider, so secrets live in the private secrets file
|
|
697
|
+
* instead: a working store, not a failure to report. A locked or unreachable
|
|
698
|
+
* keyring is never a fallback. */
|
|
611
699
|
function keychainStatus() {
|
|
612
|
-
|
|
700
|
+
const { unavailable, ...status } = _keychainStatus;
|
|
701
|
+
const fallback = unavailable === true && platform() === 'linux';
|
|
702
|
+
// `unsafeStore` ({ path: the secrets directory, kind: type | owner |
|
|
703
|
+
// permissions }, never a value) says the private file store is being ignored
|
|
704
|
+
// because its layout is unsafe — see fallbackUnsafeStore.
|
|
705
|
+
const unsafe = fallback ? fallbackUnsafeStore() : null;
|
|
706
|
+
return { ...status, fallback, ...(unsafe ? { unsafeStore: { path: unsafe.dir, kind: unsafe.kind } } : {}) };
|
|
613
707
|
}
|
|
614
708
|
|
|
615
709
|
function _linuxLoaded() {
|
|
@@ -668,6 +762,10 @@ function parseSecretToolSearchAccounts(stdout, stderr) {
|
|
|
668
762
|
async function _linuxAccountsToRead(interactive, remaining) {
|
|
669
763
|
let found = await searchMixdogItems();
|
|
670
764
|
if (found === null && !interactive) {
|
|
765
|
+
// An inconclusive passive probe is "unreachable" only while a provider may
|
|
766
|
+
// exist. If secret-tool itself is not installed (headless host without
|
|
767
|
+
// gdbus either) that is a confirmed missing provider, not a warning.
|
|
768
|
+
if (await secretToolMissing()) throw keyringUnavailable(SECRET_TOOL_MISSING_MESSAGE);
|
|
671
769
|
const error = new Error(LINUX_UNREACHABLE_MESSAGE);
|
|
672
770
|
error.code = 'KEYRING_UNREACHABLE';
|
|
673
771
|
throw error;
|
|
@@ -996,6 +1094,196 @@ function win32Delete(account) {
|
|
|
996
1094
|
}
|
|
997
1095
|
}
|
|
998
1096
|
|
|
1097
|
+
// ---------------------------------------------------------------------------
|
|
1098
|
+
// Linux fallback file — headless hosts without a usable keychain provider
|
|
1099
|
+
// ---------------------------------------------------------------------------
|
|
1100
|
+
// When secret-tool/libsecret is missing or no Secret Service is running (a
|
|
1101
|
+
// headless SSH server, WSL), a secret is kept in `<dataDir>/secrets/
|
|
1102
|
+
// <account>.secret`: one owner-only (0600) file per account in an owner-only
|
|
1103
|
+
// (0700) directory, written atomically. Never in mixdog-config.json. A read
|
|
1104
|
+
// re-verifies that layout (no symlink, regular file, this user's, no group/other
|
|
1105
|
+
// access) and treats a store that fails it as absent. A locked keyring is NOT
|
|
1106
|
+
// such a host — it fails with its unlock message instead.
|
|
1107
|
+
// Order: the keychain is always tried first and wins when it has the account;
|
|
1108
|
+
// the file is read only when the keychain has nothing (or cannot be read), so a
|
|
1109
|
+
// value saved here stays readable after a keychain becomes available, and a
|
|
1110
|
+
// later save into an available keychain supersedes (and removes) the file value.
|
|
1111
|
+
|
|
1112
|
+
function usesFallback(error) {
|
|
1113
|
+
return platform() === 'linux' && error?.code === KEYRING_UNAVAILABLE;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function fallbackFile(account) {
|
|
1117
|
+
return path.join(secretsDir(), `${encodeURIComponent(account)}.secret`);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function unsafeFallback(message, kind) {
|
|
1121
|
+
return Object.assign(new Error(message), { code: 'EUNSAFE', ...(kind ? { kind } : {}) });
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Owner and mode bits are POSIX: without getuid (Windows) there is nothing to compare.
|
|
1125
|
+
function ownedByUser(stat) {
|
|
1126
|
+
return typeof process.getuid !== 'function' || stat.uid === process.getuid();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function groupOrOtherAccessible(stat) {
|
|
1130
|
+
return typeof process.getuid === 'function' && (stat.mode & 0o077) !== 0;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Why a stat (an lstat/fstat of the store directory, or of an account file)
|
|
1134
|
+
// disqualifies it, as the repair it needs: 'type' (a symlink or not a regular
|
|
1135
|
+
// file/directory: remove it), 'owner' (someone else's: chown it), 'permissions'
|
|
1136
|
+
// (group/other can reach it: chmod it); null when it is fine.
|
|
1137
|
+
function unsafeKind(stat, directory) {
|
|
1138
|
+
if (directory ? !stat.isDirectory() : !stat.isFile()) return 'type';
|
|
1139
|
+
if (!ownedByUser(stat)) return 'owner';
|
|
1140
|
+
return groupOrOtherAccessible(stat) ? 'permissions' : null;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// The verified secrets directory, or null when absent and not `create`d. A
|
|
1144
|
+
// pre-existing symlink, a non-directory, or a directory owned by someone else is
|
|
1145
|
+
// refused: following it would chmod and write outside the data dir. lstat does
|
|
1146
|
+
// not follow links, so a symlink never passes isDirectory(). `create` repairs
|
|
1147
|
+
// the mode itself (chmod 0700); a reader passes `requirePrivate` and refuses a
|
|
1148
|
+
// directory that group/other can reach instead.
|
|
1149
|
+
function fallbackDir({ create = false, requirePrivate = false } = {}) {
|
|
1150
|
+
const dir = secretsDir();
|
|
1151
|
+
let before;
|
|
1152
|
+
try {
|
|
1153
|
+
before = fs.lstatSync(dir);
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1156
|
+
if (!create) return null;
|
|
1157
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true, mode: 0o700 });
|
|
1158
|
+
fs.mkdirSync(dir, { mode: 0o700 });
|
|
1159
|
+
before = fs.lstatSync(dir);
|
|
1160
|
+
}
|
|
1161
|
+
const kind = unsafeKind(before, true);
|
|
1162
|
+
if (kind && (kind !== 'permissions' || requirePrivate)) {
|
|
1163
|
+
throw unsafeFallback('the secrets directory is not a private directory owned by this user', kind);
|
|
1164
|
+
}
|
|
1165
|
+
if (create) {
|
|
1166
|
+
fs.chmodSync(dir, 0o700);
|
|
1167
|
+
// The chmod must have hit the directory that was verified, not a swapped-in link.
|
|
1168
|
+
const after = fs.lstatSync(dir);
|
|
1169
|
+
if (!after.isDirectory() || after.ino !== before.ino || after.dev !== before.dev) {
|
|
1170
|
+
throw unsafeFallback('the secrets directory changed while it was being secured');
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return dir;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// A stored value is served only from a private regular file owned by this user.
|
|
1177
|
+
// The file is opened without following a symlink (O_NOFOLLOW; ELOOP means it is
|
|
1178
|
+
// one) and without blocking on a FIFO (O_NONBLOCK), then judged by fstat on the
|
|
1179
|
+
// descriptor that is actually read, so nothing can be swapped in between check
|
|
1180
|
+
// and read. Anything else reads as absent: the EUNSAFE reason is thrown like the
|
|
1181
|
+
// directory's, and callers that tolerate a failed read treat it as "no secret".
|
|
1182
|
+
function fallbackGet(account) {
|
|
1183
|
+
if (platform() !== 'linux') return null;
|
|
1184
|
+
if (fallbackDir({ requirePrivate: true }) === null) return null;
|
|
1185
|
+
const { O_RDONLY, O_NOFOLLOW = 0, O_NONBLOCK = 0 } = fs.constants;
|
|
1186
|
+
let descriptor;
|
|
1187
|
+
try {
|
|
1188
|
+
descriptor = fs.openSync(fallbackFile(account), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
if (error?.code === 'ENOENT') return null;
|
|
1191
|
+
if (error?.code === 'ELOOP') {
|
|
1192
|
+
throw unsafeFallback('the secret file is a symbolic link, not a private file', 'type');
|
|
1193
|
+
}
|
|
1194
|
+
throw error;
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
const kind = unsafeKind(fs.fstatSync(descriptor), false);
|
|
1198
|
+
if (kind) throw unsafeFallback('the secret file is not a private regular file owned by this user', kind);
|
|
1199
|
+
return fs.readFileSync(descriptor, 'utf8') || null;
|
|
1200
|
+
} finally {
|
|
1201
|
+
fs.closeSync(descriptor);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Passive health check for the status path: `{ dir, kind }` when the store has a
|
|
1206
|
+
// layout fallbackGet refuses (so its keys read as absent), else null. The read
|
|
1207
|
+
// path swallows that refusal when the keychain also failed, so without this the
|
|
1208
|
+
// user would see a bare "No key". Every call scans the directory afresh, so an
|
|
1209
|
+
// external repair shows at once; callers that need it for many rows ask once
|
|
1210
|
+
// (provider setup passes the one answer down).
|
|
1211
|
+
function fallbackUnsafeStore() {
|
|
1212
|
+
if (platform() !== 'linux') return null;
|
|
1213
|
+
const dir = secretsDir();
|
|
1214
|
+
let value = null;
|
|
1215
|
+
try {
|
|
1216
|
+
if (fallbackDir({ requirePrivate: true }) !== null) {
|
|
1217
|
+
for (const name of fs.readdirSync(dir)) {
|
|
1218
|
+
if (!name.endsWith('.secret')) continue;
|
|
1219
|
+
const kind = unsafeKind(fs.lstatSync(path.join(dir, name)), false);
|
|
1220
|
+
if (kind) {
|
|
1221
|
+
value = { dir, kind };
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
if (error?.code === 'EUNSAFE') value = { dir, kind: error.kind };
|
|
1228
|
+
}
|
|
1229
|
+
return value;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** Whether a read of `account` would be served by the Linux fallback file: the
|
|
1233
|
+
* file exists and the keychain does not hold the account. */
|
|
1234
|
+
function hasFallbackSecret(account) {
|
|
1235
|
+
// A read prefers the keychain: a value it holds (cached) wins over the file.
|
|
1236
|
+
if (_cacheGet(account) != null) return false;
|
|
1237
|
+
try {
|
|
1238
|
+
return fallbackGet(account) != null;
|
|
1239
|
+
} catch {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// For a keychain that threw: an unreadable fallback must not mask that error.
|
|
1245
|
+
function fallbackGetQuiet(account) {
|
|
1246
|
+
try {
|
|
1247
|
+
return fallbackGet(account);
|
|
1248
|
+
} catch {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function fallbackSet(account, value, keychainError) {
|
|
1254
|
+
try {
|
|
1255
|
+
const dir = fallbackDir({ create: true });
|
|
1256
|
+
writeOwnerOnlyAtomicSync(path.join(dir, path.basename(fallbackFile(account))), value);
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
// The message carries the filesystem error only, never the value.
|
|
1259
|
+
const reason = error?.code || 'unknown error';
|
|
1260
|
+
throw Object.assign(
|
|
1261
|
+
new Error(`${keychainError.message}\n The private secrets file could not be written either: ${reason}`),
|
|
1262
|
+
{ cause: error }
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function fallbackRemove(account) {
|
|
1268
|
+
if (platform() !== 'linux') return;
|
|
1269
|
+
if (fallbackDir() === null) return;
|
|
1270
|
+
try {
|
|
1271
|
+
fs.unlinkSync(fallbackFile(account));
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// After a save into the keychain: the file value is now stale. Best effort —
|
|
1278
|
+
// the keychain write already succeeded.
|
|
1279
|
+
function fallbackSupersede(account) {
|
|
1280
|
+
try {
|
|
1281
|
+
fallbackRemove(account);
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
keychainDebug('could not remove superseded fallback secret', error?.message);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
999
1287
|
// ---------------------------------------------------------------------------
|
|
1000
1288
|
// Public API
|
|
1001
1289
|
// ---------------------------------------------------------------------------
|
|
@@ -1022,9 +1310,18 @@ function backend() {
|
|
|
1022
1310
|
function getSecret(account) {
|
|
1023
1311
|
const cached = _cacheGet(account);
|
|
1024
1312
|
if (cached !== undefined) return cached;
|
|
1025
|
-
|
|
1313
|
+
let value;
|
|
1314
|
+
try {
|
|
1315
|
+
value = backend().get(account);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
// Unreadable keychain (no provider, locked, not read yet): a secret saved
|
|
1318
|
+
// to the fallback file is still there; otherwise the keychain error stands.
|
|
1319
|
+
const stored = fallbackGetQuiet(account);
|
|
1320
|
+
if (stored == null) throw error;
|
|
1321
|
+
return stored;
|
|
1322
|
+
}
|
|
1026
1323
|
_cacheSet(account, value);
|
|
1027
|
-
return value;
|
|
1324
|
+
return value ?? fallbackGet(account);
|
|
1028
1325
|
}
|
|
1029
1326
|
|
|
1030
1327
|
function hasSecret(account) {
|
|
@@ -1040,19 +1337,39 @@ function hasSecret(account) {
|
|
|
1040
1337
|
|
|
1041
1338
|
function setSecret(account, value) {
|
|
1042
1339
|
let stored = false;
|
|
1340
|
+
let viaFile = false;
|
|
1043
1341
|
try {
|
|
1044
|
-
|
|
1342
|
+
try {
|
|
1343
|
+
backend().set(account, value);
|
|
1344
|
+
_noteSyncOutcome(null);
|
|
1345
|
+
fallbackSupersede(account);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
_noteSyncOutcome(error);
|
|
1348
|
+
if (!usesFallback(error)) throw error;
|
|
1349
|
+
fallbackSet(account, value, error);
|
|
1350
|
+
viaFile = true;
|
|
1351
|
+
}
|
|
1045
1352
|
stored = true;
|
|
1046
1353
|
} finally {
|
|
1047
1354
|
_cacheInvalidate(account);
|
|
1048
|
-
// Linux reads are cache-only, so a fresh write must stay readable.
|
|
1049
|
-
|
|
1355
|
+
// Linux reads are cache-only, so a fresh keychain write must stay readable.
|
|
1356
|
+
// A file-only value is not cached: reads reach it through the file, and the
|
|
1357
|
+
// cache then means "the keychain holds this".
|
|
1358
|
+
if (stored && !viaFile && platform() === 'linux') _cacheSet(account, value);
|
|
1050
1359
|
}
|
|
1051
1360
|
}
|
|
1052
1361
|
|
|
1053
1362
|
function deleteSecret(account) {
|
|
1054
1363
|
try {
|
|
1055
|
-
|
|
1364
|
+
fallbackRemove(account);
|
|
1365
|
+
try {
|
|
1366
|
+
backend().delete(account);
|
|
1367
|
+
_noteSyncOutcome(null);
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
_noteSyncOutcome(error);
|
|
1370
|
+
// No keychain provider here: nothing can be stored in it to delete.
|
|
1371
|
+
if (!usesFallback(error)) throw error;
|
|
1372
|
+
}
|
|
1056
1373
|
} finally {
|
|
1057
1374
|
_cacheInvalidate(account);
|
|
1058
1375
|
}
|
|
@@ -1065,34 +1382,42 @@ async function getSecretAsync(account) {
|
|
|
1065
1382
|
if (cached !== undefined) return cached;
|
|
1066
1383
|
const b = backend();
|
|
1067
1384
|
if (!b.getAsync) return getSecret(account);
|
|
1068
|
-
if (_linuxLoaded()) return
|
|
1385
|
+
if (_linuxLoaded()) return fallbackGet(account);
|
|
1069
1386
|
const generation = _cacheGenerations.get(account) || 0;
|
|
1070
1387
|
let value;
|
|
1071
1388
|
try {
|
|
1072
1389
|
value = await b.getAsync(account);
|
|
1073
1390
|
} catch (error) {
|
|
1074
1391
|
_noteKeychainFailure(error);
|
|
1075
|
-
|
|
1392
|
+
const stored = fallbackGetQuiet(account);
|
|
1393
|
+
if (stored == null) throw error;
|
|
1394
|
+
return stored;
|
|
1076
1395
|
}
|
|
1077
1396
|
_noteKeychainOk();
|
|
1078
1397
|
if ((_cacheGenerations.get(account) || 0) === generation) _cacheSet(account, value);
|
|
1079
|
-
return value;
|
|
1398
|
+
return value ?? fallbackGet(account);
|
|
1080
1399
|
}
|
|
1081
1400
|
|
|
1082
1401
|
async function setSecretAsync(account, value) {
|
|
1083
1402
|
const b = backend();
|
|
1084
1403
|
if (!b.setAsync) return setSecret(account, value);
|
|
1085
1404
|
let stored = false;
|
|
1405
|
+
let viaFile = false;
|
|
1086
1406
|
try {
|
|
1087
|
-
|
|
1407
|
+
try {
|
|
1408
|
+
await b.setAsync(account, value);
|
|
1409
|
+
_noteKeychainOk();
|
|
1410
|
+
fallbackSupersede(account);
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
_noteKeychainFailure(error);
|
|
1413
|
+
if (!usesFallback(error)) throw error;
|
|
1414
|
+
fallbackSet(account, value, error);
|
|
1415
|
+
viaFile = true;
|
|
1416
|
+
}
|
|
1088
1417
|
stored = true;
|
|
1089
|
-
_noteKeychainOk();
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
_noteKeychainFailure(error);
|
|
1092
|
-
throw error;
|
|
1093
1418
|
} finally {
|
|
1094
1419
|
_cacheInvalidate(account);
|
|
1095
|
-
if (stored) _cacheSet(account, value);
|
|
1420
|
+
if (stored && !viaFile) _cacheSet(account, value);
|
|
1096
1421
|
}
|
|
1097
1422
|
}
|
|
1098
1423
|
|
|
@@ -1100,11 +1425,15 @@ async function deleteSecretAsync(account) {
|
|
|
1100
1425
|
const b = backend();
|
|
1101
1426
|
if (!b.deleteAsync) return deleteSecret(account);
|
|
1102
1427
|
try {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1428
|
+
fallbackRemove(account);
|
|
1429
|
+
try {
|
|
1430
|
+
await b.deleteAsync(account);
|
|
1431
|
+
_noteKeychainOk();
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
_noteKeychainFailure(error);
|
|
1434
|
+
// No keychain provider here: nothing can be stored in it to delete.
|
|
1435
|
+
if (!usesFallback(error)) throw error;
|
|
1436
|
+
}
|
|
1108
1437
|
} finally {
|
|
1109
1438
|
_cacheInvalidate(account);
|
|
1110
1439
|
}
|
|
@@ -1118,6 +1447,7 @@ module.exports = {
|
|
|
1118
1447
|
setSecretAsync,
|
|
1119
1448
|
deleteSecretAsync,
|
|
1120
1449
|
hasSecret,
|
|
1450
|
+
hasFallbackSecret,
|
|
1121
1451
|
keychainStatus,
|
|
1122
1452
|
invalidateSecretCache,
|
|
1123
1453
|
prewarmSecrets,
|