@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.
Files changed (120) hide show
  1. package/bin/commands/minions.js +19 -0
  2. package/bin/minion.js +33 -4
  3. package/dist/server/cli/headless/claude-invoker-process.js +23 -7
  4. package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
  5. package/dist/server/cli/headless/haiku-assessments.js +2 -1
  6. package/dist/server/cli/headless/mcp-config.js +2 -1
  7. package/dist/server/engines/claude/claude-command.js +1 -0
  8. package/dist/server/engines/factory.js +2 -0
  9. package/dist/server/engines/hermes/HermesEngine.js +112 -219
  10. package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
  11. package/dist/server/index.js +10 -4
  12. package/dist/server/mcp/bouncer-integration.js +3 -2
  13. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
  14. package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
  15. package/dist/server/mcp/classifier/telemetry.js +0 -0
  16. package/dist/server/mcp/permission-channels.js +176 -0
  17. package/dist/server/mcp/security-analysis.js +9 -1
  18. package/dist/server/mcp/security-audit.js +22 -2
  19. package/dist/server/mcp/security-patterns.js +73 -1
  20. package/dist/server/mcp/server.js +80 -50
  21. package/dist/server/routes/internal.js +6 -1
  22. package/dist/server/routes/notifications.js +6 -18
  23. package/dist/server/server-setup.js +24 -48
  24. package/dist/server/services/analytics.js +4 -2
  25. package/dist/server/services/browser/host-mock.js +6 -1
  26. package/dist/server/services/browser/host.js +27 -3
  27. package/dist/server/services/browser/reasoning-agent.js +3 -1
  28. package/dist/server/services/chain/chain-engine.js +494 -0
  29. package/dist/server/services/chain/chain-store.js +542 -0
  30. package/dist/server/services/git/haiku.js +2 -2
  31. package/dist/server/services/plan/agents/check-injection.md +43 -2
  32. package/dist/server/services/plan/agents/review-code.md +62 -3
  33. package/dist/server/services/plan/agents/review-quality.md +52 -2
  34. package/dist/server/services/plan/board-export.js +4 -0
  35. package/dist/server/services/plan/composer-prompt.js +27 -8
  36. package/dist/server/services/plan/composer.js +10 -4
  37. package/dist/server/services/plan/config-installer.js +4 -83
  38. package/dist/server/services/plan/executor.js +155 -73
  39. package/dist/server/services/plan/issue-effort.js +9 -21
  40. package/dist/server/services/plan/issue-prompt-builder.js +72 -9
  41. package/dist/server/services/plan/parser-core.js +6 -1
  42. package/dist/server/services/plan/quality-delta.js +6 -24
  43. package/dist/server/services/plan/readiness-planner.js +12 -4
  44. package/dist/server/services/plan/record-file.js +52 -0
  45. package/dist/server/services/plan/review-approval.js +64 -0
  46. package/dist/server/services/plan/review-gate.js +186 -77
  47. package/dist/server/services/plan/review-outcome.js +146 -0
  48. package/dist/server/services/plan/review-report.js +28 -1
  49. package/dist/server/services/plan/review-target.js +57 -0
  50. package/dist/server/services/plan/state-reconciler.js +3 -3
  51. package/dist/server/services/plan/template-diff.js +2 -0
  52. package/dist/server/services/plan/template-instantiator.js +4 -0
  53. package/dist/server/services/plan/watcher.js +11 -1
  54. package/dist/server/services/platform-reconnect.js +7 -0
  55. package/dist/server/services/platform-token-lifecycle.js +2 -1
  56. package/dist/server/services/platform.js +293 -50
  57. package/dist/server/services/relay-http-client.js +107 -0
  58. package/dist/server/services/schedule/agent-schedule-store.js +69 -8
  59. package/dist/server/services/schedule/agent-scheduler.js +48 -7
  60. package/dist/server/services/schedule/daily-timing.js +52 -0
  61. package/dist/server/services/schedule/prompt-file.js +55 -0
  62. package/dist/server/services/schedule/run-bootstrap.js +20 -5
  63. package/dist/server/services/schedule/schedule-store.js +54 -4
  64. package/dist/server/services/schedule/schedule-wire.js +17 -0
  65. package/dist/server/services/schedule/scheduler.js +43 -9
  66. package/dist/server/services/sdk/agent-schedule.js +294 -0
  67. package/dist/server/services/sdk/agents.js +95 -13
  68. package/dist/server/services/sdk/app-tools.js +44 -7
  69. package/dist/server/services/sdk/atomic-write.js +75 -0
  70. package/dist/server/services/sdk/browser.js +5 -1
  71. package/dist/server/services/sdk/composer.js +2 -0
  72. package/dist/server/services/sdk/file-change-notify.js +60 -13
  73. package/dist/server/services/sdk/files-app.js +12 -7
  74. package/dist/server/services/sdk/files-search.js +14 -0
  75. package/dist/server/services/sdk/files-transfer.js +1 -1
  76. package/dist/server/services/sdk/files-watch.js +721 -0
  77. package/dist/server/services/sdk/files-workspace.js +37 -11
  78. package/dist/server/services/sdk/inference-claude.js +4 -2
  79. package/dist/server/services/sdk/inference.js +16 -2
  80. package/dist/server/services/sdk/pm-chain.js +0 -0
  81. package/dist/server/services/sdk/pm-schedule.js +72 -15
  82. package/dist/server/services/sdk/pm.js +22 -0
  83. package/dist/server/services/sdk/rate-limits.js +32 -0
  84. package/dist/server/services/sdk/registry.js +40 -2
  85. package/dist/server/services/sdk/terminal.js +1 -1
  86. package/dist/server/services/sentry.js +5 -1
  87. package/dist/server/services/terminal/pty-manager.js +26 -11
  88. package/dist/server/services/terminal/pty-utils.js +12 -1
  89. package/dist/server/services/terminal/terminal-modes.js +87 -0
  90. package/dist/server/services/timeline/assembler.js +2 -2
  91. package/dist/server/services/timeline/index.js +1 -1
  92. package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
  93. package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
  94. package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
  95. package/dist/server/services/websocket/browser-handlers.js +5 -2
  96. package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
  97. package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
  98. package/dist/server/services/websocket/file-transfer-http.js +12 -6
  99. package/dist/server/services/websocket/handler.js +185 -33
  100. package/dist/server/services/websocket/msg-id-tracker.js +94 -19
  101. package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
  102. package/dist/server/services/websocket/plan-handlers.js +10 -5
  103. package/dist/server/services/websocket/plan-helpers.js +23 -0
  104. package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
  105. package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
  106. package/dist/server/services/websocket/schedule-handlers.js +8 -2
  107. package/dist/server/services/websocket/sdk-agent-host.js +5 -1
  108. package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
  109. package/dist/server/services/websocket/sdk-browser-host.js +11 -4
  110. package/dist/server/services/websocket/sdk-handlers.js +6 -1
  111. package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
  112. package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
  113. package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
  114. package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
  115. package/dist/server/services/websocket/settings-handlers.js +6 -18
  116. package/dist/server/services/websocket/tab-broadcast.js +5 -1
  117. package/dist/server/services/websocket/terminal-handlers.js +51 -6
  118. package/dist/server/services/websocket/types.js +6 -5
  119. package/dist/server/services/websocket/viewer-interest.js +30 -0
  120. 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 = 80, rows = 24, requestedShell, ownerLabel) {
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
- existingSession.pty.resize(cols, rows);
46
- existingSession.cols = cols;
47
- existingSession.rows = rows;
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: getShellName(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 prompt = parsePrompt(data.prompt);
73
- if (!prompt) {
74
- reply({ success: false, error: 'prompt is required' });
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: 'Invalid timing (expected {kind:"once",at} or {kind:"interval",everyMs})' });
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 DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
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 = DEFAULT_TIMEOUT_MS, computeTimeoutAnswers } = opts;
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(['browser_agent_approve']);
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 res = await this.fetch()(this.dataUrl(data), {
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()(this.dataUrl(data), {
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()(this.dataUrl(data), {
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()(this.dataUrl(data), {
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()(this.dataUrl(data, '/result'), {
350
+ const res = await this.http.fetch(this.dataUrl(data, '/result'), {
345
351
  method: 'POST',
346
352
  headers: {
347
353
  authorization: `Bearer ${data.token}`,