@minionry/sdk 0.4.23 → 0.5.2
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/dist/cdn/v1.js +14 -14
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/provider.js +86 -2
- package/dist/transport.d.ts +1 -1
- package/dist/transports/mock.d.ts +7 -0
- package/dist/transports/mock.js +784 -86
- package/dist/types.d.ts +137 -4
- package/dist/types.js +5 -0
- package/package.json +2 -2
package/dist/transports/mock.js
CHANGED
|
@@ -45,7 +45,16 @@ const MOCK_FEATURES = [
|
|
|
45
45
|
'agentsAppEndpoint',
|
|
46
46
|
'qualityFindingState',
|
|
47
47
|
'agentsModelSelection',
|
|
48
|
+
'filesChangeKinds',
|
|
49
|
+
'filesConditionalWrite',
|
|
50
|
+
'inferenceFastMode',
|
|
48
51
|
'qualitySchedules',
|
|
52
|
+
'pmChains',
|
|
53
|
+
'agentsEngineSelection',
|
|
54
|
+
'agentSchedules',
|
|
55
|
+
'scheduleWallClock',
|
|
56
|
+
'pmScheduleZip',
|
|
57
|
+
'pmScheduleRunNow',
|
|
49
58
|
];
|
|
50
59
|
const DEFAULT_BROWSER_TABS = [
|
|
51
60
|
{
|
|
@@ -111,6 +120,11 @@ const METHOD_SCOPES = {
|
|
|
111
120
|
'agents.sessions.answerQuestion': 'agents:sessions',
|
|
112
121
|
'agents.sessions.prompt': 'agents:sessions',
|
|
113
122
|
'agents.sessions.stop': 'agents:sessions',
|
|
123
|
+
'agents.schedules.list': 'agents:schedule',
|
|
124
|
+
'agents.schedules.create': 'agents:schedule',
|
|
125
|
+
'agents.schedules.update': 'agents:schedule',
|
|
126
|
+
'agents.schedules.delete': 'agents:schedule',
|
|
127
|
+
'agents.schedules.runNow': 'agents:schedule',
|
|
114
128
|
'apps.open': null,
|
|
115
129
|
cancel: null,
|
|
116
130
|
'pm.createBoard': 'pm:write',
|
|
@@ -130,6 +144,11 @@ const METHOD_SCOPES = {
|
|
|
130
144
|
'pm.schedules.create': 'pm:schedule',
|
|
131
145
|
'pm.schedules.update': 'pm:schedule',
|
|
132
146
|
'pm.schedules.delete': 'pm:schedule',
|
|
147
|
+
'pm.schedules.runNow': 'pm:schedule',
|
|
148
|
+
'pm.chains.list': 'pm:chain',
|
|
149
|
+
'pm.chains.create': 'pm:chain',
|
|
150
|
+
'pm.chains.update': 'pm:chain',
|
|
151
|
+
'pm.chains.delete': 'pm:chain',
|
|
133
152
|
'files.read': 'files:app',
|
|
134
153
|
'files.readEntry': 'files:app',
|
|
135
154
|
'files.write': 'files:app',
|
|
@@ -271,16 +290,19 @@ const MOCK_ENDPOINT_REGISTER_KEYS = new Set([
|
|
|
271
290
|
const MOCK_CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
272
291
|
const MOCK_WHITESPACE_OR_CONTROL = /[\s\u0000-\u001f\u007f]/;
|
|
273
292
|
const MOCK_LOCAL_ONLY_SUFFIXES = ['localhost', 'local', 'internal', 'home.arpa'];
|
|
274
|
-
const MOCK_RUN_PARAMS_SHAPE = '{ prompt, title?, model?, modelSelection?, systemPrompt?, budget?: { maxTurns?, maxSeconds? }, sessionId? }';
|
|
293
|
+
const MOCK_RUN_PARAMS_SHAPE = '{ prompt, title?, model?, modelSelection?, engineSelection?, systemPrompt?, budget?: { maxTurns?, maxSeconds? }, sessionId? }';
|
|
275
294
|
const MOCK_RUN_PARAM_KEYS = new Set([
|
|
276
295
|
'prompt',
|
|
277
296
|
'title',
|
|
278
297
|
'model',
|
|
279
298
|
'modelSelection',
|
|
299
|
+
'engineSelection',
|
|
280
300
|
'systemPrompt',
|
|
281
301
|
'budget',
|
|
282
302
|
'sessionId',
|
|
283
303
|
]);
|
|
304
|
+
const MOCK_ENGINE_SELECTION_SHAPE = '{ engine?, model?, effortLevel?, fastMode? }';
|
|
305
|
+
const MOCK_ENGINE_SELECTION_KEYS = new Set(['engine', 'model', 'effortLevel', 'fastMode']);
|
|
284
306
|
const MOCK_RUN_BUDGET_KEYS = new Set(['maxTurns', 'maxSeconds']);
|
|
285
307
|
const utf8ByteLength = (value) => new TextEncoder().encode(value).length;
|
|
286
308
|
function mockValidateEndpointUrl(value) {
|
|
@@ -314,6 +336,7 @@ function mockValidateEndpointUrl(value) {
|
|
|
314
336
|
}
|
|
315
337
|
return { ok: true };
|
|
316
338
|
}
|
|
339
|
+
const MOCK_SCHEDULE_MIN_LATENESS_MS = 60_000;
|
|
317
340
|
const MOCK_TOOL_CALL_TIMEOUT_MS = 30_000;
|
|
318
341
|
const MOCK_TOOL_CALL_TIMEOUT_MAX_MS = 120_000;
|
|
319
342
|
const MOCK_GIT_MAIN_WORKTREE_PATH = '/workspace';
|
|
@@ -730,6 +753,11 @@ const mockQualityHistoryEntry = (report) => ({
|
|
|
730
753
|
});
|
|
731
754
|
const MOCK_QUALITY_FINGERPRINTS_PER_CALL = 1_000;
|
|
732
755
|
const MOCK_QUALITY_REASON_MAX = 500;
|
|
756
|
+
const MAX_CHAIN_DEPTH = 5;
|
|
757
|
+
const MAX_OUT_LINKS_PER_BOARD = 3;
|
|
758
|
+
const MAX_LINKS_PER_APP = 20;
|
|
759
|
+
const MAX_LINKS_PER_SPACE = 100;
|
|
760
|
+
const MAX_CHAIN_HISTORY = 20;
|
|
733
761
|
export function createMockTransport(config = {}) {
|
|
734
762
|
const space = { ...DEFAULT_SPACE, ...config.space };
|
|
735
763
|
const user = config.user === null ? null : { ...DEFAULT_USER, ...config.user };
|
|
@@ -746,6 +774,13 @@ export function createMockTransport(config = {}) {
|
|
|
746
774
|
const files = new Map(Object.entries(config.files ?? {}));
|
|
747
775
|
const dirs = new Set();
|
|
748
776
|
const timestamps = new Map();
|
|
777
|
+
let lastStamp = 0;
|
|
778
|
+
const stamp = (path) => {
|
|
779
|
+
lastStamp = Math.max(Date.now(), lastStamp + 1);
|
|
780
|
+
timestamps.set(path, new Date(lastStamp).toISOString());
|
|
781
|
+
};
|
|
782
|
+
for (const path of files.keys())
|
|
783
|
+
stamp(path);
|
|
749
784
|
const uploads = new Map();
|
|
750
785
|
const downloads = new Map();
|
|
751
786
|
const storage = new Map();
|
|
@@ -763,6 +798,8 @@ export function createMockTransport(config = {}) {
|
|
|
763
798
|
const execRunsByRunId = new Map();
|
|
764
799
|
const execRunsByRequestId = new Map();
|
|
765
800
|
const schedules = new Map();
|
|
801
|
+
const agentSchedules = new Map();
|
|
802
|
+
const chainLinks = new Map();
|
|
766
803
|
const fileChangeSubscriptions = new Map();
|
|
767
804
|
const browserTabs = DEFAULT_BROWSER_TABS.map((tab) => ({ ...tab }));
|
|
768
805
|
let currentTabId = browserTabs[0]?.id ?? null;
|
|
@@ -879,6 +916,108 @@ export function createMockTransport(config = {}) {
|
|
|
879
916
|
}
|
|
880
917
|
return value;
|
|
881
918
|
};
|
|
919
|
+
const isValidTimeZone = (value) => {
|
|
920
|
+
if (typeof value !== 'string' || value === '')
|
|
921
|
+
return false;
|
|
922
|
+
try {
|
|
923
|
+
new Intl.DateTimeFormat('en-US', { timeZone: value });
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
catch {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
const zonedParts = (instant, timeZone) => {
|
|
931
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
932
|
+
timeZone,
|
|
933
|
+
hourCycle: 'h23',
|
|
934
|
+
year: 'numeric',
|
|
935
|
+
month: '2-digit',
|
|
936
|
+
day: '2-digit',
|
|
937
|
+
hour: '2-digit',
|
|
938
|
+
minute: '2-digit',
|
|
939
|
+
second: '2-digit',
|
|
940
|
+
weekday: 'short',
|
|
941
|
+
}).formatToParts(new Date(instant));
|
|
942
|
+
const read = (type) => parts.find((part) => part.type === type)?.value ?? '0';
|
|
943
|
+
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
944
|
+
return {
|
|
945
|
+
y: Number(read('year')),
|
|
946
|
+
m: Number(read('month')),
|
|
947
|
+
d: Number(read('day')),
|
|
948
|
+
hh: Number(read('hour')) % 24,
|
|
949
|
+
mm: Number(read('minute')),
|
|
950
|
+
ss: Number(read('second')),
|
|
951
|
+
weekday: Math.max(0, weekdays.indexOf(read('weekday'))),
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
const zoneOffsetMs = (instant, timeZone) => {
|
|
955
|
+
const p = zonedParts(instant, timeZone);
|
|
956
|
+
return Date.UTC(p.y, p.m - 1, p.d, p.hh, p.mm, p.ss) - instant;
|
|
957
|
+
};
|
|
958
|
+
const zonedWallTimeToInstant = (y, m, d, hh, mm, timeZone) => {
|
|
959
|
+
const guess = Date.UTC(y, m - 1, d, hh, mm);
|
|
960
|
+
const first = guess - zoneOffsetMs(guess, timeZone);
|
|
961
|
+
const second = guess - zoneOffsetMs(first, timeZone);
|
|
962
|
+
const landed = zonedParts(second, timeZone);
|
|
963
|
+
if (landed.hh === hh && landed.mm === mm)
|
|
964
|
+
return second;
|
|
965
|
+
return Math.max(first, second);
|
|
966
|
+
};
|
|
967
|
+
const nextDailyFireAt = (timing, from) => {
|
|
968
|
+
const [hh, mm] = timing.time.split(':').map(Number);
|
|
969
|
+
const today = zonedParts(from, timing.timeZone);
|
|
970
|
+
for (let offset = 0; offset <= 8; offset++) {
|
|
971
|
+
const day = new Date(Date.UTC(today.y, today.m - 1, today.d + offset, 12));
|
|
972
|
+
const candidate = zonedWallTimeToInstant(day.getUTCFullYear(), day.getUTCMonth() + 1, day.getUTCDate(), hh, mm, timing.timeZone);
|
|
973
|
+
if (candidate <= from)
|
|
974
|
+
continue;
|
|
975
|
+
if (timing.days && !timing.days.includes(zonedParts(candidate, timing.timeZone).weekday))
|
|
976
|
+
continue;
|
|
977
|
+
return new Date(candidate).toISOString();
|
|
978
|
+
}
|
|
979
|
+
return new Date(from + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
980
|
+
};
|
|
981
|
+
const nextMonthlyFireAt = (timing, from) => {
|
|
982
|
+
const [hh, mm] = timing.time.split(':').map(Number);
|
|
983
|
+
const today = zonedParts(from, timing.timeZone);
|
|
984
|
+
for (let offset = 0; offset < 13; offset++) {
|
|
985
|
+
const monthIndex = today.m - 1 + offset;
|
|
986
|
+
const year = today.y + Math.floor(monthIndex / 12);
|
|
987
|
+
const month = (monthIndex % 12) + 1;
|
|
988
|
+
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
989
|
+
const day = timing.dayOfMonth === 'last' ? lastDay : timing.dayOfMonth;
|
|
990
|
+
if (day > lastDay)
|
|
991
|
+
continue;
|
|
992
|
+
const candidate = zonedWallTimeToInstant(year, month, day, hh, mm, timing.timeZone);
|
|
993
|
+
if (candidate <= from)
|
|
994
|
+
continue;
|
|
995
|
+
return new Date(candidate).toISOString();
|
|
996
|
+
}
|
|
997
|
+
return new Date(from + 31 * 24 * 60 * 60 * 1000).toISOString();
|
|
998
|
+
};
|
|
999
|
+
const wallClockTimingOf = (t, method) => {
|
|
1000
|
+
if (typeof t.time !== 'string' || !/^([01]\d|2[0-3]):[0-5]\d$/.test(t.time)) {
|
|
1001
|
+
throw new MinionryError('BAD_REQUEST', `${method}: timing.time must be 24-hour HH:MM`);
|
|
1002
|
+
}
|
|
1003
|
+
if (!isValidTimeZone(t.timeZone))
|
|
1004
|
+
throw new MinionryError('BAD_REQUEST', `${method}: timing.timeZone must be an IANA zone name`);
|
|
1005
|
+
if (t.kind === 'monthly') {
|
|
1006
|
+
const dayOfMonth = t.dayOfMonth;
|
|
1007
|
+
if (dayOfMonth !== 'last' && !(Number.isInteger(dayOfMonth) && dayOfMonth >= 1 && dayOfMonth <= 31)) {
|
|
1008
|
+
throw new MinionryError('BAD_REQUEST', `${method}: timing.dayOfMonth must be 1–31 or 'last'`);
|
|
1009
|
+
}
|
|
1010
|
+
return { kind: 'monthly', time: t.time, timeZone: t.timeZone, dayOfMonth: dayOfMonth };
|
|
1011
|
+
}
|
|
1012
|
+
const timing = { kind: 'daily', time: t.time, timeZone: t.timeZone };
|
|
1013
|
+
if (t.days !== undefined) {
|
|
1014
|
+
if (!Array.isArray(t.days) || t.days.length === 0 || t.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) {
|
|
1015
|
+
throw new MinionryError('BAD_REQUEST', `${method}: timing.days must be a non-empty array of weekdays 0 (Sunday) … 6 (Saturday)`);
|
|
1016
|
+
}
|
|
1017
|
+
timing.days = [...new Set(t.days)].sort((a, b) => a - b);
|
|
1018
|
+
}
|
|
1019
|
+
return timing;
|
|
1020
|
+
};
|
|
882
1021
|
const mockScheduleTiming = (raw, method) => {
|
|
883
1022
|
if (typeof raw !== 'object' || raw === null) {
|
|
884
1023
|
throw new MinionryError('BAD_REQUEST', `${method}: timing must be an object`);
|
|
@@ -892,8 +1031,31 @@ export function createMockTransport(config = {}) {
|
|
|
892
1031
|
}
|
|
893
1032
|
if (t.kind === 'continuous')
|
|
894
1033
|
return { kind: 'continuous' };
|
|
1034
|
+
if (t.kind === 'daily' || t.kind === 'monthly')
|
|
1035
|
+
return wallClockTimingOf(t, method);
|
|
895
1036
|
throw new MinionryError('BAD_REQUEST', `${method}: invalid timing`);
|
|
896
1037
|
};
|
|
1038
|
+
const mockAgentScheduleTiming = (raw, method) => {
|
|
1039
|
+
const timing = mockScheduleTiming(raw, method);
|
|
1040
|
+
if (timing.kind === 'continuous') {
|
|
1041
|
+
throw new MinionryError('BAD_REQUEST', `${method}: timing must be {kind:'once',at}, {kind:'interval',everyMs}, {kind:'daily',time,timeZone,days?} or {kind:'monthly',time,timeZone,dayOfMonth} — 'continuous' is not supported for agent-prompt schedules`);
|
|
1042
|
+
}
|
|
1043
|
+
return timing;
|
|
1044
|
+
};
|
|
1045
|
+
const mockMaxLatenessMs = (raw, method) => {
|
|
1046
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < MOCK_SCHEDULE_MIN_LATENESS_MS) {
|
|
1047
|
+
throw new MinionryError('BAD_REQUEST', `${method}: maxLatenessMs must be an integer >= the schedule minimum, or null to clear it`);
|
|
1048
|
+
}
|
|
1049
|
+
return raw;
|
|
1050
|
+
};
|
|
1051
|
+
const patchMaxLatenessMs = (target, raw, method) => {
|
|
1052
|
+
if (raw === undefined)
|
|
1053
|
+
return;
|
|
1054
|
+
if (raw === null)
|
|
1055
|
+
delete target.maxLatenessMs;
|
|
1056
|
+
else
|
|
1057
|
+
target.maxLatenessMs = mockMaxLatenessMs(raw, method);
|
|
1058
|
+
};
|
|
897
1059
|
const mockNextFireAt = (timing, from) => {
|
|
898
1060
|
if (timing.kind === 'once') {
|
|
899
1061
|
const at = new Date(timing.at);
|
|
@@ -901,8 +1063,220 @@ export function createMockTransport(config = {}) {
|
|
|
901
1063
|
}
|
|
902
1064
|
if (timing.kind === 'interval')
|
|
903
1065
|
return new Date(from.getTime() + timing.everyMs).toISOString();
|
|
1066
|
+
if (timing.kind === 'daily')
|
|
1067
|
+
return nextDailyFireAt(timing, from.getTime());
|
|
1068
|
+
if (timing.kind === 'monthly')
|
|
1069
|
+
return nextMonthlyFireAt(timing, from.getTime());
|
|
904
1070
|
return null;
|
|
905
1071
|
};
|
|
1072
|
+
const workspaceFileParam = (raw, field, method) => {
|
|
1073
|
+
if (typeof raw !== 'string' || !raw.startsWith(WORKSPACE_PATH_PREFIX)) {
|
|
1074
|
+
throw new MinionryError('BAD_REQUEST', `${method}: ${field} must be a workspace-rooted path ('${WORKSPACE_PATH_PREFIX}/<path>')`);
|
|
1075
|
+
}
|
|
1076
|
+
const rel = raw.slice(WORKSPACE_PATH_PREFIX.length).replace(/^\/+/, '');
|
|
1077
|
+
if (rel === '' || rel.split('/').some((segment) => segment === '..')) {
|
|
1078
|
+
throw new MinionryError('BAD_REQUEST', `${method}: ${field} '${raw}' must not be empty or contain a '..' segment`);
|
|
1079
|
+
}
|
|
1080
|
+
if (!files.has(raw))
|
|
1081
|
+
throw new MinionryError('BAD_REQUEST', `${method}: ${field} '${raw}' does not exist`);
|
|
1082
|
+
return raw;
|
|
1083
|
+
};
|
|
1084
|
+
const promptFileContent = (path, method) => {
|
|
1085
|
+
const content = (files.get(path) ?? '').trim();
|
|
1086
|
+
if (content === '')
|
|
1087
|
+
throw new MinionryError('BAD_REQUEST', `${method}: promptFile '${path}' is empty after trimming`);
|
|
1088
|
+
return content;
|
|
1089
|
+
};
|
|
1090
|
+
const cloneAgentSchedule = (schedule) => ({
|
|
1091
|
+
...schedule,
|
|
1092
|
+
timing: schedule.timing.kind === 'daily' && schedule.timing.days ? { ...schedule.timing, days: [...schedule.timing.days] } : { ...schedule.timing },
|
|
1093
|
+
...(schedule.lastFire ? { lastFire: { ...schedule.lastFire } } : {}),
|
|
1094
|
+
history: schedule.history.map((entry) => ({ ...entry })),
|
|
1095
|
+
});
|
|
1096
|
+
const ownedAgentScheduleById = (value, method) => {
|
|
1097
|
+
const id = asString(value, 'scheduleId', method);
|
|
1098
|
+
const schedule = agentSchedules.get(id);
|
|
1099
|
+
if (!schedule || schedule.initiator.appId !== MOCK_ENDPOINT_APP_ID) {
|
|
1100
|
+
throw new MinionryError('BAD_REQUEST', `schedule '${id}' not found in this Space`);
|
|
1101
|
+
}
|
|
1102
|
+
return schedule;
|
|
1103
|
+
};
|
|
1104
|
+
const chainBoardStatus = (board) => (board.completedAt ? 'completed' : board.status);
|
|
1105
|
+
const chainToSnapshot = (link) => ({
|
|
1106
|
+
id: link.id,
|
|
1107
|
+
from: link.from,
|
|
1108
|
+
to: link.to,
|
|
1109
|
+
on: link.on,
|
|
1110
|
+
passOutputs: link.passOutputs,
|
|
1111
|
+
name: link.name,
|
|
1112
|
+
enabled: link.enabled,
|
|
1113
|
+
state: link.state,
|
|
1114
|
+
createdAt: link.createdAt,
|
|
1115
|
+
ownedByCaller: true,
|
|
1116
|
+
history: link.history,
|
|
1117
|
+
});
|
|
1118
|
+
const chainArmedEdges = (extra) => {
|
|
1119
|
+
const edges = [...chainLinks.values()]
|
|
1120
|
+
.filter((link) => link.state === 'armed')
|
|
1121
|
+
.map((link) => ({ from: link.from, to: link.to }));
|
|
1122
|
+
if (extra)
|
|
1123
|
+
edges.push(extra);
|
|
1124
|
+
return edges;
|
|
1125
|
+
};
|
|
1126
|
+
const chainGraphHasCycle = (edges) => {
|
|
1127
|
+
const adjacency = new Map();
|
|
1128
|
+
for (const edge of edges) {
|
|
1129
|
+
const list = adjacency.get(edge.from);
|
|
1130
|
+
if (list)
|
|
1131
|
+
list.push(edge.to);
|
|
1132
|
+
else
|
|
1133
|
+
adjacency.set(edge.from, [edge.to]);
|
|
1134
|
+
}
|
|
1135
|
+
const color = new Map();
|
|
1136
|
+
const visit = (node) => {
|
|
1137
|
+
color.set(node, 'gray');
|
|
1138
|
+
for (const next of adjacency.get(node) ?? []) {
|
|
1139
|
+
const state = color.get(next);
|
|
1140
|
+
if (state === 'gray')
|
|
1141
|
+
return true;
|
|
1142
|
+
if (state !== 'black' && visit(next))
|
|
1143
|
+
return true;
|
|
1144
|
+
}
|
|
1145
|
+
color.set(node, 'black');
|
|
1146
|
+
return false;
|
|
1147
|
+
};
|
|
1148
|
+
for (const node of adjacency.keys()) {
|
|
1149
|
+
if (!color.has(node) && visit(node))
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
return false;
|
|
1153
|
+
};
|
|
1154
|
+
const chainGraphDepthThrough = (edges, target) => {
|
|
1155
|
+
const incoming = new Map();
|
|
1156
|
+
for (const edge of edges) {
|
|
1157
|
+
const list = incoming.get(edge.to);
|
|
1158
|
+
if (list)
|
|
1159
|
+
list.push(edge.from);
|
|
1160
|
+
else
|
|
1161
|
+
incoming.set(edge.to, [edge.from]);
|
|
1162
|
+
}
|
|
1163
|
+
const memo = new Map();
|
|
1164
|
+
const longestTo = (node, path) => {
|
|
1165
|
+
const cached = memo.get(node);
|
|
1166
|
+
if (cached !== undefined)
|
|
1167
|
+
return cached;
|
|
1168
|
+
if (path.has(node))
|
|
1169
|
+
return 1;
|
|
1170
|
+
path.add(node);
|
|
1171
|
+
let best = 0;
|
|
1172
|
+
for (const predecessor of incoming.get(node) ?? [])
|
|
1173
|
+
best = Math.max(best, longestTo(predecessor, path));
|
|
1174
|
+
path.delete(node);
|
|
1175
|
+
const depth = best + 1;
|
|
1176
|
+
memo.set(node, depth);
|
|
1177
|
+
return depth;
|
|
1178
|
+
};
|
|
1179
|
+
return longestTo(target, new Set());
|
|
1180
|
+
};
|
|
1181
|
+
const chainPushHistory = (link, entry) => {
|
|
1182
|
+
link.history.push(entry);
|
|
1183
|
+
if (link.history.length > MAX_CHAIN_HISTORY)
|
|
1184
|
+
link.history.splice(0, link.history.length - MAX_CHAIN_HISTORY);
|
|
1185
|
+
};
|
|
1186
|
+
const CHAIN_ERROR = {
|
|
1187
|
+
boardGone: 'target board no longer exists',
|
|
1188
|
+
notRunnable: 'target board is not runnable',
|
|
1189
|
+
noIssues: 'target board has no issues',
|
|
1190
|
+
};
|
|
1191
|
+
const settleBoardImpl = (boardId, outcome) => {
|
|
1192
|
+
const at = new Date().toISOString();
|
|
1193
|
+
for (const link of chainLinks.values()) {
|
|
1194
|
+
if (link.from !== boardId || !link.enabled || link.state !== 'armed')
|
|
1195
|
+
continue;
|
|
1196
|
+
const matched = link.on === 'settled' || link.on === outcome;
|
|
1197
|
+
if (!matched) {
|
|
1198
|
+
chainPushHistory(link, {
|
|
1199
|
+
at,
|
|
1200
|
+
completedAt: at,
|
|
1201
|
+
status: 'skipped',
|
|
1202
|
+
upstreamOutcome: outcome,
|
|
1203
|
+
downstreamBoardId: null,
|
|
1204
|
+
skipReason: 'condition_not_met',
|
|
1205
|
+
});
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
const edges = chainArmedEdges();
|
|
1209
|
+
if (chainGraphHasCycle(edges)) {
|
|
1210
|
+
chainPushHistory(link, {
|
|
1211
|
+
at,
|
|
1212
|
+
completedAt: at,
|
|
1213
|
+
status: 'skipped',
|
|
1214
|
+
upstreamOutcome: outcome,
|
|
1215
|
+
downstreamBoardId: null,
|
|
1216
|
+
skipReason: 'cycle',
|
|
1217
|
+
});
|
|
1218
|
+
link.state = 'disabled';
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
if (chainGraphDepthThrough(edges, link.to) > MAX_CHAIN_DEPTH) {
|
|
1222
|
+
chainPushHistory(link, {
|
|
1223
|
+
at,
|
|
1224
|
+
completedAt: at,
|
|
1225
|
+
status: 'skipped',
|
|
1226
|
+
upstreamOutcome: outcome,
|
|
1227
|
+
downstreamBoardId: null,
|
|
1228
|
+
skipReason: 'max_depth',
|
|
1229
|
+
});
|
|
1230
|
+
link.state = 'disabled';
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
const toBoard = boards.get(link.to);
|
|
1234
|
+
const nonEpicIssues = toBoard?.issues.filter((issue) => issue.type !== 'epic') ?? [];
|
|
1235
|
+
if (!toBoard) {
|
|
1236
|
+
chainPushHistory(link, {
|
|
1237
|
+
at,
|
|
1238
|
+
completedAt: at,
|
|
1239
|
+
status: 'failed',
|
|
1240
|
+
upstreamOutcome: outcome,
|
|
1241
|
+
downstreamBoardId: null,
|
|
1242
|
+
error: CHAIN_ERROR.boardGone,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
else if (chainBoardStatus(toBoard) === 'completed' || chainBoardStatus(toBoard) === 'archived') {
|
|
1246
|
+
chainPushHistory(link, {
|
|
1247
|
+
at,
|
|
1248
|
+
completedAt: at,
|
|
1249
|
+
status: 'failed',
|
|
1250
|
+
upstreamOutcome: outcome,
|
|
1251
|
+
downstreamBoardId: null,
|
|
1252
|
+
error: CHAIN_ERROR.notRunnable,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
else if (nonEpicIssues.length === 0) {
|
|
1256
|
+
chainPushHistory(link, {
|
|
1257
|
+
at,
|
|
1258
|
+
completedAt: at,
|
|
1259
|
+
status: 'failed',
|
|
1260
|
+
upstreamOutcome: outcome,
|
|
1261
|
+
downstreamBoardId: null,
|
|
1262
|
+
error: CHAIN_ERROR.noIssues,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
chainPushHistory(link, {
|
|
1267
|
+
at,
|
|
1268
|
+
completedAt: at,
|
|
1269
|
+
status: 'started',
|
|
1270
|
+
upstreamOutcome: outcome,
|
|
1271
|
+
downstreamBoardId: link.to,
|
|
1272
|
+
downstreamRunId: `chain-run-mock-${++seq}`,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
link.state = 'fired';
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
const chainsController = { settleBoard: settleBoardImpl };
|
|
1279
|
+
config.chains?.onReady?.(chainsController);
|
|
906
1280
|
const settleWaiters = (run) => {
|
|
907
1281
|
if (run.status === 'running')
|
|
908
1282
|
return;
|
|
@@ -1015,6 +1389,55 @@ export function createMockTransport(config = {}) {
|
|
|
1015
1389
|
}
|
|
1016
1390
|
return name;
|
|
1017
1391
|
};
|
|
1392
|
+
const admitEngineSelection = (raw) => {
|
|
1393
|
+
if (raw === undefined)
|
|
1394
|
+
return undefined;
|
|
1395
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
1396
|
+
throw new MinionryError('BAD_REQUEST', `agents.run engineSelection must be an object ${MOCK_ENGINE_SELECTION_SHAPE}`);
|
|
1397
|
+
}
|
|
1398
|
+
const record = raw;
|
|
1399
|
+
refuseUnknownKeys(record, MOCK_ENGINE_SELECTION_KEYS, 'agents.run engineSelection', MOCK_ENGINE_SELECTION_SHAPE);
|
|
1400
|
+
const nonEmptyString = (value, field) => {
|
|
1401
|
+
if (value === undefined)
|
|
1402
|
+
return undefined;
|
|
1403
|
+
if (typeof value !== 'string' || value === '') {
|
|
1404
|
+
throw new MinionryError('BAD_REQUEST', `agents.run engineSelection.${field} must be a non-empty string when present`);
|
|
1405
|
+
}
|
|
1406
|
+
return value;
|
|
1407
|
+
};
|
|
1408
|
+
const engine = nonEmptyString(record.engine, 'engine');
|
|
1409
|
+
const model = nonEmptyString(record.model, 'model');
|
|
1410
|
+
const effortLevel = nonEmptyString(record.effortLevel, 'effortLevel');
|
|
1411
|
+
if (record.fastMode !== undefined && typeof record.fastMode !== 'boolean') {
|
|
1412
|
+
throw new MinionryError('BAD_REQUEST', 'agents.run engineSelection.fastMode must be a boolean when present');
|
|
1413
|
+
}
|
|
1414
|
+
const fastMode = record.fastMode === true;
|
|
1415
|
+
if (!granted.has('settings:read')) {
|
|
1416
|
+
throw new MinionryError('SCOPE_DENIED', 'agents.run: engineSelection needs the settings:read scope');
|
|
1417
|
+
}
|
|
1418
|
+
if (engine !== undefined && !MOCK_ENGINES.includes(engine)) {
|
|
1419
|
+
throw new MinionryError('BAD_REQUEST', `agents.run engineSelection: unknown engine '${engine}' — expected one of ${MOCK_ENGINES.join(', ')}`);
|
|
1420
|
+
}
|
|
1421
|
+
const resolvedEngine = engine ?? (model ? mockEngineForModel(model) : MOCK_DEFAULT_ENGINE);
|
|
1422
|
+
if (!resolvedAgentRunEngines.includes(resolvedEngine)) {
|
|
1423
|
+
throw new MinionryError('ENGINE_UNAVAILABLE', `agents.run: ${resolvedEngine} cannot run a confined app session with tools on this host`);
|
|
1424
|
+
}
|
|
1425
|
+
if (model && mockEngineForModel(model) !== resolvedEngine) {
|
|
1426
|
+
throw new MinionryError('BAD_REQUEST', `agents.run: engineSelection model '${model}' is not a ${resolvedEngine} model — it belongs to ${mockEngineForModel(model)}`);
|
|
1427
|
+
}
|
|
1428
|
+
if (effortLevel !== undefined && effortLevel !== 'auto' && !MOCK_EFFORT_LEVELS[resolvedEngine]?.includes(effortLevel)) {
|
|
1429
|
+
throw new MinionryError('BAD_REQUEST', `agents.run: engineSelection effortLevel '${effortLevel}' is not a ${resolvedEngine} effort level`);
|
|
1430
|
+
}
|
|
1431
|
+
if (fastMode && resolvedEngine !== 'claude-code') {
|
|
1432
|
+
throw new MinionryError('BAD_REQUEST', `agents.run: engineSelection.fastMode is a claude-code setting — ${resolvedEngine} has no fast mode`);
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
...(engine !== undefined ? { engine } : {}),
|
|
1436
|
+
...(model !== undefined ? { model } : {}),
|
|
1437
|
+
...(effortLevel !== undefined ? { effortLevel } : {}),
|
|
1438
|
+
...(fastMode ? { fastMode } : {}),
|
|
1439
|
+
};
|
|
1440
|
+
};
|
|
1018
1441
|
const runParams = (params) => {
|
|
1019
1442
|
const record = strictRecord(params, 'agents.run', MOCK_RUN_PARAMS_SHAPE, MOCK_RUN_PARAM_KEYS);
|
|
1020
1443
|
const prompt = asString(record.prompt, 'prompt', 'agents.run');
|
|
@@ -1030,7 +1453,7 @@ export function createMockTransport(config = {}) {
|
|
|
1030
1453
|
};
|
|
1031
1454
|
const model = optional('model');
|
|
1032
1455
|
const sessionId = optional('sessionId');
|
|
1033
|
-
const { systemPrompt, budget, modelSelection } = record;
|
|
1456
|
+
const { systemPrompt, budget, modelSelection, engineSelection } = record;
|
|
1034
1457
|
if (systemPrompt !== undefined && (typeof systemPrompt !== 'string' || systemPrompt.length > AGENT_RUN_LIMITS.maxSystemPromptChars)) {
|
|
1035
1458
|
throw new MinionryError('BAD_REQUEST', `agents.run systemPrompt must be a string of at most ${AGENT_RUN_LIMITS.maxSystemPromptChars} characters`);
|
|
1036
1459
|
}
|
|
@@ -1058,6 +1481,21 @@ export function createMockTransport(config = {}) {
|
|
|
1058
1481
|
throw new MinionryError('BAD_REQUEST', 'agents.run systemPrompt and budget apply only to a run on an app-registered model — pass model');
|
|
1059
1482
|
}
|
|
1060
1483
|
admitModelSelection(modelSelection);
|
|
1484
|
+
if (engineSelection !== undefined) {
|
|
1485
|
+
const conflicts = [
|
|
1486
|
+
['model', model !== undefined],
|
|
1487
|
+
['modelSelection', modelSelection !== undefined],
|
|
1488
|
+
['systemPrompt', systemPrompt !== undefined],
|
|
1489
|
+
['budget', budget !== undefined],
|
|
1490
|
+
['sessionId', sessionId !== undefined],
|
|
1491
|
+
]
|
|
1492
|
+
.filter(([, present]) => present)
|
|
1493
|
+
.map(([field]) => field);
|
|
1494
|
+
if (conflicts.length > 0) {
|
|
1495
|
+
throw new MinionryError('BAD_REQUEST', `agents.run engineSelection is not allowed alongside ${conflicts.join(', ')} — pass only one way of choosing the run's engine or model`);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
admitEngineSelection(engineSelection);
|
|
1061
1499
|
return { prompt, title, model, sessionId };
|
|
1062
1500
|
};
|
|
1063
1501
|
const endpointRunSteps = (endpoint, prompt, turn) => {
|
|
@@ -1269,14 +1707,28 @@ export function createMockTransport(config = {}) {
|
|
|
1269
1707
|
run.issueIndex += 1;
|
|
1270
1708
|
later(() => advanceExec(run));
|
|
1271
1709
|
};
|
|
1272
|
-
const
|
|
1710
|
+
const withinMockRoot = (root, path) => {
|
|
1711
|
+
const base = root === '.' ? '' : root.replace(/\/+$/, '');
|
|
1712
|
+
if (base === '' || path === base)
|
|
1713
|
+
return path.slice(base.length);
|
|
1714
|
+
return path.startsWith(`${base}/`) ? path.slice(base.length + 1) : undefined;
|
|
1715
|
+
};
|
|
1716
|
+
const notifyMockFileChanged = (path, kind, dir = false, newPath) => {
|
|
1273
1717
|
for (const [requestId, root] of fileChangeSubscriptions) {
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1718
|
+
const from = withinMockRoot(root, path);
|
|
1719
|
+
const to = newPath === undefined ? undefined : withinMockRoot(root, newPath);
|
|
1720
|
+
let change;
|
|
1721
|
+
if (from !== undefined) {
|
|
1722
|
+
change = to !== undefined ? { path: from, kind, newPath: to } : { path: from, kind: newPath === undefined ? kind : 'deleted' };
|
|
1723
|
+
}
|
|
1724
|
+
else if (to !== undefined) {
|
|
1725
|
+
change = { path: to, kind: 'created' };
|
|
1726
|
+
}
|
|
1727
|
+
if (change)
|
|
1728
|
+
emit(requestId, 'changed', dir ? { ...change, dir } : change);
|
|
1278
1729
|
}
|
|
1279
1730
|
};
|
|
1731
|
+
const isMockDir = (path) => dirs.has(path) || [...files.keys(), ...dirs].some((key) => key.startsWith(`${path}/`));
|
|
1280
1732
|
const browserTabsPayload = () => ({
|
|
1281
1733
|
tabs: browserTabs.map((tab) => ({ ...tab })),
|
|
1282
1734
|
currentTabId,
|
|
@@ -1374,6 +1826,10 @@ export function createMockTransport(config = {}) {
|
|
|
1374
1826
|
hermes: [],
|
|
1375
1827
|
};
|
|
1376
1828
|
const MOCK_INFERENCE_ENGINES = ['claude-code'];
|
|
1829
|
+
const MOCK_AGENT_RUN_ENGINES = ['claude-code'];
|
|
1830
|
+
const resolvedAgentRunEngines = config.agentRunEngines === null || config.agentRunEngines === undefined
|
|
1831
|
+
? MOCK_AGENT_RUN_ENGINES
|
|
1832
|
+
: config.agentRunEngines;
|
|
1377
1833
|
const mockInferenceMessages = (raw) => {
|
|
1378
1834
|
const valid = Array.isArray(raw) &&
|
|
1379
1835
|
raw.length > 0 &&
|
|
@@ -1675,17 +2131,6 @@ export function createMockTransport(config = {}) {
|
|
|
1675
2131
|
...(schedule.lastFire ? { lastFire: { ...schedule.lastFire } } : {}),
|
|
1676
2132
|
history: schedule.history.map((entry) => ({ ...entry })),
|
|
1677
2133
|
});
|
|
1678
|
-
const isValidTimeZone = (value) => {
|
|
1679
|
-
if (typeof value !== 'string' || value === '')
|
|
1680
|
-
return false;
|
|
1681
|
-
try {
|
|
1682
|
-
new Intl.DateTimeFormat('en-US', { timeZone: value });
|
|
1683
|
-
return true;
|
|
1684
|
-
}
|
|
1685
|
-
catch {
|
|
1686
|
-
return false;
|
|
1687
|
-
}
|
|
1688
|
-
};
|
|
1689
2134
|
const qualityTimingOf = (value, method) => {
|
|
1690
2135
|
const raw = asRecord(value, method);
|
|
1691
2136
|
if (raw.kind === 'interval') {
|
|
@@ -1710,56 +2155,7 @@ export function createMockTransport(config = {}) {
|
|
|
1710
2155
|
}
|
|
1711
2156
|
return timing;
|
|
1712
2157
|
};
|
|
1713
|
-
const
|
|
1714
|
-
const parts = new Intl.DateTimeFormat('en-US', {
|
|
1715
|
-
timeZone,
|
|
1716
|
-
hourCycle: 'h23',
|
|
1717
|
-
year: 'numeric',
|
|
1718
|
-
month: '2-digit',
|
|
1719
|
-
day: '2-digit',
|
|
1720
|
-
hour: '2-digit',
|
|
1721
|
-
minute: '2-digit',
|
|
1722
|
-
second: '2-digit',
|
|
1723
|
-
weekday: 'short',
|
|
1724
|
-
}).formatToParts(new Date(instant));
|
|
1725
|
-
const read = (type) => parts.find((part) => part.type === type)?.value ?? '0';
|
|
1726
|
-
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1727
|
-
return {
|
|
1728
|
-
y: Number(read('year')),
|
|
1729
|
-
m: Number(read('month')),
|
|
1730
|
-
d: Number(read('day')),
|
|
1731
|
-
hh: Number(read('hour')) % 24,
|
|
1732
|
-
mm: Number(read('minute')),
|
|
1733
|
-
ss: Number(read('second')),
|
|
1734
|
-
weekday: Math.max(0, weekdays.indexOf(read('weekday'))),
|
|
1735
|
-
};
|
|
1736
|
-
};
|
|
1737
|
-
const zoneOffsetMs = (instant, timeZone) => {
|
|
1738
|
-
const p = zonedParts(instant, timeZone);
|
|
1739
|
-
return Date.UTC(p.y, p.m - 1, p.d, p.hh, p.mm, p.ss) - instant;
|
|
1740
|
-
};
|
|
1741
|
-
const zonedWallTimeToInstant = (y, m, d, hh, mm, timeZone) => {
|
|
1742
|
-
const guess = Date.UTC(y, m - 1, d, hh, mm);
|
|
1743
|
-
const first = guess - zoneOffsetMs(guess, timeZone);
|
|
1744
|
-
const offset = zoneOffsetMs(first, timeZone);
|
|
1745
|
-
return guess - offset;
|
|
1746
|
-
};
|
|
1747
|
-
const nextQualityFireAt = (timing, from) => {
|
|
1748
|
-
if (timing.kind === 'interval')
|
|
1749
|
-
return new Date(from + timing.everyMs).toISOString();
|
|
1750
|
-
const [hh, mm] = timing.time.split(':').map(Number);
|
|
1751
|
-
const today = zonedParts(from, timing.timeZone);
|
|
1752
|
-
for (let offset = 0; offset <= 8; offset++) {
|
|
1753
|
-
const day = new Date(Date.UTC(today.y, today.m - 1, today.d + offset, 12));
|
|
1754
|
-
const candidate = zonedWallTimeToInstant(day.getUTCFullYear(), day.getUTCMonth() + 1, day.getUTCDate(), hh, mm, timing.timeZone);
|
|
1755
|
-
if (candidate <= from)
|
|
1756
|
-
continue;
|
|
1757
|
-
if (timing.days && !timing.days.includes(zonedParts(candidate, timing.timeZone).weekday))
|
|
1758
|
-
continue;
|
|
1759
|
-
return new Date(candidate).toISOString();
|
|
1760
|
-
}
|
|
1761
|
-
return new Date(from + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
1762
|
-
};
|
|
2158
|
+
const nextQualityFireAt = (timing, from) => timing.kind === 'interval' ? new Date(from + timing.everyMs).toISOString() : nextDailyFireAt(timing, from);
|
|
1763
2159
|
const publishQualitySchedules = () => {
|
|
1764
2160
|
publishQualityChange({ kind: 'schedules', schedules: qualitySchedules.map(cloneQualitySchedule) });
|
|
1765
2161
|
};
|
|
@@ -2146,6 +2542,13 @@ export function createMockTransport(config = {}) {
|
|
|
2146
2542
|
if (model && mockEngineForModel(model) !== resolvedEngine) {
|
|
2147
2543
|
throw new MinionryError('BAD_REQUEST', `inference.chat: model '${model}' is not a ${resolvedEngine} model — it belongs to ${mockEngineForModel(model)}`);
|
|
2148
2544
|
}
|
|
2545
|
+
if (record.fastMode !== undefined && typeof record.fastMode !== 'boolean') {
|
|
2546
|
+
throw new MinionryError('BAD_REQUEST', 'inference.chat: fastMode must be a boolean');
|
|
2547
|
+
}
|
|
2548
|
+
const fastMode = record.fastMode === true;
|
|
2549
|
+
if (fastMode && resolvedEngine !== 'claude-code') {
|
|
2550
|
+
throw new MinionryError('BAD_REQUEST', `inference.chat: fastMode is a claude-code setting — ${resolvedEngine} has no fast mode`);
|
|
2551
|
+
}
|
|
2149
2552
|
if (!MOCK_INFERENCE_ENGINES.includes(resolvedEngine)) {
|
|
2150
2553
|
throw new MinionryError('UNSUPPORTED', `inference.chat: ${resolvedEngine} cannot run a tool-less completion on this host`);
|
|
2151
2554
|
}
|
|
@@ -2164,9 +2567,10 @@ export function createMockTransport(config = {}) {
|
|
|
2164
2567
|
status: 'running',
|
|
2165
2568
|
engine: resolvedEngine,
|
|
2166
2569
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
2570
|
+
...(fastMode ? { fastMode } : {}),
|
|
2167
2571
|
text: '',
|
|
2168
2572
|
deltas: [
|
|
2169
|
-
`(mock ${resolvedEngine}${resolvedModel ? ` · ${resolvedModel}` : ''}) `,
|
|
2573
|
+
`(mock ${resolvedEngine}${resolvedModel ? ` · ${resolvedModel}` : ''}${fastMode ? ' · fast' : ''}) `,
|
|
2170
2574
|
prompt.length > 120 ? `${prompt.slice(0, 119)}…` : prompt,
|
|
2171
2575
|
],
|
|
2172
2576
|
waiters: [],
|
|
@@ -2372,6 +2776,127 @@ export function createMockTransport(config = {}) {
|
|
|
2372
2776
|
session.isExecuting = false;
|
|
2373
2777
|
return null;
|
|
2374
2778
|
},
|
|
2779
|
+
'agents.schedules.list': () => ({
|
|
2780
|
+
schedules: [...agentSchedules.values()]
|
|
2781
|
+
.filter((schedule) => schedule.initiator.appId === MOCK_ENDPOINT_APP_ID)
|
|
2782
|
+
.map((schedule) => cloneAgentSchedule(schedule.snapshot)),
|
|
2783
|
+
}),
|
|
2784
|
+
'agents.schedules.create': (params) => {
|
|
2785
|
+
const method = 'agents.schedules.create';
|
|
2786
|
+
const record = asRecord(params, method);
|
|
2787
|
+
const hasPrompt = record.prompt !== undefined;
|
|
2788
|
+
const hasPromptFile = record.promptFile !== undefined;
|
|
2789
|
+
if (hasPrompt === hasPromptFile) {
|
|
2790
|
+
throw new MinionryError('BAD_REQUEST', `${method} requires exactly one of params.prompt or params.promptFile`);
|
|
2791
|
+
}
|
|
2792
|
+
let prompt;
|
|
2793
|
+
let promptFile;
|
|
2794
|
+
if (hasPromptFile) {
|
|
2795
|
+
promptFile = workspaceFileParam(record.promptFile, 'promptFile', method);
|
|
2796
|
+
prompt = promptFileContent(promptFile, method);
|
|
2797
|
+
}
|
|
2798
|
+
else {
|
|
2799
|
+
if (typeof record.prompt !== 'string' || record.prompt.trim() === '') {
|
|
2800
|
+
throw new MinionryError('BAD_REQUEST', `${method}: params.prompt must be a non-empty string`);
|
|
2801
|
+
}
|
|
2802
|
+
prompt = record.prompt.trim();
|
|
2803
|
+
}
|
|
2804
|
+
const timing = mockAgentScheduleTiming(record.timing, method);
|
|
2805
|
+
if (record.enabled !== undefined && typeof record.enabled !== 'boolean') {
|
|
2806
|
+
throw new MinionryError('BAD_REQUEST', `${method}: enabled must be a boolean when present`);
|
|
2807
|
+
}
|
|
2808
|
+
const now = new Date();
|
|
2809
|
+
const snapshot = {
|
|
2810
|
+
id: `agsched-mock-${++seq}`,
|
|
2811
|
+
createdAt: now.toISOString(),
|
|
2812
|
+
prompt,
|
|
2813
|
+
timing,
|
|
2814
|
+
enabled: record.enabled !== false,
|
|
2815
|
+
nextFireAt: mockNextFireAt(timing, now),
|
|
2816
|
+
history: [],
|
|
2817
|
+
};
|
|
2818
|
+
if (typeof record.name === 'string')
|
|
2819
|
+
snapshot.name = record.name;
|
|
2820
|
+
if (promptFile !== undefined)
|
|
2821
|
+
snapshot.promptFile = promptFile;
|
|
2822
|
+
if (record.maxLatenessMs !== undefined)
|
|
2823
|
+
snapshot.maxLatenessMs = mockMaxLatenessMs(record.maxLatenessMs, method);
|
|
2824
|
+
agentSchedules.set(snapshot.id, { snapshot, initiator: { appId: MOCK_ENDPOINT_APP_ID } });
|
|
2825
|
+
return cloneAgentSchedule(snapshot);
|
|
2826
|
+
},
|
|
2827
|
+
'agents.schedules.update': (params) => {
|
|
2828
|
+
const method = 'agents.schedules.update';
|
|
2829
|
+
const record = asRecord(params, method);
|
|
2830
|
+
const { snapshot } = ownedAgentScheduleById(record.scheduleId, method);
|
|
2831
|
+
if (typeof record.patch !== 'object' || record.patch === null || Array.isArray(record.patch)) {
|
|
2832
|
+
throw new MinionryError('BAD_REQUEST', `${method} requires params.patch to be an object`);
|
|
2833
|
+
}
|
|
2834
|
+
const patch = record.patch;
|
|
2835
|
+
if (patch.prompt !== undefined && patch.promptFile !== undefined) {
|
|
2836
|
+
throw new MinionryError('BAD_REQUEST', `${method}: provide at most one of patch.prompt or patch.promptFile`);
|
|
2837
|
+
}
|
|
2838
|
+
if (patch.name !== undefined)
|
|
2839
|
+
snapshot.name = asString(patch.name, 'patch.name', method);
|
|
2840
|
+
if (patch.prompt !== undefined) {
|
|
2841
|
+
if (typeof patch.prompt !== 'string' || patch.prompt.trim() === '') {
|
|
2842
|
+
throw new MinionryError('BAD_REQUEST', `${method}: patch.prompt must be a non-empty string`);
|
|
2843
|
+
}
|
|
2844
|
+
snapshot.prompt = patch.prompt.trim();
|
|
2845
|
+
delete snapshot.promptFile;
|
|
2846
|
+
}
|
|
2847
|
+
if (patch.promptFile !== undefined) {
|
|
2848
|
+
const promptFile = workspaceFileParam(patch.promptFile, 'promptFile', method);
|
|
2849
|
+
snapshot.prompt = promptFileContent(promptFile, method);
|
|
2850
|
+
snapshot.promptFile = promptFile;
|
|
2851
|
+
}
|
|
2852
|
+
if (patch.enabled !== undefined) {
|
|
2853
|
+
if (typeof patch.enabled !== 'boolean')
|
|
2854
|
+
throw new MinionryError('BAD_REQUEST', `${method}: patch.enabled must be a boolean when present`);
|
|
2855
|
+
snapshot.enabled = patch.enabled;
|
|
2856
|
+
}
|
|
2857
|
+
if (patch.timing !== undefined) {
|
|
2858
|
+
snapshot.timing = mockAgentScheduleTiming(patch.timing, method);
|
|
2859
|
+
snapshot.nextFireAt = mockNextFireAt(snapshot.timing, new Date());
|
|
2860
|
+
}
|
|
2861
|
+
patchMaxLatenessMs(snapshot, patch.maxLatenessMs, method);
|
|
2862
|
+
return cloneAgentSchedule(snapshot);
|
|
2863
|
+
},
|
|
2864
|
+
'agents.schedules.delete': (params) => {
|
|
2865
|
+
const method = 'agents.schedules.delete';
|
|
2866
|
+
const { snapshot } = ownedAgentScheduleById(asRecord(params, method).scheduleId, method);
|
|
2867
|
+
agentSchedules.delete(snapshot.id);
|
|
2868
|
+
return null;
|
|
2869
|
+
},
|
|
2870
|
+
'agents.schedules.runNow': (params) => {
|
|
2871
|
+
const method = 'agents.schedules.runNow';
|
|
2872
|
+
const { snapshot } = ownedAgentScheduleById(asRecord(params, method).scheduleId, method);
|
|
2873
|
+
if (!snapshot.enabled) {
|
|
2874
|
+
throw new MinionryError('BAD_REQUEST', `schedule '${snapshot.id}' is disabled — enable it before running it now`);
|
|
2875
|
+
}
|
|
2876
|
+
const now = new Date().toISOString();
|
|
2877
|
+
let entry;
|
|
2878
|
+
const content = snapshot.promptFile === undefined ? snapshot.prompt : (files.get(snapshot.promptFile) ?? '').trim();
|
|
2879
|
+
if (content === '') {
|
|
2880
|
+
entry = { at: now, status: 'failed', tabId: null, error: `promptFile '${snapshot.promptFile}' is missing or empty` };
|
|
2881
|
+
}
|
|
2882
|
+
else {
|
|
2883
|
+
snapshot.prompt = content;
|
|
2884
|
+
const tabId = `session-mock-${++seq}`;
|
|
2885
|
+
sessions.set(tabId, {
|
|
2886
|
+
id: tabId,
|
|
2887
|
+
name: snapshot.name ?? `Session ${sessions.size + 1}`,
|
|
2888
|
+
createdAt: now,
|
|
2889
|
+
lastActivityAt: now,
|
|
2890
|
+
isExecuting: false,
|
|
2891
|
+
entries: [],
|
|
2892
|
+
seq: 0,
|
|
2893
|
+
});
|
|
2894
|
+
entry = { at: now, status: 'started', tabId };
|
|
2895
|
+
}
|
|
2896
|
+
snapshot.lastFire = { at: entry.at, tabId: entry.tabId, status: entry.status };
|
|
2897
|
+
snapshot.history.push(entry);
|
|
2898
|
+
return cloneAgentSchedule(snapshot);
|
|
2899
|
+
},
|
|
2375
2900
|
'apps.open': (params) => {
|
|
2376
2901
|
const record = asRecord(params, 'apps.open');
|
|
2377
2902
|
const app = asString(record.app, 'app', 'apps.open');
|
|
@@ -2652,18 +3177,27 @@ export function createMockTransport(config = {}) {
|
|
|
2652
3177
|
},
|
|
2653
3178
|
'pm.schedules.list': () => ({ schedules: [...schedules.values()].map((s) => ({ ...s })) }),
|
|
2654
3179
|
'pm.schedules.create': (params) => {
|
|
2655
|
-
const
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
3180
|
+
const method = 'pm.schedules.create';
|
|
3181
|
+
const record = asRecord(params, method);
|
|
3182
|
+
const source = asRecord(record.source, method);
|
|
3183
|
+
let snapshotSource;
|
|
3184
|
+
if (source.kind === 'template-json' && typeof source.content === 'string' && source.content !== '') {
|
|
3185
|
+
snapshotSource = { kind: 'template-json' };
|
|
2659
3186
|
}
|
|
2660
|
-
|
|
3187
|
+
else if (source.kind === 'board-zip') {
|
|
3188
|
+
const path = workspaceFileParam(source.path, 'a board-zip source.path', method);
|
|
3189
|
+
snapshotSource = { kind: 'board-zip', originalName: path.slice(path.lastIndexOf('/') + 1) };
|
|
3190
|
+
}
|
|
3191
|
+
else {
|
|
3192
|
+
throw new MinionryError('BAD_REQUEST', `${method}: source must be { kind: 'template-json', content } or { kind: 'board-zip', path: 'workspace:/…' }`);
|
|
3193
|
+
}
|
|
3194
|
+
const timing = mockScheduleTiming(record.timing, method);
|
|
2661
3195
|
const id = `sched-mock-${++seq}`;
|
|
2662
3196
|
const now = new Date();
|
|
2663
3197
|
const snapshot = {
|
|
2664
3198
|
id,
|
|
2665
3199
|
createdAt: now.toISOString(),
|
|
2666
|
-
source:
|
|
3200
|
+
source: snapshotSource,
|
|
2667
3201
|
timing,
|
|
2668
3202
|
enabled: record.enabled === undefined ? true : record.enabled === true,
|
|
2669
3203
|
nextFireAt: mockNextFireAt(timing, now),
|
|
@@ -2673,6 +3207,8 @@ export function createMockTransport(config = {}) {
|
|
|
2673
3207
|
snapshot.name = record.name;
|
|
2674
3208
|
if (record.input !== undefined)
|
|
2675
3209
|
snapshot.input = record.input;
|
|
3210
|
+
if (record.maxLatenessMs !== undefined)
|
|
3211
|
+
snapshot.maxLatenessMs = mockMaxLatenessMs(record.maxLatenessMs, method);
|
|
2676
3212
|
schedules.set(id, snapshot);
|
|
2677
3213
|
return { ...snapshot };
|
|
2678
3214
|
},
|
|
@@ -2694,6 +3230,7 @@ export function createMockTransport(config = {}) {
|
|
|
2694
3230
|
updated.timing = mockScheduleTiming(patch.timing, 'pm.schedules.update');
|
|
2695
3231
|
updated.nextFireAt = mockNextFireAt(updated.timing, new Date());
|
|
2696
3232
|
}
|
|
3233
|
+
patchMaxLatenessMs(updated, patch.maxLatenessMs, 'pm.schedules.update');
|
|
2697
3234
|
schedules.set(scheduleId, updated);
|
|
2698
3235
|
return { ...updated };
|
|
2699
3236
|
},
|
|
@@ -2702,6 +3239,133 @@ export function createMockTransport(config = {}) {
|
|
|
2702
3239
|
schedules.delete(scheduleId);
|
|
2703
3240
|
return null;
|
|
2704
3241
|
},
|
|
3242
|
+
'pm.schedules.runNow': (params) => {
|
|
3243
|
+
const method = 'pm.schedules.runNow';
|
|
3244
|
+
const scheduleId = asString(asRecord(params, method).scheduleId, 'scheduleId', method);
|
|
3245
|
+
const existing = schedules.get(scheduleId);
|
|
3246
|
+
if (!existing)
|
|
3247
|
+
throw new MinionryError('BAD_REQUEST', `schedule '${scheduleId}' not found in this Space`);
|
|
3248
|
+
if (!existing.enabled) {
|
|
3249
|
+
throw new MinionryError('BAD_REQUEST', `schedule '${scheduleId}' is disabled — enable it before running it now`);
|
|
3250
|
+
}
|
|
3251
|
+
const boardId = `board-mock-${++seq}`;
|
|
3252
|
+
const board = {
|
|
3253
|
+
boardId,
|
|
3254
|
+
title: existing.name ?? 'Scheduled board',
|
|
3255
|
+
goal: existing.name ?? 'Scheduled board',
|
|
3256
|
+
status: 'active',
|
|
3257
|
+
statuses: [...BOARD_STATUSES],
|
|
3258
|
+
issues: [mockIssue('IS-1', 'Plan', 'P1'), mockIssue('IS-2', 'Implement', 'P1'), mockIssue('IS-3', 'Verify', 'P2')],
|
|
3259
|
+
subscribers: new Set(),
|
|
3260
|
+
createdAt: new Date().toISOString(),
|
|
3261
|
+
completedAt: null,
|
|
3262
|
+
};
|
|
3263
|
+
boards.set(boardId, board);
|
|
3264
|
+
later(() => progressBoard(board));
|
|
3265
|
+
const entry = { at: board.createdAt, status: 'started', boardId };
|
|
3266
|
+
const fired = {
|
|
3267
|
+
...existing,
|
|
3268
|
+
lastFire: { at: entry.at, boardId, status: entry.status },
|
|
3269
|
+
history: [...existing.history, entry],
|
|
3270
|
+
};
|
|
3271
|
+
schedules.set(scheduleId, fired);
|
|
3272
|
+
return { ...fired };
|
|
3273
|
+
},
|
|
3274
|
+
'pm.chains.list': () => ({ links: [...chainLinks.values()].map(chainToSnapshot) }),
|
|
3275
|
+
'pm.chains.create': (params) => {
|
|
3276
|
+
const method = 'pm.chains.create';
|
|
3277
|
+
const record = asRecord(params, method);
|
|
3278
|
+
const from = asString(record.from, 'from', method);
|
|
3279
|
+
const to = asString(record.to, 'to', method);
|
|
3280
|
+
const on = record.on;
|
|
3281
|
+
if (on !== 'success' && on !== 'failure' && on !== 'settled') {
|
|
3282
|
+
throw new MinionryError('BAD_REQUEST', `${method}: 'on' must be one of 'success', 'failure', 'settled'`);
|
|
3283
|
+
}
|
|
3284
|
+
if (from === to) {
|
|
3285
|
+
throw new MinionryError('BAD_REQUEST', `${method}: 'from' and 'to' must name different boards`);
|
|
3286
|
+
}
|
|
3287
|
+
const fromBoard = boards.get(from);
|
|
3288
|
+
const toBoard = boards.get(to);
|
|
3289
|
+
if (!fromBoard)
|
|
3290
|
+
throw new MinionryError('BAD_REQUEST', `${method}: unknown board '${from}'`);
|
|
3291
|
+
if (!toBoard)
|
|
3292
|
+
throw new MinionryError('BAD_REQUEST', `${method}: unknown board '${to}'`);
|
|
3293
|
+
if (chainBoardStatus(fromBoard) === 'completed' || chainBoardStatus(fromBoard) === 'archived') {
|
|
3294
|
+
throw new MinionryError('BAD_REQUEST', `${method}: 'from' board '${from}' is not runnable`);
|
|
3295
|
+
}
|
|
3296
|
+
if (chainBoardStatus(toBoard) === 'completed' || chainBoardStatus(toBoard) === 'archived') {
|
|
3297
|
+
throw new MinionryError('BAD_REQUEST', `${method}: 'to' board '${to}' is not runnable`);
|
|
3298
|
+
}
|
|
3299
|
+
const passOutputs = record.passOutputs === undefined ? false : record.passOutputs === true;
|
|
3300
|
+
const name = typeof record.name === 'string' ? record.name : undefined;
|
|
3301
|
+
const enabled = record.enabled === undefined ? true : record.enabled === true;
|
|
3302
|
+
const existing = [...chainLinks.values()].find((link) => link.state === 'armed' && link.from === from && link.to === to && link.on === on);
|
|
3303
|
+
if (existing) {
|
|
3304
|
+
if (existing.passOutputs === passOutputs && existing.name === name)
|
|
3305
|
+
return chainToSnapshot(existing);
|
|
3306
|
+
throw new MinionryError('BAD_REQUEST', `${method}: a link (${from} -> ${to}, on=${on}) already exists with a different passOutputs/name`);
|
|
3307
|
+
}
|
|
3308
|
+
const edges = chainArmedEdges({ from, to });
|
|
3309
|
+
if (chainGraphHasCycle(edges)) {
|
|
3310
|
+
throw new MinionryError('BAD_REQUEST', `${method}: would close a cycle`);
|
|
3311
|
+
}
|
|
3312
|
+
if (chainGraphDepthThrough(edges, to) > MAX_CHAIN_DEPTH) {
|
|
3313
|
+
throw new MinionryError('BAD_REQUEST', `${method}: would exceed the max chain depth of ${MAX_CHAIN_DEPTH}`);
|
|
3314
|
+
}
|
|
3315
|
+
const outLinks = [...chainLinks.values()].filter((link) => link.state === 'armed' && link.from === from).length;
|
|
3316
|
+
if (outLinks >= MAX_OUT_LINKS_PER_BOARD) {
|
|
3317
|
+
throw new MinionryError('QUOTA_EXCEEDED', `${method}: board '${from}' already has ${MAX_OUT_LINKS_PER_BOARD} outgoing links`);
|
|
3318
|
+
}
|
|
3319
|
+
const armedCount = [...chainLinks.values()].filter((link) => link.state === 'armed').length;
|
|
3320
|
+
if (armedCount >= MAX_LINKS_PER_APP || armedCount >= MAX_LINKS_PER_SPACE) {
|
|
3321
|
+
throw new MinionryError('QUOTA_EXCEEDED', `${method}: at most ${MAX_LINKS_PER_APP} links per app`);
|
|
3322
|
+
}
|
|
3323
|
+
const id = `chain-mock-${++seq}`;
|
|
3324
|
+
const link = {
|
|
3325
|
+
id,
|
|
3326
|
+
from,
|
|
3327
|
+
to,
|
|
3328
|
+
on,
|
|
3329
|
+
passOutputs,
|
|
3330
|
+
name,
|
|
3331
|
+
enabled,
|
|
3332
|
+
state: 'armed',
|
|
3333
|
+
createdAt: new Date().toISOString(),
|
|
3334
|
+
history: [],
|
|
3335
|
+
};
|
|
3336
|
+
chainLinks.set(id, link);
|
|
3337
|
+
return chainToSnapshot(link);
|
|
3338
|
+
},
|
|
3339
|
+
'pm.chains.update': (params) => {
|
|
3340
|
+
const method = 'pm.chains.update';
|
|
3341
|
+
const record = asRecord(params, method);
|
|
3342
|
+
const linkId = asString(record.linkId, 'linkId', method);
|
|
3343
|
+
const link = chainLinks.get(linkId);
|
|
3344
|
+
if (!link)
|
|
3345
|
+
throw new MinionryError('BAD_REQUEST', `${method}: unknown linkId '${linkId}'`);
|
|
3346
|
+
if (link.state !== 'armed') {
|
|
3347
|
+
throw new MinionryError('BAD_REQUEST', `${method}: link '${linkId}' is ${link.state} and can no longer be updated`);
|
|
3348
|
+
}
|
|
3349
|
+
const patch = record.patch === undefined ? {} : asRecord(record.patch, method);
|
|
3350
|
+
for (const immutable of ['from', 'to', 'on', 'passOutputs']) {
|
|
3351
|
+
if (immutable in patch) {
|
|
3352
|
+
throw new MinionryError('BAD_REQUEST', `${method}: '${immutable}' is immutable — create a new link instead`);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
if ('enabled' in patch)
|
|
3356
|
+
link.enabled = patch.enabled === true;
|
|
3357
|
+
if ('name' in patch) {
|
|
3358
|
+
link.name = patch.name === null ? undefined : typeof patch.name === 'string' ? patch.name : link.name;
|
|
3359
|
+
}
|
|
3360
|
+
return chainToSnapshot(link);
|
|
3361
|
+
},
|
|
3362
|
+
'pm.chains.delete': (params) => {
|
|
3363
|
+
const method = 'pm.chains.delete';
|
|
3364
|
+
const linkId = asString(asRecord(params, method).linkId, 'linkId', method);
|
|
3365
|
+
if (!chainLinks.delete(linkId))
|
|
3366
|
+
throw new MinionryError('BAD_REQUEST', `${method}: unknown linkId '${linkId}'`);
|
|
3367
|
+
return null;
|
|
3368
|
+
},
|
|
2705
3369
|
'stream.resume': (params) => {
|
|
2706
3370
|
const record = asRecord(params, 'stream.resume');
|
|
2707
3371
|
const requestId = asString(record.requestId, 'requestId', 'stream.resume');
|
|
@@ -2751,13 +3415,25 @@ export function createMockTransport(config = {}) {
|
|
|
2751
3415
|
const content = files.get(path);
|
|
2752
3416
|
if (content === undefined)
|
|
2753
3417
|
throw new MinionryError('BAD_REQUEST', `files.readEntry: no such file '${path}'`);
|
|
2754
|
-
|
|
3418
|
+
const size = new TextEncoder().encode(content).length;
|
|
3419
|
+
return { kind: 'text', content, modifiedAt: timestamps.get(path), size };
|
|
2755
3420
|
},
|
|
2756
3421
|
'files.write': (params) => {
|
|
2757
3422
|
const record = asRecord(params, 'files.write');
|
|
2758
3423
|
const path = asString(record.path, 'path', 'files.write');
|
|
2759
|
-
|
|
2760
|
-
|
|
3424
|
+
const content = asString(record.content, 'content', 'files.write');
|
|
3425
|
+
const expected = record.expectedModifiedAt;
|
|
3426
|
+
if (expected !== undefined) {
|
|
3427
|
+
if (typeof expected !== 'string' || Number.isNaN(Date.parse(expected))) {
|
|
3428
|
+
throw new MinionryError('BAD_REQUEST', 'files.write: expectedModifiedAt must be an ISO-8601 timestamp');
|
|
3429
|
+
}
|
|
3430
|
+
const current = files.has(path) ? timestamps.get(path) : undefined;
|
|
3431
|
+
if (current === undefined || Date.parse(current) !== Date.parse(expected)) {
|
|
3432
|
+
throw new MinionryError('CONFLICT', `files.write: '${path}' ${current === undefined ? 'no longer exists' : `was modified at ${current}`}, not ${expected} — nothing was written`);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
files.set(path, content);
|
|
3436
|
+
stamp(path);
|
|
2761
3437
|
notifyMockFileChanged(path, 'changed');
|
|
2762
3438
|
return null;
|
|
2763
3439
|
},
|
|
@@ -2802,6 +3478,8 @@ export function createMockTransport(config = {}) {
|
|
|
2802
3478
|
},
|
|
2803
3479
|
'files.delete': (params) => {
|
|
2804
3480
|
const path = asString(asRecord(params, 'files.delete').path, 'path', 'files.delete');
|
|
3481
|
+
const dir = isMockDir(path);
|
|
3482
|
+
const existed = dir || files.has(path);
|
|
2805
3483
|
files.delete(path);
|
|
2806
3484
|
dirs.delete(path);
|
|
2807
3485
|
timestamps.delete(path);
|
|
@@ -2816,12 +3494,17 @@ export function createMockTransport(config = {}) {
|
|
|
2816
3494
|
if (dirPath.startsWith(prefix))
|
|
2817
3495
|
dirs.delete(dirPath);
|
|
2818
3496
|
}
|
|
3497
|
+
if (existed)
|
|
3498
|
+
notifyMockFileChanged(path, 'deleted', dir);
|
|
2819
3499
|
return null;
|
|
2820
3500
|
},
|
|
2821
3501
|
'files.mkdir': (params) => {
|
|
2822
3502
|
const path = asString(asRecord(params, 'files.mkdir').path, 'path', 'files.mkdir');
|
|
3503
|
+
const existed = isMockDir(path);
|
|
2823
3504
|
dirs.add(path);
|
|
2824
|
-
|
|
3505
|
+
stamp(path);
|
|
3506
|
+
if (!existed)
|
|
3507
|
+
notifyMockFileChanged(path, 'created', true);
|
|
2825
3508
|
return null;
|
|
2826
3509
|
},
|
|
2827
3510
|
'files.rename': (params) => {
|
|
@@ -2829,10 +3512,8 @@ export function createMockTransport(config = {}) {
|
|
|
2829
3512
|
const from = asString(record.path, 'path', 'files.rename');
|
|
2830
3513
|
const to = asString(record.newPath, 'newPath', 'files.rename');
|
|
2831
3514
|
const fromPrefix = `${from}/`;
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
[...files.keys(), ...dirs].some((key) => key.startsWith(fromPrefix));
|
|
2835
|
-
if (!exists) {
|
|
3515
|
+
const dir = isMockDir(from);
|
|
3516
|
+
if (!dir && !files.has(from)) {
|
|
2836
3517
|
throw new MinionryError('BAD_REQUEST', `files.rename: no such file or directory '${from}'`);
|
|
2837
3518
|
}
|
|
2838
3519
|
const rewrite = (key) => (key === from ? to : `${to}${key.slice(from.length)}`);
|
|
@@ -2859,6 +3540,7 @@ export function createMockTransport(config = {}) {
|
|
|
2859
3540
|
dirs.add(newKey);
|
|
2860
3541
|
moveTimestamp(key, newKey);
|
|
2861
3542
|
}
|
|
3543
|
+
notifyMockFileChanged(from, 'renamed', dir, to);
|
|
2862
3544
|
return null;
|
|
2863
3545
|
},
|
|
2864
3546
|
'files.uploadStart': (params) => {
|
|
@@ -2917,9 +3599,11 @@ export function createMockTransport(config = {}) {
|
|
|
2917
3599
|
joined.set(chunk, offset);
|
|
2918
3600
|
offset += chunk.byteLength;
|
|
2919
3601
|
}
|
|
3602
|
+
const existed = files.has(session.path);
|
|
2920
3603
|
files.set(session.path, new TextDecoder().decode(joined));
|
|
2921
|
-
|
|
3604
|
+
stamp(session.path);
|
|
2922
3605
|
uploads.delete(uploadId);
|
|
3606
|
+
notifyMockFileChanged(session.path, existed ? 'changed' : 'created');
|
|
2923
3607
|
}
|
|
2924
3608
|
return { chunkIndex, bytesWritten: session.bytesWritten };
|
|
2925
3609
|
},
|
|
@@ -3400,6 +4084,7 @@ export function createMockTransport(config = {}) {
|
|
|
3400
4084
|
modelsStatus: { fetchedAt: new Date().toISOString(), stale: false, refreshing: false },
|
|
3401
4085
|
effortLevels: Object.fromEntries(Object.entries(MOCK_EFFORT_LEVELS).map(([id, levels]) => [id, [...levels]])),
|
|
3402
4086
|
inferenceEngines: [...MOCK_INFERENCE_ENGINES],
|
|
4087
|
+
...(config.agentRunEngines === null ? {} : { agentRunEngines: [...resolvedAgentRunEngines] }),
|
|
3403
4088
|
}),
|
|
3404
4089
|
'terminal.sessions.list': () => [...ptySessions.values()].map(mockPtySessionSummary),
|
|
3405
4090
|
'terminal.sessions.subscribe': (_params, requestId) => {
|
|
@@ -4224,6 +4909,19 @@ export function createMockTransport(config = {}) {
|
|
|
4224
4909
|
}
|
|
4225
4910
|
return METHOD_SCOPES[method];
|
|
4226
4911
|
};
|
|
4912
|
+
const resolveScopes = (method, params) => {
|
|
4913
|
+
const scope = resolveScope(method, params);
|
|
4914
|
+
const scopes = scope === null ? [] : [scope];
|
|
4915
|
+
const raw = scopeParams(params);
|
|
4916
|
+
if (method === 'agents.schedules.create' || method === 'agents.schedules.update') {
|
|
4917
|
+
if (raw.promptFile !== undefined || scopeParams(raw.patch).promptFile !== undefined)
|
|
4918
|
+
scopes.push('files:workspace:read');
|
|
4919
|
+
}
|
|
4920
|
+
else if (method === 'pm.schedules.create' && scopeParams(raw.source).kind === 'board-zip') {
|
|
4921
|
+
scopes.push('files:workspace:read');
|
|
4922
|
+
}
|
|
4923
|
+
return scopes;
|
|
4924
|
+
};
|
|
4227
4925
|
const dispatch = (frame) => {
|
|
4228
4926
|
const { requestId, method } = frame;
|
|
4229
4927
|
const forced = forcedErrors[method];
|
|
@@ -4235,8 +4933,8 @@ export function createMockTransport(config = {}) {
|
|
|
4235
4933
|
fail(requestId, { code: 'SCOPE_DENIED', message: `mock: unknown method '${String(method)}' (fail closed)` });
|
|
4236
4934
|
return;
|
|
4237
4935
|
}
|
|
4238
|
-
const scope =
|
|
4239
|
-
if (scope !==
|
|
4936
|
+
const scope = resolveScopes(method, frame.params).find((required) => !granted.has(required));
|
|
4937
|
+
if (scope !== undefined) {
|
|
4240
4938
|
const hint = granted.size === 0 ? 'call connect() first' : 'not granted on connect';
|
|
4241
4939
|
fail(requestId, { code: 'SCOPE_DENIED', message: `mock: ${method} requires scope '${scope}' — ${hint}` });
|
|
4242
4940
|
return;
|