@minionry/minion 0.7.36 → 0.7.39
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/bin/commands/minions.js +19 -0
- package/bin/minion.js +33 -4
- package/dist/server/cli/headless/claude-invoker-process.js +23 -7
- package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
- package/dist/server/cli/headless/haiku-assessments.js +2 -1
- package/dist/server/cli/headless/mcp-config.js +2 -1
- package/dist/server/engines/claude/claude-command.js +1 -0
- package/dist/server/engines/factory.js +2 -0
- package/dist/server/engines/hermes/HermesEngine.js +112 -219
- package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
- package/dist/server/index.js +10 -4
- package/dist/server/mcp/bouncer-integration.js +3 -2
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
- package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
- package/dist/server/mcp/classifier/telemetry.js +0 -0
- package/dist/server/mcp/permission-channels.js +176 -0
- package/dist/server/mcp/security-analysis.js +9 -1
- package/dist/server/mcp/security-audit.js +22 -2
- package/dist/server/mcp/security-patterns.js +73 -1
- package/dist/server/mcp/server.js +80 -50
- package/dist/server/routes/internal.js +6 -1
- package/dist/server/routes/notifications.js +6 -18
- package/dist/server/server-setup.js +24 -48
- package/dist/server/services/analytics.js +4 -2
- package/dist/server/services/browser/host-mock.js +6 -1
- package/dist/server/services/browser/host.js +27 -3
- package/dist/server/services/browser/reasoning-agent.js +3 -1
- package/dist/server/services/chain/chain-engine.js +494 -0
- package/dist/server/services/chain/chain-store.js +542 -0
- package/dist/server/services/git/haiku.js +2 -2
- package/dist/server/services/plan/agents/check-injection.md +43 -2
- package/dist/server/services/plan/agents/review-code.md +62 -3
- package/dist/server/services/plan/agents/review-quality.md +52 -2
- package/dist/server/services/plan/board-export.js +4 -0
- package/dist/server/services/plan/composer-prompt.js +27 -8
- package/dist/server/services/plan/composer.js +10 -4
- package/dist/server/services/plan/config-installer.js +4 -83
- package/dist/server/services/plan/executor.js +155 -73
- package/dist/server/services/plan/issue-effort.js +9 -21
- package/dist/server/services/plan/issue-prompt-builder.js +72 -9
- package/dist/server/services/plan/parser-core.js +6 -1
- package/dist/server/services/plan/quality-delta.js +6 -24
- package/dist/server/services/plan/readiness-planner.js +12 -4
- package/dist/server/services/plan/record-file.js +52 -0
- package/dist/server/services/plan/review-approval.js +64 -0
- package/dist/server/services/plan/review-gate.js +186 -77
- package/dist/server/services/plan/review-outcome.js +146 -0
- package/dist/server/services/plan/review-report.js +28 -1
- package/dist/server/services/plan/review-target.js +57 -0
- package/dist/server/services/plan/state-reconciler.js +3 -3
- package/dist/server/services/plan/template-diff.js +2 -0
- package/dist/server/services/plan/template-instantiator.js +4 -0
- package/dist/server/services/plan/watcher.js +11 -1
- package/dist/server/services/platform-reconnect.js +7 -0
- package/dist/server/services/platform-token-lifecycle.js +2 -1
- package/dist/server/services/platform.js +293 -50
- package/dist/server/services/relay-http-client.js +107 -0
- package/dist/server/services/schedule/agent-schedule-store.js +69 -8
- package/dist/server/services/schedule/agent-scheduler.js +48 -7
- package/dist/server/services/schedule/daily-timing.js +52 -0
- package/dist/server/services/schedule/prompt-file.js +55 -0
- package/dist/server/services/schedule/run-bootstrap.js +20 -5
- package/dist/server/services/schedule/schedule-store.js +54 -4
- package/dist/server/services/schedule/schedule-wire.js +17 -0
- package/dist/server/services/schedule/scheduler.js +43 -9
- package/dist/server/services/sdk/agent-schedule.js +294 -0
- package/dist/server/services/sdk/agents.js +95 -13
- package/dist/server/services/sdk/app-tools.js +44 -7
- package/dist/server/services/sdk/atomic-write.js +75 -0
- package/dist/server/services/sdk/browser.js +5 -1
- package/dist/server/services/sdk/composer.js +2 -0
- package/dist/server/services/sdk/file-change-notify.js +60 -13
- package/dist/server/services/sdk/files-app.js +12 -7
- package/dist/server/services/sdk/files-search.js +14 -0
- package/dist/server/services/sdk/files-transfer.js +1 -1
- package/dist/server/services/sdk/files-watch.js +721 -0
- package/dist/server/services/sdk/files-workspace.js +37 -11
- package/dist/server/services/sdk/inference-claude.js +4 -2
- package/dist/server/services/sdk/inference.js +16 -2
- package/dist/server/services/sdk/pm-chain.js +0 -0
- package/dist/server/services/sdk/pm-schedule.js +72 -15
- package/dist/server/services/sdk/pm.js +22 -0
- package/dist/server/services/sdk/rate-limits.js +32 -0
- package/dist/server/services/sdk/registry.js +40 -2
- package/dist/server/services/sdk/terminal.js +1 -1
- package/dist/server/services/sentry.js +5 -1
- package/dist/server/services/terminal/pty-manager.js +26 -11
- package/dist/server/services/terminal/pty-utils.js +12 -1
- package/dist/server/services/terminal/terminal-modes.js +87 -0
- package/dist/server/services/timeline/assembler.js +2 -2
- package/dist/server/services/timeline/index.js +1 -1
- package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
- package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
- package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
- package/dist/server/services/websocket/browser-handlers.js +5 -2
- package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
- package/dist/server/services/websocket/file-transfer-http.js +12 -6
- package/dist/server/services/websocket/handler.js +185 -33
- package/dist/server/services/websocket/msg-id-tracker.js +94 -19
- package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
- package/dist/server/services/websocket/plan-handlers.js +10 -5
- package/dist/server/services/websocket/plan-helpers.js +23 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
- package/dist/server/services/websocket/schedule-handlers.js +8 -2
- package/dist/server/services/websocket/sdk-agent-host.js +5 -1
- package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
- package/dist/server/services/websocket/sdk-browser-host.js +11 -4
- package/dist/server/services/websocket/sdk-handlers.js +6 -1
- package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
- package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
- package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
- package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
- package/dist/server/services/websocket/settings-handlers.js +6 -18
- package/dist/server/services/websocket/tab-broadcast.js +5 -1
- package/dist/server/services/websocket/terminal-handlers.js +51 -6
- package/dist/server/services/websocket/types.js +6 -5
- package/dist/server/services/websocket/viewer-interest.js +30 -0
- package/package.json +6 -6
|
@@ -6,7 +6,7 @@ import { resolvePmDir } from '../plan/parser.js';
|
|
|
6
6
|
import { wireExecutorEvents } from '../websocket/plan-execution-handlers.js';
|
|
7
7
|
import { schedulerCache } from '../websocket/plan-helpers.js';
|
|
8
8
|
import { bootstrapRun } from './run-bootstrap.js';
|
|
9
|
-
import { ScheduleStore } from './schedule-store.js';
|
|
9
|
+
import { computeNextFireAt, exceedsMaxLateness, isRecurringTiming, SCHEDULE_MISSED_LATENESS_ERROR, ScheduleStore, } from './schedule-store.js';
|
|
10
10
|
export const SCHEDULER_TICK_MS = 30_000;
|
|
11
11
|
export const SCHEDULE_ONCE_GRACE_MS = 15 * 60_000;
|
|
12
12
|
export const CONTINUOUS_MIN_SPACING_MS = 60_000;
|
|
@@ -88,6 +88,25 @@ export class SchedulerEngine {
|
|
|
88
88
|
this.runs.reset();
|
|
89
89
|
this.store = null;
|
|
90
90
|
}
|
|
91
|
+
fireNow(scheduleId) {
|
|
92
|
+
const store = this.store;
|
|
93
|
+
if (!store)
|
|
94
|
+
return { kind: 'not-found' };
|
|
95
|
+
const before = store.get(scheduleId);
|
|
96
|
+
if (!before)
|
|
97
|
+
return { kind: 'not-found' };
|
|
98
|
+
if (!before.enabled)
|
|
99
|
+
return { kind: 'disabled' };
|
|
100
|
+
const plannedNextFireAt = before.nextFireAt;
|
|
101
|
+
if (this.runs.has(scheduleId)) {
|
|
102
|
+
this.recordSkippedOverlap(before, Date.now());
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.fire(before, Date.now());
|
|
106
|
+
}
|
|
107
|
+
store.restoreNextFireAt(scheduleId, plannedNextFireAt);
|
|
108
|
+
return { kind: 'ok', record: store.get(scheduleId) };
|
|
109
|
+
}
|
|
91
110
|
tick() {
|
|
92
111
|
const store = this.store;
|
|
93
112
|
if (!store)
|
|
@@ -104,6 +123,10 @@ export class SchedulerEngine {
|
|
|
104
123
|
}
|
|
105
124
|
if (!this.isDue(record, now))
|
|
106
125
|
continue;
|
|
126
|
+
if (exceedsMaxLateness(record, now)) {
|
|
127
|
+
this.recordMissedLate(record, now);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
107
130
|
this.fire(record, now);
|
|
108
131
|
}
|
|
109
132
|
}
|
|
@@ -132,8 +155,8 @@ export class SchedulerEngine {
|
|
|
132
155
|
if (record.timing.kind === 'once') {
|
|
133
156
|
this.recoverOnce(record, now);
|
|
134
157
|
}
|
|
135
|
-
else if (record.timing
|
|
136
|
-
this.
|
|
158
|
+
else if (isRecurringTiming(record.timing)) {
|
|
159
|
+
this.recoverRecurring(record, now);
|
|
137
160
|
}
|
|
138
161
|
}
|
|
139
162
|
}
|
|
@@ -154,10 +177,14 @@ export class SchedulerEngine {
|
|
|
154
177
|
store.setEnabled(record.id, false);
|
|
155
178
|
this.broadcastSchedules();
|
|
156
179
|
}
|
|
157
|
-
|
|
180
|
+
recoverRecurring(record, now) {
|
|
158
181
|
const next = record.nextFireAt ? parseEpoch(record.nextFireAt) : Number.NaN;
|
|
159
182
|
if (Number.isFinite(next) && next > now)
|
|
160
183
|
return;
|
|
184
|
+
if (exceedsMaxLateness(record, now)) {
|
|
185
|
+
this.recordMissedLate(record, now);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
161
188
|
this.fire(record, now);
|
|
162
189
|
}
|
|
163
190
|
fire(record, now) {
|
|
@@ -243,10 +270,17 @@ export class SchedulerEngine {
|
|
|
243
270
|
store.recordFire(record.id, { at: new Date(now).toISOString(), status: 'skipped-overlap', boardId: null, error: 'previous run for this schedule is still in flight' }, this.nextFireAtAfterStart(record, now));
|
|
244
271
|
this.broadcastSchedules();
|
|
245
272
|
}
|
|
273
|
+
recordMissedLate(record, now) {
|
|
274
|
+
const store = this.store;
|
|
275
|
+
if (!store)
|
|
276
|
+
return;
|
|
277
|
+
store.recordFire(record.id, { at: new Date(now).toISOString(), status: 'missed', boardId: null, error: SCHEDULE_MISSED_LATENESS_ERROR }, this.nextFireAtAfterStart(record, now));
|
|
278
|
+
this.broadcastSchedules();
|
|
279
|
+
}
|
|
246
280
|
nextFireAtAfterStart(record, now) {
|
|
247
|
-
if (record.timing
|
|
248
|
-
return
|
|
249
|
-
return
|
|
281
|
+
if (!isRecurringTiming(record.timing))
|
|
282
|
+
return null;
|
|
283
|
+
return computeNextFireAt(record.timing, new Date(now));
|
|
250
284
|
}
|
|
251
285
|
nextFireAtAfterFailure(record, now, consecutiveEmptyRuns) {
|
|
252
286
|
if (record.timing.kind === 'once')
|
|
@@ -254,7 +288,7 @@ export class SchedulerEngine {
|
|
|
254
288
|
if (record.timing.kind === 'continuous') {
|
|
255
289
|
return new Date(now + continuousDelayMs(consecutiveEmptyRuns)).toISOString();
|
|
256
290
|
}
|
|
257
|
-
return new Date(now
|
|
291
|
+
return computeNextFireAt(record.timing, new Date(now));
|
|
258
292
|
}
|
|
259
293
|
nextFireAtAfterSettle(record, now, consecutiveEmptyRuns) {
|
|
260
294
|
if (record.timing.kind === 'once')
|
|
@@ -263,7 +297,7 @@ export class SchedulerEngine {
|
|
|
263
297
|
return new Date(now + continuousDelayMs(consecutiveEmptyRuns)).toISOString();
|
|
264
298
|
}
|
|
265
299
|
const store = this.store;
|
|
266
|
-
return store?.get(record.id)?.nextFireAt ?? new Date(now
|
|
300
|
+
return store?.get(record.id)?.nextFireAt ?? computeNextFireAt(record.timing, new Date(now));
|
|
267
301
|
}
|
|
268
302
|
broadcastSchedules() {
|
|
269
303
|
if (!this.store)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { isValidMaxLatenessMs, } from '../schedule/agent-schedule-store.js';
|
|
4
|
+
import { normalizeWallClockTiming } from '../schedule/daily-timing.js';
|
|
5
|
+
import { isValidPromptFilePath, readPromptFile } from '../schedule/prompt-file.js';
|
|
6
|
+
import { logAppAudit } from './audit.js';
|
|
7
|
+
import { guardWorkspacePath, WORKSPACE_PATH_PREFIX } from './files-workspace.js';
|
|
8
|
+
import { SdkMethodError } from './registry.js';
|
|
9
|
+
function toAgentScheduleSnapshot(record) {
|
|
10
|
+
const snapshot = {
|
|
11
|
+
id: record.id,
|
|
12
|
+
createdAt: record.createdAt,
|
|
13
|
+
prompt: record.prompt,
|
|
14
|
+
timing: record.timing,
|
|
15
|
+
enabled: record.enabled,
|
|
16
|
+
nextFireAt: record.nextFireAt,
|
|
17
|
+
history: record.history.map((entry) => ({ ...entry })),
|
|
18
|
+
};
|
|
19
|
+
if (record.name !== undefined)
|
|
20
|
+
snapshot.name = record.name;
|
|
21
|
+
if (record.promptFile !== undefined)
|
|
22
|
+
snapshot.promptFile = `${WORKSPACE_PATH_PREFIX}/${record.promptFile}`;
|
|
23
|
+
if (record.lastFire !== undefined)
|
|
24
|
+
snapshot.lastFire = { ...record.lastFire };
|
|
25
|
+
if (record.maxLatenessMs !== undefined)
|
|
26
|
+
snapshot.maxLatenessMs = record.maxLatenessMs;
|
|
27
|
+
return snapshot;
|
|
28
|
+
}
|
|
29
|
+
function ownedByCaller(record, appId) {
|
|
30
|
+
return record.initiator?.appId === appId;
|
|
31
|
+
}
|
|
32
|
+
let scheduleHost = null;
|
|
33
|
+
export function setSdkAgentScheduleHost(host) {
|
|
34
|
+
scheduleHost = host;
|
|
35
|
+
}
|
|
36
|
+
export function resetSdkAgentScheduleHost() {
|
|
37
|
+
scheduleHost = null;
|
|
38
|
+
}
|
|
39
|
+
function requireScheduleHost(method) {
|
|
40
|
+
if (!scheduleHost) {
|
|
41
|
+
throw new SdkMethodError('INTERNAL', `'${method}' host is not wired — the WebSocket handler installs it at startup`);
|
|
42
|
+
}
|
|
43
|
+
return scheduleHost;
|
|
44
|
+
}
|
|
45
|
+
function paramsObject(params) {
|
|
46
|
+
return typeof params === 'object' && params !== null && !Array.isArray(params)
|
|
47
|
+
? params
|
|
48
|
+
: {};
|
|
49
|
+
}
|
|
50
|
+
function parseScheduleIdParams(params, method) {
|
|
51
|
+
const raw = paramsObject(params);
|
|
52
|
+
if (typeof raw.scheduleId !== 'string' || raw.scheduleId.trim() === '') {
|
|
53
|
+
throw new SdkMethodError('BAD_REQUEST', `${method} requires params { scheduleId }`);
|
|
54
|
+
}
|
|
55
|
+
return raw.scheduleId;
|
|
56
|
+
}
|
|
57
|
+
function scheduleNotFound(scheduleId) {
|
|
58
|
+
return new SdkMethodError('BAD_REQUEST', `schedule '${scheduleId}' not found in this Space`);
|
|
59
|
+
}
|
|
60
|
+
function parseTiming(raw, method) {
|
|
61
|
+
if (raw && typeof raw === 'object') {
|
|
62
|
+
const t = raw;
|
|
63
|
+
if (t.kind === 'once' && typeof t.at === 'string' && !Number.isNaN(Date.parse(t.at))) {
|
|
64
|
+
return { kind: 'once', at: t.at };
|
|
65
|
+
}
|
|
66
|
+
if (t.kind === 'interval' && typeof t.everyMs === 'number' && Number.isFinite(t.everyMs)) {
|
|
67
|
+
return { kind: 'interval', everyMs: t.everyMs };
|
|
68
|
+
}
|
|
69
|
+
if (t.kind === 'daily' || t.kind === 'monthly') {
|
|
70
|
+
const normalized = normalizeWallClockTiming(raw);
|
|
71
|
+
if (normalized)
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new SdkMethodError('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`);
|
|
76
|
+
}
|
|
77
|
+
function parseMaxLatenessCreateField(raw, method) {
|
|
78
|
+
if (raw === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (!isValidMaxLatenessMs(raw)) {
|
|
81
|
+
throw new SdkMethodError('BAD_REQUEST', `${method}: maxLatenessMs must be an integer >= the schedule minimum`);
|
|
82
|
+
}
|
|
83
|
+
return raw;
|
|
84
|
+
}
|
|
85
|
+
function parseMaxLatenessPatchValue(raw, method) {
|
|
86
|
+
if (raw === null)
|
|
87
|
+
return null;
|
|
88
|
+
if (!isValidMaxLatenessMs(raw)) {
|
|
89
|
+
throw new SdkMethodError('BAD_REQUEST', `${method}: maxLatenessMs must be an integer >= the schedule minimum, or null to clear it`);
|
|
90
|
+
}
|
|
91
|
+
return raw;
|
|
92
|
+
}
|
|
93
|
+
function resolvePromptFileParam(workingDir, raw, method) {
|
|
94
|
+
if (typeof raw !== 'string' || !raw.startsWith(WORKSPACE_PATH_PREFIX)) {
|
|
95
|
+
throw new SdkMethodError('BAD_REQUEST', `${method}: promptFile must be a workspace-rooted path ('${WORKSPACE_PATH_PREFIX}/<path>')`);
|
|
96
|
+
}
|
|
97
|
+
const rel = raw.slice(WORKSPACE_PATH_PREFIX.length).replace(/^\/+/, '');
|
|
98
|
+
if (!isValidPromptFilePath(rel)) {
|
|
99
|
+
throw new SdkMethodError('BAD_REQUEST', `${method}: promptFile '${raw}' must not contain a '..' segment`);
|
|
100
|
+
}
|
|
101
|
+
guardWorkspacePath(workingDir, rel);
|
|
102
|
+
const result = readPromptFile(workingDir, rel);
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
throw new SdkMethodError('BAD_REQUEST', `${method}: ${result.reason}`);
|
|
105
|
+
}
|
|
106
|
+
return { prompt: result.content, promptFile: rel };
|
|
107
|
+
}
|
|
108
|
+
function parseCreateParams(args) {
|
|
109
|
+
const raw = paramsObject(args.params);
|
|
110
|
+
const hasPrompt = raw.prompt !== undefined;
|
|
111
|
+
const hasPromptFile = raw.promptFile !== undefined;
|
|
112
|
+
if (hasPrompt === hasPromptFile) {
|
|
113
|
+
throw new SdkMethodError('BAD_REQUEST', `${args.method} requires exactly one of params.prompt or params.promptFile`);
|
|
114
|
+
}
|
|
115
|
+
let prompt;
|
|
116
|
+
let promptFile;
|
|
117
|
+
if (hasPromptFile) {
|
|
118
|
+
const resolved = resolvePromptFileParam(args.workingDir, raw.promptFile, args.method);
|
|
119
|
+
prompt = resolved.prompt;
|
|
120
|
+
promptFile = resolved.promptFile;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
if (typeof raw.prompt !== 'string' || raw.prompt.trim() === '') {
|
|
124
|
+
throw new SdkMethodError('BAD_REQUEST', `${args.method}: params.prompt must be a non-empty string`);
|
|
125
|
+
}
|
|
126
|
+
prompt = raw.prompt.trim();
|
|
127
|
+
}
|
|
128
|
+
const timing = parseTiming(raw.timing, args.method);
|
|
129
|
+
const parsed = { prompt, timing };
|
|
130
|
+
if (promptFile !== undefined)
|
|
131
|
+
parsed.promptFile = promptFile;
|
|
132
|
+
if (typeof raw.name === 'string')
|
|
133
|
+
parsed.name = raw.name;
|
|
134
|
+
if (raw.enabled !== undefined) {
|
|
135
|
+
if (typeof raw.enabled !== 'boolean') {
|
|
136
|
+
throw new SdkMethodError('BAD_REQUEST', `${args.method}: enabled must be a boolean when present`);
|
|
137
|
+
}
|
|
138
|
+
parsed.enabled = raw.enabled;
|
|
139
|
+
}
|
|
140
|
+
const maxLatenessMs = parseMaxLatenessCreateField(raw.maxLatenessMs, args.method);
|
|
141
|
+
if (maxLatenessMs !== undefined)
|
|
142
|
+
parsed.maxLatenessMs = maxLatenessMs;
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
function applyContentPatchFields(args, patchObj, patch) {
|
|
146
|
+
if (patchObj.prompt !== undefined && patchObj.promptFile !== undefined) {
|
|
147
|
+
throw new SdkMethodError('BAD_REQUEST', `${args.method}: provide at most one of patch.prompt or patch.promptFile`);
|
|
148
|
+
}
|
|
149
|
+
if (patchObj.name !== undefined) {
|
|
150
|
+
if (typeof patchObj.name !== 'string') {
|
|
151
|
+
throw new SdkMethodError('BAD_REQUEST', `${args.method}: patch.name must be a string when present`);
|
|
152
|
+
}
|
|
153
|
+
patch.name = patchObj.name;
|
|
154
|
+
}
|
|
155
|
+
if (patchObj.prompt !== undefined) {
|
|
156
|
+
if (typeof patchObj.prompt !== 'string' || patchObj.prompt.trim() === '') {
|
|
157
|
+
throw new SdkMethodError('BAD_REQUEST', `${args.method}: patch.prompt must be a non-empty string`);
|
|
158
|
+
}
|
|
159
|
+
patch.prompt = patchObj.prompt.trim();
|
|
160
|
+
}
|
|
161
|
+
if (patchObj.promptFile !== undefined) {
|
|
162
|
+
const resolved = resolvePromptFileParam(args.workingDir, patchObj.promptFile, args.method);
|
|
163
|
+
patch.prompt = resolved.prompt;
|
|
164
|
+
patch.promptFile = resolved.promptFile;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function applyStructuredPatchFields(args, patchObj, patch) {
|
|
168
|
+
if (patchObj.enabled !== undefined) {
|
|
169
|
+
if (typeof patchObj.enabled !== 'boolean') {
|
|
170
|
+
throw new SdkMethodError('BAD_REQUEST', `${args.method}: patch.enabled must be a boolean when present`);
|
|
171
|
+
}
|
|
172
|
+
patch.enabled = patchObj.enabled;
|
|
173
|
+
}
|
|
174
|
+
if (patchObj.timing !== undefined)
|
|
175
|
+
patch.timing = parseTiming(patchObj.timing, args.method);
|
|
176
|
+
if (patchObj.maxLatenessMs !== undefined) {
|
|
177
|
+
patch.maxLatenessMs = parseMaxLatenessPatchValue(patchObj.maxLatenessMs, args.method);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function parseUpdateParams(args) {
|
|
181
|
+
const scheduleId = parseScheduleIdParams(args.params, args.method);
|
|
182
|
+
const raw = paramsObject(args.params);
|
|
183
|
+
const rawPatch = raw.patch;
|
|
184
|
+
if (typeof rawPatch !== 'object' || rawPatch === null || Array.isArray(rawPatch)) {
|
|
185
|
+
throw new SdkMethodError('BAD_REQUEST', `${args.method} requires params.patch to be an object`);
|
|
186
|
+
}
|
|
187
|
+
const patchObj = rawPatch;
|
|
188
|
+
const patch = {};
|
|
189
|
+
applyContentPatchFields(args, patchObj, patch);
|
|
190
|
+
applyStructuredPatchFields(args, patchObj, patch);
|
|
191
|
+
return { scheduleId, patch };
|
|
192
|
+
}
|
|
193
|
+
function paramsCarryPromptFile(params) {
|
|
194
|
+
const raw = paramsObject(params);
|
|
195
|
+
if (raw.promptFile !== undefined)
|
|
196
|
+
return true;
|
|
197
|
+
const patch = paramsObject(raw.patch);
|
|
198
|
+
return patch.promptFile !== undefined;
|
|
199
|
+
}
|
|
200
|
+
export function resolveAgentSchedulesScopes(params) {
|
|
201
|
+
return paramsCarryPromptFile(params) ? ['agents:schedule', 'files:workspace:read'] : ['agents:schedule'];
|
|
202
|
+
}
|
|
203
|
+
export function agentSchedulesListHandler(args) {
|
|
204
|
+
const host = requireScheduleHost(args.method);
|
|
205
|
+
const store = host.getStore(args.workingDir);
|
|
206
|
+
const schedules = store
|
|
207
|
+
.list()
|
|
208
|
+
.filter((record) => ownedByCaller(record, args.appId))
|
|
209
|
+
.map(toAgentScheduleSnapshot);
|
|
210
|
+
return { schedules };
|
|
211
|
+
}
|
|
212
|
+
export function agentSchedulesCreateHandler(args) {
|
|
213
|
+
const host = requireScheduleHost(args.method);
|
|
214
|
+
const { name, prompt, promptFile, timing, enabled, maxLatenessMs } = parseCreateParams(args);
|
|
215
|
+
const store = host.getStore(args.workingDir);
|
|
216
|
+
const record = store.create({
|
|
217
|
+
name,
|
|
218
|
+
prompt,
|
|
219
|
+
...(promptFile !== undefined ? { promptFile } : {}),
|
|
220
|
+
initiator: { appId: args.appId, origin: args.origin },
|
|
221
|
+
timing,
|
|
222
|
+
enabled,
|
|
223
|
+
maxLatenessMs,
|
|
224
|
+
});
|
|
225
|
+
host.notifyAndBroadcast(args.workingDir);
|
|
226
|
+
logAppAudit({
|
|
227
|
+
appId: args.appId,
|
|
228
|
+
origin: args.origin,
|
|
229
|
+
method: 'agents.schedules.create.created',
|
|
230
|
+
decision: 'allow',
|
|
231
|
+
requestId: args.requestId,
|
|
232
|
+
spaceKey: args.spaceKey,
|
|
233
|
+
reason: `schedule ${record.id} created (nextFireAt=${record.nextFireAt ?? 'none'})`,
|
|
234
|
+
});
|
|
235
|
+
return toAgentScheduleSnapshot(record);
|
|
236
|
+
}
|
|
237
|
+
export function agentSchedulesUpdateHandler(args) {
|
|
238
|
+
const host = requireScheduleHost(args.method);
|
|
239
|
+
const { scheduleId, patch } = parseUpdateParams(args);
|
|
240
|
+
const store = host.getStore(args.workingDir);
|
|
241
|
+
const existing = store.get(scheduleId);
|
|
242
|
+
if (!existing || !ownedByCaller(existing, args.appId))
|
|
243
|
+
throw scheduleNotFound(scheduleId);
|
|
244
|
+
const record = store.update(scheduleId, patch);
|
|
245
|
+
if (!record)
|
|
246
|
+
throw scheduleNotFound(scheduleId);
|
|
247
|
+
host.notifyAndBroadcast(args.workingDir);
|
|
248
|
+
return toAgentScheduleSnapshot(record);
|
|
249
|
+
}
|
|
250
|
+
export function agentSchedulesDeleteHandler(args) {
|
|
251
|
+
const host = requireScheduleHost(args.method);
|
|
252
|
+
const scheduleId = parseScheduleIdParams(args.params, args.method);
|
|
253
|
+
const store = host.getStore(args.workingDir);
|
|
254
|
+
const existing = store.get(scheduleId);
|
|
255
|
+
if (!existing || !ownedByCaller(existing, args.appId))
|
|
256
|
+
throw scheduleNotFound(scheduleId);
|
|
257
|
+
if (!store.delete(scheduleId))
|
|
258
|
+
throw scheduleNotFound(scheduleId);
|
|
259
|
+
host.notifyAndBroadcast(args.workingDir);
|
|
260
|
+
logAppAudit({
|
|
261
|
+
appId: args.appId,
|
|
262
|
+
origin: args.origin,
|
|
263
|
+
method: 'agents.schedules.delete.deleted',
|
|
264
|
+
decision: 'allow',
|
|
265
|
+
requestId: args.requestId,
|
|
266
|
+
spaceKey: args.spaceKey,
|
|
267
|
+
reason: `schedule ${scheduleId} deleted`,
|
|
268
|
+
});
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
export function agentSchedulesRunNowHandler(args) {
|
|
272
|
+
const host = requireScheduleHost(args.method);
|
|
273
|
+
const scheduleId = parseScheduleIdParams(args.params, args.method);
|
|
274
|
+
const store = host.getStore(args.workingDir);
|
|
275
|
+
const existing = store.get(scheduleId);
|
|
276
|
+
if (!existing || !ownedByCaller(existing, args.appId))
|
|
277
|
+
throw scheduleNotFound(scheduleId);
|
|
278
|
+
const result = host.fireNow(args.workingDir, scheduleId);
|
|
279
|
+
if (result.kind === 'not-found')
|
|
280
|
+
throw scheduleNotFound(scheduleId);
|
|
281
|
+
if (result.kind === 'disabled') {
|
|
282
|
+
throw new SdkMethodError('BAD_REQUEST', `schedule '${scheduleId}' is disabled — enable it before running it now`);
|
|
283
|
+
}
|
|
284
|
+
logAppAudit({
|
|
285
|
+
appId: args.appId,
|
|
286
|
+
origin: args.origin,
|
|
287
|
+
method: 'agents.schedules.runNow.fired',
|
|
288
|
+
decision: 'allow',
|
|
289
|
+
requestId: args.requestId,
|
|
290
|
+
spaceKey: args.spaceKey,
|
|
291
|
+
reason: `schedule ${scheduleId} fired on demand`,
|
|
292
|
+
});
|
|
293
|
+
return toAgentScheduleSnapshot(result.record);
|
|
294
|
+
}
|
|
@@ -7,6 +7,7 @@ import { hermesAvailability, MINIMUM_HERMES_APP_RUN_VERSION } from '../../engine
|
|
|
7
7
|
import { getQualityEffortLevel, getQualityEngine, getQualityFastMode, getQualityModel } from '../settings.js';
|
|
8
8
|
import { resolveAppOwnedModel } from './app-endpoints.js';
|
|
9
9
|
import { logAppAudit } from './audit.js';
|
|
10
|
+
import { resolveAutomationTarget } from './browser.js';
|
|
10
11
|
import { deregisterLiveRun, findLiveRunByTab, listLiveSubscriptions, registerLiveRun } from './live-registry.js';
|
|
11
12
|
import { SdkMethodError } from './registry.js';
|
|
12
13
|
import { dropSubscription } from './revoke.js';
|
|
@@ -90,17 +91,19 @@ function cancelRunRecord(record, cause) {
|
|
|
90
91
|
record.cancelledBy = cause;
|
|
91
92
|
record.cancelRun();
|
|
92
93
|
}
|
|
93
|
-
const RUN_PARAMS_SHAPE = '{ prompt, title?, model?, modelSelection?, systemPrompt?, budget?: { maxTurns?, maxSeconds? }, sessionId? }';
|
|
94
|
+
const RUN_PARAMS_SHAPE = '{ prompt, title?, model?, modelSelection?, engineSelection?: { engine?, model?, effortLevel?, fastMode? }, systemPrompt?, budget?: { maxTurns?, maxSeconds? }, sessionId? }';
|
|
94
95
|
const RUN_PARAM_KEYS = new Set([
|
|
95
96
|
'prompt',
|
|
96
97
|
'title',
|
|
97
98
|
'model',
|
|
98
99
|
'modelSelection',
|
|
100
|
+
'engineSelection',
|
|
99
101
|
'systemPrompt',
|
|
100
102
|
'budget',
|
|
101
103
|
'sessionId',
|
|
102
104
|
]);
|
|
103
105
|
const BUDGET_KEYS = new Set(['maxTurns', 'maxSeconds']);
|
|
106
|
+
const ENGINE_SELECTION_KEYS = new Set(['engine', 'model', 'effortLevel', 'fastMode']);
|
|
104
107
|
function badRequest(message) {
|
|
105
108
|
return new SdkMethodError('BAD_REQUEST', message);
|
|
106
109
|
}
|
|
@@ -161,14 +164,50 @@ function parseModelSelection(raw) {
|
|
|
161
164
|
}
|
|
162
165
|
return name;
|
|
163
166
|
}
|
|
164
|
-
function
|
|
165
|
-
const
|
|
167
|
+
function optionalEngineSelectionString(raw, field) {
|
|
168
|
+
const value = raw[field];
|
|
169
|
+
if (value === undefined)
|
|
170
|
+
return undefined;
|
|
171
|
+
if (typeof value !== 'string' || value === '') {
|
|
172
|
+
throw badRequest(`agents.run engineSelection.${field} must be a non-empty string when present`);
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
function parseEngineSelection(raw) {
|
|
177
|
+
if (raw === undefined)
|
|
178
|
+
return undefined;
|
|
179
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
180
|
+
throw badRequest('agents.run engineSelection must be an object { engine?, model?, effortLevel?, fastMode? }');
|
|
181
|
+
}
|
|
182
|
+
const obj = raw;
|
|
183
|
+
refuseUnknownKeys(obj, ENGINE_SELECTION_KEYS, 'agents.run engineSelection', '{ engine?, model?, effortLevel?, fastMode? }');
|
|
184
|
+
const engine = optionalEngineSelectionString(obj, 'engine');
|
|
185
|
+
const model = optionalEngineSelectionString(obj, 'model');
|
|
186
|
+
const effortLevel = optionalEngineSelectionString(obj, 'effortLevel');
|
|
187
|
+
if (obj.fastMode !== undefined && typeof obj.fastMode !== 'boolean') {
|
|
188
|
+
throw badRequest('agents.run engineSelection.fastMode must be a boolean when present');
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
...(engine !== undefined ? { engine } : {}),
|
|
192
|
+
...(model !== undefined ? { model } : {}),
|
|
193
|
+
...(effortLevel !== undefined ? { effortLevel } : {}),
|
|
194
|
+
...(obj.fastMode !== undefined ? { fastMode: obj.fastMode } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function assertRunFieldsCombine({ model, modelSelection, engineSelection, systemPrompt, budget, sessionId }) {
|
|
198
|
+
const choosesModel = model !== undefined || modelSelection !== undefined || engineSelection !== undefined;
|
|
166
199
|
if (sessionId !== undefined && (choosesModel || systemPrompt !== undefined || budget !== undefined)) {
|
|
167
|
-
throw badRequest('agents.run with sessionId continues that session on its own model, systemPrompt and budget — omit model, modelSelection, systemPrompt and budget');
|
|
200
|
+
throw badRequest('agents.run with sessionId continues that session on its own model, systemPrompt and budget — omit model, modelSelection, engineSelection, systemPrompt and budget');
|
|
168
201
|
}
|
|
169
202
|
if (model !== undefined && modelSelection !== undefined) {
|
|
170
203
|
throw badRequest('agents.run takes model or modelSelection, not both');
|
|
171
204
|
}
|
|
205
|
+
if (model !== undefined && engineSelection !== undefined) {
|
|
206
|
+
throw badRequest('agents.run takes model or engineSelection, not both');
|
|
207
|
+
}
|
|
208
|
+
if (modelSelection !== undefined && engineSelection !== undefined) {
|
|
209
|
+
throw badRequest('agents.run takes modelSelection or engineSelection, not both');
|
|
210
|
+
}
|
|
172
211
|
if (model === undefined && (systemPrompt !== undefined || budget !== undefined)) {
|
|
173
212
|
throw badRequest('agents.run systemPrompt and budget apply only to a run on an app-registered model — pass model');
|
|
174
213
|
}
|
|
@@ -185,15 +224,17 @@ function parseRunParams(params) {
|
|
|
185
224
|
const title = optionalString(raw, 'title', false);
|
|
186
225
|
const model = optionalString(raw, 'model', true);
|
|
187
226
|
const modelSelection = parseModelSelection(raw.modelSelection);
|
|
227
|
+
const engineSelection = parseEngineSelection(raw.engineSelection);
|
|
188
228
|
const sessionId = optionalString(raw, 'sessionId', true);
|
|
189
229
|
const systemPrompt = parseSystemPrompt(raw.systemPrompt);
|
|
190
230
|
const budget = raw.budget === undefined ? undefined : parseBudget(raw.budget);
|
|
191
|
-
assertRunFieldsCombine({ model, modelSelection, systemPrompt, budget, sessionId });
|
|
231
|
+
assertRunFieldsCombine({ model, modelSelection, engineSelection, systemPrompt, budget, sessionId });
|
|
192
232
|
return {
|
|
193
233
|
prompt: raw.prompt,
|
|
194
234
|
...(title !== undefined ? { title } : {}),
|
|
195
235
|
...(model !== undefined ? { model } : {}),
|
|
196
236
|
...(modelSelection !== undefined ? { modelSelection } : {}),
|
|
237
|
+
...(engineSelection !== undefined ? { engineSelection } : {}),
|
|
197
238
|
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
|
198
239
|
...(budget !== undefined ? { budget } : {}),
|
|
199
240
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
@@ -226,15 +267,26 @@ async function admitAppOwnedModel(modelValue, args) {
|
|
|
226
267
|
}
|
|
227
268
|
export function resolveAgentsRunScopes(params) {
|
|
228
269
|
const raw = typeof params === 'object' && params !== null && !Array.isArray(params) ? params : {};
|
|
229
|
-
|
|
270
|
+
const scopes = ['agents:run'];
|
|
271
|
+
if (raw.modelSelection !== undefined)
|
|
272
|
+
scopes.push('quality:read');
|
|
273
|
+
if (raw.engineSelection !== undefined)
|
|
274
|
+
scopes.push('settings:read');
|
|
275
|
+
return scopes;
|
|
276
|
+
}
|
|
277
|
+
export function appRunEngines() {
|
|
278
|
+
return ['claude-code'];
|
|
230
279
|
}
|
|
231
|
-
function
|
|
280
|
+
function engineUnavailableForAppRun(engine) {
|
|
281
|
+
return new SdkMethodError('ENGINE_UNAVAILABLE', `agents.run: ${engine} cannot run an app session with tools on this host — choose one of ${appRunEngines().join(', ') || '(none)'}`);
|
|
282
|
+
}
|
|
283
|
+
function assertUserOwnedProfile(model, engine) {
|
|
232
284
|
const parsed = parseProfileModelValue(model);
|
|
233
285
|
if (!parsed)
|
|
234
286
|
return;
|
|
235
287
|
const profile = new ModelProfileStore().get(parsed.profileId);
|
|
236
|
-
if (!profile || profile.owner || profile.engine !==
|
|
237
|
-
throw new SdkMethodError('MODEL_NOT_ALLOWED', `agents.run: the
|
|
288
|
+
if (!profile || profile.owner || profile.engine !== engine || parsed.engine !== engine) {
|
|
289
|
+
throw new SdkMethodError('MODEL_NOT_ALLOWED', `agents.run: the model '${displayValue(model)}' is not a ${engine} model added on this machine — choose another model`);
|
|
238
290
|
}
|
|
239
291
|
}
|
|
240
292
|
function admitModelSelection(name, args) {
|
|
@@ -243,15 +295,16 @@ function admitModelSelection(name, args) {
|
|
|
243
295
|
throw new SdkMethodError('SCOPE_DENIED', `agents.run: modelSelection '${name}' needs the quality:read scope`);
|
|
244
296
|
}
|
|
245
297
|
const engine = getQualityEngine();
|
|
246
|
-
if (engine
|
|
247
|
-
throw
|
|
298
|
+
if (!appRunEngines().includes(engine)) {
|
|
299
|
+
throw engineUnavailableForAppRun(engine);
|
|
248
300
|
}
|
|
249
301
|
const stored = getQualityModel();
|
|
250
302
|
const model = stored !== 'default' && engineForModel(stored) === engine ? stored : undefined;
|
|
251
303
|
if (model)
|
|
252
|
-
|
|
304
|
+
assertUserOwnedProfile(model, engine);
|
|
253
305
|
const effortLevel = getQualityEffortLevel();
|
|
254
306
|
return {
|
|
307
|
+
origin: 'quality',
|
|
255
308
|
name,
|
|
256
309
|
engine,
|
|
257
310
|
...(model ? { model } : {}),
|
|
@@ -259,6 +312,32 @@ function admitModelSelection(name, args) {
|
|
|
259
312
|
fastMode: getQualityFastMode(),
|
|
260
313
|
};
|
|
261
314
|
}
|
|
315
|
+
function admitEngineSelection(raw, args) {
|
|
316
|
+
const granted = args.grant?.scopes ?? [];
|
|
317
|
+
if (!granted.includes('settings:read')) {
|
|
318
|
+
throw new SdkMethodError('SCOPE_DENIED', 'agents.run: engineSelection needs the settings:read scope');
|
|
319
|
+
}
|
|
320
|
+
const { engine, model } = resolveAutomationTarget(raw.engine, raw.model, 'agents.run engineSelection');
|
|
321
|
+
if (!appRunEngines().includes(engine)) {
|
|
322
|
+
throw engineUnavailableForAppRun(engine);
|
|
323
|
+
}
|
|
324
|
+
if (model)
|
|
325
|
+
assertUserOwnedProfile(model, engine);
|
|
326
|
+
if (raw.effortLevel !== undefined && raw.effortLevel !== 'auto' && !ENGINE_EFFORT_LEVELS[engine].includes(raw.effortLevel)) {
|
|
327
|
+
const accepted = ENGINE_EFFORT_LEVELS[engine].length > 0 ? ENGINE_EFFORT_LEVELS[engine].join(', ') : 'auto';
|
|
328
|
+
throw badRequest(`agents.run: engineSelection.effortLevel '${displayValue(raw.effortLevel)}' is not a ${engine} effort level — expected one of ${accepted}`);
|
|
329
|
+
}
|
|
330
|
+
if (raw.fastMode === true && engine !== 'claude-code') {
|
|
331
|
+
throw badRequest(`agents.run: engineSelection.fastMode is a claude-code setting — ${engine} has no fast mode`);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
origin: 'app',
|
|
335
|
+
engine,
|
|
336
|
+
...(model ? { model } : {}),
|
|
337
|
+
effortLevel: raw.effortLevel ?? 'auto',
|
|
338
|
+
fastMode: raw.fastMode === true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
262
341
|
function findAppRunSession(args, sessionId) {
|
|
263
342
|
const session = sessionsByTab.get(sessionId);
|
|
264
343
|
if (!session || session.appId !== args.appId || session.spaceKey !== args.spaceKey) {
|
|
@@ -299,6 +378,9 @@ export async function admitAgentRun(args, claim = false) {
|
|
|
299
378
|
if (request.modelSelection !== undefined) {
|
|
300
379
|
return { kind: 'start', request, selection: admitModelSelection(request.modelSelection, args) };
|
|
301
380
|
}
|
|
381
|
+
if (request.engineSelection !== undefined) {
|
|
382
|
+
return { kind: 'start', request, selection: admitEngineSelection(request.engineSelection, args) };
|
|
383
|
+
}
|
|
302
384
|
if (request.model === undefined)
|
|
303
385
|
return { kind: 'start', request };
|
|
304
386
|
const engine = await admitAppOwnedModel(request.model, args);
|
|
@@ -448,7 +530,7 @@ function runStartReason(record, ceiling, continued, model) {
|
|
|
448
530
|
function selectionAuditSuffix(selection) {
|
|
449
531
|
if (!selection)
|
|
450
532
|
return '';
|
|
451
|
-
return ` selection=${selection.
|
|
533
|
+
return ` selection=${selection.origin} engine=${selection.engine} model=${displayValue(selection.model ?? 'default')}`;
|
|
452
534
|
}
|
|
453
535
|
export function agentsResultHandler(args) {
|
|
454
536
|
const raw = (typeof args.params === 'object' && args.params !== null && !Array.isArray(args.params))
|