mixdog 1.1.0 → 1.1.1

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.
Files changed (141) hide show
  1. package/README.md +13 -6
  2. package/package.json +1 -1
  3. package/scripts/latest-guard.mjs +63 -0
  4. package/scripts/lib/run-node-tests.mjs +103 -27
  5. package/scripts/lib/test-failure-summary.mjs +7 -2
  6. package/scripts/merge-mac-update-feed.mjs +107 -0
  7. package/scripts/mobile-release.mjs +210 -0
  8. package/scripts/post-release-check.mjs +353 -0
  9. package/scripts/publish-asset-release.mjs +43 -0
  10. package/scripts/release-guard.mjs +63 -0
  11. package/scripts/test.mjs +29 -7
  12. package/src/defaults/shipped-history.json +2 -0
  13. package/src/defaults/skills/computer-use/SKILL.md +1 -1
  14. package/src/defaults/skills/setup/references/actions.md +1 -0
  15. package/src/lib/keychain-cjs.cjs +582 -17
  16. package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
  17. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  18. package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
  19. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
  20. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
  22. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
  23. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
  24. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
  25. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
  26. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
  27. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
  28. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
  29. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
  30. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
  31. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
  33. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
  36. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
  37. package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
  38. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
  39. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
  40. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
  41. package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
  43. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
  44. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
  45. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
  48. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
  50. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
  51. package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
  52. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
  53. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
  54. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  58. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
  60. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
  61. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
  62. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
  63. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
  65. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
  66. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
  67. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
  68. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
  69. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
  70. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
  72. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
  73. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -81
  76. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
  77. package/src/runtime/computer-bridge/client.mjs +25 -1
  78. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
  79. package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
  80. package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
  81. package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
  82. package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
  83. package/src/runtime/effort-judge/judge-input.mjs +5 -2
  84. package/src/runtime/effort-judge/model-install.mjs +9 -3
  85. package/src/runtime/effort-judge/model-manifest.json +7 -7
  86. package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
  87. package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
  88. package/src/runtime/shared/config.mjs +50 -2
  89. package/src/runtime/shared/developer-options.mjs +7 -0
  90. package/src/runtime/shared/instruction-files-mode.mjs +9 -0
  91. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  92. package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
  93. package/src/runtime/shared/pristine-execution-contract.json +9 -0
  94. package/src/runtime/shared/pristine-execution.mjs +1 -1
  95. package/src/runtime/shared/provider-accounts.mjs +2 -0
  96. package/src/runtime/shared/resource-admission.mjs +10 -3
  97. package/src/runtime/shared/schedule-model-ref.mjs +11 -0
  98. package/src/runtime/shared/session-approval-wait.mjs +35 -0
  99. package/src/runtime/shared/shell-display.mjs +4 -2
  100. package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
  101. package/src/session-runtime/boot/apis.mjs +5 -0
  102. package/src/session-runtime/model-recency.mjs +3 -0
  103. package/src/session-runtime/provider-auth/login.mjs +10 -10
  104. package/src/session-runtime/provider-auth/setup.mjs +22 -2
  105. package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
  106. package/src/session-runtime/provider-readiness.mjs +4 -0
  107. package/src/session-runtime/runtime-facade.mjs +5 -0
  108. package/src/session-runtime/schedule-session-run.mjs +3 -1
  109. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
  110. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
  111. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
  112. package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
  113. package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
  114. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
  115. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
  116. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
  117. package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
  118. package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
  119. package/src/session-runtime/services/channel-admin.mjs +4 -2
  120. package/src/session-runtime/services/provider-admin.mjs +80 -17
  121. package/src/session-runtime/services/seeds.mjs +7 -10
  122. package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
  123. package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
  124. package/src/session-runtime/setup-tool/executor.mjs +5 -0
  125. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  126. package/src/session-runtime/tool-policy-refresh.mjs +3 -0
  127. package/src/session-runtime/webhook-session-run.mjs +3 -1
  128. package/src/standalone/session-protocol.mjs +1 -0
  129. package/src/tui/app/model-options.mjs +13 -1
  130. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
  131. package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
  132. package/src/tui/dist/index.mjs +84 -34
  133. package/src/tui/session/context-state.mjs +2 -0
  134. package/src/tui/session/live-share/owner.mjs +17 -0
  135. package/src/tui/session/oauth-flows.mjs +2 -0
  136. package/src/tui/session/session-api/integrations.mjs +8 -8
  137. package/src/tui/session/session-api/routes.mjs +2 -2
  138. package/src/tui/session/session-api/settings.mjs +1 -0
  139. package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
  140. package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
  141. package/src/tui/spinner-meta.mjs +26 -10
@@ -0,0 +1,247 @@
1
+ // Muse Code login: OAuth 2.0 device authorization grant against Meta, then a
2
+ // one-time exchange of the account token for the subscription Model API key.
3
+ import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
4
+ import { openLoginUrl } from './lib/open-login-url.mjs';
5
+ import {
6
+ CLIENT_ID,
7
+ DEVICE_AUTH_URL,
8
+ DEVICE_TOKEN_URL,
9
+ credentialsFromKeyResponse,
10
+ museError,
11
+ museHeaders,
12
+ requestMuseCodeKey,
13
+ saveTokens,
14
+ scrubMuseSecrets,
15
+ } from './muse-code-oauth-tokens.mjs';
16
+
17
+ const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
18
+ const DEFAULT_INTERVAL_S = 5;
19
+ const MIN_INTERVAL_S = 1;
20
+ const SLOW_DOWN_STEP_S = 5;
21
+ const MAX_BACKOFF_INTERVAL_MS = 60_000;
22
+ export const DEVICE_REQUEST_TIMEOUT_MS = 30_000;
23
+ const TRANSIENT_STATUSES = new Set([429, 500, 502, 503, 504]);
24
+ const MAX_TRANSIENT_FAILURES = 3;
25
+
26
+ // Retry-After as delta-seconds or an HTTP date; 0 when absent or unusable.
27
+ function retryAfterMs(res, now) {
28
+ const raw = res.headers?.get?.('retry-after');
29
+ if (!raw) return 0;
30
+ const seconds = Number(raw);
31
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
32
+ const at = Date.parse(raw);
33
+ return Number.isFinite(at) ? Math.max(0, at - now()) : 0;
34
+ }
35
+
36
+ export const defaultSleep = (ms, signal) =>
37
+ new Promise((resolve, reject) => {
38
+ if (signal?.aborted) return reject(cancelled(signal));
39
+ const onAbort = () => {
40
+ clearTimeout(timer);
41
+ reject(cancelled(signal));
42
+ };
43
+ const timer = setTimeout(() => {
44
+ signal?.removeEventListener('abort', onAbort);
45
+ resolve();
46
+ }, ms);
47
+ signal?.addEventListener('abort', onAbort, { once: true });
48
+ });
49
+
50
+ const cancelled = (signal) =>
51
+ museError(`[muse-code-oauth] login cancelled${signal?.reason?.message ? `: ${signal.reason.message}` : ''}`, {
52
+ kind: 'cancelled',
53
+ cancelled: true,
54
+ });
55
+
56
+ // `kind` names the failure class for transport errors (device-auth or device-token).
57
+ async function postForm(url, params, { fetchFn, signal, kind = 'device-auth' }) {
58
+ const timeout = AbortSignal.timeout(DEVICE_REQUEST_TIMEOUT_MS);
59
+ let res;
60
+ let text;
61
+ try {
62
+ res = await fetchFn(url, {
63
+ method: 'POST',
64
+ headers: museHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }),
65
+ body: new URLSearchParams(params),
66
+ // The body carries the device code: a redirect must never receive it.
67
+ redirect: 'error',
68
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
69
+ dispatcher: getLlmDispatcher(),
70
+ });
71
+ text = await res.text();
72
+ } catch (cause) {
73
+ if (signal?.aborted) throw cancelled(signal);
74
+ if (timeout.aborted) {
75
+ throw museError(`[muse-code-oauth] request to ${new URL(url).host} timed out`, { kind: 'timeout' });
76
+ }
77
+ throw museError(
78
+ `[muse-code-oauth] ${kind} request failed: ${scrubMuseSecrets(String(cause?.message || cause)).slice(0, 300)}`,
79
+ {
80
+ kind,
81
+ }
82
+ );
83
+ }
84
+ let json = null;
85
+ try {
86
+ json = text ? JSON.parse(text) : null;
87
+ } catch {
88
+ /* non-JSON body handled by callers */
89
+ }
90
+ return { res, text, json };
91
+ }
92
+
93
+ export async function requestDeviceAuthorization({ fetchFn = fetch, signal } = {}) {
94
+ const { res, text, json } = await postForm(DEVICE_AUTH_URL, { client_id: CLIENT_ID }, { fetchFn, signal });
95
+ if (!res.ok) {
96
+ throw museError(`[muse-code-oauth] device authorization ${res.status}: ${scrubMuseSecrets(text).slice(0, 300)}`, {
97
+ kind: 'device-auth',
98
+ status: res.status,
99
+ });
100
+ }
101
+ const stringOrEmpty = (v) => (typeof v === 'string' && v.length > 0 ? v : '');
102
+ const deviceCode = stringOrEmpty(json?.device_code);
103
+ const userCode = stringOrEmpty(json?.user_code);
104
+ const verificationUri = stringOrEmpty(json?.verification_uri);
105
+ if (!deviceCode || !userCode || !verificationUri) {
106
+ throw museError('[muse-code-oauth] device authorization response is missing required fields', {
107
+ kind: 'validation',
108
+ status: res.status,
109
+ });
110
+ }
111
+ const interval = typeof json.interval === 'number' && Number.isFinite(json.interval) ? json.interval : null;
112
+ // A supplied expiry (even zero or negative) is a deadline; only an absent one means none.
113
+ const expiresIn = typeof json.expires_in === 'number' && Number.isFinite(json.expires_in) ? json.expires_in : null;
114
+ return {
115
+ deviceCode,
116
+ userCode,
117
+ verificationUri,
118
+ verificationUriComplete: stringOrEmpty(json.verification_uri_complete),
119
+ intervalS: Math.max(MIN_INTERVAL_S, interval ?? DEFAULT_INTERVAL_S),
120
+ // Null means the server gave no expiry: the flow then runs until approval,
121
+ // denial, or cancellation.
122
+ expiresInS: expiresIn,
123
+ };
124
+ }
125
+
126
+ /** Poll the token endpoint until approval, denial, or expiry; resolves the account access token. */
127
+ export async function pollDeviceToken(device, { fetchFn = fetch, signal, sleep = defaultSleep, now = Date.now } = {}) {
128
+ const expired = () =>
129
+ museError('[muse-code-oauth] device code expired — start the login again', {
130
+ kind: 'device-token',
131
+ code: 'expired_token',
132
+ });
133
+ const deadline = typeof device.expiresInS === 'number' ? now() + device.expiresInS * 1000 : Number.POSITIVE_INFINITY;
134
+ let intervalMs = Math.round(Math.max(MIN_INTERVAL_S, device.intervalS) * 1000);
135
+ let failures = 0;
136
+ while (now() < deadline) {
137
+ // The in-flight request dies with the device code.
138
+ const remaining = deadline - now();
139
+ let pollSignal = signal;
140
+ if (Number.isFinite(remaining)) {
141
+ const timeout = AbortSignal.timeout(remaining);
142
+ pollSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
143
+ }
144
+ let reply;
145
+ let waitMs = intervalMs;
146
+ try {
147
+ reply = await postForm(
148
+ DEVICE_TOKEN_URL,
149
+ { client_id: CLIENT_ID, device_code: device.deviceCode, grant_type: DEVICE_GRANT_TYPE },
150
+ { fetchFn, signal: pollSignal, kind: 'device-token' }
151
+ );
152
+ } catch (err) {
153
+ if (signal?.aborted) throw err;
154
+ // Expiry aborted the request: that is the expiry outcome, not a transport error.
155
+ if (now() >= deadline || (pollSignal?.aborted && pollSignal !== signal)) break;
156
+ // Only timeouts and network failures are transient.
157
+ if ((err.kind !== 'timeout' && err.kind !== 'device-token') || ++failures > MAX_TRANSIENT_FAILURES) throw err;
158
+ // RFC 8628 §3.5: after a connection timeout the client backs off by
159
+ // increasing the polling interval; the increase persists for the rest of the flow.
160
+ // Already at (or past) the cap: backing off further is impossible, and polling
161
+ // again at the same frequency would defeat the backoff, so fail instead.
162
+ if (intervalMs >= MAX_BACKOFF_INTERVAL_MS) throw err;
163
+ intervalMs = Math.min(intervalMs * 2, MAX_BACKOFF_INTERVAL_MS);
164
+ await sleep(Math.min(intervalMs, deadline - now()), signal);
165
+ continue;
166
+ }
167
+ const { res, text, json } = reply;
168
+ const error = typeof json?.error === 'string' ? json.error : '';
169
+ if (res.ok && !error && typeof json?.access_token === 'string' && json.access_token) return json.access_token;
170
+ const knownOutcome = ['authorization_pending', 'slow_down', 'expired_token', 'access_denied'].includes(error);
171
+ const transient = !knownOutcome && TRANSIENT_STATUSES.has(res.status);
172
+ if (transient) {
173
+ if (++failures > MAX_TRANSIENT_FAILURES) {
174
+ throw museError(
175
+ `[muse-code-oauth] device token ${res.status}: ${scrubMuseSecrets(text, [device.deviceCode]).slice(0, 300)}`,
176
+ { kind: 'device-token', status: res.status }
177
+ );
178
+ }
179
+ if (res.status === 429) intervalMs += SLOW_DOWN_STEP_S * 1000;
180
+ waitMs = Math.max(intervalMs, retryAfterMs(res, now));
181
+ } else {
182
+ failures = 0;
183
+ }
184
+ if (transient) {
185
+ /* retry after waitMs */
186
+ } else if (error === 'authorization_pending') {
187
+ /* keep polling */
188
+ } else if (error === 'slow_down') {
189
+ intervalMs += SLOW_DOWN_STEP_S * 1000;
190
+ waitMs = intervalMs;
191
+ } else if (error === 'expired_token') {
192
+ throw expired();
193
+ } else if (error === 'access_denied') {
194
+ throw museError('[muse-code-oauth] sign-in was denied', {
195
+ kind: 'device-token',
196
+ code: 'access_denied',
197
+ status: res.status,
198
+ reauthRequired: true,
199
+ });
200
+ } else {
201
+ const description = typeof json?.error_description === 'string' ? json.error_description : '';
202
+ const detail = [error, description].filter(Boolean).join(': ') || text;
203
+ throw museError(
204
+ `[muse-code-oauth] device token ${res.status}: ${scrubMuseSecrets(detail, [device.deviceCode]).slice(0, 300)}`,
205
+ { kind: 'device-token', status: res.status, ...(error ? { code: error } : {}) }
206
+ );
207
+ }
208
+ const left = deadline - now();
209
+ if (left <= 0) break;
210
+ await sleep(Math.min(waitMs, left), signal);
211
+ }
212
+ throw expired();
213
+ }
214
+
215
+ /** Mint the Model API key for a fresh account token and persist the credential. */
216
+ export async function completeLogin(accessToken, { fetchFn = fetch, signal } = {}) {
217
+ const payload = await requestMuseCodeKey(accessToken, { onboard: true, fetchFn, signal });
218
+ const credentials = credentialsFromKeyResponse(payload, accessToken);
219
+ saveTokens(credentials);
220
+ return credentials;
221
+ }
222
+
223
+ export async function beginOAuthLogin({ openBrowser = true, fetchFn = fetch, sleep, now } = {}) {
224
+ const controller = new AbortController();
225
+ const device = await requestDeviceAuthorization({ fetchFn, signal: controller.signal });
226
+ const url = device.verificationUriComplete || device.verificationUri;
227
+ process.stderr.write(`\n[muse-code-oauth] Enter code: ${device.userCode}\n[muse-code-oauth] Open: ${url}\n\n`);
228
+ if (openBrowser) await openLoginUrl('muse-code-oauth', url);
229
+ const waitForCallback = pollDeviceToken(device, { fetchFn, signal: controller.signal, sleep, now }).then(
230
+ (accessToken) => completeLogin(accessToken, { fetchFn, signal: controller.signal })
231
+ );
232
+ // A cancelled flow rejects with no listener attached yet in some hosts.
233
+ waitForCallback.catch(() => {});
234
+ return {
235
+ provider: 'muse-code-oauth',
236
+ url,
237
+ manualUrl: null,
238
+ userCode: device.userCode,
239
+ waitForCallback,
240
+ cancel: () => controller.abort(new Error('Muse Code login cancelled')),
241
+ };
242
+ }
243
+
244
+ export async function loginOAuth() {
245
+ const login = await beginOAuthLogin();
246
+ return await login.waitForCallback;
247
+ }
@@ -0,0 +1,225 @@
1
+ // Muse Code subscription credentials. A Meta device-authorization login yields
2
+ // an account access token; that token is only ever used to mint (once) the
3
+ // Model API key, and inference then runs on the key. Meta issues no expiry
4
+ // and rejects refresh grants, so there is no refresh path: a 401 means the
5
+ // user signs in again. Credentials live in Mixdog's own muse-code-oauth.json.
6
+ import { readFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
7
+ import { join, resolve } from 'node:path';
8
+ import { getPluginData } from '../config.mjs';
9
+ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
10
+ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
11
+ import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
12
+ import { accountIdentityFields, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
13
+
14
+ export const CLIENT_ID = '1031625952748946';
15
+ export const DEVICE_AUTH_URL = 'https://auth.meta.com/oidc/device/authorization/';
16
+ export const DEVICE_TOKEN_URL = 'https://auth.meta.com/oidc/device/token/';
17
+ const KEY_URL = 'https://api.meta.ai/muse-code/key';
18
+ export const MODEL_API_BASE_URL = 'https://api.meta.ai/v1';
19
+ export const API_VERSION = '1.0.0';
20
+ export const REQUEST_TIMEOUT_MS = 20_000;
21
+
22
+ export function museHeaders(extra = {}) {
23
+ return { Accept: 'application/json', 'x-api-version': API_VERSION, ...extra };
24
+ }
25
+
26
+ /** Redact field-shaped secrets plus every known secret value wherever it appears. */
27
+ export function scrubMuseSecrets(text, secretValues = []) {
28
+ return scrubOAuthSecrets(text, secretValues).replace(/"api_key"\s*:\s*"[^"]+"/g, '"api_key":"[REDACTED]"');
29
+ }
30
+
31
+ export function getOwnTokenPath() {
32
+ const bound = boundProviderAuthPath('muse-code-oauth');
33
+ if (bound) return resolve(bound);
34
+ const explicit = process.env.MUSE_CODE_OAUTH_CREDENTIALS_PATH;
35
+ if (explicit) return resolve(explicit);
36
+ const dir = getPluginData();
37
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
38
+ return join(dir, 'muse-code-oauth.json');
39
+ }
40
+
41
+ const nonEmptyString = (value) => typeof value === 'string' && value.trim() !== '';
42
+
43
+ // Both halves are required: a document with only one of them is signed out.
44
+ export const isValidMuseCredentialDocument = (doc) => nonEmptyString(doc?.accessToken) && nonEmptyString(doc?.apiKey);
45
+
46
+ export function loadTokens() {
47
+ const path = getOwnTokenPath();
48
+ if (!existsSync(path)) return null;
49
+ try {
50
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
51
+ if (!isValidMuseCredentialDocument(raw)) return null;
52
+ return {
53
+ accessToken: raw.accessToken,
54
+ apiKey: raw.apiKey,
55
+ accountId: raw.accountId || '',
56
+ email: raw.email || '',
57
+ };
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export function saveTokens(tokens) {
64
+ writeJsonAtomicSync(
65
+ getOwnTokenPath(),
66
+ {
67
+ accessToken: tokens.accessToken,
68
+ apiKey: tokens.apiKey,
69
+ accountId: tokens.accountId,
70
+ ...(tokens.email ? { email: tokens.email } : {}),
71
+ },
72
+ { lock: true, fsyncDir: true, mode: 0o600, secret: true }
73
+ );
74
+ }
75
+
76
+ export function hasMuseCodeOAuthCredentials() {
77
+ try {
78
+ return Boolean(loadTokens()?.apiKey);
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ export function describeMuseCodeOAuthCredentials() {
85
+ try {
86
+ const tokens = loadTokens();
87
+ if (!tokens?.apiKey) {
88
+ return {
89
+ authenticated: false,
90
+ usable: false,
91
+ refreshable: false,
92
+ reauthRequired: false,
93
+ status: 'Not Set',
94
+ detail: 'Mixdog token store',
95
+ };
96
+ }
97
+ return {
98
+ authenticated: true,
99
+ usable: true,
100
+ refreshable: false,
101
+ reauthRequired: false,
102
+ status: 'Valid',
103
+ detail: 'Mixdog token store',
104
+ expiresAt: 0,
105
+ ...accountIdentityFields({ id: tokens.accountId, email: tokens.email }),
106
+ };
107
+ } catch (err) {
108
+ return {
109
+ authenticated: false,
110
+ usable: false,
111
+ refreshable: false,
112
+ reauthRequired: false,
113
+ status: 'Error',
114
+ detail: String(err?.message || err).slice(0, 200),
115
+ };
116
+ }
117
+ }
118
+
119
+ export function forgetMuseCodeOAuthCredentials() {
120
+ const path = getOwnTokenPath();
121
+ if (!existsSync(path)) return { removed: false };
122
+ unlinkSync(path);
123
+ return { removed: true };
124
+ }
125
+
126
+ /**
127
+ * POST the key endpoint with the Meta account token. `onboard` is sent only
128
+ * on the login exchange; usage reads omit it. Never follows redirects.
129
+ */
130
+ export async function requestMuseCodeKey(accessToken, { onboard = false, fetchFn = fetch, signal } = {}) {
131
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
132
+ const res = await fetchFn(KEY_URL, {
133
+ method: 'POST',
134
+ headers: museHeaders({ Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }),
135
+ body: JSON.stringify(onboard ? { onboard: true } : {}),
136
+ redirect: 'error',
137
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
138
+ dispatcher: getLlmDispatcher(),
139
+ });
140
+ const text = await res.text();
141
+ if (!res.ok) {
142
+ const detail = scrubMuseSecrets(text, [accessToken]).trim().slice(0, 500);
143
+ throw Object.assign(new Error(`[muse-code-oauth] key request ${res.status}${detail ? `: ${detail}` : ''}`), {
144
+ status: res.status,
145
+ kind: 'token-exchange',
146
+ });
147
+ }
148
+ let payload;
149
+ try {
150
+ payload = JSON.parse(text);
151
+ } catch {
152
+ throw museError('[muse-code-oauth] key response was not valid JSON', { kind: 'validation', status: res.status });
153
+ }
154
+ validateKeyPayload(payload);
155
+ return payload;
156
+ }
157
+
158
+ export function museError(message, meta) {
159
+ return Object.assign(new Error(message), meta);
160
+ }
161
+
162
+ const isUsageWindow = (w) => {
163
+ if (w === undefined || w === null) return true;
164
+ if (typeof w !== 'object' || Array.isArray(w)) return false;
165
+ const numeric = (v) => v === undefined || (typeof v === 'number' && Number.isFinite(v));
166
+ return (
167
+ numeric(w.used_percent) &&
168
+ numeric(w.window_duration_mins) &&
169
+ (w.resets_at === undefined || typeof w.resets_at === 'string' || typeof w.resets_at === 'number')
170
+ );
171
+ };
172
+
173
+ /** Reject a key response whose fields have the wrong types rather than stringifying them. */
174
+ function validateKeyPayload(payload) {
175
+ const bad = (field) => museError(`[muse-code-oauth] key response has an invalid ${field}`, { kind: 'validation' });
176
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw bad('body');
177
+ const optional = (field, ...types) => {
178
+ const v = payload[field];
179
+ if (v !== undefined && !types.includes(v === null ? 'null' : typeof v)) throw bad(field);
180
+ };
181
+ optional('api_key', 'string');
182
+ optional('user_id', 'string');
183
+ optional('user_email', 'string');
184
+ optional('is_subs_active', 'boolean');
185
+ optional('require_payment', 'boolean');
186
+ optional('action_url', 'string', 'null');
187
+ optional('require_payment_action_url', 'string');
188
+ optional('subs_tier_id', 'string', 'null');
189
+ optional('subs_tier_name', 'string', 'null');
190
+ const usage = payload.subs_usage;
191
+ if (usage !== undefined && usage !== null) {
192
+ if (typeof usage !== 'object' || Array.isArray(usage)) throw bad('subs_usage');
193
+ if (!isUsageWindow(usage.window)) throw bad('subs_usage.window');
194
+ if (!isUsageWindow(usage.weekly)) throw bad('subs_usage.weekly');
195
+ }
196
+ }
197
+
198
+ /** Validate a key response and turn it into the stored credential record. */
199
+ export function credentialsFromKeyResponse(payload, accessToken) {
200
+ validateKeyPayload(payload);
201
+ if (payload.is_subs_active === false) {
202
+ throw museError('[muse-code-oauth] invalid_grant: Muse Code subscription inactive', {
203
+ kind: 'token-exchange',
204
+ status: 403,
205
+ reauthRequired: true,
206
+ });
207
+ }
208
+ const apiKey = payload.api_key?.trim() || '';
209
+ if (!apiKey) {
210
+ const actionUrl = payload.action_url?.trim() || payload.require_payment_action_url?.trim() || '';
211
+ if (payload.require_payment === true || actionUrl) {
212
+ throw museError(`[muse-code-oauth] Muse Code subscription payment required${actionUrl ? `: ${actionUrl}` : ''}`, {
213
+ kind: 'entitlement',
214
+ ...(actionUrl ? { actionUrl } : {}),
215
+ });
216
+ }
217
+ throw museError('[muse-code-oauth] key response is missing api_key', { kind: 'validation' });
218
+ }
219
+ const email = payload.user_email?.trim().toLowerCase() || '';
220
+ const accountId = payload.user_id?.trim() || email;
221
+ if (!accountId) {
222
+ throw museError('[muse-code-oauth] key response is missing a stable account identity', { kind: 'validation' });
223
+ }
224
+ return { accessToken, apiKey, accountId, email };
225
+ }