mixdog 0.9.45 → 0.9.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
- package/scripts/agent-model-liveness-test.mjs +618 -0
- package/scripts/agent-trace-io-test.mjs +68 -4
- package/scripts/bench-run.mjs +30 -3
- package/scripts/compact-pressure-test.mjs +187 -4
- package/scripts/compact-prior-context-flatten-test.mjs +252 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -6
- package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
- package/scripts/context-mcp-metering-test.mjs +75 -0
- package/scripts/explore-prompt-policy-test.mjs +15 -16
- package/scripts/explore-timeout-cancel-test.mjs +345 -0
- package/scripts/headless-pristine-execution-test.mjs +614 -0
- package/scripts/internal-comms-smoke.mjs +15 -6
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/memory-core-input-test.mjs +137 -0
- package/scripts/memory-rule-contract-test.mjs +5 -5
- package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
- package/scripts/parent-abort-link-test.mjs +22 -0
- package/scripts/provider-toolcall-test.mjs +22 -0
- package/scripts/reactive-compact-persist-smoke.mjs +8 -4
- package/scripts/session-bench.mjs +3 -70
- package/scripts/task-bench.mjs +0 -2
- package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
- package/scripts/tool-smoke.mjs +21 -21
- package/scripts/tool-tui-presentation-test.mjs +68 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
- package/src/app.mjs +28 -103
- package/src/cli.mjs +17 -13
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/headless-command.mjs +139 -0
- package/src/headless-role.mjs +121 -10
- package/src/help.mjs +4 -1
- package/src/rules/agent/00-common.md +3 -3
- package/src/rules/agent/00-core.md +8 -9
- package/src/rules/agent/20-skip-protocol.md +2 -3
- package/src/rules/agent/30-explorer.md +50 -56
- package/src/rules/agent/40-cycle1-agent.md +10 -12
- package/src/rules/agent/41-cycle2-agent.md +12 -9
- package/src/rules/agent/42-cycle3-agent.md +4 -6
- package/src/rules/lead/01-general.md +5 -6
- package/src/rules/lead/02-channels.md +1 -1
- package/src/rules/lead/lead-brief.md +14 -17
- package/src/rules/lead/lead-tool.md +3 -3
- package/src/rules/shared/01-tool.md +41 -43
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
- package/src/runtime/agent/orchestrator/config.mjs +96 -30
- package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/runtime/memory/index.mjs +0 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
- package/src/runtime/memory/lib/http-router.mjs +0 -193
- package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +5 -6
- package/src/runtime/shared/config.mjs +11 -34
- package/src/runtime/shared/pristine-execution-contract.json +75 -0
- package/src/runtime/shared/pristine-execution.mjs +356 -0
- package/src/runtime/shared/provider-api-key.mjs +43 -0
- package/src/runtime/shared/provider-auth-binding.mjs +21 -0
- package/src/session-runtime/context-status.mjs +61 -13
- package/src/session-runtime/mcp-glue.mjs +29 -2
- package/src/session-runtime/plugin-mcp.mjs +7 -0
- package/src/session-runtime/resource-api.mjs +38 -5
- package/src/session-runtime/runtime-core.mjs +5 -1
- package/src/session-runtime/session-turn-api.mjs +14 -2
- package/src/session-runtime/settings-api.mjs +5 -0
- package/src/session-runtime/tool-catalog.mjs +13 -2
- package/src/session-runtime/tool-defs.mjs +1 -3
- package/src/standalone/agent-task-status.mjs +50 -11
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/explore-tool.mjs +257 -49
- package/src/standalone/seeds.mjs +1 -0
- package/src/tui/App.jsx +23 -10
- package/src/tui/app/use-transcript-scroll.mjs +4 -3
- package/src/tui/app/use-transcript-window.mjs +12 -21
- package/src/tui/components/ContextPanel.jsx +19 -25
- package/src/tui/dist/index.mjs +77 -65
- package/src/tui/engine/agent-envelope.mjs +16 -5
- package/src/tui/engine/labels.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +21 -51
- package/src/workflows/solo/WORKFLOW.md +12 -17
- package/src/workflows/solo-review/WORKFLOW.md +0 -47
|
@@ -49,7 +49,7 @@ export function createQueryHandlers({
|
|
|
49
49
|
// Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
|
|
50
50
|
// chunk_root IS NULL) are inserted immediately by ingestTranscriptFile before
|
|
51
51
|
// cycle1 runs, so they always carry the freshest turns in the DB.
|
|
52
|
-
async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope, sessionId, terms } = {}) {
|
|
52
|
+
async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope, sessionId, terms, minHits: minHitsOverride } = {}) {
|
|
53
53
|
try {
|
|
54
54
|
// Composable WHERE assembly (mirrors retrieveEntries' filter semantics so
|
|
55
55
|
// raw and chunked legs stay in filter parity: projectScope AND sessionId
|
|
@@ -78,7 +78,9 @@ export function createQueryHandlers({
|
|
|
78
78
|
// into the page. 1-2 term queries keep single-hit contains semantics —
|
|
79
79
|
// short Korean queries are often exactly two meaningful tokens and a
|
|
80
80
|
// 2-of-2 requirement silently emptied the raw leg for them.
|
|
81
|
-
const minHits =
|
|
81
|
+
const minHits = Number.isFinite(Number(minHitsOverride))
|
|
82
|
+
? Math.max(1, Math.floor(Number(minHitsOverride)))
|
|
83
|
+
: (terms.length >= 3 ? 2 : 1)
|
|
82
84
|
where.push(`(${clauses.join(' + ')}) >= ${minHits}`)
|
|
83
85
|
}
|
|
84
86
|
params.push(hardLimit)
|
|
@@ -722,7 +724,7 @@ export function createQueryHandlers({
|
|
|
722
724
|
// Deterministic tie-breaker: equal MAX(ts) across sessions would let the
|
|
723
725
|
// LIMIT/OFFSET window skip or duplicate a session between offset pages
|
|
724
726
|
// without a stable secondary sort.
|
|
725
|
-
const sessSql = `SELECT session_id, MAX(ts) AS last_ts
|
|
727
|
+
const sessSql = `SELECT session_id, MIN(ts) AS first_ts, MAX(ts) AS last_ts
|
|
726
728
|
FROM entries
|
|
727
729
|
WHERE ${selWhere.join(' AND ')}
|
|
728
730
|
GROUP BY session_id
|
|
@@ -732,6 +734,7 @@ export function createQueryHandlers({
|
|
|
732
734
|
// 2) per selected session, fetch its newest rows (roots+members, sort by
|
|
733
735
|
// date) plus the fresh raw window, capped at PER_SESSION_ROW_CAP.
|
|
734
736
|
const allRows = []
|
|
737
|
+
const sessionMeta = new Map()
|
|
735
738
|
for (const s of sessRows) {
|
|
736
739
|
const sid = String(s?.session_id || '').trim()
|
|
737
740
|
if (!sid) continue
|
|
@@ -743,7 +746,24 @@ export function createQueryHandlers({
|
|
|
743
746
|
const sRows = await retrieveEntries(db, sf)
|
|
744
747
|
let merged = sRows
|
|
745
748
|
if (includeRaw) {
|
|
746
|
-
|
|
749
|
+
let rawRows
|
|
750
|
+
if (queryTerms.length > 0) {
|
|
751
|
+
// Keep a full unfiltered recency window for the newest-row floor,
|
|
752
|
+
// while separately retaining the deeper term-matched raw window.
|
|
753
|
+
const [recentRawRows, matchedRawRows] = await Promise.all([
|
|
754
|
+
readRawRowsInWindow(db, null, Date.now(), perSessionFetchCap, { projectScope, sessionId: sid, terms: [] }),
|
|
755
|
+
readRawRowsInWindow(db, null, Date.now(), perSessionFetchCap, { projectScope, sessionId: sid, terms: queryTerms, minHits: 1 }),
|
|
756
|
+
])
|
|
757
|
+
const rawIds = new Set()
|
|
758
|
+
rawRows = [...recentRawRows, ...matchedRawRows].filter((r) => {
|
|
759
|
+
const id = Number(r.id)
|
|
760
|
+
if (rawIds.has(id)) return false
|
|
761
|
+
rawIds.add(id)
|
|
762
|
+
return true
|
|
763
|
+
})
|
|
764
|
+
} else {
|
|
765
|
+
rawRows = await readRawRowsInWindow(db, null, Date.now(), perSessionFetchCap, { projectScope, sessionId: sid, terms: [] })
|
|
766
|
+
}
|
|
747
767
|
const seenIds = new Set(sRows.map((r) => Number(r.id)))
|
|
748
768
|
for (const r of sRows) if (Array.isArray(r.members)) for (const m of r.members) seenIds.add(Number(m.id))
|
|
749
769
|
// readRawRowsInWindow carries no category filter, so a category-
|
|
@@ -768,14 +788,37 @@ export function createQueryHandlers({
|
|
|
768
788
|
merged.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0) || (Number(b.id) || 0) - (Number(a.id) || 0))
|
|
769
789
|
}
|
|
770
790
|
}
|
|
791
|
+
const fetchedCount = merged.length
|
|
792
|
+
let queryFiltered = false
|
|
771
793
|
if (queryTerms.length > 0) {
|
|
772
|
-
|
|
794
|
+
const newestRows = merged.slice(0, 3)
|
|
795
|
+
const newestIds = new Set(newestRows.map((r) => r.id))
|
|
796
|
+
const matchedRows = merged.filter(matchesQueryTerms)
|
|
797
|
+
// A topic query must not obscure a session's latest activity: keep
|
|
798
|
+
// its three newest rows, then use term matches for the remaining
|
|
799
|
+
// display slots without duplicating rows already kept for recency.
|
|
800
|
+
merged = [...newestRows, ...matchedRows.filter((r) => !newestIds.has(r.id))]
|
|
801
|
+
// Only mark query filtering when its floor+match union actually
|
|
802
|
+
// excludes fetched rows. The later display cap is independent.
|
|
803
|
+
queryFiltered = merged.length < fetchedCount
|
|
773
804
|
}
|
|
774
805
|
merged = merged.slice(0, PER_SESSION_ROW_CAP)
|
|
806
|
+
sessionMeta.set(sid, {
|
|
807
|
+
minTs: Number(s.first_ts),
|
|
808
|
+
maxTs: Number(s.last_ts),
|
|
809
|
+
fetchedCount,
|
|
810
|
+
shownCount: merged.length,
|
|
811
|
+
queryFiltered,
|
|
812
|
+
})
|
|
775
813
|
for (const r of merged) allRows.push(r)
|
|
776
814
|
}
|
|
777
815
|
const _currentSessionHint = String(args?.currentSessionId || '').trim()
|
|
778
|
-
return { text: recallCapPrefix + renderSessionGroupedLines(allRows, {
|
|
816
|
+
return { text: recallCapPrefix + renderSessionGroupedLines(allRows, {
|
|
817
|
+
currentSessionId: _currentSessionHint,
|
|
818
|
+
recencyOrder: true,
|
|
819
|
+
spanHeaders: true,
|
|
820
|
+
sessionMeta,
|
|
821
|
+
}) }
|
|
779
822
|
}
|
|
780
823
|
|
|
781
824
|
const filters = { limit: limit + offset }
|
|
@@ -336,10 +336,16 @@ function spanHeaderSuffix(minTs, maxTs, n) {
|
|
|
336
336
|
// group's lines are globally ts-desc: without it a chunk root's members (stored
|
|
337
337
|
// ts-ASC) interleave with raw rows and invert the visible timeline within a
|
|
338
338
|
// session (e.g. 04:33 rendered above 04:41).
|
|
339
|
-
export function renderSessionGroupedLines(rows, { currentSessionId, recencyOrder = false, spanHeaders = false } = {}) {
|
|
340
|
-
|
|
339
|
+
export function renderSessionGroupedLines(rows, { currentSessionId, recencyOrder = false, spanHeaders = false, sessionMeta } = {}) {
|
|
340
|
+
const hasSessionMeta = spanHeaders && Number(sessionMeta?.size) > 0
|
|
341
|
+
if ((!rows || rows.length === 0) && !hasSessionMeta) return '(no results)'
|
|
341
342
|
const groups = new Map()
|
|
342
|
-
|
|
343
|
+
// Seed selected sessions so an entirely query-filtered session still
|
|
344
|
+
// reports its real activity span and filtering status.
|
|
345
|
+
if (hasSessionMeta) {
|
|
346
|
+
for (const sid of sessionMeta.keys()) groups.set(sid, [])
|
|
347
|
+
}
|
|
348
|
+
for (const r of rows || []) {
|
|
343
349
|
const sid = String(r?.session_id || '').trim()
|
|
344
350
|
const key = sid || '(no session)'
|
|
345
351
|
if (!groups.has(key)) groups.set(key, [])
|
|
@@ -356,11 +362,20 @@ export function renderSessionGroupedLines(rows, { currentSessionId, recencyOrder
|
|
|
356
362
|
for (const [sid, groupRows] of groups) {
|
|
357
363
|
const mark = current && sid === current ? ' (current)' : ''
|
|
358
364
|
const label = sid === '(no session)' ? sid : `session ${shortSessionLabel(sid)}`
|
|
365
|
+
const meta = sessionMeta?.get?.(sid)
|
|
359
366
|
const tsAll = collectGroupTs(groupRows)
|
|
360
|
-
const
|
|
361
|
-
|
|
367
|
+
const minTs = Number(meta?.minTs)
|
|
368
|
+
const maxTs = Number(meta?.maxTs)
|
|
369
|
+
const hasActivitySpan = Number.isFinite(minTs) && Number.isFinite(maxTs)
|
|
370
|
+
const suffix = hasActivitySpan
|
|
371
|
+
? ` ${spanHeaderSuffix(minTs, maxTs, groupRows.length)}`
|
|
372
|
+
: tsAll.length
|
|
373
|
+
? ` ${spanHeaderSuffix(Math.min(...tsAll), Math.max(...tsAll), groupRows.length)}`
|
|
362
374
|
: ` (${groupRows.length} entries)`
|
|
363
|
-
|
|
375
|
+
const filterNote = meta?.queryFiltered
|
|
376
|
+
? ` · query-filtered ${meta.shownCount}/${meta.fetchedCount} rows`
|
|
377
|
+
: ''
|
|
378
|
+
parts.push(`## ${label}${mark}${suffix}${filterNote}`)
|
|
364
379
|
const bodyStr = renderEntryLines(groupRows, { recencyOrder })
|
|
365
380
|
const bodyLines = bodyStr === '(no results)'
|
|
366
381
|
? []
|
|
@@ -8,20 +8,20 @@ export const TOOL_DEFS = [
|
|
|
8
8
|
name: 'memory',
|
|
9
9
|
title: 'Memory Cycle',
|
|
10
10
|
annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
11
|
-
description: 'Core-memory mutation and status
|
|
11
|
+
description: 'Core-memory mutation and status; use recall for retrieval. Store durable rules/preferences/facts, one compact ENGLISH clause each — never transient task state. add requires project_id+category+summary; edit works by id alone.',
|
|
12
12
|
inputSchema: {
|
|
13
13
|
type: 'object',
|
|
14
14
|
properties: {
|
|
15
15
|
action: { type: 'string', enum: ['core','status'], description: 'Operation.' },
|
|
16
16
|
op: { type: 'string', enum: ['add','edit','delete','list','candidates','promote','dismiss'], description: 'Mutation op. candidates/promote/dismiss drive core-memory proposal approval.' },
|
|
17
17
|
id: { type: 'number', description: 'Exact memory id.' },
|
|
18
|
-
element: { type: 'string', maxLength: 40, description: 'Memory key/title. Max 40 chars.' },
|
|
19
|
-
summary: { type: 'string', maxLength: 100, description: 'Memory content:
|
|
18
|
+
element: { type: 'string', maxLength: 40, description: 'Memory key/title. Defaults to the first 40 chars of summary. Max 40 chars.' },
|
|
19
|
+
summary: { type: 'string', maxLength: 100, description: 'Memory content: one short English clause, max 100 chars.' },
|
|
20
20
|
category: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'], description: 'Category.' },
|
|
21
21
|
status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
|
|
22
22
|
limit: { type: 'number', description: 'Max rows/items.' },
|
|
23
23
|
confirm: { type: 'string', description: 'Exact confirmation phrase for destructive actions.' },
|
|
24
|
-
project_id: { type: 'string', description: 'Core pool: common
|
|
24
|
+
project_id: { type: 'string', description: 'Core pool: explicit common or slug. Required for core add only (edit uses the id\'s stored pool); there is no default pool.' },
|
|
25
25
|
},
|
|
26
26
|
additionalProperties: false,
|
|
27
27
|
required: ['action'],
|
|
@@ -31,7 +31,7 @@ export const TOOL_DEFS = [
|
|
|
31
31
|
name: 'recall',
|
|
32
32
|
title: 'Recall',
|
|
33
33
|
annotations: { title: 'Recall', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
34
|
-
description: 'Retrieve stored memory/session history
|
|
34
|
+
description: 'Retrieve stored memory/session history for prior-work context (resumes, earlier decisions); not a reflexive pre-step. Query is topic/semantic, not regex; period selects the time window; id for exact follow-up.',
|
|
35
35
|
inputSchema: {
|
|
36
36
|
type: 'object',
|
|
37
37
|
properties: {
|
|
@@ -44,7 +44,6 @@ export const TOOL_DEFS = [
|
|
|
44
44
|
category: { anyOf: [{ type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, { type: 'array', items: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, minItems: 1 }], description: 'Category filter.' },
|
|
45
45
|
includeArchived: { type: 'boolean', description: 'Include archived entries.' },
|
|
46
46
|
sessionId: { type: 'string', description: 'Scoped session id.' },
|
|
47
|
-
session_id: { type: 'string', description: 'Alias for sessionId.' },
|
|
48
47
|
includeMembers: { type: 'boolean', description: 'Include chunk members in output; does not widen the search pool.' },
|
|
49
48
|
includeRaw: { type: 'boolean', description: 'Include unchunked raw/episode rows.' },
|
|
50
49
|
sessionOnly: { type: 'boolean', description: 'Search this session only.' },
|
|
@@ -14,6 +14,17 @@ import {
|
|
|
14
14
|
loadLatestMixdogConfigFromBackup,
|
|
15
15
|
hasUserDataInitMarker,
|
|
16
16
|
} from './user-data-guard.mjs'
|
|
17
|
+
import {
|
|
18
|
+
AGENT_PROVIDER_ENV,
|
|
19
|
+
AGENT_PROVIDER_ENV_ALIASES,
|
|
20
|
+
getAgentApiKey,
|
|
21
|
+
} from './provider-api-key.mjs'
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
AGENT_PROVIDER_ENV,
|
|
25
|
+
AGENT_PROVIDER_ENV_ALIASES,
|
|
26
|
+
getAgentApiKey,
|
|
27
|
+
}
|
|
17
28
|
|
|
18
29
|
const _require = createRequire(import.meta.url)
|
|
19
30
|
const { getSecret: _getSecret, setSecret: _setSecret, deleteSecret: _deleteSecret, hasSecret: _hasSecret } = _require('../../lib/keychain-cjs.cjs')
|
|
@@ -453,40 +464,6 @@ export function getWebhookAuthtoken() {
|
|
|
453
464
|
return _readSecret(SECRET_ACCOUNTS.webhookAuth)
|
|
454
465
|
}
|
|
455
466
|
|
|
456
|
-
// Standard provider env names take precedence so existing OPENAI_API_KEY-style
|
|
457
|
-
// exports keep working, then MIXDOG_AGENT_<P>_APIKEY, then the OS keychain.
|
|
458
|
-
// SSOT for agent API-key providers: setup-server.mjs and config-merge.mjs import
|
|
459
|
-
// this so UI key-presence detection uses the exact same predicate the runtime
|
|
460
|
-
// loads from. Add a provider here and every path picks it up.
|
|
461
|
-
export const AGENT_PROVIDER_ENV = Object.freeze({
|
|
462
|
-
openai: 'OPENAI_API_KEY', anthropic: 'ANTHROPIC_API_KEY', gemini: 'GEMINI_API_KEY',
|
|
463
|
-
deepseek: 'DEEPSEEK_API_KEY', xai: 'XAI_API_KEY', 'opencode-go': 'OPENCODE_API_KEY',
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
// Last-resort env aliases honored AFTER the standard env / MIXDOG_AGENT_* /
|
|
467
|
-
// keychain sources. GROK_API_KEY is the established xAI alias elsewhere in the
|
|
468
|
-
// repo (search discovery, xai-api/grok-oauth backends), so honoring it here
|
|
469
|
-
// keeps provider discovery and dispatch resolving the same credential.
|
|
470
|
-
export const AGENT_PROVIDER_ENV_ALIASES = Object.freeze({
|
|
471
|
-
xai: ['GROK_API_KEY'],
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Returns the API key for an agent provider.
|
|
476
|
-
* Priority: <PROVIDER>_API_KEY env -> MIXDOG_AGENT_<PROVIDER>_APIKEY -> keychain('agent.<provider>.apiKey') -> alias env (e.g. GROK_API_KEY for xai) -> null.
|
|
477
|
-
* Never reads mixdog-config.json — provider keys are keychain-only.
|
|
478
|
-
*/
|
|
479
|
-
export function getAgentApiKey(provider) {
|
|
480
|
-
const std = AGENT_PROVIDER_ENV[provider]
|
|
481
|
-
if (std && process.env[std]) return process.env[std]
|
|
482
|
-
const fromStore = _readSecret(SECRET_ACCOUNTS.agentApiKey(provider))
|
|
483
|
-
if (fromStore) return fromStore
|
|
484
|
-
for (const alias of AGENT_PROVIDER_ENV_ALIASES[provider] || []) {
|
|
485
|
-
if (process.env[alias]) return process.env[alias]
|
|
486
|
-
}
|
|
487
|
-
return null
|
|
488
|
-
}
|
|
489
|
-
|
|
490
467
|
export function getOpenAIUsageSessionKey() {
|
|
491
468
|
return process.env.OPENAI_USAGE_SESSION_KEY
|
|
492
469
|
|| process.env.OPENAI_DASHBOARD_SESSION_KEY
|
|
@@ -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
|
+
}
|