@winmatrix/supervisor 1.0.5 → 1.0.7

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.
@@ -6,6 +6,7 @@
6
6
  * --engine <claude-code|codex|hermes|openclaw> \
7
7
  * [--model <model>] [--permission-mode <mode>] \
8
8
  * [--allowed-tools <tool1,tool2>] [--resume <sessionId>|--fork <sessionId>] \
9
+ * [--context-budget <json>] \
9
10
  * [--timeout <seconds>] [--work-dir <dir>] \
10
11
  * <prompt>
11
12
  *
@@ -13,11 +14,76 @@
13
14
  * 输出到 stdout,供 run-agent-engine.mjs 解析并写入 events.jsonl。
14
15
  */
15
16
  import { createRequire } from 'node:module';
17
+ import { isSdkFailureSubtype } from './engine-run-success.mjs';
18
+ import { createWorkstationOpenClawAdapterConfig } from './openclaw-device-store.mjs';
16
19
 
17
20
  const require = createRequire(import.meta.url);
18
21
 
19
22
  const SDK_PATH = process.env.WINMATRIX_AGENT_SDK_PATH
20
23
  || '/home/node/.local/lib/node_modules/@winmatrix/agent-sdk';
24
+ const MAX_PRIVATE_BINDINGS_BYTES = 64 * 1024;
25
+ const MCP_DYNAMIC_SECRET_KEYS = new Set([
26
+ 'WINMATRIX_TOOL_PROXY_URL',
27
+ 'WINMATRIX_TOOL_PROXY_TOKEN',
28
+ 'WINMATRIX_TOOL_PROXY_PROJECT_ID',
29
+ ]);
30
+
31
+ function normalizeEventText(value) {
32
+ if (typeof value === 'string') return value;
33
+ if (value === null || value === undefined) return '';
34
+ if (Array.isArray(value)) {
35
+ return value.map(normalizeEventText).filter(Boolean).join('\n');
36
+ }
37
+ if (typeof value === 'object') {
38
+ if (typeof value.text === 'string') return value.text;
39
+ try {
40
+ return JSON.stringify(value);
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
45
+ return String(value);
46
+ }
47
+
48
+ async function readMcpBinding() {
49
+ const chunks = [];
50
+ let size = 0;
51
+ for await (const chunk of process.stdin) {
52
+ size += chunk.length;
53
+ if (size > MAX_PRIVATE_BINDINGS_BYTES) {
54
+ return { error: 'MCP_BINDING_REQUIRED: 私有控制绑定超过允许大小。' };
55
+ }
56
+ chunks.push(chunk);
57
+ }
58
+ if (chunks.length === 0) return {};
59
+
60
+ try {
61
+ const raw = JSON.parse(Buffer.concat(chunks).toString('utf8'));
62
+ if (!raw || raw.hostKind !== 'workstation') {
63
+ return { error: 'MCP_BINDING_REQUIRED: 私有控制绑定 hostKind 无效。' };
64
+ }
65
+ const dynamicSecrets = raw.dynamicSecrets;
66
+ if (dynamicSecrets === undefined) {
67
+ return { error: 'MCP_BINDING_REQUIRED: 工作站缺少 MCP 绑定。' };
68
+ }
69
+ if (!dynamicSecrets || typeof dynamicSecrets !== 'object' || Array.isArray(dynamicSecrets)) {
70
+ return { error: 'MCP_BINDING_REQUIRED: MCP 绑定格式无效。' };
71
+ }
72
+ for (const [key, value] of Object.entries(dynamicSecrets)) {
73
+ if (!MCP_DYNAMIC_SECRET_KEYS.has(key) || typeof value !== 'string') {
74
+ return { error: 'MCP_BINDING_REQUIRED: MCP 绑定包含不允许字段。' };
75
+ }
76
+ }
77
+ const url = dynamicSecrets.WINMATRIX_TOOL_PROXY_URL?.trim();
78
+ const token = dynamicSecrets.WINMATRIX_TOOL_PROXY_TOKEN?.trim();
79
+ if (!url || !token) {
80
+ return { error: 'MCP_BINDING_REQUIRED: MCP 绑定不完整。' };
81
+ }
82
+ return { binding: { url, token } };
83
+ } catch {
84
+ return { error: 'MCP_BINDING_REQUIRED: 私有控制绑定不是有效 JSON。' };
85
+ }
86
+ }
21
87
 
22
88
  function parseArgs(argv) {
23
89
  const options = {
@@ -29,6 +95,7 @@ function parseArgs(argv) {
29
95
  fork: undefined,
30
96
  timeoutSec: undefined,
31
97
  workDir: undefined,
98
+ contextBudget: undefined,
32
99
  };
33
100
  const positionals = [];
34
101
 
@@ -50,6 +117,18 @@ function parseArgs(argv) {
50
117
  options.timeoutSec = argv[++i];
51
118
  } else if (arg === '--work-dir') {
52
119
  options.workDir = argv[++i];
120
+ } else if (arg === '--context-budget') {
121
+ const raw = argv[++i];
122
+ try {
123
+ const parsed = JSON.parse(raw);
124
+ if (parsed && typeof parsed === 'object') {
125
+ options.contextBudget = parsed;
126
+ } else {
127
+ console.error('[run-engine] --context-budget 不是有效 JSON 对象, 已忽略');
128
+ }
129
+ } catch {
130
+ console.error('[run-engine] --context-budget JSON 解析失败, 已忽略');
131
+ }
53
132
  } else if (arg.startsWith('--')) {
54
133
  // 忽略未来可能透传的 engine-specific 选项;后续可按 engineId 路由
55
134
  console.error(`[run-engine] 忽略未知选项: ${arg}`);
@@ -78,6 +157,13 @@ async function main() {
78
157
  process.exit(1);
79
158
  }
80
159
 
160
+ const mcpBinding = await readMcpBinding();
161
+ if (mcpBinding.error) {
162
+ writeEvent({ type: 'error', error: mcpBinding.error, code: 'MCP_CONNECTION_FAILED' });
163
+ process.exitCode = 1;
164
+ return;
165
+ }
166
+
81
167
  let sdk;
82
168
  try {
83
169
  sdk = require(SDK_PATH);
@@ -92,8 +178,19 @@ async function main() {
92
178
  CodexAdapter,
93
179
  HermesAdapter,
94
180
  OpenClawAdapter,
181
+ validateMcpBridgeUrl,
95
182
  } = sdk;
96
183
 
184
+ if (mcpBinding.binding) {
185
+ try {
186
+ validateMcpBridgeUrl(mcpBinding.binding.url);
187
+ } catch (err) {
188
+ writeEvent({ type: 'error', error: `MCP_BINDING_REQUIRED: MCP URL 无效: ${err.message}`, code: 'MCP_CONNECTION_FAILED' });
189
+ process.exitCode = 1;
190
+ return;
191
+ }
192
+ }
193
+
97
194
  const registry = new AdapterRegistry();
98
195
  registry.register('claude-code', ClaudeAdapter);
99
196
  registry.register('codex', CodexAdapter);
@@ -105,7 +202,12 @@ async function main() {
105
202
  process.exit(1);
106
203
  }
107
204
 
108
- const adapterConfig = {};
205
+ const adapterConfig = options.engineId === 'openclaw'
206
+ ? {
207
+ ...createWorkstationOpenClawAdapterConfig(),
208
+ ...(mcpBinding.binding ? { mcpBridgeUrl: mcpBinding.binding.url } : {}),
209
+ }
210
+ : (mcpBinding.binding ? { mcpBridgeUrl: mcpBinding.binding.url } : {});
109
211
  const adapter = registry.create(options.engineId, adapterConfig);
110
212
 
111
213
  /** @type {import('@winmatrix/agent-sdk').AgentTaskContext} */
@@ -116,9 +218,13 @@ async function main() {
116
218
  engineId: options.engineId,
117
219
  workDir: options.workDir,
118
220
  };
221
+ if (mcpBinding.binding) task.mcpToken = mcpBinding.binding.token;
222
+ if (options.contextBudget) task.contextBudget = options.contextBudget;
119
223
 
120
224
  if (options.engineId === 'claude-code') {
121
- task.claude = {};
225
+ // Coding workstation 使用 Agent SDK 路径:任务 MCP 仅随 query() 显式传入,
226
+ // 不落盘到 artifact,也不改写 Pod 注入的持久 agent-home。
227
+ task.claude = { useAgentSdk: true };
122
228
  if (options.permissionMode) task.claude.permissionMode = options.permissionMode;
123
229
  if (options.allowedTools) task.claude.tools = options.allowedTools.split(',').map((s) => s.trim());
124
230
  } else if (options.engineId === 'hermes') {
@@ -150,28 +256,53 @@ async function main() {
150
256
  }
151
257
 
152
258
  try {
259
+ const startedAt = Date.now();
260
+ // 链路日志走 stderr:stdout 是 JSONL 协议通道
261
+ console.error(`[run-engine] pid=${process.pid} adapter.execute 开始: engineId=${options.engineId} workDir=${options.workDir ?? '(cwd)'} resume=${options.resume ?? 'none'} fork=${options.fork ?? 'none'} timeoutSec=${options.timeoutSec ?? 'none'}`);
262
+ let eventCount = 0;
153
263
  for await (const event of adapter.execute(task, abortController?.signal)) {
264
+ eventCount += 1;
154
265
  switch (event.type) {
155
266
  case 'delta':
156
- writeEvent({ type: 'content_delta', text: event.content });
267
+ if (event.engineEvent) {
268
+ writeEvent(event.engineEvent);
269
+ } else {
270
+ writeEvent({ type: 'content_delta', text: normalizeEventText(event.content) });
271
+ }
157
272
  break;
158
273
  case 'thinking':
159
- writeEvent({ type: 'thinking_delta', text: event.content });
274
+ if (event.engineEvent) {
275
+ writeEvent(event.engineEvent);
276
+ } else {
277
+ writeEvent({ type: 'thinking_delta', text: normalizeEventText(event.content) });
278
+ }
160
279
  break;
161
280
  case 'tool_call':
162
281
  writeEvent({ type: 'tool_call', name: event.name, input: event.args });
163
282
  break;
164
- case 'result':
283
+ case 'result': {
284
+ const isError = event.isError === true;
285
+ const subtype = typeof event.sdkSubtype === 'string' ? event.sdkSubtype : undefined;
286
+ const sdkFailed = isError || isSdkFailureSubtype(subtype);
287
+ const text = normalizeEventText(event.content);
288
+ const error = sdkFailed
289
+ ? (text.trim() || (subtype ? `SDK result subtype=${subtype}` : 'SDK result is_error=true'))
290
+ : undefined;
291
+ console.error(`[run-engine] pid=${process.pid} adapter.execute 结束: engineId=${options.engineId} events=${eventCount} durationMs=${Date.now() - startedAt} sessionId=${event.sessionId ?? 'none'} success=${!sdkFailed} subtype=${subtype ?? 'none'}`);
165
292
  writeEvent({
166
293
  type: 'result',
167
294
  result: {
168
- success: true,
169
- text: event.content,
295
+ success: !sdkFailed,
296
+ text,
170
297
  ...(event.sessionId ? { sessionId: event.sessionId } : {}),
171
298
  ...(event.usage ? { usage: event.usage } : {}),
299
+ ...(isError ? { is_error: true } : {}),
300
+ ...(subtype ? { subtype } : {}),
301
+ ...(error ? { error } : {}),
172
302
  },
173
303
  });
174
304
  break;
305
+ }
175
306
  case 'error':
176
307
  writeEvent({ type: 'error', error: event.message, code: event.code });
177
308
  break;
@@ -182,6 +313,7 @@ async function main() {
182
313
  }
183
314
  process.exit(0);
184
315
  } catch (e) {
316
+ console.error(`[run-engine] pid=${process.pid} adapter.execute 异常: engineId=${options.engineId} error=${e instanceof Error ? e.message : String(e)}`);
185
317
  writeEvent({ type: 'error', error: e instanceof Error ? e.message : String(e) });
186
318
  process.exit(1);
187
319
  } finally {