evolcore 0.0.21 → 0.0.23

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 (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -0,0 +1,448 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { resolvePaths } from '../../paths.js';
5
+ import { isValidAid } from '../../aun/aid/validation.js';
6
+ function diagnosticPaths(overrides) {
7
+ if (overrides)
8
+ return overrides;
9
+ const paths = resolvePaths();
10
+ return {
11
+ logs: paths.logs,
12
+ agentsDir: paths.agentsDir,
13
+ daemonConfig: paths.daemonConfig,
14
+ defaultsConfig: paths.defaultsConfig,
15
+ };
16
+ }
17
+ const LOG_SOURCES = [
18
+ { id: 'daemon', label: 'Daemon', pattern: /^daemon(?:-.+)?\.log$/ },
19
+ { id: 'ecweb', label: 'ECWeb', pattern: /^(?:watch-web|ecweb[^/]*)\.(?:log|out)$/ },
20
+ { id: 'stdout', label: 'Standard output', pattern: /^stdout\.log(?:\..+)?$/ },
21
+ { id: 'aun', label: 'AUN', pattern: /^aun(?:-.+)?\.log$/ },
22
+ { id: 'sdk', label: 'TypeScript SDK', pattern: /^ts-sdk-.+\.log$/ },
23
+ { id: 'events', label: 'Events', pattern: /^events-.+\.log$/ },
24
+ { id: 'channel-in', label: 'Channel · inbound', pattern: /^channel-in-.+\.log$/ },
25
+ { id: 'channel-out', label: 'Channel · outbound', pattern: /^channel-out-.+\.log$/ },
26
+ { id: 'commands', label: 'Commands', pattern: /^commands-.+\.jsonl$/ },
27
+ { id: 'triggers', label: 'Trigger runs', pattern: /^trigger-runs(?:-.+)?\.(?:log|jsonl)$/ },
28
+ { id: 'restart', label: 'Restart', pattern: /^restart\.log$/ },
29
+ { id: 'menu-cli', label: 'Menu CLI', pattern: /^menu-cli-exec\.log$/ },
30
+ { id: 'menu-protocol', label: 'Menu Protocol', pattern: /^menu-protocol(?:-.+)?\.log$/ },
31
+ { id: 'causation', label: 'Causation spans', pattern: /^causation-spans(?:-.+)?\.log$/ },
32
+ { id: 'command-audit', label: 'Command audit', pattern: /^command-audit(?:-.+)?\.log$/ },
33
+ { id: 'role-mutations', label: 'Role mutations', pattern: /^role-mutations(?:-.+)?\.log$/ },
34
+ ];
35
+ const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
36
+ const LEVEL_RANK = new Map(LOG_LEVELS.map((level, index) => [level, index]));
37
+ const DEFAULT_LOG_LIMIT = 200;
38
+ const MAX_LOG_LIMIT = 1_000;
39
+ const MAX_LOG_SCAN_LINES = 10_000;
40
+ const MAX_FILE_TAIL_BYTES = 32 * 1024 * 1024;
41
+ const READ_CHUNK_BYTES = 256 * 1024;
42
+ const MAX_LINE_CHARS = 16_000;
43
+ const RESPONSE_CONTENT_BUDGET = 104 * 1024;
44
+ const MAX_DIAGNOSTIC_QUERY_MS = 5_000;
45
+ const cursorSecret = crypto.randomBytes(32);
46
+ function listLogFiles(paths) {
47
+ try {
48
+ return fs.readdirSync(paths.logs, { withFileTypes: true })
49
+ .filter(entry => entry.isFile())
50
+ .map(entry => {
51
+ const filePath = path.join(paths.logs, entry.name);
52
+ const stat = fs.statSync(filePath);
53
+ return { name: entry.name, path: filePath, size: stat.size, mtimeMs: stat.mtimeMs };
54
+ });
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ }
60
+ function availableLogSources(files) {
61
+ return LOG_SOURCES.map(source => {
62
+ const matches = files.filter(file => source.pattern.test(file.name));
63
+ return {
64
+ id: source.id,
65
+ label: source.label,
66
+ fileCount: matches.length,
67
+ size: matches.reduce((sum, file) => sum + file.size, 0),
68
+ updatedAt: matches.reduce((latest, file) => Math.max(latest, file.mtimeMs), 0),
69
+ };
70
+ }).filter(source => source.fileCount > 0);
71
+ }
72
+ function readTail(file, maxLines) {
73
+ if (file.size <= 0 || maxLines <= 0)
74
+ return [];
75
+ let handle;
76
+ try {
77
+ handle = fs.openSync(file.path, 'r');
78
+ const chunks = [];
79
+ let position = file.size;
80
+ let bytesRead = 0;
81
+ let newlines = 0;
82
+ while (position > 0 && bytesRead < MAX_FILE_TAIL_BYTES && newlines <= maxLines) {
83
+ const size = Math.min(READ_CHUNK_BYTES, position, MAX_FILE_TAIL_BYTES - bytesRead);
84
+ const chunk = Buffer.allocUnsafe(size);
85
+ position -= size;
86
+ fs.readSync(handle, chunk, 0, size, position);
87
+ chunks.unshift(chunk);
88
+ bytesRead += size;
89
+ for (const byte of chunk)
90
+ if (byte === 0x0a)
91
+ newlines += 1;
92
+ }
93
+ let text = Buffer.concat(chunks).toString('utf8');
94
+ if (position > 0) {
95
+ const firstBreak = text.indexOf('\n');
96
+ text = firstBreak >= 0 ? text.slice(firstBreak + 1) : '';
97
+ }
98
+ return text.split(/\r?\n/).filter(Boolean).slice(-maxLines);
99
+ }
100
+ finally {
101
+ if (handle !== undefined)
102
+ try {
103
+ fs.closeSync(handle);
104
+ }
105
+ catch { }
106
+ }
107
+ }
108
+ function parseLine(rawLine, file) {
109
+ const raw = rawLine.length > MAX_LINE_CHARS ? `${rawLine.slice(0, MAX_LINE_CHARS)} …` : rawLine;
110
+ let value;
111
+ if (rawLine.startsWith('{')) {
112
+ try {
113
+ const parsed = JSON.parse(rawLine);
114
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
115
+ value = parsed;
116
+ }
117
+ catch { }
118
+ }
119
+ const textTimestamp = raw.match(/^\[([^\]]+)]\s*(?:\[(?:TRACE|DEBUG|INFO|WARN|ERROR|FATAL)])?/i)?.[1];
120
+ const timestampValue = value?.ts ?? value?.timestamp ?? value?.startedAt ?? value?.firedAt ?? textTimestamp;
121
+ const timestamp = typeof timestampValue === 'number'
122
+ ? new Date(timestampValue).toISOString()
123
+ : typeof timestampValue === 'string' && timestampValue.trim() ? timestampValue.trim() : null;
124
+ const explicitLevel = typeof value?.level === 'string' ? value.level.toLowerCase() : undefined;
125
+ const textLevel = raw.match(/(?:^|\s|\[)(TRACE|DEBUG|INFO|WARN|WARNING|ERROR|FATAL)(?:\]|\s|:)/i)?.[1]?.toLowerCase();
126
+ const inferred = value?.error || value?.status === 'failed' ? 'error'
127
+ : value?.status === 'completed' ? 'info'
128
+ : value?.status === 'started' ? 'debug' : undefined;
129
+ const level = (explicitLevel || (textLevel === 'warning' ? 'warn' : textLevel) || inferred || null);
130
+ const scalar = (item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean' ? String(item) : null;
131
+ const fields = [];
132
+ const excluded = new Set(['ts', 'timestamp', 'level', 'message', 'msg', 'text', 'event', 'type', 'status', 'error']);
133
+ if (value) {
134
+ for (const [key, item] of Object.entries(value)) {
135
+ if (excluded.has(key) || fields.length >= 12)
136
+ continue;
137
+ const normalized = scalar(item);
138
+ if (normalized !== null)
139
+ fields.push({ key, value: normalized });
140
+ }
141
+ }
142
+ const event = typeof value?.event === 'string' ? value.event
143
+ : typeof value?.type === 'string' ? value.type
144
+ : typeof value?.status === 'string' ? value.status : null;
145
+ const module = typeof value?.module === 'string' ? value.module
146
+ : event ? event.split(/[.:]/)[0] : null;
147
+ const actor = typeof value?.actor === 'string' ? value.actor
148
+ : typeof value?.aid === 'string' ? value.aid
149
+ : typeof value?.selfAid === 'string' ? value.selfAid : null;
150
+ const directionValue = typeof value?.dir === 'string' ? value.dir.toLowerCase() : '';
151
+ const direction = directionValue === 'in' || directionValue === 'inbound'
152
+ ? 'in' : directionValue === 'out' || directionValue === 'outbound' ? 'out' : null;
153
+ const message = value
154
+ ? scalar(value.message ?? value.msg ?? value.text ?? value.reason ?? value.error) ?? ''
155
+ : raw.replace(/^\[[^\]]+]\s*(?:\[(?:TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\]\s*)?/i, '').trim();
156
+ return {
157
+ timestamp,
158
+ level,
159
+ file,
160
+ module: module || null,
161
+ actor,
162
+ direction,
163
+ event,
164
+ fields,
165
+ format: value ? 'json' : 'text',
166
+ userMessage: !value && !!module && /^\S+: .+$/.test(message),
167
+ message,
168
+ raw,
169
+ };
170
+ }
171
+ function timeBoundary(value, now) {
172
+ if (value === undefined || value === null || value === '')
173
+ return undefined;
174
+ if (typeof value !== 'string' && typeof value !== 'number')
175
+ throw { code: 'INVALID_ARGUMENT', message: 'time filter must be an ISO timestamp, epoch, or duration' };
176
+ if (typeof value === 'number' || /^\d+$/.test(value)) {
177
+ const number = Number(value);
178
+ if (!Number.isFinite(number) || number < 0)
179
+ throw { code: 'INVALID_ARGUMENT', message: `Invalid time filter: ${String(value)}` };
180
+ return number < 10_000_000_000 ? number * 1000 : number;
181
+ }
182
+ const duration = value.match(/^(\d+)(ms|s|m|h|d)$/i);
183
+ if (duration) {
184
+ const unit = duration[2].toLowerCase();
185
+ const multiplier = unit === 'ms' ? 1 : unit === 's' ? 1_000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000;
186
+ return now - Number(duration[1]) * multiplier;
187
+ }
188
+ const parsed = Date.parse(value);
189
+ if (!Number.isFinite(parsed))
190
+ throw { code: 'INVALID_ARGUMENT', message: `Invalid time filter: ${value}` };
191
+ return parsed;
192
+ }
193
+ function cursorSignature(encoded) {
194
+ return crypto.createHmac('sha256', cursorSecret).update(encoded).digest('base64url');
195
+ }
196
+ function encodeCursor(value) {
197
+ const encoded = Buffer.from(JSON.stringify(value)).toString('base64url');
198
+ return `${encoded}.${cursorSignature(encoded)}`;
199
+ }
200
+ function decodeCursor(value, expected) {
201
+ if (value === undefined || value === null || value === '')
202
+ return 0;
203
+ if (typeof value !== 'string')
204
+ throw { code: 'INVALID_ARGUMENT', message: 'cursor must be a string' };
205
+ const [encoded, signature, ...rest] = value.split('.');
206
+ if (!encoded || !signature || rest.length || signature !== cursorSignature(encoded)) {
207
+ throw { code: 'INVALID_ARGUMENT', message: 'Invalid diagnostic cursor' };
208
+ }
209
+ let decoded;
210
+ try {
211
+ decoded = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
212
+ }
213
+ catch {
214
+ throw { code: 'INVALID_ARGUMENT', message: 'Invalid diagnostic cursor' };
215
+ }
216
+ if (decoded.kind !== expected.kind || decoded.requester !== expected.requester) {
217
+ throw { code: 'INVALID_ARGUMENT', message: 'Diagnostic cursor does not belong to this request' };
218
+ }
219
+ if (decoded.fingerprint !== expected.fingerprint) {
220
+ throw { code: 'CATALOG_STALE', message: 'Diagnostic resource changed; restart pagination' };
221
+ }
222
+ const offset = Number(decoded.offset);
223
+ if (!Number.isSafeInteger(offset) || offset < 0)
224
+ throw { code: 'INVALID_ARGUMENT', message: 'Invalid diagnostic cursor offset' };
225
+ return offset;
226
+ }
227
+ function resourceFingerprint(value) {
228
+ return crypto.createHash('sha256').update(JSON.stringify(value)).digest('base64url');
229
+ }
230
+ export function listDiagnosticLogOptions(pathOverrides) {
231
+ const files = listLogFiles(diagnosticPaths(pathOverrides));
232
+ return {
233
+ views: ['status', 'monitor', 'logs'],
234
+ sources: availableLogSources(files),
235
+ levels: [...LOG_LEVELS],
236
+ limits: { default: DEFAULT_LOG_LIMIT, max: MAX_LOG_LIMIT },
237
+ };
238
+ }
239
+ export function queryDiagnosticLogs(args, requester, pathOverrides) {
240
+ const startedAt = Date.now();
241
+ const allowed = new Set(['view', 'source', 'since', 'until', 'level', 'limit', 'cursor']);
242
+ const unknown = Object.keys(args).find(key => !allowed.has(key));
243
+ if (unknown)
244
+ throw { code: 'INVALID_ARGUMENT', message: `Unsupported logs argument: ${unknown}` };
245
+ const files = listLogFiles(diagnosticPaths(pathOverrides));
246
+ const sources = availableLogSources(files);
247
+ if (args.source !== undefined && typeof args.source !== 'string') {
248
+ throw { code: 'INVALID_ARGUMENT', message: 'source must be a string' };
249
+ }
250
+ const sourceId = typeof args.source === 'string' && args.source.trim()
251
+ ? args.source.trim()
252
+ : sources.some(source => source.id === 'daemon') ? 'daemon' : sources[0]?.id;
253
+ const source = LOG_SOURCES.find(candidate => candidate.id === sourceId);
254
+ if (!source || !sources.some(candidate => candidate.id === sourceId)) {
255
+ throw { code: 'INVALID_ARGUMENT', message: `Unknown or unavailable log source: ${String(sourceId ?? '')}` };
256
+ }
257
+ const limit = args.limit === undefined ? DEFAULT_LOG_LIMIT : Number(args.limit);
258
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LOG_LIMIT) {
259
+ throw { code: 'INVALID_ARGUMENT', message: `limit must be an integer from 1 to ${MAX_LOG_LIMIT}` };
260
+ }
261
+ if (args.level !== undefined && typeof args.level !== 'string') {
262
+ throw { code: 'INVALID_ARGUMENT', message: 'level must be a string' };
263
+ }
264
+ const level = args.level === undefined ? undefined : args.level.toLowerCase();
265
+ if (level !== undefined && !LEVEL_RANK.has(level)) {
266
+ throw { code: 'INVALID_ARGUMENT', message: `Unsupported log level: ${level}` };
267
+ }
268
+ const now = Date.now();
269
+ const since = timeBoundary(args.since, now);
270
+ const until = timeBoundary(args.until, now);
271
+ if (since !== undefined && until !== undefined && since > until) {
272
+ throw { code: 'INVALID_ARGUMENT', message: 'since must not be later than until' };
273
+ }
274
+ const matching = files.filter(file => source.pattern.test(file.name)).sort((a, b) => a.mtimeMs - b.mtimeMs);
275
+ const fingerprint = resourceFingerprint({
276
+ source: sourceId, level, since: args.since ?? null, until: args.until ?? null,
277
+ files: matching.map(file => [file.name, file.size, file.mtimeMs]),
278
+ });
279
+ const offset = decodeCursor(args.cursor, { kind: 'logs', requester, fingerprint });
280
+ const rawRows = [];
281
+ for (const file of [...matching].reverse()) {
282
+ const remaining = MAX_LOG_SCAN_LINES - rawRows.length;
283
+ if (remaining <= 0)
284
+ break;
285
+ rawRows.push(...readTail(file, remaining).map(line => ({ line, file: file.name })));
286
+ }
287
+ const rows = rawRows.map(item => parseLine(item.line, item.file))
288
+ .filter(row => {
289
+ const timestamp = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
290
+ if (since !== undefined && (!Number.isFinite(timestamp) || timestamp < since))
291
+ return false;
292
+ if (until !== undefined && (!Number.isFinite(timestamp) || timestamp > until))
293
+ return false;
294
+ if (level !== undefined) {
295
+ const rowRank = row.level ? LEVEL_RANK.get(row.level) : undefined;
296
+ if (rowRank === undefined || rowRank < LEVEL_RANK.get(level))
297
+ return false;
298
+ }
299
+ return true;
300
+ });
301
+ const newestFirst = rows.reverse();
302
+ const page = [];
303
+ let bytes = 0;
304
+ let consumed = 0;
305
+ for (const row of newestFirst.slice(offset)) {
306
+ if (page.length >= limit)
307
+ break;
308
+ const rowBytes = Buffer.byteLength(JSON.stringify(row), 'utf8');
309
+ if (page.length > 0 && bytes + rowBytes > RESPONSE_CONTENT_BUDGET)
310
+ break;
311
+ page.push(row);
312
+ bytes += rowBytes;
313
+ consumed += 1;
314
+ }
315
+ const nextOffset = offset + consumed;
316
+ if (Date.now() - startedAt > MAX_DIAGNOSTIC_QUERY_MS) {
317
+ throw { code: 'TIMEOUT', message: 'Diagnostic log query exceeded the server time limit' };
318
+ }
319
+ return {
320
+ source: sourceId,
321
+ filters: { since: since ?? null, until: until ?? null, level: level ?? null },
322
+ limit,
323
+ lines: page.reverse(),
324
+ nextCursor: nextOffset < newestFirst.length
325
+ ? encodeCursor({ kind: 'logs', requester, fingerprint, offset: nextOffset })
326
+ : null,
327
+ updatedAt: now,
328
+ };
329
+ }
330
+ export function listRawConfigResources(pathOverrides) {
331
+ const paths = diagnosticPaths(pathOverrides);
332
+ const resources = [
333
+ { value: 'daemon', label: 'daemon.json' },
334
+ { value: 'defaults', label: 'agents/defaults.json' },
335
+ ];
336
+ try {
337
+ const agentsDir = paths.agentsDir;
338
+ for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
339
+ if (!entry.isDirectory() || !isValidAid(entry.name))
340
+ continue;
341
+ const configFile = path.join(agentsDir, entry.name, 'config.json');
342
+ try {
343
+ const stat = fs.lstatSync(configFile);
344
+ if (stat.isFile() && !stat.isSymbolicLink())
345
+ resources.push({ value: `agent:${entry.name}`, label: `agents/${entry.name}/config.json` });
346
+ }
347
+ catch { }
348
+ }
349
+ }
350
+ catch { }
351
+ return resources;
352
+ }
353
+ function rawConfigPath(resource, paths) {
354
+ if (resource === 'daemon' || resource === 'defaults') {
355
+ const candidate = resource === 'daemon' ? paths.daemonConfig : paths.defaultsConfig;
356
+ try {
357
+ const stat = fs.lstatSync(candidate);
358
+ return stat.isFile() && !stat.isSymbolicLink() ? candidate : undefined;
359
+ }
360
+ catch {
361
+ // Let the caller turn a missing fixed resource into NOT_FOUND.
362
+ return candidate;
363
+ }
364
+ }
365
+ if (resource.startsWith('agent:')) {
366
+ const aid = resource.slice('agent:'.length);
367
+ if (!isValidAid(aid))
368
+ return undefined;
369
+ const agentDirectory = path.join(paths.agentsDir, aid);
370
+ try {
371
+ const directoryStat = fs.lstatSync(agentDirectory);
372
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
373
+ return undefined;
374
+ }
375
+ catch {
376
+ return undefined;
377
+ }
378
+ const candidate = path.join(paths.agentsDir, aid, 'config.json');
379
+ const expectedRoot = path.resolve(paths.agentsDir);
380
+ if (path.dirname(path.resolve(candidate)) !== path.join(expectedRoot, aid))
381
+ return undefined;
382
+ try {
383
+ const stat = fs.lstatSync(candidate);
384
+ if (!stat.isFile() || stat.isSymbolicLink())
385
+ return undefined;
386
+ }
387
+ catch {
388
+ return candidate;
389
+ }
390
+ return candidate;
391
+ }
392
+ return undefined;
393
+ }
394
+ function utf8PageEnd(content, start, maxBytes) {
395
+ let low = start;
396
+ let high = content.length;
397
+ while (low < high) {
398
+ const middle = Math.ceil((low + high) / 2);
399
+ if (Buffer.byteLength(content.slice(start, middle), 'utf8') <= maxBytes)
400
+ low = middle;
401
+ else
402
+ high = middle - 1;
403
+ }
404
+ return Math.max(start, low);
405
+ }
406
+ export function readRawConfigResource(args, requester, pathOverrides) {
407
+ const startedAt = Date.now();
408
+ const allowed = new Set(['view', 'resource', 'cursor']);
409
+ const unknown = Object.keys(args).find(key => !allowed.has(key));
410
+ if (unknown)
411
+ throw { code: 'INVALID_ARGUMENT', message: `Unsupported rawFile argument: ${unknown}` };
412
+ const resource = typeof args.resource === 'string' ? args.resource.trim() : '';
413
+ const file = rawConfigPath(resource, diagnosticPaths(pathOverrides));
414
+ if (!file)
415
+ throw { code: 'INVALID_ARGUMENT', message: 'resource must be one of the advertised rawFile resources' };
416
+ let stat;
417
+ let content;
418
+ try {
419
+ stat = fs.statSync(file);
420
+ content = fs.readFileSync(file, 'utf8');
421
+ }
422
+ catch (error) {
423
+ if (error?.code === 'ENOENT')
424
+ throw { code: 'NOT_FOUND', message: `Configuration resource does not exist: ${resource}` };
425
+ throw error;
426
+ }
427
+ if (!stat.isFile())
428
+ throw { code: 'NOT_FOUND', message: `Configuration resource is not a file: ${resource}` };
429
+ const fingerprint = resourceFingerprint({ resource, size: stat.size, mtimeMs: stat.mtimeMs });
430
+ const offset = decodeCursor(args.cursor, { kind: 'rawFile', requester, fingerprint });
431
+ if (offset > content.length)
432
+ throw { code: 'INVALID_ARGUMENT', message: 'rawFile cursor offset is outside the resource' };
433
+ const end = utf8PageEnd(content, offset, RESPONSE_CONTENT_BUDGET);
434
+ const truncated = end < content.length;
435
+ if (Date.now() - startedAt > MAX_DIAGNOSTIC_QUERY_MS) {
436
+ throw { code: 'TIMEOUT', message: 'Raw configuration query exceeded the server time limit' };
437
+ }
438
+ return {
439
+ resource,
440
+ size: stat.size,
441
+ mtimeMs: stat.mtimeMs,
442
+ sha256: crypto.createHash('sha256').update(content).digest('hex'),
443
+ content: content.slice(offset, end),
444
+ offset,
445
+ truncated,
446
+ nextCursor: truncated ? encodeCursor({ kind: 'rawFile', requester, fingerprint, offset: end }) : null,
447
+ };
448
+ }
@@ -80,6 +80,9 @@ const ENTRIES = [
80
80
  { id: 'activity.agent', type: 'setting', contexts: ['agent'], name: 'activity', label: 'Agent 中间输出', section: 'agent', order: 50, query: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.current', scope: 'agent' }, options: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.current', scope: 'agent' }, update: { cmd: '/activity', args: { scope: 'agent' }, operation: 'activity.update', scope: 'agent' }, control: 'select' },
81
81
  { id: 'observable.agent', type: 'setting', contexts: ['agent'], name: 'observable', label: '观察模式', section: 'agent', order: 60, query: { cmd: '/observable', operation: 'observable.current', scope: 'agent' }, update: { cmd: '/observable', operation: 'observable.update', scope: 'agent' }, valueSchema: { type: 'boolean' }, control: 'toggle' },
82
82
  { id: 'system.status', type: 'list', contexts: ['process'], name: 'system', label: '系统状态', section: 'process', order: 10, query: { cmd: '/system', operation: 'system.status', scope: 'process' }, control: 'list' },
83
+ { id: 'system.monitor', type: 'list', contexts: ['process'], name: 'system', label: '运行监控快照', section: 'process', order: 11, query: { cmd: '/system', args: { view: 'monitor' }, operation: 'system.status', scope: 'process' }, control: 'list' },
84
+ { id: 'system.logs', type: 'list', contexts: ['process'], name: 'system', label: 'Daemon 日志', section: 'process', order: 12, query: { cmd: '/system', args: { view: 'logs' }, operation: 'system.status', scope: 'process' }, options: { cmd: '/system', args: { option: 'logs' }, operation: 'system.status', scope: 'process', mode: 'dynamic' }, control: 'list' },
85
+ { id: 'config.rawFile', type: 'list', contexts: ['process'], name: 'config', label: '原始配置文件', section: 'process', order: 13, query: { cmd: '/config', args: { view: 'rawFile' }, operation: 'config.read', scope: 'process' }, options: { cmd: '/config', args: { option: 'rawFile' }, operation: 'config.read', scope: 'process', mode: 'dynamic' }, control: 'list' },
83
86
  { id: 'system.restart', type: 'action', contexts: ['process'], name: 'system', label: '重启服务', section: 'process', order: 20, action: { cmd: '/system', action: 'restart', operation: 'system.restart', scope: 'process' } },
84
87
  { id: 'system.upgrade', type: 'action', contexts: ['process'], name: 'system', label: '升级服务', section: 'process', order: 30, action: { cmd: '/system', action: 'upgrade', operation: 'system.upgrade', scope: 'process' } },
85
88
  { id: 'agent.list', type: 'list', contexts: ['process'], name: 'agent', label: 'Agent 列表', section: 'process', order: 40, options: { cmd: '/agent', args: { options: 'all' }, operation: 'agent.list', scope: 'control', mode: 'dynamic' }, control: 'list' },