mixdog 1.2.4 → 1.2.6
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 +1 -1
- package/scripts/lib/gate-scheduler.mjs +2 -2
- package/scripts/lib/run-node-tests.mjs +5 -14
- package/scripts/mobile-release.mjs +5 -4
- package/scripts/provider-pingpong-live.mjs +9 -9
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +2 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +11 -0
- package/src/defaults/skills/browser-use/SKILL.md +2 -1
- package/src/defaults/skills/computer-use/SKILL.md +3 -1
- package/src/defaults/skills/visualize/SKILL.md +3 -2
- package/src/lib/rules-builder.cjs +1 -1
- package/src/output-styles/common.md +22 -5
- package/src/output-styles/detailed.md +7 -5
- package/src/output-styles/minimal.md +7 -3
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/AGENT.md +3 -1
- package/src/rules/lead/LEAD.md +10 -7
- package/src/rules/shared/10-tool-workflow.md +2 -1
- package/src/rules/shared/60-verification.md +4 -5
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
- package/src/runtime/agent/orchestrator/config.mjs +6 -0
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
- package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
- package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
- package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
- package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
- package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
- package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
- package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +255 -32
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
- package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
- package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
- package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/attachments/office-extract.mjs +11 -9
- package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
- package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
- package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
- package/src/runtime/office/core/tabular.mjs +1 -1
- package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
- package/src/runtime/office/portable/docx-formatting.mjs +1 -1
- package/src/runtime/office/portable/docx-runs.mjs +1 -1
- package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
- package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
- package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
- package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
- package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
- package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
- package/src/runtime/office/portable/portable-opc.mjs +27 -13
- package/src/runtime/office/portable/portable-pivot.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
- package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
- package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
- package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
- package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
- package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
- package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
- package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
- package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
- package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
- package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
- package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
- package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
- package/src/runtime/office/portable/portable-xml.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +22 -13
- package/src/runtime/shared/background-tasks.mjs +60 -20
- package/src/runtime/shared/child-spawn-gate.mjs +26 -16
- package/src/runtime/shared/config.mjs +0 -10
- package/src/runtime/shared/file-lock.mjs +7 -1
- package/src/runtime/shared/file-permissions-worker.mjs +38 -0
- package/src/runtime/shared/file-permissions.d.mts +19 -4
- package/src/runtime/shared/file-permissions.mjs +310 -4
- package/src/runtime/shared/git-runner-worker.mjs +6 -78
- package/src/runtime/shared/llm/http-agent.mjs +7 -3
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
- package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
- package/src/runtime/shared/memory-snapshot.mjs +5 -1
- package/src/runtime/shared/notify-trace.mjs +8 -1
- package/src/runtime/shared/pristine-execution-contract.json +5 -1
- package/src/runtime/shared/process-runner-worker.mjs +7 -0
- package/src/runtime/shared/process-runner.mjs +38 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -1
- package/src/runtime/shared/provider-api-key.mjs +0 -1
- package/src/runtime/shared/read-row-numbers.mjs +1 -1
- package/src/runtime/shared/resource-admission.mjs +60 -18
- package/src/runtime/shared/run-child.mjs +84 -0
- package/src/runtime/shared/session-activity-signal.mjs +23 -0
- package/src/runtime/shared/shell-output-summary.mjs +23 -0
- package/src/runtime/shared/singleton-owner.mjs +126 -42
- package/src/runtime/shared/tool-output-limit.mjs +4 -2
- package/src/runtime/shared/tool-output-marker.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +28 -9
- package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
- package/src/runtime/shared/worktrees/git-head.mjs +20 -2
- package/src/runtime/shared/worktrees/lock.mjs +3 -2
- package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
- package/src/runtime/tidy/runners/biome.mjs +3 -1
- package/src/runtime/tidy/shadow.mjs +2 -1
- package/src/runtime/web-search/index.mjs +5 -7
- package/src/runtime/web-search/lib/formatter.mjs +42 -6
- package/src/runtime/web-search/tool-defs.mjs +6 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
- package/src/session-runtime/lifecycle/shared.mjs +19 -2
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
- package/src/session-runtime/lifecycle/teardown.mjs +5 -2
- package/src/session-runtime/lifecycle-api.mjs +3 -1
- package/src/session-runtime/provider-auth/login.mjs +10 -6
- package/src/session-runtime/provider-auth/setup.mjs +19 -2
- package/src/session-runtime/provider-catalog-cache.mjs +101 -25
- package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
- package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
- package/src/session-runtime/provider-models.mjs +76 -10
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
- package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
- package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
- package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
- package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
- package/src/session-runtime/services/agent-tool.mjs +20 -0
- package/src/session-runtime/services/provider-admin.mjs +92 -20
- package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
- package/src/session-runtime/services/usage-dashboard.mjs +27 -69
- package/src/session-runtime/turn/session-ops.mjs +2 -2
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/usage-stats-api.mjs +6 -1
- package/src/standalone/channel-transport/http-routes.mjs +10 -9
- package/src/standalone/daemon-desktop-runtime.mjs +20 -7
- package/src/standalone/daemon-owner.mjs +117 -25
- package/src/standalone/daemon.mjs +43 -13
- package/src/standalone/desktop-service-registry.mjs +32 -5
- package/src/standalone/session-call-cache.mjs +6 -0
- package/src/standalone/session-protocol.mjs +1 -1
- package/src/standalone/session-service/retention/busy.mjs +7 -2
- package/src/standalone/session-transport/routes.mjs +7 -6
- package/src/tui/app/model-options.mjs +39 -2
- package/src/tui/app/model-picker.mjs +16 -2
- package/src/tui/app/provider-setup-picker.mjs +1 -2
- package/src/tui/dist/index.mjs +436 -123
- package/src/tui/session/notice-text.mjs +0 -1
- package/src/tui/session/session-api/intake/submission.mjs +7 -13
- package/src/tui/session/session-api/integrations.mjs +1 -5
- package/src/tui/session/session-flow/drain.mjs +8 -0
- package/src/tui/session/session-flow/steering.mjs +46 -0
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/turn-stream.mjs +33 -0
- package/src/tui/session/turn.mjs +30 -3
- package/src/ui/statusline.mjs +1 -6
- package/src/ui/streaming-markdown-heal.mjs +94 -25
- package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
- package/src/workflows/default/WORKFLOW.md +9 -13
|
@@ -1,11 +1,288 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { execFile, execFileSync } from 'node:child_process';
|
|
4
|
-
import {
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { execFile, execFileSync, spawn } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { MessageChannel, receiveMessageOnPort, SHARE_ENV, Worker } from 'node:worker_threads';
|
|
5
6
|
|
|
6
|
-
const execFileAsync = promisify(execFile);
|
|
7
7
|
let cachedUserSid = null;
|
|
8
8
|
|
|
9
|
+
// Resolved per call (not promisified once at load) so the live binding of
|
|
10
|
+
// child_process.execFile is used.
|
|
11
|
+
function execFileAsync(file, args, options) {
|
|
12
|
+
return new Promise((done, fail) => {
|
|
13
|
+
execFile(file, args, options, (error, stdout, stderr) => {
|
|
14
|
+
if (error) fail(Object.assign(error, { stdout, stderr }));
|
|
15
|
+
else done({ stdout, stderr });
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Securing a published secret by spawning PowerShell costs ~150-200 ms per
|
|
21
|
+
// call, and secrets are read far more often than they change. Nothing is
|
|
22
|
+
// cached: every non-fresh async call still reads the file's real DACL and
|
|
23
|
+
// tightens it when it is not owner-only. File metadata (mtime/ctime/size/id)
|
|
24
|
+
// is never treated as evidence of permissions. To make that per-call check
|
|
25
|
+
// cheap, one long-lived PowerShell helper per process answers requests over
|
|
26
|
+
// stdin/stdout ("<id> <base64 path>" in, "<id> ok|fixed|err <base64 detail>"
|
|
27
|
+
// out); concurrent callers share it and are matched by request id. If the
|
|
28
|
+
// helper cannot start, crashes, closes its pipes or misses the request
|
|
29
|
+
// deadline, every request it held (and later ones, once the restart budget is
|
|
30
|
+
// spent) falls back to the spawn-per-call update, so a lost helper never
|
|
31
|
+
// skips a check. A definite ACL failure reported by the helper is an error,
|
|
32
|
+
// exactly like a failed per-call update.
|
|
33
|
+
const ACL_HELPER_REQUEST_TIMEOUT_MS = 15_000;
|
|
34
|
+
// At most this many helper starts per window. A lifetime budget let three
|
|
35
|
+
// unrelated losses (a request deadline missed across a system sleep, an
|
|
36
|
+
// antivirus stall) pin a days-old process to a PowerShell spawn per read.
|
|
37
|
+
const ACL_HELPER_MAX_STARTS = 3;
|
|
38
|
+
const ACL_HELPER_START_WINDOW_MS = 10 * 60_000;
|
|
39
|
+
let aclHelper = null;
|
|
40
|
+
let aclHelperStartTimes = [];
|
|
41
|
+
let aclHelperSeq = 0;
|
|
42
|
+
|
|
43
|
+
function aclHelperScript(sid) {
|
|
44
|
+
return [
|
|
45
|
+
"$ErrorActionPreference = 'Stop';",
|
|
46
|
+
`$sid = [System.Security.Principal.SecurityIdentifier]::new(${quoteForPowerShell(sid)});`,
|
|
47
|
+
'$full = [System.Security.AccessControl.FileSystemRights]::FullControl;',
|
|
48
|
+
'$utf8 = [System.Text.Encoding]::UTF8;',
|
|
49
|
+
'$out = [Console]::Out;',
|
|
50
|
+
'while ($null -ne ($line = [Console]::In.ReadLine())) {',
|
|
51
|
+
'$parts = $line.Split(@([char]32), 2);',
|
|
52
|
+
'if ($parts.Count -ne 2) { continue };',
|
|
53
|
+
'try {',
|
|
54
|
+
'$path = $utf8.GetString([Convert]::FromBase64String($parts[1]));',
|
|
55
|
+
'$directory = [System.IO.Directory]::Exists($path);',
|
|
56
|
+
'$current = if ($directory) { [System.IO.Directory]::GetAccessControl($path) }',
|
|
57
|
+
'else { [System.IO.File]::GetAccessControl($path) };',
|
|
58
|
+
'$rules = @($current.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]));',
|
|
59
|
+
'$ownerOnly = $current.AreAccessRulesProtected -and $rules.Count -eq 1 -and',
|
|
60
|
+
"$rules[0].AccessControlType -eq 'Allow' -and $rules[0].IdentityReference -eq $sid -and",
|
|
61
|
+
'$rules[0].FileSystemRights -eq $full;',
|
|
62
|
+
"if ($ownerOnly) { $out.WriteLine($parts[0] + ' ok') } else {",
|
|
63
|
+
'$acl = if ($directory) { [System.Security.AccessControl.DirectorySecurity]::new() }',
|
|
64
|
+
'else { [System.Security.AccessControl.FileSecurity]::new() };',
|
|
65
|
+
'$acl.SetAccessRuleProtection($true, $false);',
|
|
66
|
+
'$acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(',
|
|
67
|
+
'$sid, $full, [System.Security.AccessControl.AccessControlType]::Allow));',
|
|
68
|
+
'if ($directory) { [System.IO.Directory]::SetAccessControl($path, $acl) }',
|
|
69
|
+
'else { [System.IO.File]::SetAccessControl($path, $acl) };',
|
|
70
|
+
"$out.WriteLine($parts[0] + ' fixed') }",
|
|
71
|
+
'} catch {',
|
|
72
|
+
"$out.WriteLine($parts[0] + ' err ' + [Convert]::ToBase64String($utf8.GetBytes([string]$_.Exception.Message)))",
|
|
73
|
+
'};',
|
|
74
|
+
'$out.Flush() }',
|
|
75
|
+
].join(' ');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function startAclHelper(icacls, sid) {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
aclHelperStartTimes = aclHelperStartTimes.filter((at) => now - at < ACL_HELPER_START_WINDOW_MS);
|
|
81
|
+
if (aclHelperStartTimes.length >= ACL_HELPER_MAX_STARTS) return null;
|
|
82
|
+
aclHelperStartTimes.push(now);
|
|
83
|
+
const command = powerShellCommand(icacls, aclHelperScript(sid));
|
|
84
|
+
let child;
|
|
85
|
+
try {
|
|
86
|
+
// Long-lived: run from System32 so it never pins the caller's working
|
|
87
|
+
// directory (a folder a process's cwd sits in cannot be removed).
|
|
88
|
+
child = spawn(command.executable, command.args, {
|
|
89
|
+
cwd: dirname(icacls),
|
|
90
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const helper = { child, pending: new Map(), buffer: '', dead: false };
|
|
97
|
+
helper.fail = (cause) => {
|
|
98
|
+
if (aclHelper === helper) aclHelper = null;
|
|
99
|
+
if (helper.dead) return;
|
|
100
|
+
helper.dead = true;
|
|
101
|
+
const lost = [...helper.pending.values()];
|
|
102
|
+
helper.pending.clear();
|
|
103
|
+
for (const request of lost) request.lost(cause);
|
|
104
|
+
try {
|
|
105
|
+
child.kill();
|
|
106
|
+
} catch {}
|
|
107
|
+
};
|
|
108
|
+
child.on('error', helper.fail);
|
|
109
|
+
child.on('exit', () => helper.fail(new Error('Windows ACL helper exited')));
|
|
110
|
+
child.stdin.on('error', helper.fail);
|
|
111
|
+
child.stdout.on('error', helper.fail);
|
|
112
|
+
child.stdout.on('close', () => helper.fail(new Error('Windows ACL helper closed its output')));
|
|
113
|
+
child.stdout.setEncoding('utf8');
|
|
114
|
+
child.stdout.on('data', (chunk) => {
|
|
115
|
+
helper.buffer += chunk;
|
|
116
|
+
let newline = helper.buffer.indexOf('\n');
|
|
117
|
+
while (newline >= 0) {
|
|
118
|
+
const [id, status, detail = ''] = helper.buffer.slice(0, newline).trim().split(' ');
|
|
119
|
+
helper.buffer = helper.buffer.slice(newline + 1);
|
|
120
|
+
newline = helper.buffer.indexOf('\n');
|
|
121
|
+
const request = helper.pending.get(id);
|
|
122
|
+
if (!request) continue;
|
|
123
|
+
helper.pending.delete(id);
|
|
124
|
+
syncAclHelperRef(helper);
|
|
125
|
+
request.answer(status, Buffer.from(detail, 'base64').toString('utf8'));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
syncAclHelperRef(helper);
|
|
129
|
+
return helper;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// An idle helper never keeps the process alive; while a request awaits its
|
|
133
|
+
// answer the helper's handles do, so the answer is delivered even when the
|
|
134
|
+
// request's deadline timer is mocked or otherwise not a live handle.
|
|
135
|
+
function syncAclHelperRef(helper) {
|
|
136
|
+
const method = helper.pending.size > 0 ? 'ref' : 'unref';
|
|
137
|
+
helper.child[method]();
|
|
138
|
+
helper.child.stdin[method]?.();
|
|
139
|
+
helper.child.stdout[method]?.();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Resolves `{ status, detail }` from the helper, or `{ lost }` when it could not answer. */
|
|
143
|
+
function askAclHelper(targetPath, icacls, sid) {
|
|
144
|
+
if (!aclHelper) aclHelper = startAclHelper(icacls, sid);
|
|
145
|
+
const helper = aclHelper;
|
|
146
|
+
if (!helper) return Promise.resolve({ lost: new Error('Windows ACL helper unavailable') });
|
|
147
|
+
return new Promise((done) => {
|
|
148
|
+
aclHelperSeq += 1;
|
|
149
|
+
const id = String(aclHelperSeq);
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
helper.pending.delete(id);
|
|
152
|
+
syncAclHelperRef(helper);
|
|
153
|
+
done({ lost: new Error('Windows ACL helper timed out') });
|
|
154
|
+
helper.fail(new Error('Windows ACL helper timed out'));
|
|
155
|
+
}, ACL_HELPER_REQUEST_TIMEOUT_MS);
|
|
156
|
+
helper.pending.set(id, {
|
|
157
|
+
answer(status, detail) {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
done({ status, detail });
|
|
160
|
+
},
|
|
161
|
+
lost(cause) {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
done({ lost: cause });
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
syncAclHelperRef(helper);
|
|
167
|
+
helper.child.stdin.write(`${id} ${Buffer.from(targetPath, 'utf8').toString('base64')}\n`);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Sync bridge ─────────────────────────────────────────────────────────
|
|
172
|
+
// Synchronous secret writes (token stores, config) cannot await the helper,
|
|
173
|
+
// and a blocking icacls spawn per write held the event loop (and the write's
|
|
174
|
+
// file lock) for seconds under load. A worker thread owns a second helper;
|
|
175
|
+
// a synchronous caller posts its path and blocks in Atomics.wait only for
|
|
176
|
+
// the answer — milliseconds once the helper runs (prewarmOwnerOnlyAclWin32
|
|
177
|
+
// starts it off the write path). Shared slots: [0] answer counter, [1] the
|
|
178
|
+
// worker exited, [2] the worker is ready. A worker not ready within
|
|
179
|
+
// ACL_SYNC_BRIDGE_READY_MS, or gone, sends the caller to the one-shot
|
|
180
|
+
// update; an answer later than ACL_SYNC_BRIDGE_TIMEOUT_MS fails the write.
|
|
181
|
+
const ACL_SYNC_BRIDGE_READY_MS = 10_000;
|
|
182
|
+
// The helper's own deadline plus its one-shot fallback's, with margin.
|
|
183
|
+
const ACL_SYNC_BRIDGE_TIMEOUT_MS = 2 * ACL_HELPER_REQUEST_TIMEOUT_MS + 5_000;
|
|
184
|
+
let aclSyncBridge = null;
|
|
185
|
+
let aclSyncBridgeBroken = false;
|
|
186
|
+
|
|
187
|
+
function syncBridge() {
|
|
188
|
+
if (aclSyncBridge || aclSyncBridgeBroken) return aclSyncBridge;
|
|
189
|
+
try {
|
|
190
|
+
const url = new URL('./file-permissions-worker.mjs', import.meta.url);
|
|
191
|
+
// Bundled copies (the desktop service) ship no worker beside them.
|
|
192
|
+
if (!existsSync(fileURLToPath(url))) throw new Error('Windows ACL bridge worker is not shipped here');
|
|
193
|
+
const { port1, port2 } = new MessageChannel();
|
|
194
|
+
const signal = new Int32Array(new SharedArrayBuffer(12));
|
|
195
|
+
const worker = new Worker(url, {
|
|
196
|
+
env: SHARE_ENV,
|
|
197
|
+
execArgv: [],
|
|
198
|
+
workerData: { port: port2, signal },
|
|
199
|
+
transferList: [port2],
|
|
200
|
+
});
|
|
201
|
+
const bridge = { worker, port: port1, signal, seq: 0, startedAt: Date.now() };
|
|
202
|
+
const drop = () => {
|
|
203
|
+
if (aclSyncBridge === bridge) aclSyncBridge = null;
|
|
204
|
+
};
|
|
205
|
+
worker.on('error', drop);
|
|
206
|
+
worker.on('exit', drop);
|
|
207
|
+
worker.unref();
|
|
208
|
+
port1.unref?.();
|
|
209
|
+
aclSyncBridge = bridge;
|
|
210
|
+
} catch {
|
|
211
|
+
aclSyncBridgeBroken = true;
|
|
212
|
+
}
|
|
213
|
+
return aclSyncBridge;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function dropSyncBridge(bridge) {
|
|
217
|
+
if (aclSyncBridge !== bridge) return;
|
|
218
|
+
aclSyncBridge = null;
|
|
219
|
+
void bridge.worker.terminate();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** `{ ok: true }`, `{ ok: false, message, code }` (a definite ACL failure),
|
|
223
|
+
* `{ timedOut: true }`, or `{ lost }` when no bridge could answer. */
|
|
224
|
+
function askSyncBridge(targetPath) {
|
|
225
|
+
const bridge = syncBridge();
|
|
226
|
+
if (!bridge) return { lost: new Error('Windows ACL bridge unavailable') };
|
|
227
|
+
bridge.seq += 1;
|
|
228
|
+
const id = bridge.seq;
|
|
229
|
+
let seen = Atomics.load(bridge.signal, 0);
|
|
230
|
+
bridge.port.postMessage({ id, path: targetPath });
|
|
231
|
+
const deadline = Date.now() + ACL_SYNC_BRIDGE_TIMEOUT_MS;
|
|
232
|
+
for (;;) {
|
|
233
|
+
for (let received = receiveMessageOnPort(bridge.port); received; received = receiveMessageOnPort(bridge.port)) {
|
|
234
|
+
if (received.message?.id === id) return received.message;
|
|
235
|
+
}
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
const ready = Atomics.load(bridge.signal, 2) === 1;
|
|
238
|
+
if (Atomics.load(bridge.signal, 1) === 1 || (!ready && now - bridge.startedAt >= ACL_SYNC_BRIDGE_READY_MS)) {
|
|
239
|
+
dropSyncBridge(bridge);
|
|
240
|
+
return { lost: new Error('Windows ACL bridge exited') };
|
|
241
|
+
}
|
|
242
|
+
if (now >= deadline) return { timedOut: true };
|
|
243
|
+
const wait = ready ? deadline - now : Math.min(deadline - now, bridge.startedAt + ACL_SYNC_BRIDGE_READY_MS - now);
|
|
244
|
+
Atomics.wait(bridge.signal, 0, seen, Math.max(1, wait));
|
|
245
|
+
seen = Atomics.load(bridge.signal, 0);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Test hook: synchronous calls skip the bridge (its one-shot fallback runs). */
|
|
250
|
+
export function disableOwnerOnlyAclSyncBridgeForTests() {
|
|
251
|
+
aclSyncBridgeBroken = true;
|
|
252
|
+
if (aclSyncBridge) dropSyncBridge(aclSyncBridge);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Starts, off any write path, what owner-only enforcement needs on Windows:
|
|
256
|
+
* this thread's SID and helper, and the sync bridge with its own helper.
|
|
257
|
+
* Best-effort; never rejects. */
|
|
258
|
+
export async function prewarmOwnerOnlyAclWin32({ bridge = true } = {}) {
|
|
259
|
+
if (process.platform !== 'win32') return;
|
|
260
|
+
try {
|
|
261
|
+
const icacls = icaclsPath();
|
|
262
|
+
if (cachedUserSid === null) cachedUserSid = await resolveCurrentUserPrincipalAsync();
|
|
263
|
+
if (!aclHelper) aclHelper = startAclHelper(icacls, cachedUserSid);
|
|
264
|
+
if (bridge) syncBridge()?.port.postMessage({ warm: true });
|
|
265
|
+
} catch {
|
|
266
|
+
/* enforcement itself reports any failure */
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Test/diagnostic hook: whether a helper is attached, its requests awaiting
|
|
271
|
+
* an answer, and the starts inside the current restart window. */
|
|
272
|
+
export function ownerOnlyAclHelperStats() {
|
|
273
|
+
return {
|
|
274
|
+
alive: aclHelper !== null,
|
|
275
|
+
pid: aclHelper?.child.pid ?? null,
|
|
276
|
+
pending: aclHelper?.pending.size ?? 0,
|
|
277
|
+
recentStarts: aclHelperStartTimes.length,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Test hook: the live helper process, or null. */
|
|
282
|
+
export function ownerOnlyAclHelperForTests() {
|
|
283
|
+
return aclHelper?.child ?? null;
|
|
284
|
+
}
|
|
285
|
+
|
|
9
286
|
function systemToolPath(name) {
|
|
10
287
|
const systemRoot = process.env.SystemRoot || process.env.windir;
|
|
11
288
|
return systemRoot ? join(systemRoot, 'System32', name) : null;
|
|
@@ -155,6 +432,18 @@ function aclFailure(targetPath, cause, label = 'icacls') {
|
|
|
155
432
|
export function enforceOwnerOnlyAclWin32(targetPath, { fresh = false } = {}) {
|
|
156
433
|
if (process.platform !== 'win32') return;
|
|
157
434
|
const icacls = icaclsPath();
|
|
435
|
+
// Fresh files (the temp and lock files of every synchronous secret write,
|
|
436
|
+
// made while its file lock is held) go to the helper through the sync
|
|
437
|
+
// bridge: one round trip, no process spawned. Only a bridge that is gone
|
|
438
|
+
// falls back to the blocking one-shot update below.
|
|
439
|
+
if (fresh) {
|
|
440
|
+
const reply = askSyncBridge(resolve(targetPath));
|
|
441
|
+
if (reply.ok === true) return;
|
|
442
|
+
if (reply.ok === false) throw Object.assign(new Error(reply.message), { code: reply.code || 'EACLFAIL' });
|
|
443
|
+
if (reply.timedOut) {
|
|
444
|
+
throw aclFailure(targetPath, new Error('the Windows ACL bridge did not answer'), 'Windows ACL helper');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
158
447
|
if (cachedUserSid === null) cachedUserSid = resolveCurrentUserPrincipal();
|
|
159
448
|
const command = ownerOnlyAclCommand(targetPath, cachedUserSid, fresh, icacls);
|
|
160
449
|
try {
|
|
@@ -168,8 +457,25 @@ export function enforceOwnerOnlyAclWin32(targetPath, { fresh = false } = {}) {
|
|
|
168
457
|
}
|
|
169
458
|
}
|
|
170
459
|
|
|
460
|
+
// Fresh files (unpublished temp/lock files, one or two per secret write) go
|
|
461
|
+
// through the helper too: never owner-only yet, each is secured explicitly by
|
|
462
|
+
// its DACL replacement. An icacls spawn per write, made inside the write's
|
|
463
|
+
// file lock, kept provider-accounts.json locked for 114 s under heavy load
|
|
464
|
+
// (each spawn blocked the event loop for seconds as well).
|
|
171
465
|
export async function enforceOwnerOnlyAclWin32Async(targetPath, { fresh = false } = {}) {
|
|
172
466
|
if (process.platform !== 'win32') return;
|
|
467
|
+
const icacls = icaclsPath();
|
|
468
|
+
if (cachedUserSid === null) cachedUserSid = await resolveCurrentUserPrincipalAsync();
|
|
469
|
+
const reply = await askAclHelper(resolve(targetPath), icacls, cachedUserSid);
|
|
470
|
+
if (reply.status === 'ok' || reply.status === 'fixed') return;
|
|
471
|
+
if (reply.status === 'err') {
|
|
472
|
+
throw aclFailure(targetPath, new Error(reply.detail || 'unknown error'), 'Windows ACL helper');
|
|
473
|
+
}
|
|
474
|
+
// The helper could not answer: verify with a one-shot update instead.
|
|
475
|
+
return applyOwnerOnlyAclAsync(targetPath, fresh);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function applyOwnerOnlyAclAsync(targetPath, fresh) {
|
|
173
479
|
const icacls = icaclsPath();
|
|
174
480
|
if (cachedUserSid === null) cachedUserSid = await resolveCurrentUserPrincipalAsync();
|
|
175
481
|
const command = ownerOnlyAclCommand(targetPath, cachedUserSid, fresh, icacls);
|
|
@@ -1,80 +1,8 @@
|
|
|
1
|
-
// Worker-thread body for git-runner.mjs
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
// Worker-thread body for git-runner.mjs: git children are spawned on this
|
|
2
|
+
// thread, never on the host event loop (run-child.mjs). Each request resolves
|
|
3
|
+
// to { code, stdout, stderr } or an error with the same codes the in-process
|
|
4
|
+
// runner used (ETIMEDOUT, EMAXBUFFER, spawn errors).
|
|
5
|
+
import { runChild } from './run-child.mjs';
|
|
7
6
|
import { serveWorkerRequests } from './worker-requests.mjs';
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
11
|
-
const hasInput = (typeof input === 'string' || ArrayBuffer.isView(input)) && input.length > 0;
|
|
12
|
-
const child = spawn('git', args, {
|
|
13
|
-
cwd,
|
|
14
|
-
...(env ? { env } : {}),
|
|
15
|
-
windowsHide: true,
|
|
16
|
-
stdio: [hasInput ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
17
|
-
});
|
|
18
|
-
const stdout = [];
|
|
19
|
-
const stderr = [];
|
|
20
|
-
let outputBytes = 0;
|
|
21
|
-
let stdinError = null;
|
|
22
|
-
let settled = false;
|
|
23
|
-
const finish = (error, result) => {
|
|
24
|
-
if (settled) return;
|
|
25
|
-
settled = true;
|
|
26
|
-
clearTimeout(timer);
|
|
27
|
-
if (error) rejectPromise(error);
|
|
28
|
-
else resolvePromise(result);
|
|
29
|
-
};
|
|
30
|
-
const timer = setTimeout(() => {
|
|
31
|
-
const error = new Error(`git ${args.join(' ')} timed out after ${timeoutMs}ms`);
|
|
32
|
-
error.code = 'ETIMEDOUT';
|
|
33
|
-
try {
|
|
34
|
-
child.kill();
|
|
35
|
-
} catch {}
|
|
36
|
-
finish(error);
|
|
37
|
-
}, timeoutMs);
|
|
38
|
-
const collect = (chunks, chunk) => {
|
|
39
|
-
const next = Buffer.from(chunk);
|
|
40
|
-
outputBytes += next.length;
|
|
41
|
-
if (outputBytes > maxBytes) {
|
|
42
|
-
const error = new Error(`git ${args.join(' ')} exceeded ${maxBytes} output bytes`);
|
|
43
|
-
error.code = 'EMAXBUFFER';
|
|
44
|
-
try {
|
|
45
|
-
child.kill();
|
|
46
|
-
} catch {}
|
|
47
|
-
finish(error);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
chunks.push(next);
|
|
51
|
-
};
|
|
52
|
-
child.stdout.on('data', (chunk) => collect(stdout, chunk));
|
|
53
|
-
child.stderr.on('data', (chunk) => collect(stderr, chunk));
|
|
54
|
-
child.once('error', (error) => finish(error));
|
|
55
|
-
child.once('close', (code) => {
|
|
56
|
-
const result = {
|
|
57
|
-
code: Number(code ?? 1),
|
|
58
|
-
// 'base64' keeps non-UTF-8 bytes (a replayable patch) intact across the thread hop.
|
|
59
|
-
stdout: Buffer.concat(stdout).toString(stdoutEncoding === 'base64' ? 'base64' : 'utf8'),
|
|
60
|
-
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
61
|
-
};
|
|
62
|
-
// A failed stdin write only matters when git otherwise reported success.
|
|
63
|
-
if (stdinError && result.code === 0) {
|
|
64
|
-
finish(stdinError);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
finish(null, result);
|
|
68
|
-
});
|
|
69
|
-
if (child.stdin) {
|
|
70
|
-
// A short-lived git command can close before stdin is flushed. Keep
|
|
71
|
-
// that transport race inside this promise instead of an unhandled EPIPE.
|
|
72
|
-
child.stdin.on('error', (error) => {
|
|
73
|
-
stdinError = error;
|
|
74
|
-
});
|
|
75
|
-
child.stdin.end(input);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
serveWorkerRequests(runGit);
|
|
8
|
+
serveWorkerRequests((request) => runChild({ ...request, command: 'git' }));
|
|
@@ -75,8 +75,8 @@ function proxyConfigured() {
|
|
|
75
75
|
* The shared singleton dispatcher for LLM API requests. Created on first use.
|
|
76
76
|
*
|
|
77
77
|
* keepAliveTimeout (~60s) keeps a socket warm across the typical idle gap
|
|
78
|
-
* between agent turns;
|
|
79
|
-
*
|
|
78
|
+
* between agent turns; sockets per origin are unbounded unless
|
|
79
|
+
* MIXDOG_LLM_CONNECTIONS sets a cap.
|
|
80
80
|
*
|
|
81
81
|
* Returns `undefined` when a proxy / any custom (non-default) global dispatcher
|
|
82
82
|
* is configured, so `fetch({ dispatcher: undefined })` falls back to that
|
|
@@ -93,7 +93,11 @@ export function getLlmDispatcher() {
|
|
|
93
93
|
_agent = new (undici().Agent)({
|
|
94
94
|
keepAliveTimeout: envInt('MIXDOG_LLM_KEEPALIVE_MS', 60_000),
|
|
95
95
|
keepAliveMaxTimeout: envInt('MIXDOG_LLM_KEEPALIVE_MAX_MS', 90_000),
|
|
96
|
-
|
|
96
|
+
// No per-origin socket cap by default: each streaming (SSE) response
|
|
97
|
+
// holds its HTTP/1.1 socket for the whole generation, so a cap of N
|
|
98
|
+
// queued the (N+1)th concurrent agent until another finished. Rate
|
|
99
|
+
// limiting is the server's call (429 → admission cooldown).
|
|
100
|
+
connections: envInt('MIXDOG_LLM_CONNECTIONS', null),
|
|
97
101
|
// undici defaults both of these to 300s, which beheaded the Anthropic
|
|
98
102
|
// NON-STREAMING stall fallback (2026-08-03 regex-chess probe:
|
|
99
103
|
// UND_ERR_HEADERS_TIMEOUT at exactly 300s) — a non-streaming completion
|
|
@@ -93,7 +93,17 @@ const PROVIDER_CATALOGS = Object.freeze({
|
|
|
93
93
|
},
|
|
94
94
|
'cursor-oauth': { relay: true },
|
|
95
95
|
'cursor-api': { relay: true },
|
|
96
|
+
// The models.dev listing id of the Go gateway: a relay catalog, not a
|
|
97
|
+
// selectable provider (the model lookup needs it flagged as a relay).
|
|
96
98
|
'opencode-go': { relay: true },
|
|
99
|
+
// The Go gateway through an OpenCode Console login; the organization
|
|
100
|
+
// config's catalog is the live roster.
|
|
101
|
+
'opencode-go-oauth': {
|
|
102
|
+
modelsDev: 'opencode-go',
|
|
103
|
+
relay: true,
|
|
104
|
+
rosterFiles: ['opencode-go-oauth-models.json'],
|
|
105
|
+
rosterLimitsAuthoritative: true,
|
|
106
|
+
},
|
|
97
107
|
});
|
|
98
108
|
|
|
99
109
|
// Confirmed upstream catalog gaps, keyed provider → model. Each entry patches
|
|
@@ -71,6 +71,8 @@ export async function accountProviderSend(provider, instance, send, model, opts
|
|
|
71
71
|
requestedModel: model,
|
|
72
72
|
pricingModel: result.pricingModel,
|
|
73
73
|
sessionId,
|
|
74
|
+
// The session this one runs for (an agent's Lead), kept with its spend.
|
|
75
|
+
ownerSessionId: opts.session?.ownerSessionId || opts.session?.parentSessionId,
|
|
74
76
|
sourceType,
|
|
75
77
|
inputTokens: usage.inputTokens,
|
|
76
78
|
inputTokensKnown: usage.inputTokensKnown,
|
|
@@ -10,6 +10,18 @@ import { createHash } from 'node:crypto';
|
|
|
10
10
|
import { makeUsageRecord, usageRouteKind } from './usage-ledger.mjs';
|
|
11
11
|
import { priceUsage } from './cost.mjs';
|
|
12
12
|
|
|
13
|
+
function legacyRollupDays(parsed) {
|
|
14
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
15
|
+
throw new SyntaxError('legacy rollup root is not an object');
|
|
16
|
+
}
|
|
17
|
+
if (!Object.hasOwn(parsed, 'days')) throw new SyntaxError('legacy rollup days is missing');
|
|
18
|
+
const { days } = parsed;
|
|
19
|
+
// A day map is an object. An array is the other accepted collection; null,
|
|
20
|
+
// strings, and other scalars are not.
|
|
21
|
+
if (days === null || typeof days !== 'object') throw new SyntaxError('legacy rollup days is not an object or array');
|
|
22
|
+
return days;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
async function optionalFile(path) {
|
|
14
26
|
try {
|
|
15
27
|
return await readFile(path, 'utf8');
|
|
@@ -165,9 +177,26 @@ export async function importUsageHistory(ledger, dataDir, { now = Date.now(), re
|
|
|
165
177
|
session_id: event.sessionId,
|
|
166
178
|
}))
|
|
167
179
|
);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
// A missing file is absence. Empty, unparsable, or wrong-shaped JSON is not:
|
|
181
|
+
// skip only this legacy import and leave importedThrough unset so a repair
|
|
182
|
+
// is retried instead of short-circuiting forever.
|
|
183
|
+
const legacyPath = join(dataDir, 'usage-rollup.local.json');
|
|
184
|
+
const legacy = await optionalFile(legacyPath);
|
|
185
|
+
let legacyReady = true;
|
|
186
|
+
if (legacy !== null) {
|
|
187
|
+
let days;
|
|
188
|
+
try {
|
|
189
|
+
days = legacyRollupDays(JSON.parse(legacy));
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
192
|
+
legacyReady = false;
|
|
193
|
+
process.stderr.write(
|
|
194
|
+
`[usage-ledger] warning: legacy rollup ${legacyPath} is empty, corrupt, or the wrong shape (${error.message}); skipping its import\n`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (legacyReady) ledger.preserveLegacyDays(repriceRestoredDays(days));
|
|
198
|
+
}
|
|
199
|
+
if (legacyReady) ledger.set('importedThrough', until);
|
|
171
200
|
ledger.set('importReceipt', JSON.stringify({ ...result, until }));
|
|
172
201
|
return result;
|
|
173
202
|
}
|
|
@@ -292,20 +292,27 @@ function mergeLegacyDays(db, { days, hourly, hourlyDay, fromDay, toDay }) {
|
|
|
292
292
|
* Compaction requests run under their own provider session id but are
|
|
293
293
|
* recorded under the source session, so this is the whole conversation
|
|
294
294
|
* across compactions. The same route choice as a rollup applies: a gateway
|
|
295
|
-
* summary never doubles a detail record.
|
|
295
|
+
* summary never doubles a detail record. `ownerIds` adds every session
|
|
296
|
+
* recorded as run for one of them (a Lead's agents, theirs in turn).
|
|
296
297
|
*/
|
|
297
|
-
export function readSessionUsage(db, sessionIds) {
|
|
298
|
+
export function readSessionUsage(db, sessionIds, ownerIds = []) {
|
|
298
299
|
const total = empty();
|
|
299
300
|
const rows = db
|
|
300
301
|
.prepare(`
|
|
301
|
-
WITH
|
|
302
|
+
WITH RECURSIVE owned(id) AS (
|
|
303
|
+
SELECT o.session FROM usage_session_owners o JOIN usage_sessions s ON s.id=o.owner
|
|
304
|
+
WHERE s.value IN (SELECT value FROM json_each(?))
|
|
305
|
+
UNION
|
|
306
|
+
SELECT o.session FROM usage_session_owners o JOIN owned ON o.owner=owned.id
|
|
307
|
+
), mine AS (
|
|
302
308
|
SELECT day,route,COUNT(*) AS turns,SUM(input) AS input,SUM(output) AS output,
|
|
303
309
|
SUM(cache_read) AS cache_read,SUM(cache_write) AS cache_write,
|
|
304
310
|
SUM(cost_usd) AS cost_usd,SUM(duration_ms) AS duration_ms,
|
|
305
311
|
SUM(CASE WHEN duration_ms>0 AND output>0 THEN output ELSE 0 END) AS speed_output,
|
|
306
312
|
SUM(CASE WHEN duration_ms>0 AND output>0 THEN duration_ms ELSE 0 END) AS speed_ms
|
|
307
313
|
FROM usage_events WHERE session IN (
|
|
308
|
-
SELECT id FROM usage_sessions WHERE value IN (SELECT value FROM json_each(?))
|
|
314
|
+
SELECT id FROM usage_sessions WHERE value IN (SELECT value FROM json_each(?))
|
|
315
|
+
UNION SELECT id FROM owned)
|
|
309
316
|
GROUP BY day,route
|
|
310
317
|
), attributed AS (
|
|
311
318
|
SELECT printf('%04d-%02d-%02d',m.day/10000,(m.day/100)%100,m.day%100) AS day,
|
|
@@ -320,7 +327,7 @@ export function readSessionUsage(db, sessionIds) {
|
|
|
320
327
|
)
|
|
321
328
|
SELECT a.* FROM attributed a JOIN (${best}) b USING(day,provider,model,rank)
|
|
322
329
|
`)
|
|
323
|
-
.all(JSON.stringify(sessionIds), '0000-01-01', '9999-12-31');
|
|
330
|
+
.all(JSON.stringify(ownerIds), JSON.stringify(sessionIds), '0000-01-01', '9999-12-31');
|
|
324
331
|
let speedOutput = 0;
|
|
325
332
|
let speedMs = 0;
|
|
326
333
|
for (const row of rows) {
|
|
@@ -60,7 +60,7 @@ function recordBatch(ledger, rows) {
|
|
|
60
60
|
const operations = {
|
|
61
61
|
rollup: ({ path, options }) => read(path, (db) => rollupUsage(db, options)),
|
|
62
62
|
rollupDelta: ({ path, options, known, now }) => read(path, (db) => rollupDelta(db, options, known, now)),
|
|
63
|
-
sessionUsage: ({ path, sessionIds }) => read(path, (db) => readSessionUsage(db, sessionIds)),
|
|
63
|
+
sessionUsage: ({ path, sessionIds, ownerIds }) => read(path, (db) => readSessionUsage(db, sessionIds, ownerIds)),
|
|
64
64
|
record: ({ path, rows }) => write(path, (ledger) => recordBatch(ledger, rows)),
|
|
65
65
|
recordQuota: ({ path, samples }) => write(path, (ledger) => ledger.recordQuota(samples)),
|
|
66
66
|
quotaHistory: ({ path, options }) => read(path, (db) => readQuotaHistory(db, options)),
|
|
@@ -78,6 +78,13 @@ const COMPACT_SCHEMA = `
|
|
|
78
78
|
-- One session's lifetime totals (the /context footer) read that session's
|
|
79
79
|
-- records only instead of scanning every record ever written.
|
|
80
80
|
CREATE INDEX IF NOT EXISTS usage_events_session ON usage_events(session);
|
|
81
|
+
-- The session each session ran for (an agent's Lead), so a Lead's agents
|
|
82
|
+
-- stay countable after their own session records are gone.
|
|
83
|
+
CREATE TABLE IF NOT EXISTS usage_session_owners (
|
|
84
|
+
session INTEGER PRIMARY KEY REFERENCES usage_sessions(id),
|
|
85
|
+
owner INTEGER NOT NULL REFERENCES usage_sessions(id)
|
|
86
|
+
);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS usage_session_owners_owner ON usage_session_owners(owner);
|
|
81
88
|
`;
|
|
82
89
|
|
|
83
90
|
// Preserve the old read surface. Only storage changes: every field, including
|
|
@@ -109,6 +116,7 @@ function compactEventWriter(db) {
|
|
|
109
116
|
const insertSession = db.prepare('INSERT OR IGNORE INTO usage_sessions(value) VALUES (?)');
|
|
110
117
|
const findSession = db.prepare('SELECT id FROM usage_sessions WHERE value=?');
|
|
111
118
|
const insert = db.prepare('INSERT INTO usage_events VALUES (?,?,?,?,?,?,?,?,?,?,?)');
|
|
119
|
+
const insertOwner = db.prepare('INSERT OR IGNORE INTO usage_session_owners VALUES (?,?)');
|
|
112
120
|
return (row) => {
|
|
113
121
|
for (const key of ['id', 'provider', 'model', 'kind', 'source_type', 'cost_source', 'origin', 'session_id']) {
|
|
114
122
|
if (row[key] == null) throw new Error(`Usage requires ${key}`);
|
|
@@ -133,6 +141,11 @@ function compactEventWriter(db) {
|
|
|
133
141
|
const route = findRoute.get(signature).id;
|
|
134
142
|
insertSession.run(String(row.session_id));
|
|
135
143
|
const session = findSession.get(String(row.session_id)).id;
|
|
144
|
+
const owner = row.owner_session_id ? String(row.owner_session_id) : '';
|
|
145
|
+
if (owner && owner !== String(row.session_id)) {
|
|
146
|
+
insertSession.run(owner);
|
|
147
|
+
insertOwner.run(session, findSession.get(owner).id);
|
|
148
|
+
}
|
|
136
149
|
insert.run(
|
|
137
150
|
id,
|
|
138
151
|
row.ts,
|
|
@@ -201,7 +214,6 @@ function migrateUsageLedger(db, path) {
|
|
|
201
214
|
|
|
202
215
|
export function usageRouteKind(provider) {
|
|
203
216
|
if (provider === 'mixdog-local') return 'local';
|
|
204
|
-
if (provider === 'opencode-go') return 'quota-api';
|
|
205
217
|
return provider.endsWith('-oauth') ? 'oauth' : 'api';
|
|
206
218
|
}
|
|
207
219
|
|
|
@@ -273,6 +285,7 @@ export function makeUsageRecord(args) {
|
|
|
273
285
|
model,
|
|
274
286
|
kind,
|
|
275
287
|
sessionId: ['no-session', '(none)'].includes(args.sessionId) ? '' : text(args.sessionId),
|
|
288
|
+
...(args.ownerSessionId ? { ownerSessionId: text(args.ownerSessionId) } : {}),
|
|
276
289
|
sourceType: text(args.sourceType),
|
|
277
290
|
input: priced.input,
|
|
278
291
|
output: num(args.outputTokens),
|
|
@@ -453,6 +466,7 @@ export class UsageLedger {
|
|
|
453
466
|
!this.insert({
|
|
454
467
|
...row,
|
|
455
468
|
session_id: row.sessionId,
|
|
469
|
+
owner_session_id: row.ownerSessionId,
|
|
456
470
|
source_type: row.sourceType,
|
|
457
471
|
cache_read: row.cacheRead,
|
|
458
472
|
cache_write: row.cacheWrite,
|
|
@@ -558,10 +572,11 @@ export class UsageLedger {
|
|
|
558
572
|
return this.workerRequest('quotaWindows', { options });
|
|
559
573
|
}
|
|
560
574
|
|
|
561
|
-
/** Every request one session made under any of its ids, compactions included, read on the ledger worker.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
575
|
+
/** Every request one session made under any of its ids, compactions included, read on the ledger worker.
|
|
576
|
+
* `ownerIds` adds every session recorded as run for them, transitively. */
|
|
577
|
+
sessionUsageAsync(sessionIds, ownerIds = []) {
|
|
578
|
+
if (this.path === ':memory:') return Promise.resolve().then(() => readSessionUsage(this.db, sessionIds, ownerIds));
|
|
579
|
+
return this.workerRequest('sessionUsage', { sessionIds, ownerIds });
|
|
565
580
|
}
|
|
566
581
|
|
|
567
582
|
preserveLegacyDays(days) {
|
|
@@ -22,8 +22,12 @@ const PERIODIC_ATTRIBUTION_FREE_BYTES = 2 * 1024 * 1024 * 1024;
|
|
|
22
22
|
let periodicTimer = null;
|
|
23
23
|
let lastPressureSnapshotAt = 0;
|
|
24
24
|
|
|
25
|
+
// Opt-in diagnostics (MIXDOG_MEMORY_SNAPSHOT=1): every sample starts a
|
|
26
|
+
// PowerShell process on Windows (two for a full snapshot, requested exactly
|
|
27
|
+
// when the host is under memory pressure), from every process that loads
|
|
28
|
+
// this module. A normal install never pays for that.
|
|
25
29
|
function enabled() {
|
|
26
|
-
return process.env.MIXDOG_MEMORY_SNAPSHOT
|
|
30
|
+
return process.env.MIXDOG_MEMORY_SNAPSHOT === '1';
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
function snapshotPaths() {
|