specrails-desktop 2.46.0 → 2.48.0
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/README.md +2 -0
- package/client/dist/assets/{ActivityFeedPage-CsHQPpvl.js → ActivityFeedPage-CGq6QhP5.js} +1 -1
- package/client/dist/assets/AgentBrowserCapture-C4GZ-kdf.js +1 -0
- package/client/dist/assets/AgentModeAnalyticsPane-_IPmL0ZS.js +2 -0
- package/client/dist/assets/AgentModeCodePane-B5WG5DOP.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-rnD-EPMd.js → AgentModeJobsPane-w1N_HIXh.js} +1 -1
- package/client/dist/assets/AgentsPage-CmwnnT46.js +87 -0
- package/client/dist/assets/AnalyticsPage-BUMsKtsJ.js +1 -0
- package/client/dist/assets/CodePage-PjgywJPS.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-BXY1pH4y.js +1 -0
- package/client/dist/assets/DocsDialog-ccQETHKB.js +11 -0
- package/client/dist/assets/DocsPage-DzEgplB3.js +11 -0
- package/client/dist/assets/{ExportDropdown-pWZGYCv3.js → ExportDropdown-B8bBGopr.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-CEwGkJ8F.js +19 -0
- package/client/dist/assets/JobDetailModal-BYXmuTDe.js +1 -0
- package/client/dist/assets/JobDetailPage-CWp6MwJj.js +1 -0
- package/client/dist/assets/JobsPage-D1O1uyG8.js +1 -0
- package/client/dist/assets/{LoopBuilderPage-CtmOE4-b.js → LoopBuilderPage-DQE-7Vby.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-BYeLjQbr.js → LoopPreviewModal-Bk7zIXD5.js} +1 -1
- package/client/dist/assets/LoopsPage-CqG7mx56.js +1 -0
- package/client/dist/assets/MinimizedChatsContext-UpnqccPx.js +1 -0
- package/client/dist/assets/PluginsPage-CjgVvmpW.js +2 -0
- package/client/dist/assets/{ProjectSettingsDialog-DsCRIc4v.js → ProjectSettingsDialog-D6vBCoay.js} +1 -1
- package/client/dist/assets/{RepositoryDeliveries-BQm_3TO8.js → RepositoryDeliveries-C36ipKp_.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-DnAFtjTz.js → RepositoryScopeSelector-0Gz_fR1E.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-Dr9RYs7q.js +1 -0
- package/client/dist/assets/TemplatePreviewModal-sCMHYFJo.js +1 -0
- package/client/dist/assets/TicketDetailModalContext-C5GgJlEt.js +14 -0
- package/client/dist/assets/{Trans-BwLgX6n7.js → Trans-9pHTy2fH.js} +1 -1
- package/client/dist/assets/{addspec-D2YIQ0GU.js → addspec-BOvgmcgk.js} +1 -1
- package/client/dist/assets/{addspec-B_TPJt-R.js → addspec-Clmyfn_R.js} +1 -1
- package/client/dist/assets/{addspec-A1lDQa4M.js → addspec-CsNaBpHp.js} +1 -1
- package/client/dist/assets/{addspec-CQBjzt24.js → addspec-D2tvWmmV.js} +1 -1
- package/client/dist/assets/{addspec-CLYLvaVP.js → addspec-D3nFwvD0.js} +1 -1
- package/client/dist/assets/{addspec-BPpZ_olp.js → addspec-DVLEhK3w.js} +1 -1
- package/client/dist/assets/{addspec-iFdQ6D99.js → addspec-DXbK-tb0.js} +1 -1
- package/client/dist/assets/{addspec-BHZd0N5F.js → addspec-OHG8DFEn.js} +1 -1
- package/client/dist/assets/agentRuntime-BUimiX__.js +1 -0
- package/client/dist/assets/agentRuntime-BcN97sAB.js +1 -0
- package/client/dist/assets/agentRuntime-Ckyg-ahT.js +1 -0
- package/client/dist/assets/agentRuntime-DJCwyX4t.js +1 -0
- package/client/dist/assets/agentRuntime-DVzPJOl0.js +1 -0
- package/client/dist/assets/agentRuntime-V9AZTG3c.js +1 -0
- package/client/dist/assets/agentRuntime-bAAjw1vw.js +1 -0
- package/client/dist/assets/agentRuntime-v4qKAYH-.js +1 -0
- package/client/dist/assets/{agents-45GbINfh.js → agents-CA5eT1d0.js} +1 -1
- package/client/dist/assets/{agents-DMKFStHL.js → agents-CAgy-5uS.js} +1 -1
- package/client/dist/assets/{agents-IUOgqT5t.js → agents-CNm6Qzlv.js} +1 -1
- package/client/dist/assets/{agents-ms7TwG16.js → agents-CTwbx-fb.js} +1 -1
- package/client/dist/assets/{agents-DaYcxAr1.js → agents-DEGV1Vao.js} +1 -1
- package/client/dist/assets/{agents-BHp0_FQt.js → agents-HsDF1aQU.js} +1 -1
- package/client/dist/assets/{agents-Bvyi3y6R.js → agents-IQjdA-z9.js} +1 -1
- package/client/dist/assets/{agents-rzPnqoBR.js → agents-RkGoteXh.js} +1 -1
- package/client/dist/assets/analytics-BNgZ1Nj7.js +1 -0
- package/client/dist/assets/analytics-BlypLLR6.js +1 -0
- package/client/dist/assets/analytics-CoEzWPx4.js +1 -0
- package/client/dist/assets/analytics-DFh5lSOl.js +1 -0
- package/client/dist/assets/analytics-DZp-DS2U.js +1 -0
- package/client/dist/assets/analytics-DjLlE7jS.js +1 -0
- package/client/dist/assets/analytics-jzCkOec3.js +1 -0
- package/client/dist/assets/analytics-nZIjjtCc.js +1 -0
- package/client/dist/assets/{dashboard-BK_mez-z.js → dashboard-C29Qwa-L.js} +1 -1
- package/client/dist/assets/{dashboard-BwkjxI6H.js → dashboard-CSJzf5OT.js} +1 -1
- package/client/dist/assets/{dashboard-C3bDKf7s.js → dashboard-CTfEtkqX.js} +1 -1
- package/client/dist/assets/{dashboard-Bt0cCzeG.js → dashboard-Cr4D8oMn.js} +1 -1
- package/client/dist/assets/{dashboard-fRrP0Kv0.js → dashboard-Csf1yGSN.js} +1 -1
- package/client/dist/assets/{dashboard-CdvjGfog.js → dashboard-D40HP2ks.js} +1 -1
- package/client/dist/assets/dashboard-DBAP2_SQ.js +1 -0
- package/client/dist/assets/{dashboard-B14uYXkG.js → dashboard-DMklepmz.js} +1 -1
- package/client/dist/assets/file-text-CFT-xXKN.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-CeZv93iR.js → formatDistanceToNow-w_2U5pm0.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DxAkjvJb.js → getTimezoneOffsetInMilliseconds-BRymg1Kk.js} +1 -1
- package/client/dist/assets/index-D0ScFZvR.css +2 -0
- package/client/dist/assets/index-DORCrgbI.js +77 -0
- package/client/dist/assets/jira-api-BqHbSdxK.js +1 -0
- package/client/dist/assets/jobs-C6qmvUAi.js +1 -0
- package/client/dist/assets/jobs-C9j2PuwW.js +1 -0
- package/client/dist/assets/jobs-DLHdDkHc.js +1 -0
- package/client/dist/assets/jobs-DSp9DoD_.js +1 -0
- package/client/dist/assets/jobs-DTQO08nJ.js +1 -0
- package/client/dist/assets/jobs-DoFaEl9g.js +1 -0
- package/client/dist/assets/jobs-Dpw6AEOP.js +1 -0
- package/client/dist/assets/jobs-DrF8Do8s.js +1 -0
- package/client/dist/assets/{narration-BSldQfjd.js → narration--hUhjM5v.js} +1 -1
- package/client/dist/assets/{narration-DKLjXIFI.js → narration-CHI06czm.js} +1 -1
- package/client/dist/assets/{narration-4OSg0b5-.js → narration-DjKB3pQZ.js} +1 -1
- package/client/dist/assets/{narration-BPMOQPFe.js → narration-DtCYvRLM.js} +1 -1
- package/client/dist/assets/{narration-D-e9ssxA.js → narration-DvLQBW9U.js} +1 -1
- package/client/dist/assets/{narration-DnrKC3mF.js → narration-eSx3gR0W.js} +1 -1
- package/client/dist/assets/{narration-Dypjxeel.js → narration-n3hOA7LY.js} +1 -1
- package/client/dist/assets/{narration-CvQdWdR5.js → narration-thJOeT2-.js} +1 -1
- package/client/dist/assets/packet-BNW8ODQw.js +1 -0
- package/client/dist/assets/packet-CXYvfdjy.js +1 -0
- package/client/dist/assets/packet-CtIlB8YE.js +1 -0
- package/client/dist/assets/packet-DA1SJkMH.js +1 -0
- package/client/dist/assets/packet-DYcWfBip.js +1 -0
- package/client/dist/assets/packet-DseX9yKC.js +1 -0
- package/client/dist/assets/packet-_m5aOy_d.js +1 -0
- package/client/dist/assets/packet-qOemUAm3.js +1 -0
- package/client/dist/assets/{project-repositories-BvBj9XH9.js → project-repositories-BsCs1PPA.js} +1 -1
- package/client/dist/assets/provider-capabilities-Vi1ZGvAK.js +1 -0
- package/client/dist/assets/settings-BsehKpty.js +1 -0
- package/client/dist/assets/settings-CQWob3SB.js +1 -0
- package/client/dist/assets/settings-CgCl0hzy.js +1 -0
- package/client/dist/assets/settings-DLRLd22L.js +1 -0
- package/client/dist/assets/settings-Dn5prAFl.js +1 -0
- package/client/dist/assets/settings-HZSu-K4l.js +1 -0
- package/client/dist/assets/{settings-DEgcA0jd.js → settings-p4OVIyRh.js} +1 -1
- package/client/dist/assets/settings-rddaldUp.js +1 -0
- package/client/dist/assets/setup-B5FHuxvB.js +1 -0
- package/client/dist/assets/setup-BihzXadx.js +1 -0
- package/client/dist/assets/setup-BtC6Hg4K.js +1 -0
- package/client/dist/assets/setup-C9Fy9PKQ.js +1 -0
- package/client/dist/assets/setup-CROyqD5N.js +1 -0
- package/client/dist/assets/setup-DpqzaEEA.js +1 -0
- package/client/dist/assets/setup-Dwzt5nO1.js +1 -0
- package/client/dist/assets/setup-zLEDnG7N.js +1 -0
- package/client/dist/assets/{spending-DNM6NAoA.js → spending-hcgxHP4O.js} +1 -1
- package/client/dist/assets/{useDesktop-mxHVQOdx.js → useDesktop-CAisjKWC.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-DnxHj8jP.js → useSharedWebSocket-BHIfW4QT.js} +2 -2
- package/client/dist/assets/wrench-BfmNO9Vw.js +1 -0
- package/client/dist/index.html +27 -26
- package/docs/guide/de/integrations/7-local-engines.md +37 -0
- package/docs/guide/en/integrations/7-local-engines.md +37 -0
- package/docs/guide/es/integrations/7-local-engines.md +37 -0
- package/docs/guide/fr/integrations/7-local-engines.md +37 -0
- package/docs/guide/it/integrations/7-local-engines.md +37 -0
- package/docs/guide/ja/integrations/7-local-engines.md +37 -0
- package/docs/guide/pt/integrations/7-local-engines.md +37 -0
- package/docs/guide/zh/integrations/7-local-engines.md +37 -0
- package/docs/internals/adding-a-provider.md +12 -0
- package/docs/internals/local-agent-runner.md +153 -0
- package/docs/internals/review-packet.md +8 -0
- package/docs/local-providers.md +215 -0
- package/docs/plans/implementation-efficiency-artifact.json +18 -0
- package/docs/plans/implementation-efficiency-evaluation.json +1817 -0
- package/docs/plans/implementation-efficiency-evaluation.md +41 -0
- package/docs/plans/implementation-efficiency-progress.md +48 -0
- package/docs/plans/implementation-efficiency-release.md +41 -0
- package/docs/plans/implementation-efficiency.md +124 -0
- package/local-runner/dist/specrails-local-runner.js +15815 -0
- package/package.json +7 -5
- package/server/dist/agent-chat-manager.js +8 -1
- package/server/dist/agent-chat-router.js +5 -2
- package/server/dist/agent-fence-promotion.js +53 -0
- package/server/dist/agent-mcp-config.js +4 -1
- package/server/dist/agent-runtime-bridge.js +35 -26
- package/server/dist/agent-runtime-controls-router.js +20 -1
- package/server/dist/agent-runtime-controls.js +94 -12
- package/server/dist/agent-runtime-effective-config.js +53 -0
- package/server/dist/agent-runtime-events.js +46 -0
- package/server/dist/agent-runtime-history.js +47 -0
- package/server/dist/agent-runtime-loader.js +84 -5
- package/server/dist/agent-runtime-metrics.js +77 -1
- package/server/dist/agent-runtime-package.js +205 -0
- package/server/dist/agent-runtime-settings-router.js +96 -5
- package/server/dist/agent-runtime-settings.js +163 -7
- package/server/dist/agent-spec-framing.js +10 -4
- package/server/dist/binary-probe.js +12 -0
- package/server/dist/blueprint-router.js +3 -2
- package/server/dist/chat-manager.js +5 -1
- package/server/dist/desktop-router.js +87 -4
- package/server/dist/index.js +13 -0
- package/server/dist/interactive-job-session.js +3 -1
- package/server/dist/local-engine-detection.js +118 -0
- package/server/dist/loop-executors.js +4 -3
- package/server/dist/loop-role-engines.js +129 -0
- package/server/dist/loop-run-manager.js +14 -6
- package/server/dist/mcp/tools/app-settings.js +30 -4
- package/server/dist/mcp/tools/rails.js +2 -1
- package/server/dist/pr-body.js +11 -2
- package/server/dist/pricing.js +15 -0
- package/server/dist/provider-detection.js +63 -1
- package/server/dist/provider-selection.js +26 -1
- package/server/dist/providers/index.js +4 -1
- package/server/dist/providers/local-adapter-registry.js +56 -0
- package/server/dist/providers/local-adapter.js +426 -0
- package/server/dist/providers/registry.js +39 -4
- package/server/dist/rail-isolated-launch.js +4 -3
- package/server/dist/rails-router.js +90 -8
- package/server/dist/result-event.js +17 -0
- package/server/dist/review-packet.js +1 -0
- package/server/dist/schemas/agent-runtime.schema.json +28 -3
- package/server/dist/schemas/fixtures/runtime-efficiency-summary.v1.json +790 -0
- package/server/dist/setup-manager.js +5 -1
- package/server/dist/spec-models.js +14 -2
- package/server/dist/worktree-node-modules.js +97 -65
- package/client/dist/assets/AgentBrowserCapture-BVcCK7x0.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-C5F3m29G.js +0 -2
- package/client/dist/assets/AgentModeCodePane-C5QuKsu0.js +0 -2
- package/client/dist/assets/AgentsPage-DiyJWZcU.js +0 -87
- package/client/dist/assets/AnalyticsPage-Dkmb0dnd.js +0 -1
- package/client/dist/assets/CodePage-C0dH99-t.js +0 -3
- package/client/dist/assets/DesktopAnalyticsPage-DuTjr2n8.js +0 -1
- package/client/dist/assets/DocsDialog-BVZJZHVR.js +0 -11
- package/client/dist/assets/DocsPage-D6KHTv8V.js +0 -11
- package/client/dist/assets/InteractiveJobComposer-Ck3PQxjN.js +0 -19
- package/client/dist/assets/JobDetailModal-DkoMH3ie.js +0 -1
- package/client/dist/assets/JobDetailPage-CBVivcl2.js +0 -1
- package/client/dist/assets/JobsPage-CzxEeca9.js +0 -1
- package/client/dist/assets/LoopsPage-6iW7g25k.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-B6wVevPq.js +0 -1
- package/client/dist/assets/PluginsPage-Bf1z3WPF.js +0 -2
- package/client/dist/assets/ReviewPacketPage-CaXAwtR7.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-9aRFpJ_J.js +0 -1
- package/client/dist/assets/TicketDetailModalContext-B1X5glzV.js +0 -14
- package/client/dist/assets/agentRuntime-BUagD2I_.js +0 -1
- package/client/dist/assets/agentRuntime-BkL70WtX.js +0 -1
- package/client/dist/assets/agentRuntime-CWXVqgqE.js +0 -1
- package/client/dist/assets/agentRuntime-CvNBdUAe.js +0 -1
- package/client/dist/assets/agentRuntime-D4RmeuNY.js +0 -1
- package/client/dist/assets/agentRuntime-DTidBWKg.js +0 -1
- package/client/dist/assets/agentRuntime-DhKYfyaU.js +0 -1
- package/client/dist/assets/agentRuntime-LL8_GLx9.js +0 -1
- package/client/dist/assets/analytics-47pYygtV.js +0 -1
- package/client/dist/assets/analytics-B0vt1g5U.js +0 -1
- package/client/dist/assets/analytics-BMzCvrCU.js +0 -1
- package/client/dist/assets/analytics-BlFN3Zwx.js +0 -1
- package/client/dist/assets/analytics-CZYzL-nR.js +0 -1
- package/client/dist/assets/analytics-DAsb9MJ8.js +0 -1
- package/client/dist/assets/analytics-FXQlQ2Yt.js +0 -1
- package/client/dist/assets/analytics-RnFCNkah.js +0 -1
- package/client/dist/assets/dashboard-BfBir_6o.js +0 -1
- package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
- package/client/dist/assets/index-Cfh9aaep.css +0 -2
- package/client/dist/assets/index-DYoyDWim.js +0 -76
- package/client/dist/assets/jira-api-ChjVGJx_.js +0 -1
- package/client/dist/assets/jobs-5PQJbdOf.js +0 -1
- package/client/dist/assets/jobs-B2B08Oy9.js +0 -1
- package/client/dist/assets/jobs-BJZtBbwb.js +0 -1
- package/client/dist/assets/jobs-BlWEsQb7.js +0 -1
- package/client/dist/assets/jobs-ConfD8xD.js +0 -1
- package/client/dist/assets/jobs-DMPU64ms.js +0 -1
- package/client/dist/assets/jobs-DojASiTp.js +0 -1
- package/client/dist/assets/jobs-kVHkNpB-.js +0 -1
- package/client/dist/assets/packet-BQ2tXfJI.js +0 -1
- package/client/dist/assets/packet-BdpOJ1Cy.js +0 -1
- package/client/dist/assets/packet-C3Ievb9R.js +0 -1
- package/client/dist/assets/packet-C4zJIG0Y.js +0 -1
- package/client/dist/assets/packet-Cx-zRuWn.js +0 -1
- package/client/dist/assets/packet-DUTGX52M.js +0 -1
- package/client/dist/assets/packet-DhsKtUPB.js +0 -1
- package/client/dist/assets/packet-z3LJ8Zyy.js +0 -1
- package/client/dist/assets/provider-capabilities-CXipwrXJ.js +0 -1
- package/client/dist/assets/settings-B3rWEwOx.js +0 -1
- package/client/dist/assets/settings-BcVoiMyw.js +0 -1
- package/client/dist/assets/settings-BlYvaQ7w.js +0 -1
- package/client/dist/assets/settings-CD5uEBiL.js +0 -1
- package/client/dist/assets/settings-CyhS0DLJ.js +0 -1
- package/client/dist/assets/settings-DCMFUGDs.js +0 -1
- package/client/dist/assets/settings-DmbYKyMJ.js +0 -1
- package/client/dist/assets/setup-2xGgu4Sk.js +0 -1
- package/client/dist/assets/setup-Ba11l2JA.js +0 -1
- package/client/dist/assets/setup-BzW4kMok.js +0 -1
- package/client/dist/assets/setup-COzwYhkJ.js +0 -1
- package/client/dist/assets/setup-CRVULhtF.js +0 -1
- package/client/dist/assets/setup-DgvlxwpE.js +0 -1
- package/client/dist/assets/setup-Dw_wYeEY.js +0 -1
- package/client/dist/assets/setup-ZPtEeJPW.js +0 -1
|
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.AgentRuntimeConfigError = exports.REVIEW_THRESHOLD_FLOORS = void 0;
|
|
6
|
+
exports.AgentRuntimeConfigError = exports.REVIEW_THRESHOLD_FLOORS = exports.MAX_OUTPUT_TOKENS_MAX = exports.MAX_OUTPUT_TOKENS_MIN = exports.CONTEXT_WINDOW_TOKENS_MAX = exports.CONTEXT_WINDOW_TOKENS_MIN = void 0;
|
|
7
|
+
exports.stripDesktopConnectionFields = stripDesktopConnectionFields;
|
|
8
|
+
exports.coreConnectionFieldGates = coreConnectionFieldGates;
|
|
7
9
|
exports.validateAgentRuntimeConfig = validateAgentRuntimeConfig;
|
|
8
10
|
exports.defaultAgentRuntimeConfig = defaultAgentRuntimeConfig;
|
|
9
11
|
exports.agentRuntimeConfigPath = agentRuntimeConfigPath;
|
|
@@ -17,6 +19,8 @@ exports.saveAgentRuntimeConfig = saveAgentRuntimeConfig;
|
|
|
17
19
|
exports.validateRuntimeRolePrompts = validateRuntimeRolePrompts;
|
|
18
20
|
exports.loadRuntimeRolePrompts = loadRuntimeRolePrompts;
|
|
19
21
|
exports.saveRuntimeRolePrompts = saveRuntimeRolePrompts;
|
|
22
|
+
exports.validateRuntimeProviderOverride = validateRuntimeProviderOverride;
|
|
23
|
+
exports.forCoreRuntime = forCoreRuntime;
|
|
20
24
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
25
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
26
|
const node_os_1 = __importDefault(require("node:os"));
|
|
@@ -24,6 +28,89 @@ const node_crypto_1 = require("node:crypto");
|
|
|
24
28
|
const ajv_1 = __importDefault(require("ajv"));
|
|
25
29
|
const agent_runtime_schema_json_1 = __importDefault(require("./schemas/agent-runtime.schema.json"));
|
|
26
30
|
const workspace_resolution_1 = require("./workspace-resolution");
|
|
31
|
+
const registry_1 = require("./providers/registry");
|
|
32
|
+
const DESKTOP_CONNECTION_FIELDS = ['label', 'defaultModel', 'rates'];
|
|
33
|
+
/** Core-owned small-model fields: known only to cores advertising `capabilities.compactAgentLoop`
|
|
34
|
+
* (`supportsReasoningEffort` moved here: core forwards it as OpenAI `reasoning_effort`). */
|
|
35
|
+
const CORE_COMPACT_FIELDS = ['agentLoop', 'contextWindowTokens', 'supportsReasoningEffort'];
|
|
36
|
+
/** Known only to cores advertising `capabilities.compactOutputBudget`. */
|
|
37
|
+
const CORE_OUTPUT_BUDGET_FIELDS = ['maxOutputTokens'];
|
|
38
|
+
exports.CONTEXT_WINDOW_TOKENS_MIN = 4096;
|
|
39
|
+
exports.CONTEXT_WINDOW_TOKENS_MAX = 4_000_000;
|
|
40
|
+
exports.MAX_OUTPUT_TOKENS_MIN = 1024;
|
|
41
|
+
exports.MAX_OUTPUT_TOKENS_MAX = 1_000_000;
|
|
42
|
+
const MODEL_PATTERN = /^[^-\r\n\u0000][^\r\n\u0000]*$/;
|
|
43
|
+
/** Core-shaped providers: desktop-only connection fields removed. */
|
|
44
|
+
function stripDesktopConnectionFields(providers, opts) {
|
|
45
|
+
return providers.map((provider) => {
|
|
46
|
+
if (provider.kind !== 'openai-compatible')
|
|
47
|
+
return { ...provider };
|
|
48
|
+
const copy = { ...provider };
|
|
49
|
+
for (const field of DESKTOP_CONNECTION_FIELDS)
|
|
50
|
+
delete copy[field];
|
|
51
|
+
// Older cores reject unknown keys: only hand the compact-loop fields to a
|
|
52
|
+
// core that advertises them. Absent opts ⇒ conservative (strip).
|
|
53
|
+
if (opts?.coreCompactLoop !== true)
|
|
54
|
+
for (const field of CORE_COMPACT_FIELDS)
|
|
55
|
+
delete copy[field];
|
|
56
|
+
if (opts?.coreOutputBudget !== true)
|
|
57
|
+
for (const field of CORE_OUTPUT_BUDGET_FIELDS)
|
|
58
|
+
delete copy[field];
|
|
59
|
+
return copy;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function validateDesktopConnectionFields(provider, index) {
|
|
63
|
+
const at = `/providers/${index}`;
|
|
64
|
+
const extras = {};
|
|
65
|
+
if (provider.label !== undefined) {
|
|
66
|
+
if (typeof provider.label !== 'string' || !provider.label.trim() || provider.label.length > 64 || /[\r\n\u0000]/.test(provider.label))
|
|
67
|
+
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/label: must be 1–64 characters`);
|
|
68
|
+
extras.label = provider.label;
|
|
69
|
+
}
|
|
70
|
+
if (provider.defaultModel !== undefined) {
|
|
71
|
+
if (typeof provider.defaultModel !== 'string' || !provider.defaultModel.trim() || provider.defaultModel.length > 256 || !MODEL_PATTERN.test(provider.defaultModel))
|
|
72
|
+
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/defaultModel: must be a model id (no leading '-', no control characters)`);
|
|
73
|
+
extras.defaultModel = provider.defaultModel;
|
|
74
|
+
}
|
|
75
|
+
if (provider.rates !== undefined) {
|
|
76
|
+
const rates = provider.rates;
|
|
77
|
+
if (!rates || typeof rates !== 'object' || Array.isArray(rates) || Object.keys(rates).some((key) => key !== 'inputPer1M' && key !== 'outputPer1M'))
|
|
78
|
+
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/rates: expected { inputPer1M, outputPer1M }`);
|
|
79
|
+
for (const key of ['inputPer1M', 'outputPer1M']) {
|
|
80
|
+
const value = rates[key];
|
|
81
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
82
|
+
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/rates/${key}: must be a non-negative number`);
|
|
83
|
+
}
|
|
84
|
+
extras.rates = { inputPer1M: rates.inputPer1M, outputPer1M: rates.outputPer1M };
|
|
85
|
+
}
|
|
86
|
+
if (provider.supportsReasoningEffort !== undefined) {
|
|
87
|
+
if (typeof provider.supportsReasoningEffort !== 'boolean')
|
|
88
|
+
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/supportsReasoningEffort: must be a boolean`);
|
|
89
|
+
extras.supportsReasoningEffort = provider.supportsReasoningEffort;
|
|
90
|
+
}
|
|
91
|
+
if (provider.agentLoop !== undefined) {
|
|
92
|
+
if (provider.agentLoop !== 'compact' && provider.agentLoop !== 'free')
|
|
93
|
+
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/agentLoop: must be "compact" or "free"`);
|
|
94
|
+
extras.agentLoop = provider.agentLoop;
|
|
95
|
+
}
|
|
96
|
+
if (provider.contextWindowTokens !== undefined) {
|
|
97
|
+
const n = provider.contextWindowTokens;
|
|
98
|
+
if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < exports.CONTEXT_WINDOW_TOKENS_MIN || n > exports.CONTEXT_WINDOW_TOKENS_MAX)
|
|
99
|
+
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/contextWindowTokens: must be an integer between ${exports.CONTEXT_WINDOW_TOKENS_MIN} and ${exports.CONTEXT_WINDOW_TOKENS_MAX}`);
|
|
100
|
+
extras.contextWindowTokens = n;
|
|
101
|
+
}
|
|
102
|
+
if (provider.maxOutputTokens !== undefined) {
|
|
103
|
+
const n = provider.maxOutputTokens;
|
|
104
|
+
if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < exports.MAX_OUTPUT_TOKENS_MIN || n > exports.MAX_OUTPUT_TOKENS_MAX)
|
|
105
|
+
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${at}/maxOutputTokens: must be an integer between ${exports.MAX_OUTPUT_TOKENS_MIN} and ${exports.MAX_OUTPUT_TOKENS_MAX}`);
|
|
106
|
+
extras.maxOutputTokens = n;
|
|
107
|
+
}
|
|
108
|
+
return extras;
|
|
109
|
+
}
|
|
110
|
+
/** The strip options for a loaded core: each core-owned field family travels only when the core advertises it. */
|
|
111
|
+
function coreConnectionFieldGates(capabilities) {
|
|
112
|
+
return { coreCompactLoop: capabilities?.compactAgentLoop === 1, coreOutputBudget: capabilities?.compactOutputBudget === 1 };
|
|
113
|
+
}
|
|
27
114
|
/** Core's own review gate. Configured thresholds may only tighten it (mirrors Core's config.ts). */
|
|
28
115
|
exports.REVIEW_THRESHOLD_FLOORS = {
|
|
29
116
|
minScore: 70,
|
|
@@ -39,6 +126,19 @@ exports.AgentRuntimeConfigError = AgentRuntimeConfigError;
|
|
|
39
126
|
/** Vendored from Core's schema; parity tests keep offline configuration aligned.
|
|
40
127
|
* Semantic checks mirror Core because JSON Schema cannot check provider refs. */
|
|
41
128
|
function validateAgentRuntimeConfig(input) {
|
|
129
|
+
// Desktop-only connection fields ride the openai-compatible entries; the
|
|
130
|
+
// vendored (byte-identical to Core) schema forbids unknown keys, so they are
|
|
131
|
+
// validated here and re-attached after the schema pass.
|
|
132
|
+
const extrasByIndex = new Map();
|
|
133
|
+
if (input && typeof input === 'object' && !Array.isArray(input) && Array.isArray(input.providers)) {
|
|
134
|
+
const rawProviders = input.providers;
|
|
135
|
+
rawProviders.forEach((provider, index) => {
|
|
136
|
+
if (!provider || typeof provider !== 'object' || provider.kind !== 'openai-compatible')
|
|
137
|
+
return;
|
|
138
|
+
extrasByIndex.set(index, validateDesktopConnectionFields(provider, index));
|
|
139
|
+
});
|
|
140
|
+
input = { ...input, providers: stripDesktopConnectionFields(rawProviders) };
|
|
141
|
+
}
|
|
42
142
|
if (!schemaValidator(input)) {
|
|
43
143
|
const error = schemaValidator.errors?.[0];
|
|
44
144
|
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${error?.instancePath || '/'}: ${error?.message ?? 'invalid value'}`);
|
|
@@ -51,6 +151,10 @@ function validateAgentRuntimeConfig(input) {
|
|
|
51
151
|
for (const provider of config.providers) {
|
|
52
152
|
if (provider.kind !== 'openai-compatible')
|
|
53
153
|
continue;
|
|
154
|
+
// A connection id may never shadow a registered CLI adapter (claude/codex/
|
|
155
|
+
// gemini/kimi): local adapters register under the connection id.
|
|
156
|
+
if ((0, registry_1.hasAdapter)(provider.id) && (0, registry_1.adapterKind)(provider.id) !== 'local')
|
|
157
|
+
throw new AgentRuntimeConfigError(`Provider id '${provider.id}' is reserved for the ${provider.id} CLI adapter`);
|
|
54
158
|
let url;
|
|
55
159
|
try {
|
|
56
160
|
url = new URL(provider.baseUrl);
|
|
@@ -61,23 +165,43 @@ function validateAgentRuntimeConfig(input) {
|
|
|
61
165
|
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || provider.baseUrl.includes('\0'))
|
|
62
166
|
throw new AgentRuntimeConfigError('Provider URLs must use HTTP(S) without credentials, query parameters or fragments');
|
|
63
167
|
}
|
|
64
|
-
for (const role of ROLES) {
|
|
65
|
-
const agent = config.agents[role];
|
|
168
|
+
for (const role of [...ROLES, ...(config.fixer ? ['fixer'] : [])]) {
|
|
169
|
+
const agent = role === 'fixer' ? config.fixer : config.agents[role];
|
|
66
170
|
const provider = config.providers.find(({ id }) => id === agent.provider);
|
|
67
171
|
if (!provider)
|
|
68
172
|
throw new AgentRuntimeConfigError(`Role ${role} references a provider that is not configured`);
|
|
69
|
-
|
|
173
|
+
// Core requires a model on API providers, but a LOCAL engine (a registered
|
|
174
|
+
// connection with a discovered/stored default) may be saved as "provider
|
|
175
|
+
// default": the launch path fills the connection's CURRENT default
|
|
176
|
+
// (`fillDefaultRoleModels`), so changing the connection's default later
|
|
177
|
+
// applies to every role that did not pin one. Persisting the filled value
|
|
178
|
+
// used to freeze gpt-oss-fast into the architect after the user switched
|
|
179
|
+
// the connection to qwen3-coder.
|
|
180
|
+
if (provider.kind === 'openai-compatible' && !agent.model && !((0, registry_1.hasAdapter)(provider.id) && (0, registry_1.isLocalAdapterId)(provider.id)))
|
|
70
181
|
throw new AgentRuntimeConfigError(`Role ${role} requires a model for its API provider`);
|
|
71
182
|
if (agent.model !== undefined && !agent.model.trim())
|
|
72
183
|
throw new AgentRuntimeConfigError(`Role ${role} requires a nonempty model identifier`);
|
|
184
|
+
for (const value of [agent.model, agent.effort, agent.escalation?.model, agent.escalation?.effort])
|
|
185
|
+
if (value !== undefined && (!value.trim() || value.includes('\0')))
|
|
186
|
+
throw new AgentRuntimeConfigError(`Role ${role} has an invalid model or effort`);
|
|
187
|
+
if (agent.escalation && (!agent.model || (agent.escalation.model === agent.model && agent.escalation.effort === agent.effort)))
|
|
188
|
+
throw new AgentRuntimeConfigError(`Role ${role} escalation requires an explicit base and a different model or effort`);
|
|
73
189
|
if (agent.maxTurns !== undefined && !Number.isSafeInteger(agent.maxTurns))
|
|
74
190
|
throw new AgentRuntimeConfigError(`Role ${role} maxTurns must be a safe integer`);
|
|
191
|
+
if (agent.thinking !== undefined && agent.thinking !== 'on' && agent.thinking !== 'off')
|
|
192
|
+
throw new AgentRuntimeConfigError(`Role ${role} thinking must be "on" or "off"`);
|
|
75
193
|
}
|
|
76
194
|
for (const [key, value] of Object.entries(config.limits ?? {})) {
|
|
77
195
|
if (!Number.isFinite(value) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value)))
|
|
78
196
|
throw new AgentRuntimeConfigError('Workflow limits must be finite, safe numbers');
|
|
79
197
|
}
|
|
198
|
+
const keys = config.verification.flatMap(command => command.key ? [command.key] : []);
|
|
199
|
+
if (new Set(keys).size !== keys.length)
|
|
200
|
+
throw new AgentRuntimeConfigError('Verification check keys must be unique');
|
|
80
201
|
for (const command of config.verification) {
|
|
202
|
+
for (const value of [command.label, ...(command.policy?.inputs ?? []), ...(command.policy?.toolchainInputs ?? []), ...(command.policy?.resources ?? [])])
|
|
203
|
+
if (value !== undefined && (!value.trim() || value.includes('\0')))
|
|
204
|
+
throw new AgentRuntimeConfigError('Invalid verification label or policy input');
|
|
81
205
|
if (![command.command, ...command.args, ...(command.cwd === undefined ? [] : [command.cwd])].every((value) => !value.includes('\0')) || !command.command.trim() || (command.cwd !== undefined && !command.cwd.trim()))
|
|
82
206
|
throw new AgentRuntimeConfigError('Verification command contains an empty or invalid value');
|
|
83
207
|
if (command.timeoutMs !== undefined && !Number.isSafeInteger(command.timeoutMs))
|
|
@@ -95,7 +219,10 @@ function validateAgentRuntimeConfig(input) {
|
|
|
95
219
|
if (value < exports.REVIEW_THRESHOLD_FLOORS.aspects[aspect])
|
|
96
220
|
throw new AgentRuntimeConfigError(`Review threshold review.aspects.${aspect} must be at least ${exports.REVIEW_THRESHOLD_FLOORS.aspects[aspect]} (Core's own review gate)`);
|
|
97
221
|
}
|
|
98
|
-
|
|
222
|
+
const cloned = structuredClone(config);
|
|
223
|
+
for (const [index, extras] of extrasByIndex)
|
|
224
|
+
cloned.providers[index] = { ...cloned.providers[index], ...extras };
|
|
225
|
+
return cloned;
|
|
99
226
|
}
|
|
100
227
|
function defaultAgentRuntimeConfig(project) {
|
|
101
228
|
const provider = CLI_PROVIDERS.includes(project.provider) ? project.provider : 'claude';
|
|
@@ -211,13 +338,14 @@ function saveAgentRuntimeConfig(project, input) {
|
|
|
211
338
|
atomicJson(file, settings);
|
|
212
339
|
return config;
|
|
213
340
|
}
|
|
341
|
+
const PROMPT_ROLES = [...ROLES, 'fixer'];
|
|
214
342
|
function validateRuntimeRolePrompts(input) {
|
|
215
343
|
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
216
344
|
throw new AgentRuntimeConfigError('Role prompts must be an object');
|
|
217
345
|
const prompts = {};
|
|
218
346
|
for (const [role, text] of Object.entries(input)) {
|
|
219
|
-
if (!
|
|
220
|
-
throw new AgentRuntimeConfigError('Role prompts must use architect, developer or
|
|
347
|
+
if (!PROMPT_ROLES.includes(role) || typeof text !== 'string' || !text.trim() || text.length > 20000 || text.includes('\0'))
|
|
348
|
+
throw new AgentRuntimeConfigError('Role prompts must use architect, developer, reviewer or fixer with 1–20000 nonblank characters and no null bytes');
|
|
221
349
|
prompts[role] = text;
|
|
222
350
|
}
|
|
223
351
|
return prompts;
|
|
@@ -237,3 +365,31 @@ function saveRuntimeRolePrompts(input) {
|
|
|
237
365
|
atomicJson(node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-role-prompts.json'), prompts);
|
|
238
366
|
return prompts;
|
|
239
367
|
}
|
|
368
|
+
function validateRuntimeProviderOverride(value) {
|
|
369
|
+
if (value === undefined)
|
|
370
|
+
return undefined;
|
|
371
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
372
|
+
throw new AgentRuntimeConfigError('Invalid runtime provider override');
|
|
373
|
+
const raw = value;
|
|
374
|
+
if (Object.keys(raw).some(key => !['provider', 'model', 'effort'].includes(key)) || typeof raw.provider !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(raw.provider))
|
|
375
|
+
throw new AgentRuntimeConfigError('Invalid runtime provider override provider');
|
|
376
|
+
if (raw.model !== undefined && (typeof raw.model !== 'string' || !raw.model.trim() || raw.model.length > 256 || /^-|[\r\n\0]/.test(raw.model)))
|
|
377
|
+
throw new AgentRuntimeConfigError('Invalid runtime provider override model');
|
|
378
|
+
if (raw.effort !== undefined && (typeof raw.effort !== 'string' || !/^[a-z][a-z0-9_-]{0,31}$/.test(raw.effort)))
|
|
379
|
+
throw new AgentRuntimeConfigError('Invalid runtime provider override effort');
|
|
380
|
+
return structuredClone(raw);
|
|
381
|
+
}
|
|
382
|
+
/** Older Core runtimes reject unknown top-level keys: drop `guardrails` unless the loaded Core advertises `configurableGuardrails`. */
|
|
383
|
+
function forCoreRuntime(config, capabilities) {
|
|
384
|
+
let out = config;
|
|
385
|
+
if (capabilities?.configurableGuardrails !== 1 && out.guardrails !== undefined) {
|
|
386
|
+
const { guardrails: _dropped, ...rest } = out;
|
|
387
|
+
out = rest;
|
|
388
|
+
}
|
|
389
|
+
// The per-role thinking switch is known only to cores advertising `roleThinkingControl`.
|
|
390
|
+
if (capabilities?.roleThinkingControl !== 1 && (Object.values(out.agents ?? {}).some(agent => agent?.thinking !== undefined) || out.fixer?.thinking !== undefined)) {
|
|
391
|
+
const strip = (agent) => { const { thinking: _t, ...rest } = agent; return rest; };
|
|
392
|
+
out = { ...out, ...(out.agents ? { agents: Object.fromEntries(Object.entries(out.agents).map(([role, agent]) => [role, strip(agent)])) } : {}), ...(out.fixer ? { fixer: strip(out.fixer) } : {}) };
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
@@ -33,6 +33,8 @@ exports.framingRefusalMessage = framingRefusalMessage;
|
|
|
33
33
|
exports.checkSpecFraming = checkSpecFraming;
|
|
34
34
|
exports.recordSpecCommitted = recordSpecCommitted;
|
|
35
35
|
const agent_store_1 = require("./agent-store");
|
|
36
|
+
const agent_fence_promotion_1 = require("./agent-fence-promotion");
|
|
37
|
+
const registry_1 = require("./providers/registry");
|
|
36
38
|
/** User-typed command word that switches framing off for the conversation. */
|
|
37
39
|
exports.FRAMING_WAIVER_TOKEN = '#noframe';
|
|
38
40
|
/** User-typed command word that switches it back on. */
|
|
@@ -78,7 +80,10 @@ function payloadIsValidFrame(raw) {
|
|
|
78
80
|
return true;
|
|
79
81
|
}
|
|
80
82
|
/** True when an assistant message carries at least one VALID frame block. */
|
|
81
|
-
function hasValidProblemFrame(
|
|
83
|
+
function hasValidProblemFrame(rawContent, opts) {
|
|
84
|
+
// Local (OpenAI-compatible) engines only — mirrors the client: a ```json
|
|
85
|
+
// fence with a frame-shaped body counts. CLI providers stay strict.
|
|
86
|
+
const content = opts?.tolerantFences ? (0, agent_fence_promotion_1.promoteAgentProtocolFences)(rawContent) : rawContent;
|
|
82
87
|
if (!content.includes(OPEN_FENCE))
|
|
83
88
|
return false;
|
|
84
89
|
FENCE_RE.lastIndex = 0;
|
|
@@ -125,7 +130,7 @@ function isFramingMarker(content) {
|
|
|
125
130
|
* rather than a series. A waiver is sticky for the rest of the conversation and
|
|
126
131
|
* is cleared only by the restore token.
|
|
127
132
|
*/
|
|
128
|
-
function evaluateFramingState(messages) {
|
|
133
|
+
function evaluateFramingState(messages, opts) {
|
|
129
134
|
let waived = false;
|
|
130
135
|
let pendingFrame = false; // emitted, awaiting the user's answer
|
|
131
136
|
let answeredFrame = false; // answered and not yet spent on a spec
|
|
@@ -137,7 +142,7 @@ function evaluateFramingState(messages) {
|
|
|
137
142
|
continue;
|
|
138
143
|
}
|
|
139
144
|
if (msg.role === 'assistant') {
|
|
140
|
-
if (hasValidProblemFrame(msg.content)) {
|
|
145
|
+
if (hasValidProblemFrame(msg.content, opts)) {
|
|
141
146
|
pendingFrame = true;
|
|
142
147
|
sawFrame = true;
|
|
143
148
|
}
|
|
@@ -182,7 +187,8 @@ function framingRefusalMessage(reason) {
|
|
|
182
187
|
* MCP client cannot render the card and holds no conversation here.
|
|
183
188
|
*/
|
|
184
189
|
function checkSpecFraming(db, conversationId) {
|
|
185
|
-
const
|
|
190
|
+
const tolerantFences = (0, registry_1.isLocalAdapterId)((0, agent_store_1.getAgentConversation)(db, conversationId)?.provider);
|
|
191
|
+
const state = evaluateFramingState((0, agent_store_1.listAgentMessages)(db, conversationId), { tolerantFences });
|
|
186
192
|
if (state.satisfied)
|
|
187
193
|
return null;
|
|
188
194
|
return framingRefusalMessage(state.reason ?? 'no_frame');
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.binaryOnPath = binaryOnPath;
|
|
4
7
|
exports.__resetBinaryProbeCacheForTest = __resetBinaryProbeCacheForTest;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
5
10
|
const child_process_1 = require("child_process");
|
|
6
11
|
const win_spawn_1 = require("./util/win-spawn");
|
|
7
12
|
// Windows has no `which`; probe via `where` instead. Both exit non-zero
|
|
@@ -20,6 +25,13 @@ function binaryOnPath(binary) {
|
|
|
20
25
|
if (hit && now - hit.at < PROBE_TTL_MS)
|
|
21
26
|
return hit.onPath;
|
|
22
27
|
let onPath;
|
|
28
|
+
// An absolute binary (the bundled Node that runs the local AI runner) is
|
|
29
|
+
// probed by existence: `where C:\\…\\node.exe` does not resolve a path.
|
|
30
|
+
if (node_path_1.default.isAbsolute(binary)) {
|
|
31
|
+
onPath = node_fs_1.default.existsSync(binary);
|
|
32
|
+
_cache.set(binary, { at: now, onPath });
|
|
33
|
+
return onPath;
|
|
34
|
+
}
|
|
23
35
|
try {
|
|
24
36
|
// `where` runs via cmd.exe; pass a SystemRoot-backfilled env so the probe
|
|
25
37
|
// (a HARD gate before job/chat spawns) can't be falsely cached as missing
|
|
@@ -8,6 +8,7 @@ const providers_1 = require("./providers");
|
|
|
8
8
|
const runtime_1 = require("./providers/runtime");
|
|
9
9
|
const blueprint_store_1 = require("./blueprint-store");
|
|
10
10
|
const blueprint_spec_quality_1 = require("./blueprint-spec-quality");
|
|
11
|
+
const provider_selection_1 = require("./provider-selection");
|
|
11
12
|
function isProjectBuilderEnabled() {
|
|
12
13
|
return process.env.SPECRAILS_PROJECT_BUILDER !== 'false';
|
|
13
14
|
}
|
|
@@ -33,7 +34,7 @@ function createBlueprintRouter(deps) {
|
|
|
33
34
|
next();
|
|
34
35
|
});
|
|
35
36
|
router.get('/models', (req, res) => {
|
|
36
|
-
const provider = validProvider(req.query.provider) ??
|
|
37
|
+
const provider = validProvider(req.query.provider) ?? (0, provider_selection_1.defaultMachineProvider)();
|
|
37
38
|
const adapter = (0, providers_1.getAdapter)(provider);
|
|
38
39
|
const toolPolicy = (0, runtime_1.pureOutputToolPolicy)(adapter);
|
|
39
40
|
const requestedModel = typeof req.query.model === 'string' && req.query.model
|
|
@@ -63,7 +64,7 @@ function createBlueprintRouter(deps) {
|
|
|
63
64
|
});
|
|
64
65
|
router.post('/conversations', (req, res) => {
|
|
65
66
|
const body = (req.body ?? {});
|
|
66
|
-
const provider = validProvider(body.provider) ??
|
|
67
|
+
const provider = validProvider(body.provider) ?? (0, provider_selection_1.defaultMachineProvider)();
|
|
67
68
|
if (!(0, runtime_1.pureOutputToolPolicy)((0, providers_1.getAdapter)(provider))) {
|
|
68
69
|
res.status(409).json({ error: 'provider_tool_policy_unsupported', provider });
|
|
69
70
|
return;
|
|
@@ -21,6 +21,7 @@ const providers_1 = require("./providers");
|
|
|
21
21
|
const runtime_1 = require("./providers/runtime");
|
|
22
22
|
const context_scope_1 = require("./context-scope");
|
|
23
23
|
const user_mcp_config_1 = require("./user-mcp-config");
|
|
24
|
+
const registry_1 = require("./providers/registry");
|
|
24
25
|
const binary_probe_1 = require("./binary-probe");
|
|
25
26
|
const explore_stdin_session_1 = require("./explore-stdin-session");
|
|
26
27
|
const workspace_resolution_1 = require("./workspace-resolution");
|
|
@@ -937,7 +938,10 @@ class ChatManager {
|
|
|
937
938
|
// gating is therefore claude-only today — codex inherits its sandbox
|
|
938
939
|
// and approval policy from the project's `.codex/config.toml` (or the
|
|
939
940
|
// `-c sandbox_mode=` override the adapter already attaches on resume).
|
|
940
|
-
|
|
941
|
+
// Local (OpenAI-compatible) runners accept the SAME `--tools` /
|
|
942
|
+
// `--disallowedTools` shape (the adapter's extraArgs allowlist drops any
|
|
943
|
+
// claude-only flag), so the scope tiers apply to them too.
|
|
944
|
+
const scopeFlags = conversationScope && (adapter.id === 'claude' || (0, registry_1.isLocalAdapterId)(adapter.id))
|
|
941
945
|
? (0, context_scope_1.toolFlagsForScope)(conversationScope).args
|
|
942
946
|
: [];
|
|
943
947
|
if (this._cwd && conversation.kind !== 'milestone' && (!conversationScope || conversationScope.mcp)) {
|
|
@@ -21,6 +21,9 @@ const specrails_tech_client_1 = require("./specrails-tech-client");
|
|
|
21
21
|
const core_compat_1 = require("./core-compat");
|
|
22
22
|
const providers_1 = require("./providers");
|
|
23
23
|
const provider_detection_1 = require("./provider-detection");
|
|
24
|
+
const local_engine_detection_1 = require("./local-engine-detection");
|
|
25
|
+
const local_adapter_registry_1 = require("./providers/local-adapter-registry");
|
|
26
|
+
const local_adapter_1 = require("./providers/local-adapter");
|
|
24
27
|
const agent_defaults_1 = require("./agent-defaults");
|
|
25
28
|
const external_mcp_1 = require("./external-mcp");
|
|
26
29
|
const workspace_manager_1 = require("./workspace-manager");
|
|
@@ -397,6 +400,17 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
397
400
|
// provider surfaces here with no edit. Apply per-provider beta gates: codex
|
|
398
401
|
// is forced unavailable when SPECRAILS_CODEX_BETA=0 (emergency rollback).
|
|
399
402
|
const gated = { ...providers };
|
|
403
|
+
// Local (OpenAI-compatible) engines have no CLI to `which`: their binary is
|
|
404
|
+
// the bundled node, so the PATH probe above always says "installed" even
|
|
405
|
+
// when the endpoint is down. Their availability is the bounded HTTP probe
|
|
406
|
+
// from the detection cycle (refreshed here so a first call after boot is
|
|
407
|
+
// not blind); an id the snapshot does not know stays unavailable.
|
|
408
|
+
const localIds = (0, providers_1.listAdapters)().filter(local_adapter_1.isLocalAdapter).map((adapter) => adapter.id);
|
|
409
|
+
if (localIds.length > 0) {
|
|
410
|
+
const snapshot = await (0, provider_detection_1.getDetectionSnapshot)();
|
|
411
|
+
for (const id of localIds)
|
|
412
|
+
gated[id] = snapshot.providers[id]?.usable === true;
|
|
413
|
+
}
|
|
400
414
|
if ((0, provider_detection_1.isCodexBetaDisabled)())
|
|
401
415
|
gated.codex = false;
|
|
402
416
|
// Gemini: enabled by default; forced unavailable only when
|
|
@@ -430,7 +444,10 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
430
444
|
adapter.id,
|
|
431
445
|
{ command: adapter.binary, args: [] },
|
|
432
446
|
]));
|
|
433
|
-
|
|
447
|
+
// Display names so app-level selectors (Agent composer, Builder) can label
|
|
448
|
+
// a local engine with its connection label instead of its raw id.
|
|
449
|
+
const labels = Object.fromEntries((0, providers_1.listAdapters)().map((adapter) => [adapter.id, adapter.displayName]));
|
|
450
|
+
res.json({ ...gated, tiers, providerIssues, launchDescriptors, labels });
|
|
434
451
|
});
|
|
435
452
|
router.get('/setup-prerequisites', (req, res) => {
|
|
436
453
|
const status = (0, setup_prerequisites_1.getSetupPrerequisitesStatus)();
|
|
@@ -1230,28 +1247,94 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
1230
1247
|
res.json({ language, monthlyBudgetUsd });
|
|
1231
1248
|
});
|
|
1232
1249
|
(0, runtime_role_prompts_router_1.registerRuntimeRolePromptRoutes)(router);
|
|
1250
|
+
// ─── Runtime provider connections (local AI engines) ──────────────────────
|
|
1251
|
+
// GET returns every connection plus a per-id status: local ids read the
|
|
1252
|
+
// detection cache (reachable/authState/models), CLI ids the detection
|
|
1253
|
+
// snapshot. PUT saves, re-syncs the local adapters, re-probes them (awaited)
|
|
1254
|
+
// and broadcasts `providers.detected_changed` when the usable set changed so
|
|
1255
|
+
// open engine selectors update live. POST /test probes an UNSAVED draft.
|
|
1256
|
+
const runtimeProviderStatus = (providers) => {
|
|
1257
|
+
const snapshot = (0, provider_detection_1.getSnapshotSync)();
|
|
1258
|
+
const status = {};
|
|
1259
|
+
for (const provider of providers) {
|
|
1260
|
+
if (provider.kind === 'openai-compatible') {
|
|
1261
|
+
const probe = (0, local_engine_detection_1.getCachedProbe)(provider.id);
|
|
1262
|
+
status[provider.id] = probe
|
|
1263
|
+
? {
|
|
1264
|
+
kind: 'local', reachable: probe.reachable, authState: probe.authState, models: probe.models,
|
|
1265
|
+
latencyMs: probe.latencyMs, ...(probe.error ? { error: probe.error } : {}),
|
|
1266
|
+
...(probe.apiKeyEnvMissing ? { apiKeyEnvMissing: true } : {}),
|
|
1267
|
+
}
|
|
1268
|
+
: { kind: 'local', reachable: false, authState: 'unknown', models: [], probing: true };
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
const row = snapshot?.providers[provider.cli];
|
|
1272
|
+
status[provider.id] = row
|
|
1273
|
+
? { kind: 'cli', reachable: row.usable, installed: row.installed, executable: row.executable, authState: row.authState, models: [], ...(row.version ? { version: row.version } : {}), ...(row.error ? { error: row.error } : {}) }
|
|
1274
|
+
: { kind: 'cli', reachable: false, installed: false, executable: false, authState: 'unknown', models: [], probing: true };
|
|
1275
|
+
}
|
|
1276
|
+
return status;
|
|
1277
|
+
};
|
|
1233
1278
|
router.get('/runtime-providers', (_req, res) => {
|
|
1234
1279
|
try {
|
|
1235
1280
|
for (const project of (0, desktop_db_1.listProjects)(registry.desktopDb))
|
|
1236
1281
|
(0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project);
|
|
1237
|
-
|
|
1282
|
+
const providers = (0, agent_runtime_settings_1.loadRuntimeProviders)();
|
|
1283
|
+
res.json({ providers, status: runtimeProviderStatus(providers), localEnginesEnabled: (0, local_adapter_registry_1.isLocalEnginesEnabled)() });
|
|
1238
1284
|
}
|
|
1239
1285
|
catch (error) {
|
|
1240
1286
|
res.status(422).json({ message: error.message });
|
|
1241
1287
|
}
|
|
1242
1288
|
});
|
|
1243
|
-
router.put('/runtime-providers', (req, res) => {
|
|
1289
|
+
router.put('/runtime-providers', async (req, res) => {
|
|
1290
|
+
let saved;
|
|
1244
1291
|
try {
|
|
1245
1292
|
const projects = (0, desktop_db_1.listProjects)(registry.desktopDb).map(project => (0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project));
|
|
1246
1293
|
const providers = (0, agent_runtime_settings_1.validateRuntimeProviders)(req.body.providers);
|
|
1247
1294
|
for (const config of projects)
|
|
1248
1295
|
if (config)
|
|
1249
1296
|
(0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...config, providers });
|
|
1250
|
-
|
|
1297
|
+
saved = (0, agent_runtime_settings_1.saveRuntimeProviders)(providers);
|
|
1251
1298
|
}
|
|
1252
1299
|
catch (error) {
|
|
1253
1300
|
res.status(error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? 400 : 500).json({ message: error.message });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
try {
|
|
1304
|
+
(0, local_adapter_registry_1.syncLocalAdapters)(saved);
|
|
1305
|
+
const { snapshot, changed } = await (0, provider_detection_1.refreshLocalDetection)();
|
|
1306
|
+
if (changed) {
|
|
1307
|
+
broadcast({
|
|
1308
|
+
type: 'providers.detected_changed',
|
|
1309
|
+
detected: snapshot.detected,
|
|
1310
|
+
providers: snapshot.providers,
|
|
1311
|
+
timestamp: new Date().toISOString(),
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1254
1314
|
}
|
|
1315
|
+
catch (error) {
|
|
1316
|
+
console.warn('[runtime-providers] local engine re-sync failed (non-fatal):', error.message);
|
|
1317
|
+
}
|
|
1318
|
+
res.json({ providers: saved, status: runtimeProviderStatus(saved), localEnginesEnabled: (0, local_adapter_registry_1.isLocalEnginesEnabled)() });
|
|
1319
|
+
});
|
|
1320
|
+
router.post('/runtime-providers/test', async (req, res) => {
|
|
1321
|
+
const body = (req.body ?? {});
|
|
1322
|
+
// Same shape rules as the connections schema (absolute http(s), no
|
|
1323
|
+
// credentials/query/fragment; env NAME only, never a key value).
|
|
1324
|
+
if (typeof body.baseUrl !== 'string' || !/^https?:\/\/[^/?#@]+(?:\/[^?#]*)?$/.test(body.baseUrl)) {
|
|
1325
|
+
res.status(400).json({ error: 'invalid_base_url', message: 'baseUrl must be an absolute HTTP(S) URL without credentials, query or fragment' });
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
if (body.apiKeyEnv !== undefined && body.apiKeyEnv !== null && body.apiKeyEnv !== '' && (typeof body.apiKeyEnv !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(body.apiKeyEnv))) {
|
|
1329
|
+
res.status(400).json({ error: 'invalid_api_key_env', message: 'apiKeyEnv must be an environment variable name' });
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const apiKeyEnv = typeof body.apiKeyEnv === 'string' && body.apiKeyEnv ? body.apiKeyEnv : undefined;
|
|
1333
|
+
const probe = await (0, local_engine_detection_1.probeConnection)({ baseUrl: body.baseUrl, apiKeyEnv });
|
|
1334
|
+
res.json({
|
|
1335
|
+
reachable: probe.reachable, authState: probe.authState, models: probe.models, latencyMs: probe.latencyMs,
|
|
1336
|
+
...(probe.error ? { error: probe.error } : {}), ...(probe.apiKeyEnvMissing ? { apiKeyEnvMissing: true } : {}),
|
|
1337
|
+
});
|
|
1255
1338
|
});
|
|
1256
1339
|
// ─── Specrails Agents defaults (global per-provider agent model/effort) ───
|
|
1257
1340
|
// App-level layer read AT SPAWN TIME by QueueManager / rails-router / loop
|
package/server/dist/index.js
CHANGED
|
@@ -53,6 +53,19 @@ const transient_children_1 = require("./transient-children");
|
|
|
53
53
|
// before any manager constructs a project context. See
|
|
54
54
|
// openspec/changes/add-multi-provider-support/specs/multi-provider-architecture/spec.md.
|
|
55
55
|
require("./providers");
|
|
56
|
+
const local_adapter_registry_1 = require("./providers/local-adapter-registry");
|
|
57
|
+
const agent_runtime_settings_1 = require("./agent-runtime-settings");
|
|
58
|
+
// Local AI engines: every OpenAI-compatible connection in
|
|
59
|
+
// ~/.specrails/runtime-providers.json becomes a registered adapter BEFORE the
|
|
60
|
+
// first detection cycle (kill switch SPECRAILS_LOCAL_ENGINES=false ⇒ no-op).
|
|
61
|
+
try {
|
|
62
|
+
const sync = (0, local_adapter_registry_1.syncLocalAdapters)((0, agent_runtime_settings_1.loadRuntimeProviders)());
|
|
63
|
+
if (sync.registered.length)
|
|
64
|
+
console.log(`[local-engines] registered: ${sync.registered.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.warn('[local-engines] could not load runtime provider connections (non-fatal):', err.message);
|
|
68
|
+
}
|
|
56
69
|
const hostControlToken = (0, host_control_1.consumeHostControlToken)();
|
|
57
70
|
const inheritedPathBeforeResolve = (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean).length;
|
|
58
71
|
// Backfill SystemRoot/ComSpec/etc into process.env BEFORE anything spawns, so a
|
|
@@ -39,6 +39,7 @@ const explore_stdin_session_1 = require("./explore-stdin-session");
|
|
|
39
39
|
const result_event_1 = require("./result-event");
|
|
40
40
|
const db_1 = require("./db");
|
|
41
41
|
const stream_display_1 = require("./util/stream-display");
|
|
42
|
+
const registry_1 = require("./providers/registry");
|
|
42
43
|
const runtime_1 = require("./providers/runtime");
|
|
43
44
|
const terminal_result_1 = require("./providers/terminal-result");
|
|
44
45
|
const claude_background_tasks_1 = require("./providers/claude-background-tasks");
|
|
@@ -227,7 +228,8 @@ class InteractiveJobSession {
|
|
|
227
228
|
// system context, not in the command's argument string. Preserve any
|
|
228
229
|
// existing appended system prompt supplied by the queue/loop owner.
|
|
229
230
|
const args = [...spec.args];
|
|
230
|
-
|
|
231
|
+
// Local runners share claude's argv shape for the appended system prompt.
|
|
232
|
+
if ((this._adapter.id === 'claude' || (0, registry_1.isLocalAdapterId)(this._adapter.id)) && this._settleMode === 'auto') {
|
|
231
233
|
const index = args.indexOf('--append-system-prompt');
|
|
232
234
|
if (index >= 0)
|
|
233
235
|
args[index + 1] = `${args[index + 1] ?? ''}\n\n${loop_constants_1.FOREGROUND_RULE}`;
|