mixdog 0.9.44 → 0.9.46

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 (117) hide show
  1. package/package.json +5 -2
  2. package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
  3. package/scripts/agent-model-liveness-test.mjs +618 -0
  4. package/scripts/agent-trace-io-test.mjs +68 -4
  5. package/scripts/bench-run.mjs +30 -3
  6. package/scripts/compact-pressure-test.mjs +187 -4
  7. package/scripts/compact-prior-context-flatten-test.mjs +252 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +8 -6
  9. package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
  10. package/scripts/context-mcp-metering-test.mjs +75 -0
  11. package/scripts/explore-prompt-policy-test.mjs +15 -16
  12. package/scripts/explore-timeout-cancel-test.mjs +345 -0
  13. package/scripts/headless-pristine-execution-test.mjs +614 -0
  14. package/scripts/internal-comms-smoke.mjs +15 -6
  15. package/scripts/live-worker-smoke.mjs +1 -1
  16. package/scripts/memory-core-input-test.mjs +137 -0
  17. package/scripts/memory-rule-contract-test.mjs +5 -5
  18. package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
  19. package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
  20. package/scripts/parent-abort-link-test.mjs +22 -0
  21. package/scripts/provider-toolcall-test.mjs +22 -0
  22. package/scripts/reactive-compact-persist-smoke.mjs +8 -4
  23. package/scripts/session-bench.mjs +3 -70
  24. package/scripts/task-bench.mjs +0 -2
  25. package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
  26. package/scripts/tool-smoke.mjs +21 -21
  27. package/scripts/tool-tui-presentation-test.mjs +68 -0
  28. package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
  29. package/src/app.mjs +28 -103
  30. package/src/cli.mjs +17 -13
  31. package/src/headless-command.mjs +139 -0
  32. package/src/headless-role.mjs +121 -10
  33. package/src/help.mjs +4 -1
  34. package/src/rules/agent/00-common.md +3 -3
  35. package/src/rules/agent/00-core.md +8 -9
  36. package/src/rules/agent/20-skip-protocol.md +2 -3
  37. package/src/rules/agent/30-explorer.md +50 -56
  38. package/src/rules/agent/40-cycle1-agent.md +10 -12
  39. package/src/rules/agent/41-cycle2-agent.md +12 -9
  40. package/src/rules/agent/42-cycle3-agent.md +4 -6
  41. package/src/rules/lead/01-general.md +5 -6
  42. package/src/rules/lead/02-channels.md +1 -1
  43. package/src/rules/lead/lead-brief.md +14 -17
  44. package/src/rules/lead/lead-tool.md +3 -3
  45. package/src/rules/shared/01-tool.md +41 -43
  46. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
  48. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
  49. package/src/runtime/agent/orchestrator/config.mjs +96 -30
  50. package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  52. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
  53. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  54. package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
  55. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
  56. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
  58. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
  60. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
  61. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
  64. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
  65. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
  66. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
  67. package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
  69. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
  71. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
  74. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
  76. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
  77. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
  78. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
  80. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
  81. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  82. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
  84. package/src/runtime/memory/index.mjs +0 -1
  85. package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
  86. package/src/runtime/memory/lib/http-router.mjs +0 -193
  87. package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
  88. package/src/runtime/memory/tool-defs.mjs +5 -6
  89. package/src/runtime/shared/config.mjs +11 -34
  90. package/src/runtime/shared/pristine-execution-contract.json +75 -0
  91. package/src/runtime/shared/pristine-execution.mjs +356 -0
  92. package/src/runtime/shared/provider-api-key.mjs +43 -0
  93. package/src/runtime/shared/provider-auth-binding.mjs +21 -0
  94. package/src/runtime/shared/update-checker.mjs +40 -10
  95. package/src/session-runtime/context-status.mjs +61 -13
  96. package/src/session-runtime/mcp-glue.mjs +29 -2
  97. package/src/session-runtime/plugin-mcp.mjs +7 -0
  98. package/src/session-runtime/resource-api.mjs +38 -5
  99. package/src/session-runtime/runtime-core.mjs +5 -1
  100. package/src/session-runtime/session-turn-api.mjs +14 -2
  101. package/src/session-runtime/settings-api.mjs +5 -0
  102. package/src/session-runtime/tool-catalog.mjs +13 -2
  103. package/src/session-runtime/tool-defs.mjs +1 -3
  104. package/src/standalone/agent-task-status.mjs +50 -11
  105. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  106. package/src/standalone/explore-tool.mjs +257 -49
  107. package/src/standalone/seeds.mjs +1 -0
  108. package/src/tui/App.jsx +26 -16
  109. package/src/tui/app/use-transcript-scroll.mjs +4 -3
  110. package/src/tui/app/use-transcript-window.mjs +12 -21
  111. package/src/tui/components/ContextPanel.jsx +19 -25
  112. package/src/tui/dist/index.mjs +89 -78
  113. package/src/tui/engine/agent-envelope.mjs +16 -5
  114. package/src/tui/engine/labels.mjs +1 -1
  115. package/src/workflows/default/WORKFLOW.md +21 -51
  116. package/src/workflows/solo/WORKFLOW.md +12 -17
  117. package/src/workflows/solo-review/WORKFLOW.md +0 -47
@@ -0,0 +1,75 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "configFileName": "mixdog-config.json",
4
+ "guardEnv": {
5
+ "MIXDOG_DISABLE_MCP": "1",
6
+ "MIXDOG_DISABLE_SKILLS": "1",
7
+ "MIXDOG_BOOT_CORE_MEMORY": "0",
8
+ "MIXDOG_DISABLE_CHANNEL_START": "1",
9
+ "MIXDOG_SKIP_USER_DATA_BACKUP": "1",
10
+ "MIXDOG_DISABLE_STAGED_INSTALL": "1",
11
+ "MIXDOG_DISABLE_STAGED_SWAP": "1"
12
+ },
13
+ "approvedExecutionEnv": [
14
+ "MIXDOG_AGENT_TRACE_PATH",
15
+ "MIXDOG_ANTHROPIC_OAUTH_REFRESH_DISABLED",
16
+ "MIXDOG_DRIVER_DEADLINE_MS",
17
+ "MIXDOG_DISABLE_PROVIDER_WARMUP",
18
+ "MIXDOG_DISABLE_MODEL_PREFETCH",
19
+ "MIXDOG_DISABLE_MODEL_CATALOG_WARMUP",
20
+ "MIXDOG_PROVIDER_FIRST_BYTE_TIMEOUT_MS",
21
+ "MIXDOG_STALL_FIRST_BYTE_ABORT_S"
22
+ ],
23
+ "benchmarkRouteEnv": [
24
+ "MIXDOG_PROMPT",
25
+ "MIXDOG_PROVIDER",
26
+ "MIXDOG_MODEL",
27
+ "MIXDOG_EFFORT",
28
+ "MIXDOG_FAST",
29
+ "MIXDOG_WORKFLOW"
30
+ ],
31
+ "oauthProviders": {
32
+ "anthropic-oauth": {
33
+ "credentialFile": "anthropic-oauth-credentials.json",
34
+ "credentialPathEnv": "ANTHROPIC_OAUTH_CREDENTIALS_PATH",
35
+ "modelCatalogFile": "anthropic-oauth-models.json"
36
+ },
37
+ "openai-oauth": {
38
+ "credentialFile": "openai-oauth.json",
39
+ "credentialPathEnv": "OPENAI_OAUTH_CREDENTIALS_PATH",
40
+ "modelCatalogFile": "openai-oauth-models.json"
41
+ },
42
+ "grok-oauth": {
43
+ "credentialFile": "grok-oauth.json",
44
+ "credentialPathEnv": "GROK_OAUTH_CREDENTIALS_PATH",
45
+ "modelCatalogFile": "grok-oauth-models.json"
46
+ }
47
+ },
48
+ "apiKeyProviders": {
49
+ "openai": "OPENAI_API_KEY",
50
+ "anthropic": "ANTHROPIC_API_KEY",
51
+ "gemini": "GEMINI_API_KEY",
52
+ "deepseek": "DEEPSEEK_API_KEY",
53
+ "xai": "XAI_API_KEY",
54
+ "opencode-go": "OPENCODE_API_KEY"
55
+ },
56
+ "personalStateCounters": [
57
+ "behavioralStateFilesCopied",
58
+ "memoryFilesCopied",
59
+ "sessionFilesCopied",
60
+ "userProfileFilesCopied",
61
+ "pluginStateFilesCopied",
62
+ "channelStateFilesCopied",
63
+ "otherPersonalStateFilesCopied"
64
+ ],
65
+ "disabledFeatures": [
66
+ "mcpSources",
67
+ "skills",
68
+ "bundledSkillSeeding",
69
+ "coreMemoryBoot",
70
+ "channelStart",
71
+ "plugins",
72
+ "userProfiles",
73
+ "priorSessions"
74
+ ]
75
+ }
@@ -0,0 +1,356 @@
1
+ import { createHash } from 'node:crypto';
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs';
11
+ import { homedir, tmpdir } from 'node:os';
12
+ import { join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { replaceProviderAuthBindings } from './provider-auth-binding.mjs';
15
+ import {
16
+ AGENT_PROVIDER_ENV_ALIASES,
17
+ getAgentApiKey,
18
+ } from './provider-api-key.mjs';
19
+
20
+ const contractPath = fileURLToPath(
21
+ new URL('./pristine-execution-contract.json', import.meta.url),
22
+ );
23
+ const patchManifestPath = fileURLToPath(
24
+ new URL('../agent/orchestrator/tools/patch-manifest.json', import.meta.url),
25
+ );
26
+
27
+ export const PRISTINE_EXECUTION_CONTRACT = Object.freeze(
28
+ JSON.parse(readFileSync(contractPath, 'utf8')),
29
+ );
30
+
31
+ const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
32
+
33
+ function clean(value) {
34
+ return String(value ?? '').trim();
35
+ }
36
+
37
+ export function validateExplicitPristineRoute({ provider, model, effort, fast } = {}) {
38
+ const selectedProvider = clean(provider);
39
+ const selectedModel = clean(model);
40
+ if (!selectedProvider || !selectedModel) {
41
+ return 'headless role commands require both --provider <name> and --model <name>; host route fallback is disabled';
42
+ }
43
+ const selectedEffort = clean(effort).toLowerCase();
44
+ if (selectedEffort && !EFFORTS.has(selectedEffort)) {
45
+ return `invalid --effort ${JSON.stringify(clean(effort))}; expected low, medium, high, xhigh, or max`;
46
+ }
47
+ if (fast !== undefined && typeof fast !== 'boolean') {
48
+ return '--fast must be an explicit boolean flag';
49
+ }
50
+ return null;
51
+ }
52
+
53
+ export function buildMinimalPristineConfig({ provider, model, effort, fast } = {}) {
54
+ const routeError = validateExplicitPristineRoute({ provider, model, effort, fast });
55
+ if (routeError) throw new Error(routeError);
56
+ const selectedProvider = clean(provider);
57
+ const selectedModel = clean(model);
58
+ const selectedEffort = clean(effort).toLowerCase();
59
+ const route = {
60
+ provider: selectedProvider,
61
+ model: selectedModel,
62
+ ...(selectedEffort ? { effort: selectedEffort } : {}),
63
+ ...(fast === true ? { fast: true } : {}),
64
+ };
65
+ const providerConfig = {
66
+ enabled: true,
67
+ ...(selectedProvider === 'openai-oauth' ? { websocket: true } : {}),
68
+ };
69
+ const modelSettings = {};
70
+ if (selectedEffort || fast === true) {
71
+ modelSettings[`${selectedProvider}/${selectedModel}`] = {
72
+ ...(selectedEffort ? { effort: selectedEffort } : {}),
73
+ ...(fast === true ? { fast: true } : {}),
74
+ };
75
+ }
76
+ return {
77
+ agent: {
78
+ providers: { [selectedProvider]: providerConfig },
79
+ presets: [{
80
+ id: 'headless-explicit-route',
81
+ name: 'HEADLESS EXPLICIT ROUTE',
82
+ type: 'agent',
83
+ tools: 'full',
84
+ ...route,
85
+ }],
86
+ default: 'headless-explicit-route',
87
+ workflow: { active: 'default' },
88
+ workflowRoutes: {},
89
+ agents: {},
90
+ modelSettings,
91
+ mcpServers: {},
92
+ },
93
+ };
94
+ }
95
+
96
+ function cloneJson(value) {
97
+ return JSON.parse(JSON.stringify(value));
98
+ }
99
+
100
+ export function createSelectedProviderPristineLoader({
101
+ config,
102
+ provider,
103
+ apiKey = null,
104
+ } = {}) {
105
+ const selectedProvider = clean(provider);
106
+ const runtimeConfig = cloneJson(config?.agent || {});
107
+ const selectedConfig = {
108
+ ...(runtimeConfig.providers?.[selectedProvider] || {}),
109
+ enabled: true,
110
+ ...(apiKey ? { apiKey } : {}),
111
+ };
112
+ runtimeConfig.providers = { [selectedProvider]: selectedConfig };
113
+ return () => cloneJson(runtimeConfig);
114
+ }
115
+
116
+ function hostDataDir(env) {
117
+ if (clean(env.MIXDOG_DATA_DIR)) return resolve(env.MIXDOG_DATA_DIR);
118
+ const home = clean(env.MIXDOG_HOME)
119
+ ? resolve(env.MIXDOG_HOME)
120
+ : join(homedir(), '.mixdog');
121
+ return join(home, 'data');
122
+ }
123
+
124
+ function patchPlatformKey() {
125
+ const os = process.platform === 'win32' ? 'win32' : process.platform;
126
+ return `${os}-${process.arch}`;
127
+ }
128
+
129
+ export function seedVerifiedPatchBinaryCache(
130
+ sourceDataDir,
131
+ dataDir,
132
+ { manifestPath = patchManifestPath } = {},
133
+ ) {
134
+ try {
135
+ const sourcePatchDir = join(sourceDataDir, 'patch-bin');
136
+ const manifestBytes = readFileSync(manifestPath);
137
+ const manifest = JSON.parse(manifestBytes.toString('utf8'));
138
+ const asset = manifest?.assets?.[patchPlatformKey()];
139
+ const version = String(manifest?.version || '');
140
+ const expectedSha256 = clean(asset?.sha256).toLowerCase();
141
+ if (!/^[A-Za-z0-9._-]+$/.test(version) || !/^[a-f0-9]{64}$/.test(expectedSha256)) return false;
142
+
143
+ const binaryName = `mixdog-patch-${version}${process.platform === 'win32' ? '.exe' : ''}`;
144
+ const sourceBinary = join(sourcePatchDir, binaryName);
145
+ if (!existsSync(sourceBinary)) return false;
146
+ const binaryBytes = readFileSync(sourceBinary);
147
+ const actualSha256 = createHash('sha256').update(binaryBytes).digest('hex');
148
+ if (actualSha256 !== expectedSha256) return false;
149
+
150
+ const targetPatchDir = join(dataDir, 'patch-bin');
151
+ mkdirSync(targetPatchDir, { recursive: true, mode: 0o700 });
152
+ writeFileSync(join(targetPatchDir, binaryName), binaryBytes, { mode: 0o700 });
153
+ writeFileSync(join(targetPatchDir, 'manifest.json'), manifestBytes, { mode: 0o600 });
154
+ return true;
155
+ } catch {
156
+ // A missing, stale, or malformed host cache is optional acceleration only.
157
+ // Leave the pristine runtime empty so the normal fetch path handles it.
158
+ return false;
159
+ }
160
+ }
161
+
162
+ function auditDocument({
163
+ provider,
164
+ model,
165
+ effort,
166
+ fast,
167
+ configBytes,
168
+ authMode,
169
+ catalogCount,
170
+ }) {
171
+ const contract = PRISTINE_EXECUTION_CONTRACT;
172
+ return {
173
+ schemaVersion: contract.schemaVersion,
174
+ mode: 'headless-role-pristine',
175
+ provider: clean(provider),
176
+ model: clean(model),
177
+ effort: clean(effort).toLowerCase() || null,
178
+ fast: fast === true,
179
+ configSha256: createHash('sha256').update(configBytes).digest('hex'),
180
+ authMode,
181
+ authArtifactFilesCopied: 0,
182
+ injectedModelCatalogFileCount: catalogCount,
183
+ personalState: {
184
+ hostConfigRead: false,
185
+ ...Object.fromEntries(contract.personalStateCounters.map((name) => [name, 0])),
186
+ },
187
+ featuresEnabled: Object.fromEntries(
188
+ contract.disabledFeatures.map((name) => [name, false]),
189
+ ),
190
+ };
191
+ }
192
+
193
+ export function formatPristineExecutionAudit(audit) {
194
+ return [
195
+ `pristine-execution-audit v${audit.schemaVersion}`,
196
+ 'mode=headless-role',
197
+ 'personal-files=0',
198
+ 'host-config=0',
199
+ 'mcp=0',
200
+ 'skills=0',
201
+ 'core-memory=0',
202
+ 'channels=0',
203
+ 'plugins=0',
204
+ 'profiles=0',
205
+ 'prior-sessions=0',
206
+ `provider=${audit.provider}`,
207
+ `model=${audit.model}`,
208
+ `effort=${audit.effort || 'provider-default'}`,
209
+ `fast=${String(audit.fast)}`,
210
+ `auth=${audit.authMode}`,
211
+ `catalogs=${audit.injectedModelCatalogFileCount}`,
212
+ ].join(' ');
213
+ }
214
+
215
+ export function createPristineExecutionBoundary({
216
+ provider,
217
+ model,
218
+ effort,
219
+ fast,
220
+ env = process.env,
221
+ approvedExecutionEnv = {},
222
+ apiKeyResolver = getAgentApiKey,
223
+ } = {}) {
224
+ const routeError = validateExplicitPristineRoute({ provider, model, effort, fast });
225
+ if (routeError) throw new Error(routeError);
226
+
227
+ const contract = PRISTINE_EXECUTION_CONTRACT;
228
+ const hostEnv = { ...env };
229
+ const originalEnv = new Map();
230
+ const touchedKeys = new Set();
231
+ const setEnv = (name, value) => {
232
+ if (!touchedKeys.has(name)) {
233
+ originalEnv.set(name, Object.prototype.hasOwnProperty.call(env, name) ? env[name] : undefined);
234
+ touchedKeys.add(name);
235
+ }
236
+ env[name] = String(value);
237
+ };
238
+ const unsetEnv = (name) => {
239
+ if (!touchedKeys.has(name)) {
240
+ originalEnv.set(name, Object.prototype.hasOwnProperty.call(env, name) ? env[name] : undefined);
241
+ touchedKeys.add(name);
242
+ }
243
+ delete env[name];
244
+ };
245
+ const rootDir = mkdtempSync(join(tmpdir(), 'mixdog-headless-pristine-'));
246
+ const dataDir = join(rootDir, 'data');
247
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 });
248
+ let cleaned = false;
249
+ let restoreAuthBindings = () => {};
250
+ const cleanup = () => {
251
+ if (cleaned) return;
252
+ cleaned = true;
253
+ for (const [name, value] of originalEnv) {
254
+ if (value === undefined) delete env[name];
255
+ else env[name] = value;
256
+ }
257
+ restoreAuthBindings();
258
+ rmSync(rootDir, { recursive: true, force: true });
259
+ };
260
+
261
+ try {
262
+ const sourceDataDir = hostDataDir(hostEnv);
263
+ seedVerifiedPatchBinaryCache(sourceDataDir, dataDir);
264
+ const approvedNames = new Set(contract.approvedExecutionEnv || []);
265
+ for (const name of Object.keys(approvedExecutionEnv || {})) {
266
+ if (!approvedNames.has(name)) {
267
+ throw new Error(`unapproved pristine execution environment override: ${name}`);
268
+ }
269
+ }
270
+ const inheritedExecutionEnv = Object.fromEntries(
271
+ [...approvedNames]
272
+ .filter((name) => hostEnv[name] !== undefined)
273
+ .map((name) => [name, hostEnv[name]]),
274
+ );
275
+ const selectedProvider = clean(provider);
276
+ const oauth = contract.oauthProviders[selectedProvider];
277
+ const apiKeyEnv = contract.apiKeyProviders[selectedProvider];
278
+ let selectedApiKey = null;
279
+ if (apiKeyEnv) {
280
+ // Resolve before scrubbing so the selected provider may use its standard,
281
+ // MIXDOG_AGENT_*, alias, or one provider-scoped keychain account. No
282
+ // general config loader or all-provider credential scan is involved.
283
+ selectedApiKey = clean(apiKeyResolver(selectedProvider));
284
+ }
285
+ // Fail closed: no inherited Mixdog behavior reaches the child runtime.
286
+ // Only contract-listed operational controls are reintroduced below.
287
+ for (const name of Object.keys(env)) {
288
+ if (name.startsWith('MIXDOG_')) unsetEnv(name);
289
+ }
290
+ for (const entry of Object.values(contract.oauthProviders)) {
291
+ unsetEnv(entry.credentialPathEnv);
292
+ }
293
+ for (const name of Object.values(contract.apiKeyProviders)) unsetEnv(name);
294
+ for (const aliases of Object.values(AGENT_PROVIDER_ENV_ALIASES)) {
295
+ for (const name of aliases) unsetEnv(name);
296
+ }
297
+ setEnv('MIXDOG_HOME', rootDir);
298
+ setEnv('MIXDOG_DATA_DIR', dataDir);
299
+ for (const [name, value] of Object.entries({
300
+ ...inheritedExecutionEnv,
301
+ ...approvedExecutionEnv,
302
+ })) {
303
+ setEnv(name, value);
304
+ }
305
+ for (const [name, value] of Object.entries(contract.guardEnv)) setEnv(name, value);
306
+
307
+ let authMode = 'provider-managed';
308
+ let catalogCount = 0;
309
+ if (oauth) {
310
+ const sourceCredential = clean(hostEnv[oauth.credentialPathEnv])
311
+ ? resolve(hostEnv[oauth.credentialPathEnv])
312
+ : join(sourceDataDir, oauth.credentialFile);
313
+ if (!existsSync(sourceCredential)) {
314
+ throw new Error(`required ${clean(provider)} credentials are unavailable; sign in before using the headless role command`);
315
+ }
316
+ // Authentication is the sole host-state exception. Bind in-process (not
317
+ // via child-visible environment) so tools cannot discover the auth path.
318
+ restoreAuthBindings = replaceProviderAuthBindings({
319
+ [selectedProvider]: sourceCredential,
320
+ });
321
+ authMode = 'in-process-host-oauth-binding';
322
+ const sourceCatalog = join(sourceDataDir, oauth.modelCatalogFile);
323
+ if (existsSync(sourceCatalog)) {
324
+ copyFileSync(sourceCatalog, join(dataDir, oauth.modelCatalogFile));
325
+ catalogCount = 1;
326
+ }
327
+ } else if (apiKeyEnv) {
328
+ if (!selectedApiKey) {
329
+ throw new Error(`required ${selectedProvider} authentication is unavailable; set ${apiKeyEnv}`);
330
+ }
331
+ authMode = 'in-process-api-key';
332
+ }
333
+
334
+ const config = buildMinimalPristineConfig({ provider, model, effort, fast });
335
+ const loadConfig = createSelectedProviderPristineLoader({
336
+ config,
337
+ provider,
338
+ apiKey: selectedApiKey,
339
+ });
340
+ const configBytes = `${JSON.stringify(config, null, 2)}\n`;
341
+ writeFileSync(join(dataDir, contract.configFileName), configBytes, { mode: 0o600 });
342
+ const audit = auditDocument({
343
+ provider,
344
+ model,
345
+ effort,
346
+ fast,
347
+ configBytes,
348
+ authMode,
349
+ catalogCount,
350
+ });
351
+ return { rootDir, dataDir, config, audit, loadConfig, cleanup };
352
+ } catch (error) {
353
+ cleanup();
354
+ throw error;
355
+ }
356
+ }
@@ -0,0 +1,43 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const { getSecret } = require('../../lib/keychain-cjs.cjs');
5
+
6
+ // Lightweight API-key lookup shared by pristine startup and the general config
7
+ // loader. This module deliberately has no mixdog-config.json dependency: asking
8
+ // for one provider probes only that provider's environment/keychain account.
9
+ export const AGENT_PROVIDER_ENV = Object.freeze({
10
+ openai: 'OPENAI_API_KEY',
11
+ anthropic: 'ANTHROPIC_API_KEY',
12
+ gemini: 'GEMINI_API_KEY',
13
+ deepseek: 'DEEPSEEK_API_KEY',
14
+ xai: 'XAI_API_KEY',
15
+ 'opencode-go': 'OPENCODE_API_KEY',
16
+ });
17
+
18
+ export const AGENT_PROVIDER_ENV_ALIASES = Object.freeze({
19
+ xai: ['GROK_API_KEY'],
20
+ });
21
+
22
+ function providerSecretEnv(provider) {
23
+ return `MIXDOG_AGENT_${String(provider || '').replace(/[.\s]+/g, '_').toUpperCase()}_APIKEY`;
24
+ }
25
+
26
+ export function getAgentApiKey(provider) {
27
+ const id = String(provider || '').trim();
28
+ const standardEnv = AGENT_PROVIDER_ENV[id];
29
+ if (standardEnv && process.env[standardEnv]) return process.env[standardEnv];
30
+ const mixdogEnv = providerSecretEnv(id);
31
+ if (process.env[mixdogEnv]) return process.env[mixdogEnv];
32
+ try {
33
+ const stored = getSecret(`agent.${id}.apiKey`);
34
+ if (stored) return stored;
35
+ } catch {
36
+ // A missing/locked keychain is equivalent to no stored key. Explicit
37
+ // environment aliases remain available as the final provider-local source.
38
+ }
39
+ for (const alias of AGENT_PROVIDER_ENV_ALIASES[id] || []) {
40
+ if (process.env[alias]) return process.env[alias];
41
+ }
42
+ return null;
43
+ }
@@ -0,0 +1,21 @@
1
+ const AUTH_BINDINGS = Symbol.for('mixdog.providerAuthBindings');
2
+
3
+ export function boundProviderAuthPath(provider) {
4
+ const bindings = globalThis[AUTH_BINDINGS];
5
+ const value = bindings && typeof bindings === 'object'
6
+ ? bindings[String(provider || '').trim()]
7
+ : null;
8
+ return typeof value === 'string' && value ? value : null;
9
+ }
10
+
11
+ export function replaceProviderAuthBindings(next = {}) {
12
+ const previous = globalThis[AUTH_BINDINGS];
13
+ globalThis[AUTH_BINDINGS] = Object.freeze({ ...next });
14
+ let restored = false;
15
+ return () => {
16
+ if (restored) return;
17
+ restored = true;
18
+ if (previous === undefined) delete globalThis[AUTH_BINDINGS];
19
+ else globalThis[AUTH_BINDINGS] = previous;
20
+ };
21
+ }
@@ -13,8 +13,8 @@
13
13
  * the child exits, reporting the resolved version on success.
14
14
  */
15
15
 
16
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
- import { dirname, join } from 'node:path';
16
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
17
+ import { basename, delimiter, dirname, join } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { spawn } from 'node:child_process';
20
20
  import { resolvePluginData } from './plugin-paths.mjs';
@@ -199,19 +199,49 @@ export async function checkLatestVersion({ force = false, dataDir } = {}) {
199
199
  * `-WindowStyle Hidden` style flags that antivirus heuristics flag).
200
200
  */
201
201
  export function npmCliJsPath() {
202
- const execDir = dirname(process.execPath);
203
- const candidates = [
204
- // Windows: npm ships beside node.exe.
205
- join(execDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
206
- // Unix layout: <prefix>/bin/node → <prefix>/lib/node_modules/npm.
207
- join(execDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
208
- ];
209
202
  // When launched via npm itself, npm_execpath is authoritative.
210
203
  const envPath = process.env.npm_execpath;
211
- if (envPath && /npm-cli\.js$/i.test(envPath)) candidates.unshift(envPath);
204
+ const candidates = envPath && /npm-cli\.js$/i.test(envPath) ? [envPath] : [];
205
+ const execDirs = [dirname(process.execPath)];
206
+ try {
207
+ const realExecDir = dirname(realpathSync(process.execPath));
208
+ if (!execDirs.includes(realExecDir)) execDirs.push(realExecDir);
209
+ } catch { /* retain the raw executable path */ }
210
+
211
+ for (const execDir of execDirs) {
212
+ // Windows: npm ships beside node.exe.
213
+ candidates.push(join(execDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'));
214
+ // Unix layout: <prefix>/bin/node → <prefix>/lib/node_modules/npm.
215
+ candidates.push(join(execDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
216
+ // Homebrew: <prefix>/bin/node → <prefix>/libexec/lib/node_modules/npm.
217
+ candidates.push(join(execDir, '..', 'libexec', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
218
+ }
212
219
  for (const candidate of candidates) {
213
220
  try { if (existsSync(candidate)) return candidate; } catch { /* keep looking */ }
214
221
  }
222
+
223
+ // Last resort: npm shims on PATH commonly resolve into npm's package bin dir.
224
+ const pathDirs = (process.env.PATH || process.env.Path || '').split(delimiter);
225
+ for (const pathDir of pathDirs) {
226
+ if (!pathDir) continue;
227
+ const npmPaths = process.platform === 'win32'
228
+ ? [join(pathDir, 'npm'), join(pathDir, 'npm.cmd')]
229
+ : [join(pathDir, 'npm')];
230
+ for (const npmPath of npmPaths) {
231
+ try {
232
+ if (!existsSync(npmPath)) continue;
233
+ const resolvedNpm = realpathSync(npmPath);
234
+ const npmBinDir = dirname(resolvedNpm);
235
+ if (
236
+ basename(npmBinDir) !== 'bin'
237
+ || basename(dirname(npmBinDir)) !== 'npm'
238
+ || basename(dirname(dirname(npmBinDir))) !== 'node_modules'
239
+ ) continue;
240
+ const cliJs = join(npmBinDir, 'npm-cli.js');
241
+ if (existsSync(cliJs)) return cliJs;
242
+ } catch { /* keep looking */ }
243
+ }
244
+ }
215
245
  return null;
216
246
  }
217
247