specrails-desktop 2.46.0 → 2.47.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/client/dist/assets/{ActivityFeedPage-CsHQPpvl.js → ActivityFeedPage-DSCpSOGV.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-BVcCK7x0.js → AgentBrowserCapture-mc1c5pgS.js} +1 -1
- package/client/dist/assets/{AgentModeAnalyticsPane-C5F3m29G.js → AgentModeAnalyticsPane--UkamBTm.js} +2 -2
- package/client/dist/assets/{AgentModeCodePane-C5QuKsu0.js → AgentModeCodePane-DJCswsvU.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-rnD-EPMd.js → AgentModeJobsPane-Dw3DNFkh.js} +1 -1
- package/client/dist/assets/{AgentsPage-DiyJWZcU.js → AgentsPage-ChElPUL9.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-Dkmb0dnd.js → AnalyticsPage-BGtkFHBp.js} +1 -1
- package/client/dist/assets/{CodePage-C0dH99-t.js → CodePage-BBnw3VGn.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-DuTjr2n8.js → DesktopAnalyticsPage-B85tNScc.js} +1 -1
- package/client/dist/assets/{DocsDialog-BVZJZHVR.js → DocsDialog-D4cXrOMl.js} +1 -1
- package/client/dist/assets/{DocsPage-D6KHTv8V.js → DocsPage-BCAbC6qy.js} +1 -1
- package/client/dist/assets/{ExportDropdown-pWZGYCv3.js → ExportDropdown-BwuvqS2Y.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-Ck3PQxjN.js → InteractiveJobComposer-Coq-Zhk0.js} +2 -2
- package/client/dist/assets/{JobDetailModal-DkoMH3ie.js → JobDetailModal--Mjam0Nc.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CBVivcl2.js → JobDetailPage-B6ShWSBh.js} +1 -1
- package/client/dist/assets/{JobsPage-CzxEeca9.js → JobsPage-DHU_ep_B.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-CtmOE4-b.js → LoopBuilderPage-Byvq8Oqx.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-BYeLjQbr.js → LoopPreviewModal-BMC9Mgj1.js} +1 -1
- package/client/dist/assets/{LoopsPage-6iW7g25k.js → LoopsPage-CFGl-Nw8.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-B6wVevPq.js → MinimizedChatsContext-DHjHBYqa.js} +1 -1
- package/client/dist/assets/{PluginsPage-Bf1z3WPF.js → PluginsPage-BHygRnW6.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-DsCRIc4v.js → ProjectSettingsDialog-nspqxH-M.js} +1 -1
- package/client/dist/assets/{RepositoryDeliveries-BQm_3TO8.js → RepositoryDeliveries-Dq9NJOWi.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-DnAFtjTz.js → RepositoryScopeSelector-C3jKG2z0.js} +1 -1
- package/client/dist/assets/{ReviewPacketPage-CaXAwtR7.js → ReviewPacketPage-C9mqhi65.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-9aRFpJ_J.js → TemplatePreviewModal-BmwoAjOr.js} +1 -1
- package/client/dist/assets/{TicketDetailModalContext-B1X5glzV.js → TicketDetailModalContext-lYmoONrd.js} +1 -1
- package/client/dist/assets/{Trans-BwLgX6n7.js → Trans-C0KyPo2O.js} +1 -1
- package/client/dist/assets/agentRuntime-62GWdc26.js +1 -0
- package/client/dist/assets/agentRuntime-BN1mKAuY.js +1 -0
- package/client/dist/assets/agentRuntime-BNBCzQLM.js +1 -0
- package/client/dist/assets/agentRuntime-BQE_K1GG.js +1 -0
- package/client/dist/assets/agentRuntime-CCU3HCtS.js +1 -0
- package/client/dist/assets/agentRuntime-DT72Bpdv.js +1 -0
- package/client/dist/assets/agentRuntime-ZfMPeeiP.js +1 -0
- package/client/dist/assets/agentRuntime-g3W8Ry4d.js +1 -0
- package/client/dist/assets/{agents-IUOgqT5t.js → agents-BIT1UdTy.js} +1 -1
- package/client/dist/assets/{agents-DaYcxAr1.js → agents-C7fKfvm8.js} +1 -1
- package/client/dist/assets/{agents-ms7TwG16.js → agents-CST-lpYX.js} +1 -1
- package/client/dist/assets/{agents-Bvyi3y6R.js → agents-Cm2JYrAa.js} +1 -1
- package/client/dist/assets/{agents-BHp0_FQt.js → agents-DsgGMGwU.js} +1 -1
- package/client/dist/assets/{agents-45GbINfh.js → agents-DyI4rbHa.js} +1 -1
- package/client/dist/assets/{agents-rzPnqoBR.js → agents-H6tVQUNP.js} +1 -1
- package/client/dist/assets/{agents-DMKFStHL.js → agents-_5mJMkAE.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-CeZv93iR.js → formatDistanceToNow-D44phX1J.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DxAkjvJb.js → getTimezoneOffsetInMilliseconds-EAh9LFSx.js} +1 -1
- package/client/dist/assets/{index-DYoyDWim.js → index-DgMBighL.js} +9 -9
- package/client/dist/assets/{jira-api-ChjVGJx_.js → jira-api-BQR6G0YY.js} +1 -1
- package/client/dist/assets/{narration-D-e9ssxA.js → narration-B1uGfofH.js} +1 -1
- package/client/dist/assets/{narration-DnrKC3mF.js → narration-BKcL4Srl.js} +1 -1
- package/client/dist/assets/{narration-CvQdWdR5.js → narration-BtjKyHVM.js} +1 -1
- package/client/dist/assets/{narration-BPMOQPFe.js → narration-CqNvgRi9.js} +1 -1
- package/client/dist/assets/{narration-DKLjXIFI.js → narration-DiPyj06T.js} +1 -1
- package/client/dist/assets/{narration-4OSg0b5-.js → narration-DrBAf8uv.js} +1 -1
- package/client/dist/assets/{narration-Dypjxeel.js → narration-OyujFouL.js} +1 -1
- package/client/dist/assets/{narration-BSldQfjd.js → narration-UWi_xIZ-.js} +1 -1
- package/client/dist/assets/{project-repositories-BvBj9XH9.js → project-repositories-GRYdjlKB.js} +1 -1
- package/client/dist/assets/{spending-DNM6NAoA.js → spending-CN9PHM65.js} +1 -1
- package/client/dist/assets/{useDesktop-mxHVQOdx.js → useDesktop-iius_s5d.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-DnxHj8jP.js → useSharedWebSocket-D7hkZ3Ka.js} +2 -2
- package/client/dist/index.html +16 -16
- 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/package.json +1 -1
- package/server/dist/agent-runtime-bridge.js +29 -25
- package/server/dist/agent-runtime-controls-router.js +8 -0
- package/server/dist/agent-runtime-controls.js +57 -10
- package/server/dist/agent-runtime-effective-config.js +32 -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 +76 -0
- package/server/dist/agent-runtime-package.js +205 -0
- package/server/dist/agent-runtime-settings-router.js +25 -4
- package/server/dist/agent-runtime-settings.js +26 -0
- package/server/dist/loop-executors.js +2 -2
- package/server/dist/loop-run-manager.js +10 -2
- package/server/dist/mcp/tools/rails.js +1 -0
- package/server/dist/rail-isolated-launch.js +1 -0
- package/server/dist/rails-router.js +26 -0
- package/server/dist/schemas/agent-runtime.schema.json +18 -1
- package/server/dist/schemas/fixtures/runtime-efficiency-summary.v1.json +790 -0
- 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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runtimeEfficiencyEventLine = runtimeEfficiencyEventLine;
|
|
4
|
+
exports.isRecordedRuntimeEfficiencyEvent = isRecordedRuntimeEfficiencyEvent;
|
|
5
|
+
/** Efficiency events are advisory measurements, never workflow or delivery authority. */
|
|
6
|
+
function runtimeEfficiencyEventLine(line, runId) {
|
|
7
|
+
if (line.length > 16384)
|
|
8
|
+
return null;
|
|
9
|
+
let event;
|
|
10
|
+
try {
|
|
11
|
+
event = JSON.parse(line);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (event?.type !== 'runtime-efficiency-event' || event.schemaVersion !== 1 || event.runId !== runId || typeof event.eventId !== 'string' || !event.eventId.startsWith(runId + ':') || event.eventId.length > 256 || typeof event.attemptId !== 'string' || event.attemptId.length > 256 || !['role-context', 'role-route', 'check-started', 'check-finished', 'check-reused', 'check-invalidated'].includes(String(event.kind)))
|
|
17
|
+
return null;
|
|
18
|
+
if (!event.payload || typeof event.payload !== 'object' || Array.isArray(event.payload))
|
|
19
|
+
return null;
|
|
20
|
+
const raw = event.payload, payload = {};
|
|
21
|
+
for (const key of ['invocationId', 'provider', 'model', 'requestedEffort', 'kind', 'tier', 'routeReason', 'contextMode', 'status', 'executionId', 'repositoryId', 'checkId', 'label', 'reason']) {
|
|
22
|
+
const value = raw[key];
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
if (value !== null && (typeof value !== 'string' || value.length > 256))
|
|
26
|
+
return null;
|
|
27
|
+
payload[key] = value;
|
|
28
|
+
}
|
|
29
|
+
for (const key of ['ordinal', 'promptBytes', 'contextBytes', 'handoffBytes', 'durationMs', 'toolCalls']) {
|
|
30
|
+
if (raw[key] === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
if (typeof raw[key] !== 'number' || !Number.isFinite(raw[key]) || raw[key] < 0)
|
|
33
|
+
return null;
|
|
34
|
+
payload[key] = raw[key];
|
|
35
|
+
}
|
|
36
|
+
if (raw.exitCode !== undefined) {
|
|
37
|
+
if (typeof raw.exitCode !== 'number' || !Number.isSafeInteger(raw.exitCode))
|
|
38
|
+
return null;
|
|
39
|
+
payload.exitCode = raw.exitCode;
|
|
40
|
+
}
|
|
41
|
+
return JSON.stringify({ type: event.type, schemaVersion: 1, eventId: event.eventId, runId, attemptId: event.attemptId, kind: event.kind, payload });
|
|
42
|
+
}
|
|
43
|
+
function isRecordedRuntimeEfficiencyEvent(db, runId, line) {
|
|
44
|
+
const event = JSON.parse(line);
|
|
45
|
+
return Boolean(db.prepare("SELECT 1 FROM events WHERE job_id = ? AND event_type = 'runtime-efficiency-event' AND CASE WHEN json_valid(payload) THEN json_extract(payload, '$.eventId') END = ? LIMIT 1").get(runId, event.eventId));
|
|
46
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.writeRuntimeHistory = writeRuntimeHistory;
|
|
7
|
+
exports.readRuntimeHistory = readRuntimeHistory;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const agent_runtime_metrics_1 = require("./agent-runtime-metrics");
|
|
12
|
+
const hash = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
|
|
13
|
+
/** A replaceable projection, not an authority to resume, accept or deliver code. */
|
|
14
|
+
function writeRuntimeHistory(contextPath, input) {
|
|
15
|
+
const contextText = node_fs_1.default.readFileSync(contextPath, 'utf8'), context = JSON.parse(contextText);
|
|
16
|
+
let selection;
|
|
17
|
+
try {
|
|
18
|
+
selection = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-selection.json'), 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch { /* Old hosts have no explicit provenance. */ }
|
|
21
|
+
const result = { runId: context.runId, status: typeof input.status === 'string' ? input.status : 'failed', nextStep: typeof input.nextStep === 'string' ? input.nextStep : null,
|
|
22
|
+
updatedAt: new Date().toISOString(), ...(typeof input.error === 'string' ? { error: input.error.slice(0, 4000) } : {}),
|
|
23
|
+
metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(input.metrics), efficiencySummary: (0, agent_runtime_metrics_1.applyRuntimeSelectionOrigins)((0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(input.efficiencySummary), selection, context.runId), steps: {} };
|
|
24
|
+
const payload = JSON.stringify({ schemaVersion: 1, contextHash: hash(contextText), result });
|
|
25
|
+
const file = node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-history.json'), temporary = file + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
|
|
26
|
+
try {
|
|
27
|
+
node_fs_1.default.writeFileSync(temporary, JSON.stringify({ payload, integrity: hash(payload) }), { mode: 0o600, flag: 'wx' });
|
|
28
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
node_fs_1.default.rmSync(temporary, { force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function readRuntimeHistory(contextPath) {
|
|
35
|
+
const file = node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-history.json');
|
|
36
|
+
if (!node_fs_1.default.existsSync(file))
|
|
37
|
+
return null;
|
|
38
|
+
if (node_fs_1.default.lstatSync(file).isSymbolicLink() || node_fs_1.default.statSync(file).size > 128 * 1024)
|
|
39
|
+
throw new Error('Historical runtime projection is invalid');
|
|
40
|
+
const envelope = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
|
|
41
|
+
if (typeof envelope.payload !== 'string' || hash(envelope.payload) !== envelope.integrity)
|
|
42
|
+
throw new Error('Historical runtime projection integrity failed');
|
|
43
|
+
const value = JSON.parse(envelope.payload), contextText = node_fs_1.default.readFileSync(contextPath, 'utf8');
|
|
44
|
+
if (value.schemaVersion !== 1 || value.contextHash !== hash(contextText) || value.result?.runId !== JSON.parse(contextText).runId || !['running', 'succeeded', 'paused', 'failed', 'blocked', 'cancelled'].includes(value.result.status))
|
|
45
|
+
throw new Error('Historical runtime projection scope changed');
|
|
46
|
+
return { ...value.result, metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(value.result.metrics), efficiencySummary: (0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(value.result.efficiencySummary), steps: {} };
|
|
47
|
+
}
|
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.findCoreAgentRuntimeEntry = findCoreAgentRuntimeEntry;
|
|
4
4
|
exports.resetCoreAgentRuntimeApiCache = resetCoreAgentRuntimeApiCache;
|
|
5
5
|
exports.loadCoreAgentRuntime = loadCoreAgentRuntime;
|
|
6
|
+
exports.requireRuntimeCapabilities = requireRuntimeCapabilities;
|
|
7
|
+
exports.validateRoleCapabilities = validateRoleCapabilities;
|
|
8
|
+
exports.validateRequestedRoleEfforts = validateRequestedRoleEfforts;
|
|
6
9
|
exports.findCoreAgentRuntimeCli = findCoreAgentRuntimeCli;
|
|
7
10
|
const node_fs_1 = require("node:fs");
|
|
8
11
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -11,6 +14,7 @@ const node_path_1 = require("node:path");
|
|
|
11
14
|
const core_runtime_1 = require("./core-runtime");
|
|
12
15
|
const core_node_runtime_1 = require("./core-node-runtime");
|
|
13
16
|
const win_spawn_1 = require("./util/win-spawn");
|
|
17
|
+
const agent_runtime_package_1 = require("./agent-runtime-package");
|
|
14
18
|
/** An explicit or bundled installation is authoritative: never silently replace
|
|
15
19
|
* a broken production bundle with a developer's checkout. */
|
|
16
20
|
function findCoreAgentRuntimeEntry() {
|
|
@@ -42,7 +46,7 @@ function findCoreAgentRuntimeEntry() {
|
|
|
42
46
|
}
|
|
43
47
|
return null;
|
|
44
48
|
}
|
|
45
|
-
/** The api probe is stable for
|
|
49
|
+
/** The api probe is stable for executable package content; settings saves need not respawn it.
|
|
46
50
|
* Validation stays uncached because its answer depends on the submitted configuration. */
|
|
47
51
|
const apiProbeCache = new Map();
|
|
48
52
|
function resetCoreAgentRuntimeApiCache() { apiProbeCache.clear(); }
|
|
@@ -57,17 +61,34 @@ async function loadCoreAgentRuntime() {
|
|
|
57
61
|
cwd: (0, node_path_1.dirname)(cli), env: (0, win_spawn_1.windowsSpawnEnv)(process.env), input, encoding: 'utf8',
|
|
58
62
|
timeout: 15_000, maxBuffer: 2 * 1024 * 1024, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
59
63
|
});
|
|
60
|
-
const
|
|
64
|
+
const fingerprint = (0, agent_runtime_package_1.runtimeEntryFingerprint)(cli);
|
|
61
65
|
const cached = apiProbeCache.get(cli);
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
let api = cached?.fingerprint === fingerprint ? cached.api : undefined;
|
|
67
|
+
if (!api) {
|
|
68
|
+
api = JSON.parse(invoke(['api']));
|
|
64
69
|
if (api.type !== 'runtime-api' || api.apiVersion !== 1) {
|
|
65
70
|
throw new Error('Incompatible Core agent runtime API; expected version 1. Update the paired Core bundle.');
|
|
66
71
|
}
|
|
67
|
-
|
|
72
|
+
if (api.capabilities !== undefined && (!api.capabilities || typeof api.capabilities !== 'object' || Array.isArray(api.capabilities) || Object.values(api.capabilities).some(value => !Number.isSafeInteger(value) || value < 1)))
|
|
73
|
+
throw new Error('Core returned malformed runtime capabilities');
|
|
74
|
+
if (api.workflowVersions !== undefined && (!Array.isArray(api.workflowVersions) || !api.workflowVersions.every(value => typeof value === 'string' && /^\d+$/.test(value))))
|
|
75
|
+
throw new Error('Core returned malformed workflow versions');
|
|
76
|
+
if (api.runtimeIdentity !== undefined) {
|
|
77
|
+
const identity = api.runtimeIdentity;
|
|
78
|
+
if (!identity || identity.apiVersion !== 1 || !['packageVersion', 'workflowVersion', 'instructionsVersion', 'packageIntegrity'].every(key => typeof identity[key] === 'string') || !/^sha256:[a-f0-9]{64}$/.test(identity.packageIntegrity))
|
|
79
|
+
throw new Error('Core returned malformed runtime identity');
|
|
80
|
+
}
|
|
81
|
+
apiProbeCache.set(cli, { fingerprint, api });
|
|
68
82
|
}
|
|
69
83
|
return {
|
|
70
84
|
RUNTIME_API_VERSION: 1,
|
|
85
|
+
api,
|
|
86
|
+
capabilities(input) {
|
|
87
|
+
if (api.capabilities?.efficientRoleExecution !== 1)
|
|
88
|
+
throw new Error('Installed Core does not support role capability introspection. Update the paired Core package.');
|
|
89
|
+
const result = JSON.parse(invoke(['capabilities', '--stdin'], JSON.stringify(input)));
|
|
90
|
+
return { ...validateRoleCapabilities(result, input), runtimeIdentity: api.runtimeIdentity };
|
|
91
|
+
},
|
|
71
92
|
rolePromptDefaults() {
|
|
72
93
|
const result = JSON.parse(invoke(['prompts']));
|
|
73
94
|
if (result.type !== 'runtime-role-prompts' || !['architect', 'developer', 'reviewer'].every(role => typeof result.defaults?.[role] === 'string' && result.defaults[role].trim()))
|
|
@@ -75,6 +96,7 @@ async function loadCoreAgentRuntime() {
|
|
|
75
96
|
return result.defaults;
|
|
76
97
|
},
|
|
77
98
|
validateRuntimeConfig(input) {
|
|
99
|
+
requireRuntimeCapabilities(api, input);
|
|
78
100
|
const result = JSON.parse(invoke(['validate', '--stdin'], JSON.stringify(input)));
|
|
79
101
|
if (result.type !== 'runtime-config-valid')
|
|
80
102
|
throw new Error('Core did not validate the runtime configuration');
|
|
@@ -82,6 +104,63 @@ async function loadCoreAgentRuntime() {
|
|
|
82
104
|
},
|
|
83
105
|
};
|
|
84
106
|
}
|
|
107
|
+
function requireRuntimeCapabilities(api, input) {
|
|
108
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
109
|
+
return;
|
|
110
|
+
const config = input;
|
|
111
|
+
if ((config.efficiency !== undefined || Object.values(config.agents ?? {}).some(role => role?.effort !== undefined || role?.escalation !== undefined)) && api.capabilities?.efficientRoleExecution !== 1)
|
|
112
|
+
throw new Error('Requested role efficiency policy is unsupported by this Core runtime. Update the paired Core package.');
|
|
113
|
+
if (Array.isArray(config.verification) && config.verification.some(check => check?.policy !== undefined || check?.key !== undefined || check?.label !== undefined) && api.capabilities?.reproducibleVerification !== 1)
|
|
114
|
+
throw new Error('Requested verification policy is unsupported by this Core runtime. Update the paired Core package.');
|
|
115
|
+
}
|
|
116
|
+
/** Validate transport evidence against the exact submitted role selections. */
|
|
117
|
+
function validateRoleCapabilities(value, input) {
|
|
118
|
+
const invalid = () => { throw new Error('Core returned malformed role capabilities'); };
|
|
119
|
+
const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : invalid();
|
|
120
|
+
const result = object(value), config = object(input);
|
|
121
|
+
const choices = Object.entries(object(config.agents));
|
|
122
|
+
if (result.type !== 'runtime-capabilities' || result.schemaVersion !== 1 || !Array.isArray(result.roles) || result.roles.length > 6)
|
|
123
|
+
return invalid();
|
|
124
|
+
const expected = choices.flatMap(([role, value]) => {
|
|
125
|
+
const selected = object(value);
|
|
126
|
+
return [['base', selected], ...(selected.escalation ? [['escalation', object(selected.escalation)]] : [])].map(([tier, choice]) => {
|
|
127
|
+
const model = choice;
|
|
128
|
+
return { role, tier: String(tier), provider: selected.provider, model: model.model ?? null, requestedEffort: model.effort ?? null };
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
if (expected.length !== result.roles.length || expected.length < 3)
|
|
132
|
+
return invalid();
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const roles = result.roles.map(value => {
|
|
135
|
+
const row = object(value);
|
|
136
|
+
const selected = expected.find(item => Object.entries(item).every(([key, value]) => row[key] === value));
|
|
137
|
+
if (!selected)
|
|
138
|
+
return invalid();
|
|
139
|
+
const key = String(row.role) + ':' + String(row.tier);
|
|
140
|
+
if (seen.has(key))
|
|
141
|
+
return invalid();
|
|
142
|
+
seen.add(key);
|
|
143
|
+
if (typeof row.transport !== 'string' || !row.transport || row.transport.length > 128 || !['supported', 'unsupported', 'unknown'].includes(String(row.continuation)) || !['supported', 'unsupported', 'unknown'].includes(String(row.effortSupport)) || typeof row.observedModel !== 'boolean' || typeof row.observedEffort !== 'boolean')
|
|
144
|
+
return invalid();
|
|
145
|
+
if (row.supportedEfforts !== null && (!Array.isArray(row.supportedEfforts) || row.supportedEfforts.length > 32 || !row.supportedEfforts.every((level) => typeof level === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(level))))
|
|
146
|
+
return invalid();
|
|
147
|
+
const levels = row.supportedEfforts;
|
|
148
|
+
if (row.effortSupport === 'supported' && !levels?.length || row.effortSupport === 'unknown' && levels !== null || row.effortSupport === 'unsupported' && levels?.length !== 0)
|
|
149
|
+
return invalid();
|
|
150
|
+
return { ...selected, transport: row.transport, continuation: row.continuation, effortSupport: row.effortSupport, supportedEfforts: levels, observedModel: row.observedModel, observedEffort: row.observedEffort };
|
|
151
|
+
});
|
|
152
|
+
return { type: 'runtime-capabilities', schemaVersion: 1, roles };
|
|
153
|
+
}
|
|
154
|
+
function validateRequestedRoleEfforts(runtime, config) {
|
|
155
|
+
if (!Object.values(config.agents).some(role => role.effort !== undefined || role.escalation?.effort !== undefined))
|
|
156
|
+
return;
|
|
157
|
+
if (!runtime.capabilities)
|
|
158
|
+
throw new Error('Installed Core cannot validate requested role effort');
|
|
159
|
+
const result = validateRoleCapabilities(runtime.capabilities(config), config);
|
|
160
|
+
for (const row of result.roles)
|
|
161
|
+
if (row.requestedEffort !== null && (row.effortSupport !== 'supported' || !row.supportedEfforts?.includes(String(row.requestedEffort))))
|
|
162
|
+
throw new Error(`${row.role} (${row.tier}): effort '${row.requestedEffort}' is not confirmed for ${row.transport}. Select provider default or a supported effort.`);
|
|
163
|
+
}
|
|
85
164
|
function findCoreAgentRuntimeCli() {
|
|
86
165
|
const entry = findCoreAgentRuntimeEntry();
|
|
87
166
|
if (!entry)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.readRuntimeEfficiency = readRuntimeEfficiency;
|
|
4
|
+
exports.readRuntimeEfficiencySummary = readRuntimeEfficiencySummary;
|
|
5
|
+
exports.applyRuntimeSelectionOrigins = applyRuntimeSelectionOrigins;
|
|
4
6
|
const COUNTS = ['attempts', 'measuredAttempts', 'providerCalls', 'toolCalls', 'inputTokens', 'outputTokens', 'uncachedInputTokens', 'cacheReadInputTokens', 'cacheWriteInputTokens'];
|
|
5
7
|
const KEYS = [...COUNTS, 'durationMs', 'agentDurationMs', 'costUsd'];
|
|
6
8
|
const PHASES = ['architect', 'developer', 'verify', 'reviewer', 'archive'];
|
|
@@ -44,3 +46,77 @@ function readRuntimeEfficiency(value) {
|
|
|
44
46
|
}
|
|
45
47
|
return { schemaVersion: 1, total, phases };
|
|
46
48
|
}
|
|
49
|
+
function readRuntimeEfficiencySummary(value) {
|
|
50
|
+
const input = object(value), invocations = object(input?.invocations), checks = object(input?.checks);
|
|
51
|
+
if (!input || input.schemaVersion !== 1 || typeof input.runId !== 'string' || input.runId.length > 128 || typeof input.workflowVersion !== 'string' || input.workflowVersion.length > 16 || !['pending', 'validated', 'with-exceptions', 'blocked'].includes(String(input.technicalAcceptance)) || !invocations || !checks || !Array.isArray(input.roles) || input.roles.length > 3)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (typeof input.archive !== 'string' || input.archive.length > 32 || typeof input.delivery !== 'string' || input.delivery.length > 32 || typeof invocations.complete !== 'boolean' || typeof checks.available !== 'boolean')
|
|
54
|
+
return undefined;
|
|
55
|
+
const count = (value, nullable = true) => (nullable && value === null) || typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
56
|
+
for (const key of ['total', 'fullContexts', 'incrementalContexts'])
|
|
57
|
+
if (!count(invocations[key], false))
|
|
58
|
+
return undefined;
|
|
59
|
+
for (const key of ['promptBytes', 'contextBytes', 'handoffBytes'])
|
|
60
|
+
if (!count(invocations[key]))
|
|
61
|
+
return undefined;
|
|
62
|
+
if (checks.complete !== undefined && typeof checks.complete !== 'boolean' || checks.invalidated !== undefined && !count(checks.invalidated))
|
|
63
|
+
return undefined;
|
|
64
|
+
for (const key of ['executed', 'reused', 'notRun'])
|
|
65
|
+
if (!count(checks[key]))
|
|
66
|
+
return undefined;
|
|
67
|
+
if (checks.durationMs !== null && (typeof checks.durationMs !== 'number' || !Number.isFinite(checks.durationMs) || checks.durationMs < 0))
|
|
68
|
+
return undefined;
|
|
69
|
+
for (const key of ['planHash', 'candidateHash'])
|
|
70
|
+
if (input[key] !== null && (typeof input[key] !== 'string' || !/^[a-f0-9]{64}$/.test(input[key])))
|
|
71
|
+
return undefined;
|
|
72
|
+
if (!Array.isArray(input.currentEvidenceIds) || input.currentEvidenceIds.length > 100 || !input.currentEvidenceIds.every(id => typeof id === 'string' && /^[a-f0-9]{64}$/.test(id)))
|
|
73
|
+
return undefined;
|
|
74
|
+
const kinds = object(invocations.byKind);
|
|
75
|
+
if (!kinds || Object.keys(kinds).length !== 5 || !['initial', 'correction', 'repair', 'deepen', 'session-fallback'].every(key => count(kinds[key], false)))
|
|
76
|
+
return undefined;
|
|
77
|
+
const roles = [];
|
|
78
|
+
for (const raw of input.roles) {
|
|
79
|
+
const role = object(raw);
|
|
80
|
+
if (!role || !['architect', 'developer', 'reviewer'].includes(String(role.role)) || roles.some(item => item.role === role.role))
|
|
81
|
+
return undefined;
|
|
82
|
+
const row = { role: String(role.role) };
|
|
83
|
+
for (const key of ['provider', 'model', 'effort', 'observedModel', 'observedEffort', 'origin', 'tier']) {
|
|
84
|
+
if (role[key] !== null && (typeof role[key] !== 'string' || role[key].length > 256))
|
|
85
|
+
return undefined;
|
|
86
|
+
row[key] = role[key];
|
|
87
|
+
}
|
|
88
|
+
if (!['base', 'escalation'].includes(String(row.tier)) || typeof row.origin !== 'string')
|
|
89
|
+
return undefined;
|
|
90
|
+
roles.push(row);
|
|
91
|
+
}
|
|
92
|
+
let escalations;
|
|
93
|
+
if (input.escalations !== undefined) {
|
|
94
|
+
if (!Array.isArray(input.escalations) || input.escalations.length > 16 || typeof input.escalationsTruncated !== 'boolean')
|
|
95
|
+
return undefined;
|
|
96
|
+
escalations = [];
|
|
97
|
+
for (const item of input.escalations) {
|
|
98
|
+
const raw = object(item);
|
|
99
|
+
if (!raw || !['architect', 'developer', 'reviewer'].includes(String(raw.role)))
|
|
100
|
+
return undefined;
|
|
101
|
+
const row = { role: String(raw.role) };
|
|
102
|
+
for (const key of ['attemptId', 'provider', 'model', 'effort', 'reason']) {
|
|
103
|
+
if (raw[key] !== null && (typeof raw[key] !== 'string' || raw[key].length > 256) || ['attemptId', 'provider'].includes(key) && typeof raw[key] !== 'string')
|
|
104
|
+
return undefined;
|
|
105
|
+
row[key] = raw[key];
|
|
106
|
+
}
|
|
107
|
+
escalations.push(row);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ...(escalations ? { escalations, escalationsTruncated: input.escalationsTruncated } : {}), schemaVersion: 1, currentEvidenceIds: [...input.currentEvidenceIds], planHash: input.planHash, candidateHash: input.candidateHash, runId: input.runId, workflowVersion: input.workflowVersion, technicalAcceptance: input.technicalAcceptance, archive: input.archive, delivery: input.delivery, roles,
|
|
111
|
+
invocations: { total: invocations.total, complete: invocations.complete, byKind: { ...kinds }, promptBytes: invocations.promptBytes, contextBytes: invocations.contextBytes, handoffBytes: invocations.handoffBytes, fullContexts: invocations.fullContexts, incrementalContexts: invocations.incrementalContexts },
|
|
112
|
+
checks: { ...(checks.complete === undefined ? {} : { complete: checks.complete }), ...(checks.invalidated === undefined ? {} : { invalidated: checks.invalidated }), available: checks.available, executed: checks.executed, reused: checks.reused, notRun: checks.notRun, durationMs: checks.durationMs } };
|
|
113
|
+
}
|
|
114
|
+
/** Provenance is host-owned; it never changes Core's frozen provider selection. */
|
|
115
|
+
function applyRuntimeSelectionOrigins(summary, selection, runId) {
|
|
116
|
+
const record = object(selection), origins = object(record?.origins);
|
|
117
|
+
if (!summary || record?.schemaVersion !== 1 || record.runId !== runId || !origins)
|
|
118
|
+
return summary;
|
|
119
|
+
if (!['architect', 'developer', 'reviewer'].every(role => ['project-role', 'default', ...(role === 'developer' ? ['explicit-launch-override'] : [])].includes(String(origins[role]))))
|
|
120
|
+
return summary;
|
|
121
|
+
return { ...summary, roles: summary.roles.map(role => ({ ...role, origin: String(origins[role.role]) })) };
|
|
122
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runtimeEntryFingerprint = runtimeEntryFingerprint;
|
|
7
|
+
exports.retainAgentRuntime = retainAgentRuntime;
|
|
8
|
+
exports.resolveRetainedAgentRuntime = resolveRetainedAgentRuntime;
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_module_1 = require("node:module");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const SHIPPED = ['package.json', 'dist', 'bin', 'templates', 'schemas', 'commands', 'integration-contract.json', 'pinned-versions.json'];
|
|
14
|
+
const SAFE_PACKAGE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
|
|
15
|
+
function packageRoot(file, name) {
|
|
16
|
+
let directory = node_path_1.default.dirname(node_fs_1.default.realpathSync(file));
|
|
17
|
+
for (;;) {
|
|
18
|
+
const manifest = node_path_1.default.join(directory, 'package.json');
|
|
19
|
+
if (node_fs_1.default.existsSync(manifest)) {
|
|
20
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf8'));
|
|
21
|
+
if (pkg.name === (name ?? 'specrails-core'))
|
|
22
|
+
return directory;
|
|
23
|
+
}
|
|
24
|
+
const parent = node_path_1.default.dirname(directory);
|
|
25
|
+
if (directory === parent)
|
|
26
|
+
throw new Error('Core runtime does not resolve to an identifiable package');
|
|
27
|
+
directory = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Content identity also detects same-size edits with preserved timestamps. */
|
|
31
|
+
function runtimeEntryFingerprint(cli) {
|
|
32
|
+
let root;
|
|
33
|
+
try {
|
|
34
|
+
root = packageRoot(cli);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return (0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(cli)).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
return treeDigest(root, SHIPPED);
|
|
40
|
+
}
|
|
41
|
+
function treeDigest(root, entries = node_fs_1.default.readdirSync(root).sort()) {
|
|
42
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
43
|
+
let files = 0, bytes = 0;
|
|
44
|
+
const visit = (relative) => {
|
|
45
|
+
const file = node_path_1.default.join(root, relative), stat = node_fs_1.default.lstatSync(file);
|
|
46
|
+
if (stat.isSymbolicLink())
|
|
47
|
+
throw new Error('Retained Core package contains a symbolic link');
|
|
48
|
+
if (stat.isDirectory())
|
|
49
|
+
for (const name of node_fs_1.default.readdirSync(file).sort())
|
|
50
|
+
visit(node_path_1.default.posix.join(relative, name));
|
|
51
|
+
else if (stat.isFile()) {
|
|
52
|
+
if (++files > 100_000 || (bytes += stat.size) > 1024 * 1024 * 1024)
|
|
53
|
+
throw new Error('Core runtime package exceeds retention limits');
|
|
54
|
+
hash.update(JSON.stringify([relative, stat.size])).update('\0').update(node_fs_1.default.readFileSync(file)).update('\0');
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
throw new Error('Core runtime package contains a nonregular file');
|
|
58
|
+
};
|
|
59
|
+
for (const entry of entries)
|
|
60
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(root, entry)))
|
|
61
|
+
visit(entry);
|
|
62
|
+
return hash.digest('hex');
|
|
63
|
+
}
|
|
64
|
+
/** Copy only the selected package and its installed production dependency
|
|
65
|
+
* closure. Resolution follows npm's actual nested versions, without downloads. */
|
|
66
|
+
function copyInstalledPackage(source, destination) {
|
|
67
|
+
const installed = new Map();
|
|
68
|
+
const copying = new Set();
|
|
69
|
+
let files = 0, bytes = 0, packages = 0;
|
|
70
|
+
const copy = (from, to, boundary) => {
|
|
71
|
+
const stat = node_fs_1.default.lstatSync(from);
|
|
72
|
+
if (stat.isSymbolicLink()) {
|
|
73
|
+
const resolved = node_fs_1.default.realpathSync(from), relative = node_path_1.default.relative(boundary, resolved);
|
|
74
|
+
if (relative.startsWith('..') || node_path_1.default.isAbsolute(relative))
|
|
75
|
+
throw new Error('Core package symlink escapes its package');
|
|
76
|
+
copy(resolved, to, boundary);
|
|
77
|
+
}
|
|
78
|
+
else if (stat.isDirectory()) {
|
|
79
|
+
const real = node_fs_1.default.realpathSync(from);
|
|
80
|
+
if (copying.has(real))
|
|
81
|
+
throw new Error('Core package contains a cyclic symbolic link');
|
|
82
|
+
copying.add(real);
|
|
83
|
+
node_fs_1.default.mkdirSync(to, { recursive: true });
|
|
84
|
+
try {
|
|
85
|
+
for (const name of node_fs_1.default.readdirSync(from).sort())
|
|
86
|
+
if (!['node_modules', '.git', '.bin'].includes(name))
|
|
87
|
+
copy(node_path_1.default.join(from, name), node_path_1.default.join(to, name), boundary);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
copying.delete(real);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (stat.isFile()) {
|
|
94
|
+
if (++files > 100_000 || (bytes += stat.size) > 1024 * 1024 * 1024)
|
|
95
|
+
throw new Error('Core runtime package exceeds retention limits');
|
|
96
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(to), { recursive: true });
|
|
97
|
+
node_fs_1.default.copyFileSync(from, to);
|
|
98
|
+
node_fs_1.default.chmodSync(to, stat.mode & 0o777);
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
throw new Error('Core package contains a nonregular file');
|
|
102
|
+
};
|
|
103
|
+
const resolveDependency = (name, from) => {
|
|
104
|
+
const require = (0, node_module_1.createRequire)(node_path_1.default.join(from, 'package.json'));
|
|
105
|
+
try {
|
|
106
|
+
return packageRoot(require.resolve(name + '/package.json'), name);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return packageRoot(require.resolve(name), name);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const install = (from, to, root = false) => {
|
|
113
|
+
if (++packages > 2000)
|
|
114
|
+
throw new Error('Core dependency closure exceeds retention limits');
|
|
115
|
+
if (root)
|
|
116
|
+
for (const entry of SHIPPED) {
|
|
117
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(from, entry)))
|
|
118
|
+
copy(node_path_1.default.join(from, entry), node_path_1.default.join(to, entry), from);
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
copy(from, to, from);
|
|
122
|
+
installed.set(node_fs_1.default.realpathSync(to), from);
|
|
123
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(from, 'package.json'), 'utf8'));
|
|
124
|
+
for (const name of Object.keys({ ...pkg.dependencies, ...pkg.optionalDependencies }).sort()) {
|
|
125
|
+
if (!SAFE_PACKAGE.test(name))
|
|
126
|
+
throw new Error('Invalid Core package dependency');
|
|
127
|
+
let dependency;
|
|
128
|
+
try {
|
|
129
|
+
dependency = resolveDependency(name, from);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (pkg.optionalDependencies?.[name])
|
|
133
|
+
continue;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const present = resolveDependency(name, to);
|
|
138
|
+
if (installed.get(present) === dependency)
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
catch { /* Not copied into this resolution scope yet. */ }
|
|
142
|
+
install(dependency, node_path_1.default.join(to, 'node_modules', name));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
install(source, destination, true);
|
|
146
|
+
}
|
|
147
|
+
function retainAgentRuntime(cli, contextPath) {
|
|
148
|
+
const pinFile = node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-package.json');
|
|
149
|
+
if (node_fs_1.default.existsSync(pinFile))
|
|
150
|
+
return resolveRetainedAgentRuntime(contextPath);
|
|
151
|
+
const realCli = node_fs_1.default.realpathSync(cli);
|
|
152
|
+
const root = packageRoot(realCli);
|
|
153
|
+
const relativeCli = node_path_1.default.relative(root, realCli);
|
|
154
|
+
if (relativeCli.startsWith('..') || node_path_1.default.isAbsolute(relativeCli))
|
|
155
|
+
throw new Error('Core CLI escapes its package');
|
|
156
|
+
const cache = node_path_1.default.join(node_path_1.default.dirname(node_path_1.default.dirname(contextPath)), 'runtime-packages');
|
|
157
|
+
node_fs_1.default.mkdirSync(cache, { recursive: true, mode: 0o700 });
|
|
158
|
+
const staged = node_path_1.default.join(cache, '.staged-' + (0, node_crypto_1.randomUUID)());
|
|
159
|
+
try {
|
|
160
|
+
copyInstalledPackage(root, staged);
|
|
161
|
+
const integrity = treeDigest(staged);
|
|
162
|
+
const retained = node_path_1.default.join(cache, integrity);
|
|
163
|
+
if (node_fs_1.default.existsSync(retained)) {
|
|
164
|
+
if (treeDigest(retained) !== integrity)
|
|
165
|
+
throw new Error('Retained Core runtime integrity changed');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
try {
|
|
169
|
+
node_fs_1.default.renameSync(staged, retained);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (!node_fs_1.default.existsSync(retained) || treeDigest(retained) !== integrity)
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const pin = { schemaVersion: 1, packageVersion: JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(root, 'package.json'), 'utf8')).version, integrity, root: retained, cli: relativeCli };
|
|
177
|
+
try {
|
|
178
|
+
node_fs_1.default.writeFileSync(pinFile, JSON.stringify(pin) + '\n', { flag: 'wx', mode: 0o600 });
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.code !== 'EEXIST' || resolveRetainedAgentRuntime(contextPath) !== node_path_1.default.join(retained, relativeCli))
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
return node_path_1.default.join(retained, relativeCli);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
node_fs_1.default.rmSync(staged, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function resolveRetainedAgentRuntime(contextPath) {
|
|
191
|
+
const pinFile = node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-package.json');
|
|
192
|
+
if (!node_fs_1.default.existsSync(pinFile))
|
|
193
|
+
throw new Error('Original runtime identity is unrecorded. Restore a proven original Core package before continuing this saved execution.');
|
|
194
|
+
const pin = JSON.parse(node_fs_1.default.readFileSync(pinFile, 'utf8'));
|
|
195
|
+
const expected = node_path_1.default.join(node_path_1.default.dirname(node_path_1.default.dirname(contextPath)), 'runtime-packages', pin.integrity);
|
|
196
|
+
if (pin.schemaVersion !== 1 || !/^[a-f0-9]{64}$/.test(pin.integrity) || pin.root !== expected || !node_fs_1.default.existsSync(expected) || node_fs_1.default.realpathSync(expected) !== node_path_1.default.resolve(expected)
|
|
197
|
+
|| typeof pin.cli !== 'string' || node_path_1.default.isAbsolute(pin.cli) || pin.cli.split(/[\\/]/).includes('..'))
|
|
198
|
+
throw new Error('The original retained Core package is unavailable or invalid. Restore it before continuing.');
|
|
199
|
+
if (treeDigest(expected) !== pin.integrity)
|
|
200
|
+
throw new Error('The original retained Core package failed its integrity check. Restore it before continuing.');
|
|
201
|
+
const cli = node_path_1.default.join(expected, pin.cli);
|
|
202
|
+
if (!node_fs_1.default.statSync(cli).isFile())
|
|
203
|
+
throw new Error('The original Core executable is unavailable');
|
|
204
|
+
return cli;
|
|
205
|
+
}
|
|
@@ -7,12 +7,17 @@ const agent_runtime_settings_1 = require("./agent-runtime-settings");
|
|
|
7
7
|
const agent_runtime_verification_suggestions_1 = require("./agent-runtime-verification-suggestions");
|
|
8
8
|
const project_repositories_1 = require("./project-repositories");
|
|
9
9
|
function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
|
|
10
|
-
router.get('/:projectId/agent-runtime/config', (_req, res) => {
|
|
10
|
+
router.get('/:projectId/agent-runtime/config', async (_req, res) => {
|
|
11
11
|
try {
|
|
12
12
|
const project = ctx(_req).project;
|
|
13
13
|
const configured = (0, node_fs_1.existsSync)((0, agent_runtime_settings_1.agentRuntimeConfigPath)(project));
|
|
14
14
|
const config = (0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project);
|
|
15
|
-
|
|
15
|
+
let efficiencyAvailable = false;
|
|
16
|
+
try {
|
|
17
|
+
efficiencyAvailable = (await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).api?.capabilities?.efficientRoleExecution === 1;
|
|
18
|
+
}
|
|
19
|
+
catch { /* Ordinary settings remain readable without capability support. */ }
|
|
20
|
+
res.json({ configured, config, runtimeAvailable: (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null, efficiencyAvailable });
|
|
16
21
|
}
|
|
17
22
|
catch (err) {
|
|
18
23
|
const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
|
|
@@ -32,6 +37,18 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
|
|
|
32
37
|
res.status(500).json({ error: 'verification_suggestions_failed', message: 'Could not inspect the project repositories' });
|
|
33
38
|
}
|
|
34
39
|
});
|
|
40
|
+
router.post('/:projectId/agent-runtime/capabilities', async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const config = (0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...req.body, providers: (0, agent_runtime_settings_1.loadRuntimeProviders)(), enabled: true });
|
|
43
|
+
const runtime = await (0, agent_runtime_loader_1.loadCoreAgentRuntime)();
|
|
44
|
+
if (!runtime.capabilities)
|
|
45
|
+
throw new Error('Installed Core does not support capability introspection');
|
|
46
|
+
res.json(await runtime.capabilities(config));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
res.status(error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? 400 : 503).json({ error: 'runtime_capabilities_unavailable', message: error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? error.message : 'Update the paired Core runtime to inspect model capabilities' });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
35
52
|
router.put('/:projectId/agent-runtime/config', async (req, res) => {
|
|
36
53
|
try {
|
|
37
54
|
(0, agent_runtime_settings_1.validateAgentRuntimeConfig)(req.body);
|
|
@@ -41,9 +58,13 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
|
|
|
41
58
|
res.status(503).json({ error: 'runtime_unavailable', message: 'Update Core to an installation with the agent runtime before enabling it' });
|
|
42
59
|
return;
|
|
43
60
|
}
|
|
61
|
+
let efficiencyAvailable = false;
|
|
44
62
|
if (config.enabled && runtimeAvailable) {
|
|
45
63
|
try {
|
|
46
|
-
|
|
64
|
+
const runtime = await (0, agent_runtime_loader_1.loadCoreAgentRuntime)();
|
|
65
|
+
runtime.validateRuntimeConfig(config);
|
|
66
|
+
(0, agent_runtime_loader_1.validateRequestedRoleEfforts)(runtime, config);
|
|
67
|
+
efficiencyAvailable = runtime.api?.capabilities?.efficientRoleExecution === 1;
|
|
47
68
|
}
|
|
48
69
|
catch {
|
|
49
70
|
res.status(503).json({ error: 'runtime_incompatible', message: 'The installed Core runtime could not validate this configuration; update Core and retry' });
|
|
@@ -51,7 +72,7 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
|
|
|
51
72
|
}
|
|
52
73
|
}
|
|
53
74
|
const saved = (0, agent_runtime_settings_1.saveAgentRuntimeConfig)(ctx(req).project, config);
|
|
54
|
-
res.json({ configured: true, config: saved, runtimeAvailable });
|
|
75
|
+
res.json({ configured: true, config: saved, runtimeAvailable, efficiencyAvailable });
|
|
55
76
|
}
|
|
56
77
|
catch (err) {
|
|
57
78
|
const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
|
|
@@ -17,6 +17,7 @@ exports.saveAgentRuntimeConfig = saveAgentRuntimeConfig;
|
|
|
17
17
|
exports.validateRuntimeRolePrompts = validateRuntimeRolePrompts;
|
|
18
18
|
exports.loadRuntimeRolePrompts = loadRuntimeRolePrompts;
|
|
19
19
|
exports.saveRuntimeRolePrompts = saveRuntimeRolePrompts;
|
|
20
|
+
exports.validateRuntimeProviderOverride = validateRuntimeProviderOverride;
|
|
20
21
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
23
|
const node_os_1 = __importDefault(require("node:os"));
|
|
@@ -70,6 +71,11 @@ function validateAgentRuntimeConfig(input) {
|
|
|
70
71
|
throw new AgentRuntimeConfigError(`Role ${role} requires a model for its API provider`);
|
|
71
72
|
if (agent.model !== undefined && !agent.model.trim())
|
|
72
73
|
throw new AgentRuntimeConfigError(`Role ${role} requires a nonempty model identifier`);
|
|
74
|
+
for (const value of [agent.model, agent.effort, agent.escalation?.model, agent.escalation?.effort])
|
|
75
|
+
if (value !== undefined && (!value.trim() || value.includes('\0')))
|
|
76
|
+
throw new AgentRuntimeConfigError(`Role ${role} has an invalid model or effort`);
|
|
77
|
+
if (agent.escalation && (!agent.model || (agent.escalation.model === agent.model && agent.escalation.effort === agent.effort)))
|
|
78
|
+
throw new AgentRuntimeConfigError(`Role ${role} escalation requires an explicit base and a different model or effort`);
|
|
73
79
|
if (agent.maxTurns !== undefined && !Number.isSafeInteger(agent.maxTurns))
|
|
74
80
|
throw new AgentRuntimeConfigError(`Role ${role} maxTurns must be a safe integer`);
|
|
75
81
|
}
|
|
@@ -77,7 +83,13 @@ function validateAgentRuntimeConfig(input) {
|
|
|
77
83
|
if (!Number.isFinite(value) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value)))
|
|
78
84
|
throw new AgentRuntimeConfigError('Workflow limits must be finite, safe numbers');
|
|
79
85
|
}
|
|
86
|
+
const keys = config.verification.flatMap(command => command.key ? [command.key] : []);
|
|
87
|
+
if (new Set(keys).size !== keys.length)
|
|
88
|
+
throw new AgentRuntimeConfigError('Verification check keys must be unique');
|
|
80
89
|
for (const command of config.verification) {
|
|
90
|
+
for (const value of [command.label, ...(command.policy?.inputs ?? []), ...(command.policy?.toolchainInputs ?? []), ...(command.policy?.resources ?? [])])
|
|
91
|
+
if (value !== undefined && (!value.trim() || value.includes('\0')))
|
|
92
|
+
throw new AgentRuntimeConfigError('Invalid verification label or policy input');
|
|
81
93
|
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
94
|
throw new AgentRuntimeConfigError('Verification command contains an empty or invalid value');
|
|
83
95
|
if (command.timeoutMs !== undefined && !Number.isSafeInteger(command.timeoutMs))
|
|
@@ -237,3 +249,17 @@ function saveRuntimeRolePrompts(input) {
|
|
|
237
249
|
atomicJson(node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-role-prompts.json'), prompts);
|
|
238
250
|
return prompts;
|
|
239
251
|
}
|
|
252
|
+
function validateRuntimeProviderOverride(value) {
|
|
253
|
+
if (value === undefined)
|
|
254
|
+
return undefined;
|
|
255
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
256
|
+
throw new AgentRuntimeConfigError('Invalid runtime provider override');
|
|
257
|
+
const raw = value;
|
|
258
|
+
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))
|
|
259
|
+
throw new AgentRuntimeConfigError('Invalid runtime provider override provider');
|
|
260
|
+
if (raw.model !== undefined && (typeof raw.model !== 'string' || !raw.model.trim() || raw.model.length > 256 || /^-|[\r\n\0]/.test(raw.model)))
|
|
261
|
+
throw new AgentRuntimeConfigError('Invalid runtime provider override model');
|
|
262
|
+
if (raw.effort !== undefined && (typeof raw.effort !== 'string' || !/^[a-z][a-z0-9_-]{0,31}$/.test(raw.effort)))
|
|
263
|
+
throw new AgentRuntimeConfigError('Invalid runtime provider override effort');
|
|
264
|
+
return structuredClone(raw);
|
|
265
|
+
}
|