mixdog 1.1.0 → 1.1.2

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 (174) hide show
  1. package/README.md +13 -6
  2. package/package.json +1 -2
  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/release-paths.mjs +39 -2
  12. package/scripts/test.mjs +29 -7
  13. package/src/defaults/shipped-history.json +2 -0
  14. package/src/defaults/skills/computer-use/SKILL.md +1 -1
  15. package/src/defaults/skills/setup/references/actions.md +1 -0
  16. package/src/headless-role.mjs +2 -2
  17. package/src/lib/keychain-cjs.cjs +582 -17
  18. package/src/lib/rules-builder.cjs +0 -23
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
  21. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  22. package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
  23. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
  24. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
  25. package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
  27. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
  28. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
  29. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
  30. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
  31. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
  32. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
  34. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
  35. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
  36. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
  40. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
  41. package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
  42. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
  43. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
  44. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -5
  45. package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
  46. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
  47. package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
  49. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
  50. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
  51. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
  52. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
  53. package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
  54. package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
  55. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
  56. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
  57. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
  59. package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
  61. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
  65. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  66. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
  68. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
  69. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
  70. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
  71. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
  72. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
  74. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
  77. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
  81. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
  83. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -91
  84. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
  85. package/src/runtime/channels/index.mjs +1 -5
  86. package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
  87. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
  88. package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
  89. package/src/runtime/channels/lib/worker-main.mjs +4 -34
  90. package/src/runtime/computer-bridge/client.mjs +25 -1
  91. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
  92. package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
  93. package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
  94. package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
  95. package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
  96. package/src/runtime/effort-judge/judge-input.mjs +5 -2
  97. package/src/runtime/effort-judge/model-install.mjs +9 -3
  98. package/src/runtime/effort-judge/model-manifest.json +7 -7
  99. package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
  100. package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
  101. package/src/runtime/shared/config.mjs +50 -2
  102. package/src/runtime/shared/developer-options.mjs +7 -0
  103. package/src/runtime/shared/instruction-files-mode.mjs +9 -0
  104. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  105. package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
  106. package/src/runtime/shared/pristine-execution-contract.json +9 -0
  107. package/src/runtime/shared/pristine-execution.mjs +1 -1
  108. package/src/runtime/shared/provider-accounts.mjs +2 -0
  109. package/src/runtime/shared/resource-admission.mjs +10 -3
  110. package/src/runtime/shared/schedule-model-ref.mjs +11 -0
  111. package/src/runtime/shared/semver.d.mts +1 -0
  112. package/src/runtime/shared/semver.mjs +57 -0
  113. package/src/runtime/shared/session-approval-wait.mjs +35 -0
  114. package/src/runtime/shared/shell-display.mjs +4 -2
  115. package/src/runtime/shared/tool-surface.mjs +2 -2
  116. package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
  117. package/src/runtime/shared/update-checker.mjs +2 -31
  118. package/src/session-runtime/boot/apis.mjs +5 -0
  119. package/src/session-runtime/boot/begin.mjs +0 -2
  120. package/src/session-runtime/boot/tools.mjs +5 -4
  121. package/src/session-runtime/model-recency.mjs +3 -0
  122. package/src/session-runtime/provider-auth/login.mjs +10 -10
  123. package/src/session-runtime/provider-auth/setup.mjs +22 -2
  124. package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
  125. package/src/session-runtime/provider-readiness.mjs +4 -0
  126. package/src/session-runtime/resource-mcp-api.mjs +7 -3
  127. package/src/session-runtime/runtime-bootstrap.mjs +0 -4
  128. package/src/session-runtime/runtime-facade.mjs +5 -0
  129. package/src/session-runtime/runtime-paths.mjs +0 -1
  130. package/src/session-runtime/schedule-session-run.mjs +3 -1
  131. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
  132. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
  133. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
  134. package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
  135. package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
  136. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
  137. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
  138. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
  139. package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
  140. package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
  141. package/src/session-runtime/services/channel-admin.mjs +4 -2
  142. package/src/session-runtime/services/channel-worker.mjs +1 -1
  143. package/src/session-runtime/services/provider-admin.mjs +80 -17
  144. package/src/session-runtime/services/seeds.mjs +7 -10
  145. package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
  146. package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
  147. package/src/session-runtime/settings-profile-api.mjs +27 -2
  148. package/src/session-runtime/setup-tool/executor.mjs +5 -0
  149. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  150. package/src/session-runtime/starter-guide-state.mjs +62 -0
  151. package/src/session-runtime/tool-policy-refresh.mjs +3 -0
  152. package/src/session-runtime/webhook-session-run.mjs +3 -1
  153. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
  154. package/src/standalone/channel-binding.mjs +1 -2
  155. package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
  156. package/src/standalone/daemon-channels-loader.mjs +2 -3
  157. package/src/standalone/local-session-runtime.mjs +2 -78
  158. package/src/standalone/session-protocol.mjs +2 -0
  159. package/src/tui/app/model-options.mjs +13 -1
  160. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
  161. package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
  162. package/src/tui/dist/index.mjs +84 -34
  163. package/src/tui/session/context-state.mjs +2 -0
  164. package/src/tui/session/live-share/owner.mjs +17 -0
  165. package/src/tui/session/oauth-flows.mjs +2 -0
  166. package/src/tui/session/session-api/extensions.mjs +12 -8
  167. package/src/tui/session/session-api/integrations.mjs +9 -8
  168. package/src/tui/session/session-api/routes.mjs +2 -2
  169. package/src/tui/session/session-api/settings.mjs +1 -0
  170. package/src/tui/session/session-api/shared.mjs +10 -6
  171. package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
  172. package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
  173. package/src/tui/spinner-meta.mjs +26 -10
  174. package/src/runtime/channels/tool-defs.mjs +0 -17
@@ -9,7 +9,6 @@
9
9
  * - buildAgentRoleContent — BP3: agent role/system rules
10
10
  * - buildAgentRetrievalInjectionContent — BP3: narrow read-only retrieval role
11
11
  * - buildLeadMetaContent — BP2: Lead profile/settings context
12
- * - buildInjectionContent — legacy joined Lead session content
13
12
  *
14
13
  * 4-BP cache layout (composeSystemPrompt):
15
14
  * BP1 = shared tool policy
@@ -400,27 +399,6 @@ function buildLeadLanguageContent({ DATA_DIR }) {
400
399
  return buildLanguageSection(DATA_DIR);
401
400
  }
402
401
 
403
- /**
404
- * Build the Lead injection content.
405
- */
406
- function buildInjectionContent({ PLUGIN_ROOT, DATA_DIR }) {
407
- const parts = [];
408
-
409
- const tool = buildSharedToolContent({ PLUGIN_ROOT });
410
- if (tool) parts.push(tool);
411
-
412
- const meta = buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR });
413
- if (meta) parts.push(meta);
414
-
415
- const role = buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR });
416
- if (role) parts.push(role);
417
-
418
- const language = buildLeadLanguageContent({ DATA_DIR });
419
- if (language) parts.push(language);
420
-
421
- return parts.join('\n\n');
422
- }
423
-
424
402
  function buildAgentRoleContent({ PLUGIN_ROOT, profile = 'full' }) {
425
403
  const AGENT_DIR = path.join(PLUGIN_ROOT, 'rules', 'agent');
426
404
  if (String(profile || 'full') === 'retrieval') {
@@ -455,7 +433,6 @@ module.exports = {
455
433
  buildLeadMetaContent,
456
434
  buildLeadLanguageContent,
457
435
  buildAgentRoleContent,
458
- buildInjectionContent,
459
436
  buildAgentRetrievalInjectionContent,
460
437
  omitToolRoutes,
461
438
  };
@@ -10,8 +10,8 @@
10
10
  never per-call narration or plan repeats.
11
11
  - Mid-task: a replacement supersedes, an addition folds in, a status question
12
12
  gets a brief answer while work continues.
13
- - Context is managed automatically: never propose stopping or stop on your own
14
- judgment.
13
+ - Context is managed automatically: never stop or propose stopping because of
14
+ context size.
15
15
  - Be a grounded, candid, and attentive collaborator: natural and respectful,
16
16
  without flattery, forced humor, mimicry, or stereotyping.
17
17
  <!-- tools: agent -->
@@ -2,6 +2,11 @@ import { hasExtensionScopes, normalizeExtensionScopes } from '../../shared/exten
2
2
  import { agentRouteStorageNeedsMigration, canonicalizeAgentRouteStorage } from '../../shared/agent-route-config.mjs';
3
3
  import profileConfig from '../../shared/profile-config.cjs';
4
4
  import { normalizeAgentProviderId, normalizePreset } from './config-presets.mjs';
5
+ import {
6
+ loadSavedCodexCatalog,
7
+ migrateCodexRoute,
8
+ supersededCodexSettingsKey,
9
+ } from './providers/openai-oauth-catalog-store.mjs';
5
10
 
6
11
  const { normalizeProfileConfig } = profileConfig;
7
12
  const RETIRED_LOCAL_PROVIDER_IDS = new Set(['ollama', 'lmstudio']);
@@ -205,7 +210,20 @@ function retiredLocalProviderStoragePresent(value = {}) {
205
210
  );
206
211
  }
207
212
 
213
+ // Saved routes on a model the provider catalog has superseded (see
214
+ // openai-oauth-catalog-store.mjs) — read from the last fetched catalog.
215
+ function supersededRouteStoragePresent(value = {}, catalog = loadSavedCodexCatalog()) {
216
+ const superseded = (route) => migrateCodexRoute(route, catalog) !== route;
217
+ return (
218
+ (Array.isArray(value?.presets) && value.presets.some(superseded)) ||
219
+ Object.values(configObject(value?.agents)).some(superseded) ||
220
+ Object.values(configObject(value?.maintenance)).some(superseded) ||
221
+ Object.keys(configObject(value?.modelSettings)).some((key) => supersededCodexSettingsKey(key, catalog))
222
+ );
223
+ }
224
+
208
225
  export function canonicalizeAgentStorage(value = {}) {
226
+ const catalog = loadSavedCodexCatalog();
209
227
  const next = canonicalizeAgentRouteStorage(value);
210
228
  const removedPresetIds = new Set(
211
229
  (Array.isArray(next.presets) ? next.presets : [])
@@ -216,17 +234,26 @@ export function canonicalizeAgentStorage(value = {}) {
216
234
  next.providers = configObject(next.providers);
217
235
  for (const id of RETIRED_LOCAL_PROVIDER_IDS) delete next.providers[id];
218
236
  next.presets = Array.isArray(next.presets)
219
- ? next.presets.map((preset) => normalizePreset(preset)).filter((preset) => preset && !isRetiredLocalRoute(preset))
237
+ ? next.presets
238
+ .map((preset) => normalizePreset(migrateCodexRoute(preset, catalog)))
239
+ .filter((preset) => preset && !isRetiredLocalRoute(preset))
220
240
  : [];
221
241
  if (removedPresetIds.has(String(next.default || '').trim())) next.default = null;
222
242
  next.agents = Object.fromEntries(
223
- Object.entries(configObject(next.agents)).filter(([, route]) => !isRetiredLocalRoute(route))
243
+ Object.entries(configObject(next.agents))
244
+ .filter(([, route]) => !isRetiredLocalRoute(route))
245
+ .map(([id, route]) => [id, migrateCodexRoute(route, catalog)])
224
246
  );
225
247
  next.maintenance = Object.fromEntries(
226
- Object.entries(configObject(next.maintenance)).filter(([, route]) => !isRetiredLocalRoute(route))
248
+ Object.entries(configObject(next.maintenance))
249
+ .filter(([, route]) => !isRetiredLocalRoute(route))
250
+ .map(([slot, route]) => [slot, migrateCodexRoute(route, catalog)])
227
251
  );
252
+ // A superseded model's own settings have no model left to apply to.
228
253
  next.modelSettings = Object.fromEntries(
229
- Object.entries(normalizedModelSettings(value)).filter(([key]) => !isRetiredLocalModelKey(key))
254
+ Object.entries(normalizedModelSettings(value)).filter(
255
+ ([key]) => !isRetiredLocalModelKey(key) && !supersededCodexSettingsKey(key, catalog)
256
+ )
230
257
  );
231
258
  setOrDelete(next, 'autoClear', canonicalizeAutoClearStorage(next.autoClear));
232
259
  setOrDelete(next, 'compaction', canonicalizeCompactionStorage(next.compaction));
@@ -266,6 +293,7 @@ export function agentConfigStorageNeedsMigration(value = {}) {
266
293
  return (
267
294
  agentRouteStorageNeedsMigration(value) ||
268
295
  retiredLocalProviderStoragePresent(value) ||
296
+ supersededRouteStoragePresent(value) ||
269
297
  RETIRED_AGENT_FIELDS.some((key) => key !== 'guide' && Object.hasOwn(value || {}, key)) ||
270
298
  presets.some((preset) => !normalizePreset(preset)) ||
271
299
  (modules && typeof modules === 'object' && Object.hasOwn(modules, 'memory')) ||
@@ -4,6 +4,10 @@ import { mergeStoredProviders } from './config-providers-merge.mjs';
4
4
  import { normalizeExtensionScopes } from '../../shared/extension-scopes.mjs';
5
5
  import { applyConfigPatch, diffConfig } from '../../shared/config-patch.mjs';
6
6
  import { normalizeWorkflowSelection } from '../../shared/orchestration.mjs';
7
+ import {
8
+ DEFAULT_INSTRUCTION_FILES_MODE,
9
+ normalizeInstructionFilesMode,
10
+ } from '../../shared/instruction-files-mode.mjs';
7
11
  import {
8
12
  DEFAULT_DISABLED_AGENT_ROSTER,
9
13
  canonicalizeAgentRouteStorage,
@@ -107,9 +111,13 @@ export function buildDefaultConfig(options = {}) {
107
111
  // stored in mixdog-config.json — enabled at runtime from the presence of
108
112
  // Mixdog-owned credentials.
109
113
  providers['grok-oauth'] = oauthEntry('grok-oauth');
114
+ // Muse Code subscription (Meta Muse Spark), enabled from its stored key.
115
+ providers['muse-code-oauth'] = oauthEntry('muse-code-oauth');
110
116
  // Dev-only providers (Settings → Developer → Providers, one option each):
111
117
  // omitted entirely from the default config while their option is off so
112
118
  // they never surface in settings.
119
+ // Experimental MiMo subscription: enabled from a stored platform API key.
120
+ if (isOAuthProviderAvailable('mimo-oauth')) providers['mimo-oauth'] = oauthEntry('mimo-oauth');
113
121
  // Experimental direct Cursor wire provider. It remains disabled unless a
114
122
  // Mixdog-owned login or CURSOR_ACCESS_TOKEN is present.
115
123
  if (isOAuthProviderAvailable('cursor-oauth')) providers['cursor-oauth'] = oauthEntry('cursor-oauth');
@@ -187,6 +195,13 @@ function normalizeRecapConfig(rawRecap) {
187
195
  return recap;
188
196
  }
189
197
 
198
+ // `instructionFiles` (instruction-files-mode.mjs): only a valid non-default
199
+ // mode is kept; absent means the default.
200
+ function storedInstructionFiles(agent) {
201
+ const mode = normalizeInstructionFilesMode(agent?.instructionFiles);
202
+ return mode === DEFAULT_INSTRUCTION_FILES_MODE ? {} : { instructionFiles: mode };
203
+ }
204
+
190
205
  export function loadConfig(options = {}) {
191
206
  const includeSecrets = options.secrets !== false;
192
207
  try {
@@ -245,6 +260,7 @@ export function loadConfig(options = {}) {
245
260
  // canonicalizeAgentRouteStorage drops it when empty.
246
261
  disabledAgents: legacyDisabledAgents ? disabledAgentIds(raw) : effectiveDisabledAgents(raw),
247
262
  ...(raw.memoryTools && typeof raw.memoryTools === 'object' ? { memoryTools: { ...raw.memoryTools } } : {}),
263
+ ...storedInstructionFiles(raw),
248
264
  ...normalizeWorkflowSelection(raw),
249
265
  profile: normalizeProfileConfig(raw.profile),
250
266
  skills: normalizeSkillsConfig(raw.skills),
@@ -420,6 +436,8 @@ function buildAgentSaveBuilder(config) {
420
436
  } else Object.assign(next, disabledAgentDelta(canonicalRoutes.disabledAgents));
421
437
  if (config.memoryTools && typeof config.memoryTools === 'object') next.memoryTools = { ...config.memoryTools };
422
438
  else delete next.memoryTools;
439
+ delete next.instructionFiles;
440
+ Object.assign(next, storedInstructionFiles(config));
423
441
  delete next.workflowRoutes;
424
442
  // Defaults live in code: values equal to the current default are not stored.
425
443
  return storedAgentForm(removeRetiredAgentFields(next));
@@ -0,0 +1,130 @@
1
+ // The user's instruction files (AGENTS.md / CLAUDE.md), global then project,
2
+ // rendered as one `# Instruction Files` block for a new session's BP3 core.
3
+ import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { isAbsolute, join, relative, resolve } from 'node:path';
6
+ import { readSection } from '../../../shared/config.mjs';
7
+ import { normalizeInstructionFilesMode } from '../../../shared/instruction-files-mode.mjs';
8
+ import { featureEnvOverride } from '../runtime-core/config-helpers.mjs';
9
+
10
+ // Per-file budget in UTF-8 bytes (same byte-based cap as core memory).
11
+ const INSTRUCTION_FILE_MAX_BYTES = 32 * 1024;
12
+ const TRUNCATED_MARK = '\n\n[... truncated]';
13
+ const KINDS_BY_MODE = { all: ['agents', 'claude'], agents: ['agents'], claude: ['claude'], off: [] };
14
+ const GLOBAL_DIR = { agents: '.codex', claude: '.claude' };
15
+ const FILE_NAME = { agents: 'AGENTS.md', claude: 'CLAUDE.md' };
16
+
17
+ /** The configured mode, forced off by MIXDOG_BOOT_INSTRUCTION_FILES=0. */
18
+ export function effectiveInstructionFilesMode(mode) {
19
+ if (featureEnvOverride('MIXDOG_BOOT_INSTRUCTION_FILES') === false) return 'off';
20
+ return normalizeInstructionFilesMode(mode);
21
+ }
22
+
23
+ /** The stored agent-config value; an unreadable config reads as the default. */
24
+ export function configuredInstructionFilesMode() {
25
+ try {
26
+ return normalizeInstructionFilesMode(readSection('agent')?.instructionFiles);
27
+ } catch {
28
+ return normalizeInstructionFilesMode(undefined);
29
+ }
30
+ }
31
+
32
+ const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]);
33
+
34
+ /** Candidate files in load order: global files first, then the project root's.
35
+ * Project files are confined to the real directory of their `root`; global
36
+ * files carry no root and may resolve anywhere. */
37
+ function instructionFileCandidates({ mode, cwd, homeDir = homedir() } = {}) {
38
+ const kinds = KINDS_BY_MODE[effectiveInstructionFilesMode(mode)];
39
+ const candidates = [];
40
+ if (homeDir) {
41
+ const root = resolve(homeDir);
42
+ // Global files may be symlinks into a dotfiles repo: no containment root.
43
+ for (const kind of kinds) candidates.push({ path: join(root, GLOBAL_DIR[kind], FILE_NAME[kind]) });
44
+ }
45
+ if (cwd) {
46
+ const root = resolve(cwd);
47
+ for (const kind of kinds) candidates.push({ path: join(root, FILE_NAME[kind]), root });
48
+ }
49
+ return candidates;
50
+ }
51
+
52
+ function isInside(root, path) {
53
+ const rel = relative(root, path);
54
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
55
+ }
56
+
57
+ function skipNote(path, reason) {
58
+ process.stderr.write(`[instruction-files] skipped ${path}: ${reason}\n`);
59
+ }
60
+
61
+ /** At most the file's first cap bytes (plus a BOM allowance and one probe byte)
62
+ * from an open fd, so a huge or endless file is never read whole. */
63
+ function readHead(fd) {
64
+ const buffer = Buffer.allocUnsafe(INSTRUCTION_FILE_MAX_BYTES + UTF8_BOM.length + 1);
65
+ let length = 0;
66
+ while (length < buffer.length) {
67
+ const count = readSync(fd, buffer, length, buffer.length - length, null);
68
+ if (count === 0) break;
69
+ length += count;
70
+ }
71
+ return buffer.subarray(0, length);
72
+ }
73
+
74
+ /** Decode a head that may end mid-character: the incomplete tail is dropped. */
75
+ function decodeHead(bytes) {
76
+ return new TextDecoder('utf-8').decode(bytes, { stream: true });
77
+ }
78
+
79
+ /** The bounded, trimmed text of one instruction file; over the budget it is cut
80
+ * at a word boundary and ends with the truncation notice. */
81
+ function boundedText(head) {
82
+ const bytes = head.subarray(0, UTF8_BOM.length).equals(UTF8_BOM) ? head.subarray(UTF8_BOM.length) : head;
83
+ if (bytes.length <= INSTRUCTION_FILE_MAX_BYTES) return decodeHead(bytes).trim();
84
+ const budget = INSTRUCTION_FILE_MAX_BYTES - Buffer.byteLength(TRUNCATED_MARK, 'utf8');
85
+ const text = decodeHead(bytes.subarray(0, budget)).trim();
86
+ return `${text.replace(/\s+\S*$/, '')}${TRUNCATED_MARK}`;
87
+ }
88
+
89
+ /** The file's bounded content, or '' when it is absent, unreadable, not a
90
+ * regular file, or resolves (symlink/junction) outside `root` when one is set. */
91
+ function readInstructionFile({ path, root }) {
92
+ let fd;
93
+ try {
94
+ const realPath = realpathSync.native(path);
95
+ if (root && !isInside(realpathSync.native(root), realPath)) {
96
+ skipNote(path, 'resolves outside its root');
97
+ return '';
98
+ }
99
+ if (!statSync(realPath).isFile()) return '';
100
+ fd = openSync(realPath, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
101
+ if (!fstatSync(fd).isFile()) return '';
102
+ return boundedText(readHead(fd));
103
+ } catch (error) {
104
+ if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR' && error?.code !== 'EISDIR') {
105
+ skipNote(path, error?.code || error?.message || 'unreadable');
106
+ }
107
+ return '';
108
+ } finally {
109
+ if (fd !== undefined) closeSync(fd);
110
+ }
111
+ }
112
+
113
+ /** The `# Instruction Files` block, or '' when no file has content. Files
114
+ * whose bounded content repeats an included one (symlinks, copies) are dropped. */
115
+ export function loadInstructionFilesContext({ mode, cwd, homeDir = homedir() } = {}) {
116
+ const seen = new Set();
117
+ const sections = [];
118
+ for (const candidate of instructionFileCandidates({ mode, cwd, homeDir })) {
119
+ const content = readInstructionFile(candidate);
120
+ if (!content || seen.has(content)) continue;
121
+ seen.add(content);
122
+ sections.push(`## ${candidate.path}\n\n${content}`);
123
+ }
124
+ if (!sections.length) return '';
125
+ return [
126
+ '# Instruction Files',
127
+ "The user's instruction files. Project files take precedence over global ones when they conflict.",
128
+ ...sections,
129
+ ].join('\n\n');
130
+ }
@@ -268,12 +268,16 @@ export function composeSystemPrompt(opts) {
268
268
  // Keep the active workflow first within BP3 so it leads the role and
269
269
  // environment material that varies with the session.
270
270
  const coreMemory = skip.memory ? '' : trimmedPromptSlice(opts.coreMemoryContext);
271
+ // User AGENTS.md / CLAUDE.md block (context/instruction-files.mjs), already
272
+ // headed `# Instruction Files`; rides right after core memory.
273
+ const instructionFiles = skip.claudemd ? '' : trimmedPromptSlice(opts.instructionFilesContext);
271
274
  const sessionMarkerCore = joinPromptSlices([
272
275
  trimmedPromptSlice(opts.workflowContext),
273
276
  opts.roleRules || '',
274
277
  opts.userPrompt || '',
275
278
  roleInstructionContext,
276
279
  coreMemory ? `# Core Memory\n${coreMemory}` : '',
280
+ instructionFiles,
277
281
  ]);
278
282
  // Response language is the LAST system text before the conversation. It
279
283
  // rides the environment block (not the tier3 core) because the `# Session`
@@ -1,3 +1,14 @@
1
+ import { statSync } from 'node:fs';
2
+
3
+ /** The file's modification time in ms; 0 when it cannot be stat-ed. */
4
+ export function fileMtimeMs(path) {
5
+ try {
6
+ return statSync(path).mtimeMs;
7
+ } catch {
8
+ return 0;
9
+ }
10
+ }
11
+
1
12
  export function decodeJwtPayload(token) {
2
13
  try {
3
14
  const parts = String(token || '').split('.');
@@ -0,0 +1,9 @@
1
+ /** Open an OAuth login URL in the browser, best effort: a failure is logged, never thrown. */
2
+ export async function openLoginUrl(provider, url) {
3
+ try {
4
+ const { openInBrowser } = await import('../../../../shared/open-url.mjs');
5
+ openInBrowser(url);
6
+ } catch (err) {
7
+ process.stderr.write(`[${provider}] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
8
+ }
9
+ }
@@ -0,0 +1,254 @@
1
+ // MiMo browser login. The platform encrypts the minted API key to a one-shot
2
+ // X25519 public key we send in the authorize URL; the reply (loopback redirect
3
+ // or a pasted code) carries `u` = ephemeralPub(32) | nonce(12) | ciphertext | tag(16).
4
+ import { createServer } from 'node:http';
5
+ import { lookup } from 'node:dns/promises';
6
+ import { createDecipheriv, createHash, createPublicKey, diffieHellman, generateKeyPairSync } from 'node:crypto';
7
+ import { openLoginUrl } from './lib/open-login-url.mjs';
8
+ import {
9
+ authorizeEndpoint,
10
+ manualRedirectUri,
11
+ resultUrl,
12
+ LOGIN_TIMEOUT_MS,
13
+ ensureTokenPlanBaseUrl,
14
+ getKeyName,
15
+ normalizeMimoBaseUrl,
16
+ saveTokens,
17
+ } from './mimo-oauth-tokens.mjs';
18
+
19
+ // Fixed SPKI prefix for a raw 32-byte X25519 public key.
20
+ const X25519_SPKI_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');
21
+ const EPHEMERAL_LEN = 32;
22
+ const NONCE_LEN = 12;
23
+ const TAG_LEN = 16;
24
+ const MAX_API_KEY_LEN = 512;
25
+ const MAX_UID_LEN = 128;
26
+
27
+ export function generateLoginKeyPair() {
28
+ const { publicKey, privateKey } = generateKeyPairSync('x25519');
29
+ return { privateKey, publicKeyB64: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url') };
30
+ }
31
+
32
+ // Decrypt the `u` payload into { apiKey, uid, baseUrl }.
33
+ export function decryptMimoPayload(u, privateKey) {
34
+ const bytes = Buffer.from(String(u || '').trim(), 'base64url');
35
+ if (bytes.length <= EPHEMERAL_LEN + NONCE_LEN + TAG_LEN) throw new Error('[mimo-oauth] payload too short');
36
+ const ephemeral = bytes.subarray(0, EPHEMERAL_LEN);
37
+ const nonce = bytes.subarray(EPHEMERAL_LEN, EPHEMERAL_LEN + NONCE_LEN);
38
+ const sealed = bytes.subarray(EPHEMERAL_LEN + NONCE_LEN, bytes.length - TAG_LEN);
39
+ const tag = bytes.subarray(bytes.length - TAG_LEN);
40
+ const publicKey = createPublicKey({
41
+ key: Buffer.concat([X25519_SPKI_PREFIX, ephemeral]),
42
+ format: 'der',
43
+ type: 'spki',
44
+ });
45
+ const key = createHash('sha256').update(diffieHellman({ privateKey, publicKey })).digest();
46
+ const decipher = createDecipheriv('aes-256-gcm', key, nonce);
47
+ decipher.setAuthTag(tag);
48
+ let doc;
49
+ try {
50
+ doc = JSON.parse(Buffer.concat([decipher.update(sealed), decipher.final()]).toString('utf-8'));
51
+ } catch {
52
+ throw new Error('[mimo-oauth] could not decrypt the authorization payload');
53
+ }
54
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
55
+ throw new Error('[mimo-oauth] authorization payload is not an object');
56
+ }
57
+ const apiKey = typeof doc.sk === 'string' ? doc.sk.trim() : '';
58
+ if (!apiKey) throw new Error('[mimo-oauth] authorization payload carried no API key');
59
+ if (apiKey.length > MAX_API_KEY_LEN || !/^[\x21-\x7e]+$/.test(apiKey)) {
60
+ throw new Error('[mimo-oauth] authorization payload carried a malformed API key');
61
+ }
62
+ const uidPresent = doc.uid !== undefined && doc.uid !== null && doc.uid !== '';
63
+ if (uidPresent && typeof doc.uid !== 'string' && !Number.isFinite(doc.uid)) {
64
+ throw new Error('[mimo-oauth] authorization payload carried a malformed account id');
65
+ }
66
+ const uid = uidPresent ? String(doc.uid) : '';
67
+ if (uid.length > MAX_UID_LEN || /[\x00-\x1f\x7f]/.test(uid)) {
68
+ throw new Error('[mimo-oauth] authorization payload carried a malformed account id');
69
+ }
70
+ const baseUrl = doc.url === undefined || doc.url === null || doc.url === '' ? '' : normalizeMimoBaseUrl(doc.url);
71
+ return { apiKey, uid, baseUrl };
72
+ }
73
+
74
+ // A pasted value may be the bare code or the full callback URL containing it.
75
+ function codeFromInput(input) {
76
+ const text = String(input || '').trim();
77
+ if (!text) throw new Error('[mimo-oauth] authorization code is required');
78
+ if (/^https?:\/\//i.test(text)) {
79
+ const u = new URL(text).searchParams.get('u');
80
+ if (!u) throw new Error('[mimo-oauth] callback URL has no authorization code');
81
+ return u;
82
+ }
83
+ return text;
84
+ }
85
+
86
+ function authorizeUrl(pk, redirectUri, keyName) {
87
+ const url = new URL(authorizeEndpoint());
88
+ url.searchParams.set('pk', pk);
89
+ url.searchParams.set('redirect_uri', redirectUri);
90
+ url.searchParams.set('kn', 'mimocode');
91
+ url.searchParams.set('key_name', keyName);
92
+ return url.toString();
93
+ }
94
+
95
+ function resultRedirect(error) {
96
+ const url = new URL(resultUrl());
97
+ url.searchParams.set('status', error ? 'error' : 'success');
98
+ if (error) url.searchParams.set('message', String(error.message || error).slice(0, 200));
99
+ return url.toString();
100
+ }
101
+
102
+ // Every loopback address `localhost` can resolve to (IPv4 and IPv6), so the
103
+ // browser reaches the callback whichever family it picks.
104
+ async function loopbackAddresses() {
105
+ try {
106
+ const found = await lookup('localhost', { all: true });
107
+ const addresses = [...new Set(found.map((entry) => entry.address))];
108
+ if (addresses.length) return addresses;
109
+ } catch {
110
+ /* fall back below */
111
+ }
112
+ return ['127.0.0.1'];
113
+ }
114
+
115
+ function listenOn(server, port, host) {
116
+ return new Promise((resolve, reject) => {
117
+ server.once('error', reject);
118
+ server.listen(port, host, () => {
119
+ server.off('error', reject);
120
+ resolve();
121
+ });
122
+ });
123
+ }
124
+
125
+ export async function beginOAuthLogin({
126
+ openBrowser = true,
127
+ timeoutMs = LOGIN_TIMEOUT_MS,
128
+ fetchImpl = undefined,
129
+ addresses = null,
130
+ printUrl = false,
131
+ } = {}) {
132
+ const { privateKey, publicKeyB64 } = generateLoginKeyPair();
133
+ const keyName = getKeyName();
134
+
135
+ const servers = [];
136
+ const work = new AbortController();
137
+ let timer = null;
138
+ let finish = null;
139
+ let settled = false;
140
+ let url = '';
141
+ const waitForCallback = new Promise((resolve, reject) => {
142
+ finish = (value, error = null) => {
143
+ if (settled) return;
144
+ settled = true;
145
+ if (timer) clearTimeout(timer);
146
+ work.abort();
147
+ for (const server of servers) {
148
+ try {
149
+ server.close();
150
+ } catch {
151
+ /* already closed */
152
+ }
153
+ }
154
+ if (error) reject(error);
155
+ else resolve(value);
156
+ };
157
+ timer = setTimeout(
158
+ () => finish(null, new Error(`[mimo-oauth] login timed out after ${Math.round(timeoutMs / 1000)}s`)),
159
+ timeoutMs
160
+ );
161
+ });
162
+ // The caller may only use the pasted-code path; a later timeout/cancel must not be an unhandled rejection.
163
+ waitForCallback.catch(() => {});
164
+
165
+ // A Token Plan key without a gateway URL is routed to its region first. The
166
+ // credential is written only while the login is still open, then the login
167
+ // is completed in the same synchronous step.
168
+ const settle = async (decrypted) => {
169
+ const resolved = await ensureTokenPlanBaseUrl(decrypted, { fetchImpl, signal: work.signal, persist: false });
170
+ if (settled) throw new Error('[mimo-oauth] login is no longer active');
171
+ const tokens = saveTokens(resolved);
172
+ finish(tokens);
173
+ return tokens;
174
+ };
175
+
176
+ const onRequest = async (req, res) => {
177
+ const target = new URL(req.url || '/', 'http://localhost');
178
+ const u = target.pathname === '/' ? target.searchParams.get('u') : null;
179
+ if (!u || settled) {
180
+ res.writeHead(404);
181
+ res.end();
182
+ return;
183
+ }
184
+ // Only a payload sealed to this login's key settles it; anything else is
185
+ // answered with an error and the login keeps waiting.
186
+ let decrypted;
187
+ try {
188
+ decrypted = decryptMimoPayload(u, privateKey);
189
+ } catch {
190
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
191
+ res.end('Invalid authorization payload');
192
+ return;
193
+ }
194
+ try {
195
+ await settle(decrypted);
196
+ res.writeHead(302, { Location: resultRedirect(null) });
197
+ res.end();
198
+ } catch (err) {
199
+ res.writeHead(302, { Location: resultRedirect(err) });
200
+ res.end();
201
+ finish(null, err instanceof Error ? err : new Error(String(err)));
202
+ }
203
+ };
204
+
205
+ let port = 0;
206
+ let bindError = null;
207
+ for (const address of addresses || (await loopbackAddresses())) {
208
+ const server = createServer(onRequest);
209
+ try {
210
+ await listenOn(server, port, address);
211
+ } catch (err) {
212
+ // A failed address only matters when no other listener comes up.
213
+ bindError = err;
214
+ continue;
215
+ }
216
+ // Once bound, a runtime fault on this listener ends the login.
217
+ server.on('error', (err) => finish(null, new Error(`[mimo-oauth] callback server failed: ${err?.message || err}`)));
218
+ servers.push(server);
219
+ port = server.address().port;
220
+ }
221
+ if (!servers.length) {
222
+ const err = bindError || new Error('[mimo-oauth] no loopback address to listen on');
223
+ finish(null, err);
224
+ throw err;
225
+ }
226
+ url = authorizeUrl(publicKeyB64, `http://localhost:${port}/`, keyName);
227
+ const manualUrl = authorizeUrl(publicKeyB64, manualRedirectUri(), keyName);
228
+ // The URL carries the one-shot login public key. The interactive surfaces
229
+ // (TUI, desktop) receive it in the returned handle and show it to the user
230
+ // only; it is printed to the terminal solely for the headless loginOAuth().
231
+ if (printUrl) {
232
+ process.stderr.write(
233
+ `\n[mimo-oauth] Open this URL to log in:\n${url}\n\nOr, if the browser is on another machine:\n${manualUrl}\n\n`
234
+ );
235
+ }
236
+ if (openBrowser) await openLoginUrl('mimo-oauth', url);
237
+
238
+ return {
239
+ provider: 'mimo-oauth',
240
+ url,
241
+ manualUrl,
242
+ port,
243
+ waitForCallback,
244
+ completeCode: async (input) => settle(decryptMimoPayload(codeFromInput(input), privateKey)),
245
+ cancel: () => {
246
+ finish?.(null);
247
+ },
248
+ };
249
+ }
250
+
251
+ export async function loginOAuth() {
252
+ const login = await beginOAuthLogin({ printUrl: true });
253
+ return await login.waitForCallback;
254
+ }