@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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import { homedir, platform } from 'node:os';
|
|
5
|
-
import { detectShell, getPty, getPtyInstallInstructions, getShellName, isPtyAvailable, SCROLLBACK_MAX_LENGTH, ScrollbackBuffer, } from './pty-utils.js';
|
|
5
|
+
import { detectShell, foregroundProcessName, getPty, getPtyInstallInstructions, getShellName, isPtyAvailable, SCROLLBACK_MAX_LENGTH, ScrollbackBuffer, } from './pty-utils.js';
|
|
6
|
+
import { OrphanedModeReaper, TerminalModeTracker } from './terminal-modes.js';
|
|
6
7
|
export { isPtyAvailable, reloadPty } from './pty-utils.js';
|
|
7
8
|
export class PTYManager extends EventEmitter {
|
|
8
9
|
terminals = new Map();
|
|
@@ -35,16 +36,18 @@ export class PTYManager extends EventEmitter {
|
|
|
35
36
|
getPtyInstallInstructions() {
|
|
36
37
|
return getPtyInstallInstructions();
|
|
37
38
|
}
|
|
38
|
-
async create(terminalId, workingDir, cols
|
|
39
|
+
async create(terminalId, workingDir, cols, rows, requestedShell, ownerLabel) {
|
|
39
40
|
const pty = getPty();
|
|
40
41
|
if (!pty) {
|
|
41
42
|
throw new Error(`PTY_NOT_AVAILABLE:${getPtyInstallInstructions()}`);
|
|
42
43
|
}
|
|
43
44
|
if (this.terminals.has(terminalId)) {
|
|
44
45
|
const existingSession = this.terminals.get(terminalId);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (cols !== undefined && rows !== undefined) {
|
|
47
|
+
existingSession.pty.resize(cols, rows);
|
|
48
|
+
existingSession.cols = cols;
|
|
49
|
+
existingSession.rows = rows;
|
|
50
|
+
}
|
|
48
51
|
return { shell: existingSession.shell, cwd: existingSession.cwd, isReconnect: true, platform: platform() };
|
|
49
52
|
}
|
|
50
53
|
const ALLOWED_SHELLS = new Set([
|
|
@@ -55,22 +58,28 @@ export class PTYManager extends EventEmitter {
|
|
|
55
58
|
]);
|
|
56
59
|
const shell = (requestedShell && ALLOWED_SHELLS.has(requestedShell)) ? requestedShell : detectShell();
|
|
57
60
|
const cwd = workingDir || homedir();
|
|
61
|
+
const spawnCols = cols ?? 80;
|
|
62
|
+
const spawnRows = rows ?? 24;
|
|
58
63
|
try {
|
|
59
64
|
const env = { ...process.env, HOME: homedir(), TERM: 'xterm-256color', COLORTERM: 'truecolor', FORCE_HYPERLINK: '1' };
|
|
60
|
-
const ptyProcess = pty.spawn(shell, [], { name: 'xterm-256color', cols, rows, cwd, env });
|
|
65
|
+
const ptyProcess = pty.spawn(shell, [], { name: 'xterm-256color', cols: spawnCols, rows: spawnRows, cwd, env });
|
|
66
|
+
const shellName = getShellName(shell);
|
|
67
|
+
const modes = new TerminalModeTracker();
|
|
61
68
|
const session = {
|
|
62
69
|
id: terminalId,
|
|
63
70
|
pty: ptyProcess,
|
|
64
|
-
shell:
|
|
71
|
+
shell: shellName,
|
|
65
72
|
cwd,
|
|
66
73
|
createdAt: Date.now(),
|
|
67
74
|
lastActivityAt: Date.now(),
|
|
68
|
-
cols,
|
|
69
|
-
rows,
|
|
75
|
+
cols: spawnCols,
|
|
76
|
+
rows: spawnRows,
|
|
70
77
|
...(ownerLabel !== undefined ? { ownerLabel } : {}),
|
|
71
78
|
_outputBuffer: '',
|
|
72
79
|
_outputTimer: null,
|
|
73
80
|
scrollback: new ScrollbackBuffer(SCROLLBACK_MAX_LENGTH),
|
|
81
|
+
modes,
|
|
82
|
+
modeReaper: new OrphanedModeReaper(modes, shellName, () => foregroundProcessName(ptyProcess)),
|
|
74
83
|
};
|
|
75
84
|
this.terminals.set(terminalId, session);
|
|
76
85
|
this.attachOutputHandlers(session, terminalId);
|
|
@@ -92,6 +101,11 @@ export class PTYManager extends EventEmitter {
|
|
|
92
101
|
clearTimeout(session._outputTimer);
|
|
93
102
|
session._outputTimer = null;
|
|
94
103
|
}
|
|
104
|
+
const reaped = session.modeReaper.check();
|
|
105
|
+
if (reaped) {
|
|
106
|
+
session.scrollback.append(reaped);
|
|
107
|
+
session._outputBuffer += reaped;
|
|
108
|
+
}
|
|
95
109
|
const buffered = session._outputBuffer;
|
|
96
110
|
session._outputBuffer = '';
|
|
97
111
|
const chunkSize = outputLimit();
|
|
@@ -101,6 +115,7 @@ export class PTYManager extends EventEmitter {
|
|
|
101
115
|
};
|
|
102
116
|
session.pty.onData((data) => {
|
|
103
117
|
session.scrollback.append(data);
|
|
118
|
+
session.modes.observe(data);
|
|
104
119
|
session.lastActivityAt = Date.now();
|
|
105
120
|
session._outputBuffer += data;
|
|
106
121
|
if (session._outputBuffer.length >= outputLimit()) {
|
|
@@ -166,8 +181,8 @@ export class PTYManager extends EventEmitter {
|
|
|
166
181
|
}
|
|
167
182
|
session._outputTimer = null;
|
|
168
183
|
}
|
|
169
|
-
session.pty.kill();
|
|
170
184
|
this.terminals.delete(terminalId);
|
|
185
|
+
session.pty.kill();
|
|
171
186
|
return true;
|
|
172
187
|
}
|
|
173
188
|
catch (error) {
|
|
@@ -180,7 +195,7 @@ export class PTYManager extends EventEmitter {
|
|
|
180
195
|
const session = this.terminals.get(terminalId);
|
|
181
196
|
if (!session)
|
|
182
197
|
return null;
|
|
183
|
-
return session.scrollback.getContents();
|
|
198
|
+
return session.scrollback.getContents() + session.modes.stateSequence();
|
|
184
199
|
}
|
|
185
200
|
getSession(terminalId) {
|
|
186
201
|
return this.terminals.get(terminalId);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { platform } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
import { MINIONRY_ROOT } from '../../utils/paths.js';
|
|
8
8
|
const FALLBACK_SPECIFIERS = ['@lydell/node-pty', 'node-pty'];
|
|
@@ -119,6 +119,17 @@ export function getShellName(shellPath) {
|
|
|
119
119
|
const parts = shellPath.split(/[/\\]/);
|
|
120
120
|
return parts[parts.length - 1] || 'shell';
|
|
121
121
|
}
|
|
122
|
+
export function foregroundProcessName(pty) {
|
|
123
|
+
if (platform() === 'win32')
|
|
124
|
+
return null;
|
|
125
|
+
try {
|
|
126
|
+
const name = pty.process;
|
|
127
|
+
return typeof name === 'string' && name !== '' ? basename(name) : null;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
122
133
|
export const SCROLLBACK_MAX_LENGTH = 256 * 1024;
|
|
123
134
|
export class ScrollbackBuffer {
|
|
124
135
|
chunks = [];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
3
|
+
const MOUSE_TRACKING_MODES = [9, 1000, 1001, 1002, 1003];
|
|
4
|
+
const MOUSE_ENCODING_MODES = [1005, 1006, 1015, 1016];
|
|
5
|
+
const FOCUS_REPORT_MODE = 1004;
|
|
6
|
+
const BRACKETED_PASTE_MODE = 2004;
|
|
7
|
+
export const REAPABLE_MODES = [
|
|
8
|
+
...MOUSE_TRACKING_MODES,
|
|
9
|
+
...MOUSE_ENCODING_MODES,
|
|
10
|
+
FOCUS_REPORT_MODE,
|
|
11
|
+
];
|
|
12
|
+
export const TRACKED_MODES = [...REAPABLE_MODES, BRACKETED_PASTE_MODE];
|
|
13
|
+
const TRACKED = new Set(TRACKED_MODES);
|
|
14
|
+
const DEC_PRIVATE_MODE_RE = /\x1b\[\?([0-9;]*)([hl])/g;
|
|
15
|
+
const MAX_CARRY = 64;
|
|
16
|
+
function trailingPartialSequence(text) {
|
|
17
|
+
const tail = text.slice(-MAX_CARRY);
|
|
18
|
+
const esc = tail.lastIndexOf('\x1b');
|
|
19
|
+
return esc === -1 ? '' : tail.slice(esc);
|
|
20
|
+
}
|
|
21
|
+
export class TerminalModeTracker {
|
|
22
|
+
active = new Set();
|
|
23
|
+
carry = '';
|
|
24
|
+
observe(data) {
|
|
25
|
+
const text = this.carry + data;
|
|
26
|
+
DEC_PRIVATE_MODE_RE.lastIndex = 0;
|
|
27
|
+
for (let match = DEC_PRIVATE_MODE_RE.exec(text); match !== null; match = DEC_PRIVATE_MODE_RE.exec(text)) {
|
|
28
|
+
const set = match[2] === 'h';
|
|
29
|
+
for (const param of match[1].split(';')) {
|
|
30
|
+
const mode = Number(param);
|
|
31
|
+
if (!TRACKED.has(mode))
|
|
32
|
+
continue;
|
|
33
|
+
if (set)
|
|
34
|
+
this.active.add(mode);
|
|
35
|
+
else
|
|
36
|
+
this.active.delete(mode);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.carry = trailingPartialSequence(text);
|
|
40
|
+
}
|
|
41
|
+
isActive(mode) {
|
|
42
|
+
return this.active.has(mode);
|
|
43
|
+
}
|
|
44
|
+
get reportingActive() {
|
|
45
|
+
return MOUSE_TRACKING_MODES.some((mode) => this.active.has(mode)) || this.active.has(FOCUS_REPORT_MODE);
|
|
46
|
+
}
|
|
47
|
+
stateSequence() {
|
|
48
|
+
if (this.active.size === 0)
|
|
49
|
+
return '';
|
|
50
|
+
return TRACKED_MODES.map((mode) => `\x1b[?${mode}${this.active.has(mode) ? 'h' : 'l'}`).join('');
|
|
51
|
+
}
|
|
52
|
+
clearReporting() {
|
|
53
|
+
for (const mode of REAPABLE_MODES)
|
|
54
|
+
this.active.delete(mode);
|
|
55
|
+
return REAPABLE_MODES.map((mode) => `\x1b[?${mode}l`).join('');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class OrphanedModeReaper {
|
|
59
|
+
modes;
|
|
60
|
+
shellName;
|
|
61
|
+
probeForeground;
|
|
62
|
+
owner = null;
|
|
63
|
+
constructor(modes,
|
|
64
|
+
shellName,
|
|
65
|
+
probeForeground) {
|
|
66
|
+
this.modes = modes;
|
|
67
|
+
this.shellName = shellName;
|
|
68
|
+
this.probeForeground = probeForeground;
|
|
69
|
+
}
|
|
70
|
+
check() {
|
|
71
|
+
if (!this.modes.reportingActive) {
|
|
72
|
+
this.owner = null;
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
const foreground = this.probeForeground();
|
|
76
|
+
if (foreground === null)
|
|
77
|
+
return '';
|
|
78
|
+
if (this.owner === null) {
|
|
79
|
+
this.owner = foreground;
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
if (foreground === this.owner || foreground !== this.shellName)
|
|
83
|
+
return '';
|
|
84
|
+
this.owner = null;
|
|
85
|
+
return this.modes.clearReporting();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -843,12 +843,12 @@ function tokenUsage(raw) {
|
|
|
843
843
|
...(raw.isFinal === true ? { isFinal: true } : {}),
|
|
844
844
|
};
|
|
845
845
|
}
|
|
846
|
-
function clampText(text, max) {
|
|
846
|
+
export function clampText(text, max) {
|
|
847
847
|
if (text.length <= max)
|
|
848
848
|
return text;
|
|
849
849
|
return `${text.slice(0, max)}\n… [truncated ${text.length - max} chars]`;
|
|
850
850
|
}
|
|
851
|
-
function clampInput(value, max) {
|
|
851
|
+
export function clampInput(value, max) {
|
|
852
852
|
const input = asRecord(value);
|
|
853
853
|
let size;
|
|
854
854
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
-
export { ChatTimeline, ChatTimelineRegistry, classifyEngineOutput, countMovementEntries, DEFAULT_CHAT_TIMELINE_LIMITS, formatErrorMarker, formatTimelineDuration, isTimelineTabEvent, LEGACY_THINKING_MARKER, legacyThinkingChunk, parseErrorMarker, } from './assembler.js';
|
|
3
|
+
export { ChatTimeline, ChatTimelineRegistry, clampInput, clampText, classifyEngineOutput, countMovementEntries, DEFAULT_CHAT_TIMELINE_LIMITS, formatErrorMarker, formatTimelineDuration, isTimelineTabEvent, LEGACY_THINKING_MARKER, legacyThinkingChunk, parseErrorMarker, } from './assembler.js';
|
|
4
4
|
export { applyTimelineEvent, applyTimelineEvents, CHAT_TIMELINE_CONTRACT_VERSION, emptyTimelineState, foldTimeline, TIMELINE_PROMPT_INDEX_HEAD_ENTRIES, TIMELINE_PROMPT_INDEX_MAX_ENTRIES, TIMELINE_PROMPT_PREVIEW_CHARS, timelineEventEntryId, timelineStateFromSnapshot, } from './contract.js';
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { can } from '@minionry/roles';
|
|
4
|
+
import { isValidMaxLatenessMs } from '../schedule/agent-schedule-store.js';
|
|
4
5
|
import { getAgentScheduler } from '../schedule/agent-scheduler.js';
|
|
6
|
+
import { normalizeWallClockTiming } from '../schedule/daily-timing.js';
|
|
7
|
+
import { isValidPromptFilePath, readPromptFile } from '../schedule/prompt-file.js';
|
|
8
|
+
import { parseMaxLatenessMs } from '../schedule/schedule-wire.js';
|
|
5
9
|
import { dispatchByType } from './dispatch-util.js';
|
|
6
10
|
import { parseTabEngineOverride } from './tab-handlers.js';
|
|
7
11
|
export function handleAgentScheduleMessage(ctx, ws, msg, workingDir, role) {
|
|
@@ -41,14 +45,35 @@ function parseAgentTiming(raw) {
|
|
|
41
45
|
? { kind: 'interval', everyMs: t.everyMs }
|
|
42
46
|
: null;
|
|
43
47
|
}
|
|
48
|
+
if (t.kind === 'daily' || t.kind === 'monthly')
|
|
49
|
+
return normalizeWallClockTiming(raw);
|
|
44
50
|
return null;
|
|
45
51
|
}
|
|
52
|
+
const INVALID_TIMING_ERROR = 'Invalid timing (expected {kind:"once",at}, {kind:"interval",everyMs}, {kind:"daily",time,timeZone,days?} or {kind:"monthly",time,timeZone,dayOfMonth})';
|
|
53
|
+
const INVALID_MAX_LATENESS_ERROR = 'Invalid maxLatenessMs (integer >= 60000)';
|
|
54
|
+
const INVALID_PROMPT_FILE_ERROR = "Invalid promptFile (must be a Space-relative POSIX path with no leading '/' and no '..' segment)";
|
|
46
55
|
function parsePrompt(raw) {
|
|
47
56
|
if (typeof raw !== 'string')
|
|
48
57
|
return null;
|
|
49
58
|
const trimmed = raw.trim();
|
|
50
59
|
return trimmed.length > 0 ? trimmed : null;
|
|
51
60
|
}
|
|
61
|
+
function resolveCreatePrompt(workingDir, rawPrompt, rawPromptFile) {
|
|
62
|
+
const hasPrompt = rawPrompt !== undefined;
|
|
63
|
+
const hasPromptFile = rawPromptFile !== undefined;
|
|
64
|
+
if (hasPrompt === hasPromptFile) {
|
|
65
|
+
return { ok: false, error: 'Provide exactly one of prompt or promptFile' };
|
|
66
|
+
}
|
|
67
|
+
if (!hasPromptFile) {
|
|
68
|
+
const prompt = parsePrompt(rawPrompt);
|
|
69
|
+
return prompt ? { ok: true, prompt } : { ok: false, error: 'prompt is required' };
|
|
70
|
+
}
|
|
71
|
+
if (typeof rawPromptFile !== 'string' || !isValidPromptFilePath(rawPromptFile)) {
|
|
72
|
+
return { ok: false, error: INVALID_PROMPT_FILE_ERROR };
|
|
73
|
+
}
|
|
74
|
+
const result = readPromptFile(workingDir, rawPromptFile);
|
|
75
|
+
return result.ok ? { ok: true, prompt: result.content, promptFile: rawPromptFile } : { ok: false, error: result.reason };
|
|
76
|
+
}
|
|
52
77
|
function parseEngineOverrideField(raw, present) {
|
|
53
78
|
if (!present)
|
|
54
79
|
return { ok: true, value: undefined };
|
|
@@ -69,14 +94,15 @@ function handleAgentScheduleCreate(ctx, ws, msg, workingDir, permission) {
|
|
|
69
94
|
reply({ success: false, error: 'requestId required' });
|
|
70
95
|
return;
|
|
71
96
|
}
|
|
72
|
-
const
|
|
73
|
-
if (!
|
|
74
|
-
reply({ success: false, error:
|
|
97
|
+
const resolved = resolveCreatePrompt(workingDir, data.prompt, data.promptFile);
|
|
98
|
+
if (!resolved.ok) {
|
|
99
|
+
reply({ success: false, error: resolved.error });
|
|
75
100
|
return;
|
|
76
101
|
}
|
|
102
|
+
const { prompt, promptFile } = resolved;
|
|
77
103
|
const timing = parseAgentTiming(data.timing);
|
|
78
104
|
if (!timing) {
|
|
79
|
-
reply({ success: false, error:
|
|
105
|
+
reply({ success: false, error: INVALID_TIMING_ERROR });
|
|
80
106
|
return;
|
|
81
107
|
}
|
|
82
108
|
const override = parseEngineOverrideField(data.engineOverride, 'engineOverride' in data);
|
|
@@ -84,6 +110,11 @@ function handleAgentScheduleCreate(ctx, ws, msg, workingDir, permission) {
|
|
|
84
110
|
reply({ success: false, error: 'Invalid engineOverride' });
|
|
85
111
|
return;
|
|
86
112
|
}
|
|
113
|
+
const maxLateness = parseMaxLatenessMs(data.maxLatenessMs);
|
|
114
|
+
if (!maxLateness.ok) {
|
|
115
|
+
reply({ success: false, error: INVALID_MAX_LATENESS_ERROR });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
87
118
|
const store = getAgentScheduleStore(ctx, workingDir);
|
|
88
119
|
if (!store) {
|
|
89
120
|
reply({ success: false, error: 'No working directory for this Space' });
|
|
@@ -92,9 +123,11 @@ function handleAgentScheduleCreate(ctx, ws, msg, workingDir, permission) {
|
|
|
92
123
|
const record = store.create({
|
|
93
124
|
name: data.name,
|
|
94
125
|
prompt,
|
|
126
|
+
...(promptFile !== undefined ? { promptFile } : {}),
|
|
95
127
|
engineOverride: override.value ?? undefined,
|
|
96
128
|
timing,
|
|
97
129
|
enabled: data.enabled,
|
|
130
|
+
maxLatenessMs: maxLateness.value,
|
|
98
131
|
});
|
|
99
132
|
notifySchedulerAndBroadcast(ctx, workingDir);
|
|
100
133
|
reply({ success: true, schedule: record });
|
|
@@ -110,11 +143,28 @@ function handleAgentScheduleUpdate(ctx, ws, msg, workingDir, permission) {
|
|
|
110
143
|
reply({ success: false, error: 'requestId and scheduleId required' });
|
|
111
144
|
return;
|
|
112
145
|
}
|
|
146
|
+
if (patch && typeof patch === 'object' && patch.prompt !== undefined && patch.promptFile !== undefined) {
|
|
147
|
+
reply({ success: false, error: 'Provide at most one of prompt or promptFile in a single update' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
113
150
|
const safePatch = parseUpdatePatch(patch);
|
|
114
151
|
if (!safePatch) {
|
|
115
152
|
reply({ success: false, error: 'Invalid patch' });
|
|
116
153
|
return;
|
|
117
154
|
}
|
|
155
|
+
if (patch?.promptFile !== undefined) {
|
|
156
|
+
if (typeof patch.promptFile !== 'string' || !isValidPromptFilePath(patch.promptFile)) {
|
|
157
|
+
reply({ success: false, error: INVALID_PROMPT_FILE_ERROR });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const result = readPromptFile(workingDir, patch.promptFile);
|
|
161
|
+
if (!result.ok) {
|
|
162
|
+
reply({ success: false, error: result.reason });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
safePatch.prompt = result.content;
|
|
166
|
+
safePatch.promptFile = patch.promptFile;
|
|
167
|
+
}
|
|
118
168
|
const store = getAgentScheduleStore(ctx, workingDir);
|
|
119
169
|
if (!store) {
|
|
120
170
|
reply({ success: false, error: 'No working directory for this Space' });
|
|
@@ -164,6 +214,14 @@ function applyStructuredPatchFields(patch, safe) {
|
|
|
164
214
|
if (override.value !== undefined)
|
|
165
215
|
safe.engineOverride = override.value;
|
|
166
216
|
}
|
|
217
|
+
if (patch.maxLatenessMs === null) {
|
|
218
|
+
safe.maxLatenessMs = null;
|
|
219
|
+
}
|
|
220
|
+
else if (patch.maxLatenessMs !== undefined) {
|
|
221
|
+
if (!isValidMaxLatenessMs(patch.maxLatenessMs))
|
|
222
|
+
return null;
|
|
223
|
+
safe.maxLatenessMs = patch.maxLatenessMs;
|
|
224
|
+
}
|
|
167
225
|
return safe;
|
|
168
226
|
}
|
|
169
227
|
function handleAgentScheduleDelete(ctx, ws, msg, workingDir, permission) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
|
-
const
|
|
4
|
+
export const ASK_USER_QUESTION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
5
5
|
const pending = new Map();
|
|
6
6
|
const bouncerSharedSecret = randomUUID();
|
|
7
7
|
export function getBouncerSecret() {
|
|
@@ -43,7 +43,7 @@ export function mcpCapabilityTokenCount() {
|
|
|
43
43
|
return capabilities.size;
|
|
44
44
|
}
|
|
45
45
|
export function registerPendingQuestion(opts) {
|
|
46
|
-
const { toolUseId, tabId, questions = [], timeoutMs =
|
|
46
|
+
const { toolUseId, tabId, questions = [], timeoutMs = ASK_USER_QUESTION_TIMEOUT_MS, computeTimeoutAnswers } = opts;
|
|
47
47
|
const existing = pending.get(toolUseId);
|
|
48
48
|
if (existing) {
|
|
49
49
|
clearTimeout(existing.timer);
|
|
@@ -92,7 +92,8 @@ export function _resetBrowserAgentsForTests() {
|
|
|
92
92
|
agentPlannerOverride = (input) => [input.task];
|
|
93
93
|
plannerByModel.clear();
|
|
94
94
|
}
|
|
95
|
-
export function startBrowserAgentRun(host, task, sink
|
|
95
|
+
export function startBrowserAgentRun(host, task, sink,
|
|
96
|
+
opener) {
|
|
96
97
|
const agentId = `browser-agent-${randomUUID()}`;
|
|
97
98
|
const agentLabel = 'Browser Agent';
|
|
98
99
|
log.info('agent.start', `agent=${agentId.slice(14, 22)} taskId=${task.taskId?.slice(0, 8) ?? '-'} tab=${task.tabId?.slice(0, 8) ?? 'new'} engine=${task.engine ?? 'default'} model=${task.model ?? 'default'} effort=${task.effortLevel ?? 'auto'} active=${activeAgents.size}`);
|
|
@@ -106,6 +107,7 @@ export function startBrowserAgentRun(host, task, sink) {
|
|
|
106
107
|
task,
|
|
107
108
|
agentId,
|
|
108
109
|
agentLabel,
|
|
110
|
+
...(opener !== undefined ? { opener } : {}),
|
|
109
111
|
...(task.tabId ? { tabId: task.tabId } : {}),
|
|
110
112
|
...(agentSettleDelayMsOverride !== null
|
|
111
113
|
? { settleDelayMs: agentSettleDelayMsOverride }
|
|
@@ -22,7 +22,10 @@ const IF_READY_TYPES = new Set([
|
|
|
22
22
|
'browser_webrtc_state',
|
|
23
23
|
'browser_lease_release',
|
|
24
24
|
]);
|
|
25
|
-
const HOST_FREE_TYPES = new Set([
|
|
25
|
+
const HOST_FREE_TYPES = new Set([
|
|
26
|
+
'browser_agent_approve',
|
|
27
|
+
'browser_webrtc_ice_config',
|
|
28
|
+
]);
|
|
26
29
|
export function browserHostPolicy(type) {
|
|
27
30
|
if (HOST_FREE_TYPES.has(type))
|
|
28
31
|
return 'none';
|
|
@@ -131,7 +134,7 @@ const DISPATCH = {
|
|
|
131
134
|
browser_tab_open: async (host, data, ctx) => {
|
|
132
135
|
const url = extractString(data, 'url') ?? 'about:blank';
|
|
133
136
|
log.info('tab.open', `viewer=${ctx.viewerId.slice(0, 8)} existingTabs=${host.getTabsSnapshot?.().tabs.length ?? '?'} url=${url === 'about:blank' ? 'about:blank' : 'set'}`);
|
|
134
|
-
const { id } = await host.openTab(url);
|
|
137
|
+
const { id } = await host.openTab(url, { kind: 'user' });
|
|
135
138
|
if (isCapturableUrl(url)) {
|
|
136
139
|
await ensureTabProfileCaptured(host, id, navigatingFormFactor(ctx.viewerId));
|
|
137
140
|
}
|
|
@@ -59,6 +59,14 @@ export function onWsContextClosed(host, ws) {
|
|
|
59
59
|
releaseDeviceInput(viewerId);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
export function moveViewerToContext(viewerId, from, to) {
|
|
63
|
+
const viewer = viewers.getByViewer(viewerId);
|
|
64
|
+
if (viewer?.ws === from)
|
|
65
|
+
viewer.ws = to;
|
|
66
|
+
const input = deviceInputTabs.get(viewerId);
|
|
67
|
+
if (input?.ws === from)
|
|
68
|
+
input.ws = to;
|
|
69
|
+
}
|
|
62
70
|
export function onViewerDisconnected(host, viewerId) {
|
|
63
71
|
for (const sdkViewer of viewers.detachSdkByOwnerViewer(viewerId)) {
|
|
64
72
|
log.info('viewer.detachSdkByOwner', `session=${sdkViewer.sessionId.slice(0, 8)} viewers=${viewers.size()}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { statSync } from 'node:fs';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
4
5
|
import { can } from '@minionry/roles';
|
|
5
6
|
import { getClientId } from '../client-id.js';
|
|
@@ -219,6 +220,14 @@ function handleWriteFile(ctx, ws, msg, tabId, workingDir, role) {
|
|
|
219
220
|
notifyFileChanged(makeSpaceKey(getClientId(), workingDir), workingDir, result.path, 'changed');
|
|
220
221
|
}
|
|
221
222
|
}
|
|
223
|
+
function isDirectoryPath(path, workingDir) {
|
|
224
|
+
try {
|
|
225
|
+
return statSync(resolve(workingDir, path)).isDirectory();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
222
231
|
function handleCreateFile(ctx, ws, msg, tabId, workingDir) {
|
|
223
232
|
if (!msg.data?.filePath)
|
|
224
233
|
throw new Error('File path is required');
|
|
@@ -230,6 +239,7 @@ function handleCreateFile(ctx, ws, msg, tabId, workingDir) {
|
|
|
230
239
|
type: 'fileCreated',
|
|
231
240
|
data: { path: result.path, name, size: 0, modifiedAt: new Date().toISOString() }
|
|
232
241
|
});
|
|
242
|
+
notifyFileChanged(makeSpaceKey(getClientId(), workingDir), workingDir, result.path, 'created');
|
|
233
243
|
}
|
|
234
244
|
}
|
|
235
245
|
function handleCreateDirectory(ctx, ws, msg, tabId, workingDir) {
|
|
@@ -243,11 +253,13 @@ function handleCreateDirectory(ctx, ws, msg, tabId, workingDir) {
|
|
|
243
253
|
type: 'directoryCreated',
|
|
244
254
|
data: { path: result.path, name }
|
|
245
255
|
});
|
|
256
|
+
notifyFileChanged(makeSpaceKey(getClientId(), workingDir), workingDir, result.path, 'created', { dir: true });
|
|
246
257
|
}
|
|
247
258
|
}
|
|
248
259
|
function handleDeleteFile(ctx, ws, msg, tabId, workingDir) {
|
|
249
260
|
if (!msg.data?.filePath)
|
|
250
261
|
throw new Error('File path is required');
|
|
262
|
+
const dir = isDirectoryPath(msg.data.filePath, workingDir);
|
|
251
263
|
const result = deleteFile(msg.data.filePath, workingDir);
|
|
252
264
|
sendFileResult(ctx, ws, 'fileDeleted', tabId, result);
|
|
253
265
|
if (result.success && result.path) {
|
|
@@ -255,6 +267,7 @@ function handleDeleteFile(ctx, ws, msg, tabId, workingDir) {
|
|
|
255
267
|
type: 'fileDeleted',
|
|
256
268
|
data: { path: result.path }
|
|
257
269
|
});
|
|
270
|
+
notifyFileChanged(makeSpaceKey(getClientId(), workingDir), workingDir, result.path, 'deleted', { dir });
|
|
258
271
|
}
|
|
259
272
|
}
|
|
260
273
|
function handleRenameFile(ctx, ws, msg, tabId, workingDir) {
|
|
@@ -271,6 +284,10 @@ function handleRenameFile(ctx, ws, msg, tabId, workingDir) {
|
|
|
271
284
|
type: 'fileRenamed',
|
|
272
285
|
data: { oldPath: msg.data.oldPath, newPath: result.path, name }
|
|
273
286
|
});
|
|
287
|
+
notifyFileChanged(makeSpaceKey(getClientId(), workingDir), workingDir, msg.data.oldPath, 'renamed', {
|
|
288
|
+
newPath: result.path,
|
|
289
|
+
dir: isDirectoryPath(result.path, workingDir),
|
|
290
|
+
});
|
|
274
291
|
}
|
|
275
292
|
}
|
|
276
293
|
function handleNotifyFileOpened(ctx, ws, msg, workingDir) {
|
|
@@ -7,6 +7,7 @@ import { Readable } from 'node:stream';
|
|
|
7
7
|
import { pipeline } from 'node:stream/promises';
|
|
8
8
|
import { can, normalizeStoredRole } from '@minionry/roles';
|
|
9
9
|
import { buildDirectoryZipBuffer } from '../file-transfer-zip.js';
|
|
10
|
+
import { RelayHttpClient, relayHttp } from '../relay-http-client.js';
|
|
10
11
|
import { getMaxUploadBytes, uploadTooLargeMessage } from '../settings.js';
|
|
11
12
|
import { resolveFileTreeTarget } from './file-upload-handler.js';
|
|
12
13
|
import { isPathInSafeLocation } from './file-utils.js';
|
|
@@ -81,8 +82,10 @@ function parseRequestData(message) {
|
|
|
81
82
|
export class FileTransferAgent {
|
|
82
83
|
deps;
|
|
83
84
|
active = 0;
|
|
85
|
+
http;
|
|
84
86
|
constructor(deps) {
|
|
85
87
|
this.deps = deps;
|
|
88
|
+
this.http = deps.http ?? (deps.fetchImpl ? new RelayHttpClient({ fetchImpl: deps.fetchImpl }) : relayHttp);
|
|
86
89
|
}
|
|
87
90
|
handleRequest(message) {
|
|
88
91
|
const data = parseRequestData(message);
|
|
@@ -230,19 +233,22 @@ export class FileTransferAgent {
|
|
|
230
233
|
if (payload.range)
|
|
231
234
|
headers['x-mn-range'] = payload.range;
|
|
232
235
|
const isStream = !(payload.body instanceof Uint8Array);
|
|
233
|
-
const
|
|
236
|
+
const init = {
|
|
234
237
|
method: 'POST',
|
|
235
238
|
headers,
|
|
236
239
|
body: payload.body,
|
|
237
240
|
...(isStream ? { duplex: 'half' } : {}),
|
|
238
|
-
}
|
|
241
|
+
};
|
|
242
|
+
const res = isStream
|
|
243
|
+
? await this.fetch()(this.dataUrl(data), init)
|
|
244
|
+
: await this.http.fetch(this.dataUrl(data), init);
|
|
239
245
|
if (!res.ok) {
|
|
240
246
|
console.warn(`[FileTransfer] Relay refused data leg for ${data.transferId}: ${res.status}`);
|
|
241
247
|
}
|
|
242
248
|
await res.text().catch(() => { });
|
|
243
249
|
}
|
|
244
250
|
async pushError(data, status, message, extraHeaders = {}) {
|
|
245
|
-
const res = await this.fetch(
|
|
251
|
+
const res = await this.http.fetch(this.dataUrl(data), {
|
|
246
252
|
method: 'POST',
|
|
247
253
|
headers: {
|
|
248
254
|
authorization: `Bearer ${data.token}`,
|
|
@@ -304,7 +310,7 @@ export class FileTransferAgent {
|
|
|
304
310
|
await this.postResult(data, { ok: false, error: `Upload failed: ${message}`, code: 'stream_failed' });
|
|
305
311
|
}
|
|
306
312
|
async pullUploadBytes(data, finalPath, tempPath) {
|
|
307
|
-
const res = await this.fetch(
|
|
313
|
+
const res = await this.http.fetch(this.dataUrl(data), {
|
|
308
314
|
headers: { authorization: `Bearer ${data.token}` },
|
|
309
315
|
});
|
|
310
316
|
if (!res.ok || !res.body) {
|
|
@@ -332,7 +338,7 @@ export class FileTransferAgent {
|
|
|
332
338
|
}
|
|
333
339
|
async claimUploadLeg(data) {
|
|
334
340
|
try {
|
|
335
|
-
const res = await this.fetch(
|
|
341
|
+
const res = await this.http.fetch(this.dataUrl(data), {
|
|
336
342
|
headers: { authorization: `Bearer ${data.token}` },
|
|
337
343
|
});
|
|
338
344
|
await res.body?.cancel().catch(() => { });
|
|
@@ -341,7 +347,7 @@ export class FileTransferAgent {
|
|
|
341
347
|
}
|
|
342
348
|
async postResult(data, result) {
|
|
343
349
|
try {
|
|
344
|
-
const res = await this.fetch(
|
|
350
|
+
const res = await this.http.fetch(this.dataUrl(data, '/result'), {
|
|
345
351
|
method: 'POST',
|
|
346
352
|
headers: {
|
|
347
353
|
authorization: `Bearer ${data.token}`,
|