ec-web 0.0.26 → 0.0.28
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/dist/index.js +1 -1
- package/dist/paths.js +4 -0
- package/dist/server.js +35 -20
- package/dist/sources/msg.js +21 -4
- package/dist/sources/role-definitions.js +53 -29
- package/dist/sources/session-codex.js +23 -5
- package/dist/sources/session-ecagent.js +665 -0
- package/dist/sources/session-origin.js +16 -0
- package/dist/sources/session.js +69 -165
- package/dist/sources/system.js +1 -33
- package/dist/sources/triggers.js +1 -1
- package/dist/static/app.js +340 -98
- package/dist/static/basic-settings.js +3 -3
- package/dist/static/index.html +1 -1
- package/dist/static/style.css +37 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -107,7 +107,7 @@ for (const list of Object.values(ifaces)) {
|
|
|
107
107
|
}
|
|
108
108
|
process.stdout.write(`\n${BOLD}${CYAN}EC Web — EvolCore${RST} ${DIM}(home: ${p.root})${RST}\n\n`);
|
|
109
109
|
process.stdout.write(handle.pairingCode
|
|
110
|
-
? ` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(daemon 提供,
|
|
110
|
+
? ` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(daemon 提供,10 分钟内有效,仅可配对一次,token 缓存 7 天,有访问自动续期)${RST}\n\n`
|
|
111
111
|
: ` ${BOLD}配对码:${RST} ${YELLOW}daemon 暂不可用,请先启动 EvolCore${RST}\n\n`);
|
|
112
112
|
process.stdout.write(` ${BOLD}本机:${RST} http://localhost:${handle.port}\n`);
|
|
113
113
|
for (const ip of lanIps)
|
package/dist/paths.js
CHANGED
|
@@ -14,6 +14,10 @@ import os from 'os';
|
|
|
14
14
|
import crypto from 'crypto';
|
|
15
15
|
const isWindows = process.platform === 'win32';
|
|
16
16
|
let _root = null;
|
|
17
|
+
/** Test helper: drop the cached root so a new EVOLCORE_HOME is picked up. */
|
|
18
|
+
export function _resetRoot() {
|
|
19
|
+
_root = null;
|
|
20
|
+
}
|
|
17
21
|
export function resolveRoot() {
|
|
18
22
|
if (_root)
|
|
19
23
|
return _root;
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - HTTP: 静态资源 + 配对 API
|
|
5
5
|
* - WebSocket: 订阅式实时推送(aid / msg / session)
|
|
6
|
-
* - 鉴权: daemon 通过本地 IPC 提供 6
|
|
6
|
+
* - 鉴权: daemon 通过本地 IPC 提供 6 位一次性配对码(10 分钟有效)和远程访问 token(7 天滑动续期)
|
|
7
7
|
* - 安全: 绑定 0.0.0.0(支持远程访问);本地自动登录,远程配对后使用 token,登录后权限一致
|
|
8
8
|
*
|
|
9
9
|
* 与 evolcore daemon 通过 IPC socket(状态、Menu 和远程访问凭据)+ 文件系统(历史数据)旁路通信。
|
|
@@ -20,7 +20,7 @@ import { ipcQuery } from './ipc-client.js';
|
|
|
20
20
|
import { setDebugLog, dlog } from './debug-log.js';
|
|
21
21
|
import { aidSource } from './sources/aid.js';
|
|
22
22
|
import { msgSource } from './sources/msg.js';
|
|
23
|
-
import { sessionSource, buildBindMap, readClaudeSessionResult } from './sources/session.js';
|
|
23
|
+
import { sessionSource, buildBindMap, readClaudeSessionResult, resolveSessionBaseagent, refreshConfiguredBaseagent } from './sources/session.js';
|
|
24
24
|
import { cacheSource } from './sources/cache.js';
|
|
25
25
|
import { systemSource } from './sources/system.js';
|
|
26
26
|
import { triggersSource } from './sources/triggers.js';
|
|
@@ -33,8 +33,8 @@ import { ccProjectsDir } from './paths.js';
|
|
|
33
33
|
import { relationRolesSource, handleRelationRolesApi, handlePeerRoleApi } from './sources/relation-roles.js';
|
|
34
34
|
import { roleDefinitionsSource, handleRoleDefinitionsApi } from './sources/role-definitions.js';
|
|
35
35
|
import { handleModelsApi } from './sources/models.js';
|
|
36
|
-
import { detectBaseAgentsAsync } from './sources/baseagent-detector.js';
|
|
37
36
|
import { readCodexSessionResult } from './sources/session-codex.js';
|
|
37
|
+
import { readEcagentSessionResult } from './sources/session-ecagent.js';
|
|
38
38
|
import { handleBasicSettingsApi } from './sources/basic-settings.js';
|
|
39
39
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
40
40
|
const STATIC_DIR = path.join(__dirname, 'static');
|
|
@@ -585,16 +585,30 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
585
585
|
params.source = msg.source;
|
|
586
586
|
if (msg.limit)
|
|
587
587
|
params.limit = msg.limit;
|
|
588
|
+
if (msg.archived !== undefined)
|
|
589
|
+
params.archived = msg.archived;
|
|
588
590
|
dlog(`▸ 订阅 ${msg.view}${msg.aid ? ` aid=${String(msg.aid).split('.')[0]}` : ''}${msg.peer ? ` peer=${String(msg.peer).split('.')[0]}` : ''}${msg.sessionId ? ` session=${String(msg.sessionId).slice(0, 8)}` : ''}${msg.baseagent ? ` baseagent=${msg.baseagent}` : ''} from ${ip}`);
|
|
589
591
|
await switchSubscription(msg.view, params);
|
|
590
592
|
return;
|
|
591
593
|
}
|
|
592
594
|
if (msg.type === 'session.result') {
|
|
593
595
|
const params = msg.params && typeof msg.params === 'object' ? msg.params : {};
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
596
|
+
if (!params.baseagent)
|
|
597
|
+
await refreshConfiguredBaseagent();
|
|
598
|
+
const baseagent = resolveSessionBaseagent(params);
|
|
599
|
+
const result = baseagent === 'codex'
|
|
600
|
+
? readCodexSessionResult({ ...params, baseagent })
|
|
601
|
+
: baseagent === 'ecagent'
|
|
602
|
+
? readEcagentSessionResult({ ...params, baseagent })
|
|
603
|
+
: baseagent
|
|
604
|
+
? readClaudeSessionResult({ ...params, baseagent })
|
|
605
|
+
: Promise.resolve(null);
|
|
606
|
+
void Promise.resolve(result)
|
|
607
|
+
.then(text => send({ type: 'session.result', requestId: msg.requestId, data: text == null ? null : { text } }))
|
|
608
|
+
.catch(error => {
|
|
609
|
+
dlog(`session.result failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
610
|
+
send({ type: 'session.result', requestId: msg.requestId, data: null });
|
|
611
|
+
});
|
|
598
612
|
return;
|
|
599
613
|
}
|
|
600
614
|
if (msg.type === 'menu' && msg.payload) {
|
|
@@ -731,6 +745,8 @@ function bindPort(server, preferred, log) {
|
|
|
731
745
|
export async function startWatchWebServer(opts = {}) {
|
|
732
746
|
const log = opts.log || (() => { });
|
|
733
747
|
setDebugLog(log);
|
|
748
|
+
// 会话读取器默认后端来自 defaults.json;启动时同步一次。
|
|
749
|
+
await refreshConfiguredBaseagent();
|
|
734
750
|
// soft 版本校验:ping daemon 检查 protocolVersion
|
|
735
751
|
const p = resolvePaths();
|
|
736
752
|
const pingResp = await ipcQuery(p.socket, { type: 'ping' }, 1000);
|
|
@@ -740,17 +756,12 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
740
756
|
const server = http.createServer(async (req, res) => {
|
|
741
757
|
const pathname = requestPath(req.url || '');
|
|
742
758
|
const localDirect = isLocalDirect(req);
|
|
743
|
-
if (req.method === 'GET' && (req.url || '') === '/api/
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
res.writeHead(500, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
752
|
-
res.end(JSON.stringify({ success: false, error: error?.message || String(error) }));
|
|
753
|
-
}
|
|
759
|
+
if (req.method === 'GET' && (req.url || '') === '/api/health') {
|
|
760
|
+
// Keep readiness checks independent from daemon auth and pairing state.
|
|
761
|
+
// The endpoint is intentionally small so supervisors and proxies can
|
|
762
|
+
// distinguish ECWeb from an unrelated process on the configured port.
|
|
763
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
764
|
+
res.end(JSON.stringify({ ok: true, service: 'ecweb', version: ECWEB_VERSION }));
|
|
754
765
|
}
|
|
755
766
|
else if (req.method === 'GET' && (req.url || '') === '/api/pair-code') {
|
|
756
767
|
// 取码 API:仅真本地直连可用(socket 回环 + 无 x-aun-provider-aid)。
|
|
@@ -761,7 +772,9 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
761
772
|
res.end(JSON.stringify({ error: 'forbidden' }));
|
|
762
773
|
return;
|
|
763
774
|
}
|
|
764
|
-
const pair = await remoteAuth('remote-auth.pair-code'
|
|
775
|
+
const pair = await remoteAuth('remote-auth.pair-code', {
|
|
776
|
+
source: 'ecweb-local',
|
|
777
|
+
});
|
|
765
778
|
if (!pair?.ok || typeof pair.code !== 'string' || typeof pair.expiresAt !== 'number') {
|
|
766
779
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
767
780
|
res.end(JSON.stringify({ error: 'daemon unavailable' }));
|
|
@@ -912,7 +925,9 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
912
925
|
});
|
|
913
926
|
});
|
|
914
927
|
const { port, displaced } = await bindPort(server, opts.port ?? DEFAULT_PORT, log);
|
|
915
|
-
const initialPair = await remoteAuth('remote-auth.pair-code'
|
|
928
|
+
const initialPair = await remoteAuth('remote-auth.pair-code', {
|
|
929
|
+
source: 'ecweb-local',
|
|
930
|
+
});
|
|
916
931
|
return {
|
|
917
932
|
url: `http://0.0.0.0:${port}`,
|
|
918
933
|
port,
|
package/dist/sources/msg.js
CHANGED
|
@@ -10,6 +10,13 @@ import { ipcQuery } from '../ipc-client.js';
|
|
|
10
10
|
import { resolvePaths } from '../paths.js';
|
|
11
11
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
12
12
|
import { decodeDirSegment, isTransientProtocolMessage, readAllJsonlLines, readJsonFile, scanChatDirs, tryParseChannelSessionKey, } from '../fs-utils.js';
|
|
13
|
+
// ECWeb builds as a separate package. Keep this wire contract aligned with session-mapper.ts.
|
|
14
|
+
function topicMatchesArchiveFilter(chat, archived) {
|
|
15
|
+
if (archived === 'all')
|
|
16
|
+
return true;
|
|
17
|
+
const isArchived = chat.partition === 'thread' && chat.lable === '归档';
|
|
18
|
+
return archived === true ? isArchived : !isArchived;
|
|
19
|
+
}
|
|
13
20
|
let contextModulesPromise = null;
|
|
14
21
|
const sessionAdapterPromises = new Map();
|
|
15
22
|
let peerDisplayResolverPromise = null;
|
|
@@ -96,6 +103,7 @@ function getSessionAdapter(baseagent) {
|
|
|
96
103
|
claude: 'ClaudeSessionFileAdapter',
|
|
97
104
|
codex: 'CodexSessionFileAdapter',
|
|
98
105
|
gemini: 'GeminiSessionFileAdapter',
|
|
106
|
+
ecagent: 'EcagentSessionFileAdapter',
|
|
99
107
|
};
|
|
100
108
|
const className = classNames[baseagent];
|
|
101
109
|
const promise = !className
|
|
@@ -581,6 +589,8 @@ function loadChats(peerDisplayResolver, metrics) {
|
|
|
581
589
|
threadName: item.partition === 'thread'
|
|
582
590
|
? String(partitionActive.name || partitionActive.metadata?.topicName || item.threadId)
|
|
583
591
|
: null,
|
|
592
|
+
lable: item.partition === 'thread' && typeof partitionActive.lable === 'string'
|
|
593
|
+
? partitionActive.lable || null : null,
|
|
584
594
|
updatedAt,
|
|
585
595
|
messages: item.messages,
|
|
586
596
|
active: partitionActive,
|
|
@@ -690,6 +700,7 @@ function mergePeerInfo(chats) {
|
|
|
690
700
|
partition: chat.partition,
|
|
691
701
|
threadId: chat.threadId,
|
|
692
702
|
threadName: chat.threadName,
|
|
703
|
+
lable: chat.lable,
|
|
693
704
|
inbound: 0,
|
|
694
705
|
outbound: 0,
|
|
695
706
|
lastAt: 0,
|
|
@@ -766,7 +777,7 @@ async function sessionTurns(active, messages) {
|
|
|
766
777
|
if (baseagent && agentSessionId && projectPath) {
|
|
767
778
|
try {
|
|
768
779
|
const adapter = await getSessionAdapter(baseagent);
|
|
769
|
-
const turns = Number(adapter?.getFileInfo(projectPath, agentSessionId)?.turns);
|
|
780
|
+
const turns = Number(adapter?.getFileInfo(projectPath, agentSessionId, active.selfAID)?.turns);
|
|
770
781
|
if (Number.isFinite(turns) && turns > 0)
|
|
771
782
|
return turns;
|
|
772
783
|
}
|
|
@@ -820,6 +831,7 @@ async function buildSessionContext(chat) {
|
|
|
820
831
|
sessionId: active.id || null,
|
|
821
832
|
evolcoreSessionId: active.id || null,
|
|
822
833
|
name: active.name || null,
|
|
834
|
+
lable: chat.lable,
|
|
823
835
|
baseagent: active.baseagent || active.agentType || null,
|
|
824
836
|
chatMode,
|
|
825
837
|
permissionMode,
|
|
@@ -830,6 +842,10 @@ async function buildSessionContext(chat) {
|
|
|
830
842
|
};
|
|
831
843
|
}
|
|
832
844
|
async function buildSnapshot(params) {
|
|
845
|
+
const archived = params.archived;
|
|
846
|
+
if (archived !== undefined && archived !== false && archived !== true && archived !== 'all') {
|
|
847
|
+
throw new Error('archived 必须是布尔值或 "all"');
|
|
848
|
+
}
|
|
833
849
|
const startedAt = Date.now();
|
|
834
850
|
const metrics = {
|
|
835
851
|
files: 0,
|
|
@@ -853,13 +869,14 @@ async function buildSnapshot(params) {
|
|
|
853
869
|
: requestedScope;
|
|
854
870
|
const peer = params.peer || null;
|
|
855
871
|
const inScope = selectedChats(chats, scope);
|
|
856
|
-
const
|
|
872
|
+
const visibleChats = inScope.filter(chat => topicMatchesArchiveFilter(chat, archived));
|
|
873
|
+
const peers = scope ? mergePeerInfo(visibleChats) : [];
|
|
857
874
|
const requestedLimit = Number(params.limit);
|
|
858
875
|
const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(Math.trunc(requestedLimit), 20), 200) : 100;
|
|
859
876
|
const requestedBefore = params.before == null ? Number.NaN : Number(params.before);
|
|
860
877
|
const before = Number.isFinite(requestedBefore) && requestedBefore >= 0 ? Math.trunc(requestedBefore) : null;
|
|
861
|
-
const page = scope && peer ? messagesFor(
|
|
862
|
-
const currentChat = peer ?
|
|
878
|
+
const page = scope && peer ? messagesFor(visibleChats, peer, before, limit) : { messages: [], nextBefore: null, hasMore: false };
|
|
879
|
+
const currentChat = peer ? visibleChats.find(chat => chatKey(chat) === peer) : undefined;
|
|
863
880
|
const sessionContext = await buildSessionContext(currentChat);
|
|
864
881
|
metrics.totalMs = Date.now() - startedAt;
|
|
865
882
|
return {
|
|
@@ -29,6 +29,7 @@ async function getRolesModule() {
|
|
|
29
29
|
const resolverMod = await import(toFileUrl(resolverPath));
|
|
30
30
|
const roleServiceMod = await import(toFileUrl(roleServicePath));
|
|
31
31
|
const authMod = await import(toFileUrl(authPath));
|
|
32
|
+
const schemaMod = await import(toFileUrl(resolveParentDistModule('config', 'role-schema.js')));
|
|
32
33
|
if (!rolesMod.readRolesConfig)
|
|
33
34
|
throw new Error('readRolesConfig not found in roles.js');
|
|
34
35
|
if (!rolesMod.roleExists)
|
|
@@ -54,6 +55,8 @@ async function getRolesModule() {
|
|
|
54
55
|
read: cmMod.read,
|
|
55
56
|
write: cmMod.write,
|
|
56
57
|
ConfigTarget: cmMod.ConfigTarget,
|
|
58
|
+
resolveEffective: cmMod.resolveEffective,
|
|
59
|
+
rolePermissionFields: schemaMod.rolePermissionFields,
|
|
57
60
|
resolver: resolverMod,
|
|
58
61
|
buildAuthSubject: authMod.buildAuthSubject,
|
|
59
62
|
authorizeOperation: authMod.authorizeOperation,
|
|
@@ -66,10 +69,10 @@ async function getRolesModule() {
|
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
async function getRoleConstraintsModule() {
|
|
69
|
-
const constraintsPath = resolveParentDistModule('
|
|
72
|
+
const constraintsPath = resolveParentDistModule('core', 'model', 'model-permission.js');
|
|
70
73
|
const mod = await import(toFileUrl(constraintsPath));
|
|
71
74
|
if (!mod.isModelAllowedByPatterns)
|
|
72
|
-
throw new Error('isModelAllowedByPatterns not found in
|
|
75
|
+
throw new Error('isModelAllowedByPatterns not found in model-permission.js');
|
|
73
76
|
return {
|
|
74
77
|
isModelAllowedByPatterns: mod.isModelAllowedByPatterns,
|
|
75
78
|
};
|
|
@@ -164,8 +167,16 @@ function isDaemonOwnerAuth(processConfig, auth) {
|
|
|
164
167
|
: [];
|
|
165
168
|
return !!actor && processOwners.includes(actor);
|
|
166
169
|
}
|
|
167
|
-
|
|
168
|
-
const
|
|
170
|
+
function roleModelContext(modules, aid, requested) {
|
|
171
|
+
const effective = modules.resolveEffective({ self: aid });
|
|
172
|
+
const baseagent = requested ?? effective.active_baseagent;
|
|
173
|
+
const field = `baseagents.${baseagent}.model`;
|
|
174
|
+
const descriptor = modules.rolePermissionFields().find((item) => item.value === field);
|
|
175
|
+
if (!descriptor) {
|
|
176
|
+
throw Object.assign(new Error(`Unsupported role model backend: ${String(baseagent)}`), { statusCode: 400 });
|
|
177
|
+
}
|
|
178
|
+
return { baseagent, field, patternOptions: descriptor.patterns?.map((item) => item.value) ?? ['*'] };
|
|
179
|
+
}
|
|
169
180
|
const COMMAND_PERMISSION_SCOPES = new Set(['relation', 'role', 'agent', 'process', 'filesystem', 'control', 'raw-cli']);
|
|
170
181
|
const COMMAND_CONSTRAINT_KEYS = new Set([
|
|
171
182
|
'ownPeerOnly',
|
|
@@ -495,11 +506,11 @@ function normalizeRoleDefinitionForSave(roleDef) {
|
|
|
495
506
|
return roleDef;
|
|
496
507
|
return { ...roleDef, usageLimits: normalizeRoleUsageLimits(roleDef.usageLimits) };
|
|
497
508
|
}
|
|
498
|
-
function getModelPermission(roleDef) {
|
|
509
|
+
function getModelPermission(roleDef, field) {
|
|
499
510
|
const permissions = roleDef?.permissions;
|
|
500
511
|
if (!permissions || typeof permissions !== 'object')
|
|
501
512
|
return {};
|
|
502
|
-
const perm = permissions[
|
|
513
|
+
const perm = permissions[field];
|
|
503
514
|
return perm && typeof perm === 'object' ? perm : {};
|
|
504
515
|
}
|
|
505
516
|
function stripSelectionModeFromPermission(perm) {
|
|
@@ -544,38 +555,42 @@ async function validateRoleModelPermission(roleDef) {
|
|
|
544
555
|
const permissions = roleDef?.permissions;
|
|
545
556
|
if (!permissions || typeof permissions !== 'object')
|
|
546
557
|
return { ok: true, errors: [] };
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
558
|
+
const errors = [];
|
|
559
|
+
for (const [field, perm] of Object.entries(permissions)) {
|
|
560
|
+
if (!/^baseagents\.[a-z0-9_-]+\.model$/.test(field))
|
|
561
|
+
continue;
|
|
562
|
+
permissions[field] = stripSelectionModeFromPermission(perm);
|
|
563
|
+
const result = await validateModelPermissionInput({
|
|
564
|
+
defaultModel: permissions[field]?.default,
|
|
565
|
+
allowOverride: permissions[field]?.allowOverride,
|
|
566
|
+
allowedModels: permissions[field]?.allowedModels,
|
|
567
|
+
});
|
|
568
|
+
errors.push(...result.errors.map(error => `${field}: ${error}`));
|
|
569
|
+
}
|
|
570
|
+
return { ok: errors.length === 0, errors };
|
|
557
571
|
}
|
|
558
572
|
async function filterAllowedCatalogModels(models, allowedModels) {
|
|
559
573
|
const { isModelAllowedByPatterns } = await getRoleConstraintsModule();
|
|
560
574
|
return models.filter(model => !model.isAlias && isModelAllowedByPatterns(model.id, allowedModels));
|
|
561
575
|
}
|
|
562
|
-
async function buildModelPermissionResponse(roleName, roleDef, override, aid) {
|
|
563
|
-
const currentPerm = getModelPermission(roleDef);
|
|
576
|
+
async function buildModelPermissionResponse(roleName, roleDef, modelContext, override, aid) {
|
|
577
|
+
const currentPerm = getModelPermission(roleDef, modelContext.field);
|
|
564
578
|
const defaultModel = override?.defaultModel ?? currentPerm.default ?? '';
|
|
565
579
|
const allowOverride = override?.allowOverride ?? currentPerm.allowOverride ?? false;
|
|
566
580
|
const allowedModels = override?.allowedModels ?? (Array.isArray(currentPerm.allowedModels) ? currentPerm.allowedModels : []);
|
|
567
|
-
const catalog = await getModelCatalogSnapshot(
|
|
581
|
+
const catalog = await getModelCatalogSnapshot(modelContext.baseagent, aid);
|
|
568
582
|
const matchingModels = await filterAllowedCatalogModels(catalog.models, allowedModels);
|
|
569
583
|
return {
|
|
570
584
|
role: roleName,
|
|
571
|
-
|
|
585
|
+
baseagent: modelContext.baseagent,
|
|
586
|
+
field: modelContext.field,
|
|
572
587
|
permission: {
|
|
573
588
|
default: defaultModel,
|
|
574
589
|
allowOverride,
|
|
575
590
|
allowedModels,
|
|
576
591
|
},
|
|
577
592
|
selectionMode: inferSelectionMode(allowedModels),
|
|
578
|
-
patternOptions:
|
|
593
|
+
patternOptions: modelContext.patternOptions,
|
|
579
594
|
catalog,
|
|
580
595
|
matchingModels,
|
|
581
596
|
stats: {
|
|
@@ -613,10 +628,16 @@ async function buildSnapshot(params = {}, auth = {}) {
|
|
|
613
628
|
};
|
|
614
629
|
}
|
|
615
630
|
const config = modules.readRolesConfig(aid);
|
|
631
|
+
const activeBaseagent = modules.resolveEffective({ self: aid }).active_baseagent;
|
|
632
|
+
const modelContext = activeBaseagent ? roleModelContext(modules, aid, activeBaseagent) : undefined;
|
|
616
633
|
return {
|
|
617
634
|
$schema_version: 1,
|
|
618
635
|
aid,
|
|
619
636
|
self: aid,
|
|
637
|
+
baseagent: modelContext?.baseagent ?? null,
|
|
638
|
+
modelPermissionField: modelContext?.field ?? null,
|
|
639
|
+
modelPatternOptions: modelContext?.patternOptions ?? [],
|
|
640
|
+
permissionFields: modules.rolePermissionFields(),
|
|
620
641
|
agents: summarizeAgents(agents),
|
|
621
642
|
defaultRoles: config.defaultRoles ?? { private: null, group: null },
|
|
622
643
|
policyRevision: JSON.stringify(config),
|
|
@@ -866,7 +887,8 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
866
887
|
sendJson(res, 404, { success: false, error: 'Role not found' });
|
|
867
888
|
return;
|
|
868
889
|
}
|
|
869
|
-
const
|
|
890
|
+
const modelContext = roleModelContext(modules, aid, parsedUrl.query.baseagent);
|
|
891
|
+
const data = await buildModelPermissionResponse(roleName, roleDef, modelContext, undefined, aid || undefined);
|
|
870
892
|
sendJson(res, 200, { success: true, data });
|
|
871
893
|
return;
|
|
872
894
|
}
|
|
@@ -879,13 +901,14 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
879
901
|
return;
|
|
880
902
|
}
|
|
881
903
|
const payload = await readJsonBody(req);
|
|
882
|
-
const
|
|
904
|
+
const modelContext = roleModelContext(modules, aid, parsedUrl.query.baseagent);
|
|
905
|
+
const currentPerm = getModelPermission(roleDef, modelContext.field);
|
|
883
906
|
const validation = await validateModelPermissionInput(extractModelPermissionPayload(payload, currentPerm));
|
|
884
907
|
if (!validation.ok || !validation.value) {
|
|
885
908
|
sendJson(res, 400, { success: false, error: 'Invalid model permissions', errors: validation.errors });
|
|
886
909
|
return;
|
|
887
910
|
}
|
|
888
|
-
const data = await buildModelPermissionResponse(roleName, roleDef, validation.value, aid || undefined);
|
|
911
|
+
const data = await buildModelPermissionResponse(roleName, roleDef, modelContext, validation.value, aid || undefined);
|
|
889
912
|
sendJson(res, 200, { success: true, data });
|
|
890
913
|
return;
|
|
891
914
|
}
|
|
@@ -898,7 +921,8 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
898
921
|
return;
|
|
899
922
|
}
|
|
900
923
|
const payload = await readJsonBody(req);
|
|
901
|
-
const
|
|
924
|
+
const modelContext = roleModelContext(modules, aid, parsedUrl.query.baseagent);
|
|
925
|
+
const currentPerm = getModelPermission(roleDef, modelContext.field);
|
|
902
926
|
const validation = await validateModelPermissionInput(extractModelPermissionPayload(payload, currentPerm));
|
|
903
927
|
if (!validation.ok || !validation.value) {
|
|
904
928
|
sendJson(res, 400, { success: false, error: 'Invalid model permissions', errors: validation.errors });
|
|
@@ -907,15 +931,15 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
907
931
|
roleDef.permissions = roleDef.permissions && typeof roleDef.permissions === 'object'
|
|
908
932
|
? roleDef.permissions
|
|
909
933
|
: {};
|
|
910
|
-
const existingPerm = stripSelectionModeFromPermission(getModelPermission(roleDef));
|
|
911
|
-
roleDef.permissions[
|
|
934
|
+
const existingPerm = stripSelectionModeFromPermission(getModelPermission(roleDef, modelContext.field));
|
|
935
|
+
roleDef.permissions[modelContext.field] = {
|
|
912
936
|
...existingPerm,
|
|
913
937
|
default: validation.value.defaultModel,
|
|
914
938
|
allowOverride: validation.value.allowOverride,
|
|
915
939
|
allowedModels: validation.value.allowedModels,
|
|
916
940
|
};
|
|
917
941
|
modules.roleService.writeRoleDefinition(aid, roleName, roleDef);
|
|
918
|
-
const data = await buildModelPermissionResponse(roleName, roleDef, validation.value, aid || undefined);
|
|
942
|
+
const data = await buildModelPermissionResponse(roleName, roleDef, modelContext, validation.value, aid || undefined);
|
|
919
943
|
sendJson(res, 200, { success: true, data });
|
|
920
944
|
return;
|
|
921
945
|
}
|
|
@@ -1029,6 +1053,6 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
1029
1053
|
}
|
|
1030
1054
|
catch (err) {
|
|
1031
1055
|
console.error('[role-definitions] API error:', err);
|
|
1032
|
-
sendJson(res, 500, { error: err.message });
|
|
1056
|
+
sendJson(res, err.statusCode ?? 500, { error: err.message });
|
|
1033
1057
|
}
|
|
1034
1058
|
}
|
|
@@ -15,7 +15,7 @@ import { resolvePaths } from '../paths.js';
|
|
|
15
15
|
import { encodePath } from '../fs-utils.js';
|
|
16
16
|
import { getAgentSelfNames } from './msg.js';
|
|
17
17
|
import { parseTranscriptResultKey, previewLargeToolResults, windowTranscriptTurns } from './transcript-window.js';
|
|
18
|
-
import { buildSessionBindMap, classifyTranscriptSource, isEvolcoreRoutedMessage, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
18
|
+
import { buildSessionBindMap, readRunnerTaskTurns, classifyTranscriptSource, isEvolcoreRoutedMessage, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
19
19
|
const requireFromHere = createRequire(import.meta.url);
|
|
20
20
|
let _dlog = null;
|
|
21
21
|
export function setDebugLog(log) { _dlog = log; }
|
|
@@ -817,7 +817,7 @@ function readTranscriptFile(threadId, cwd, readStats) {
|
|
|
817
817
|
cacheTranscriptDetail(rolloutPath, stat.mtimeMs, stat.size, detail);
|
|
818
818
|
return detail;
|
|
819
819
|
}
|
|
820
|
-
export function readCodexSessionResult(params) {
|
|
820
|
+
export async function readCodexSessionResult(params) {
|
|
821
821
|
const projects = listProjects();
|
|
822
822
|
const project = resolveProject(params, projects);
|
|
823
823
|
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
|
|
@@ -825,7 +825,10 @@ export function readCodexSessionResult(params) {
|
|
|
825
825
|
if (!project || !sessionId || !key || !/^[A-Za-z0-9-]+$/.test(sessionId))
|
|
826
826
|
return null;
|
|
827
827
|
const detail = readTranscriptFile(sessionId, project.cwd);
|
|
828
|
-
const
|
|
828
|
+
const bind = buildBindMap().get(sessionId);
|
|
829
|
+
const taskTurns = bind?.evolSessionId ? await readRunnerTaskTurns(resolvePaths().logs, bind.evolSessionId) : [];
|
|
830
|
+
const turns = detail.turns.concat(taskTurns).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
831
|
+
const block = turns[key[0]]?.blocks?.[key[1]];
|
|
829
832
|
return block?.kind === 'tool_result' && typeof block.text === 'string' ? block.text : null;
|
|
830
833
|
}
|
|
831
834
|
// ── Snapshot ──
|
|
@@ -899,8 +902,11 @@ async function buildSnapshot(params) {
|
|
|
899
902
|
const detailStartedAt = Date.now();
|
|
900
903
|
const detail = readTranscriptFile(sessionId, project.cwd, metrics);
|
|
901
904
|
metrics.detailMs = Date.now() - detailStartedAt;
|
|
902
|
-
const page = windowTranscriptTurns(detail.turns, params.turnLimit, params.turnBefore);
|
|
903
905
|
const bind = bindMap.get(sessionId);
|
|
906
|
+
const taskTurns = bind?.evolSessionId ? await readRunnerTaskTurns(resolvePaths().logs, bind.evolSessionId) : [];
|
|
907
|
+
const turns = detail.turns.concat(taskTurns).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
908
|
+
const page = windowTranscriptTurns(turns, params.turnLimit, params.turnBefore);
|
|
909
|
+
const counts = { ...detail.counts, subtask: taskTurns.length };
|
|
904
910
|
const transcript = metas.find(meta => meta.id === sessionId);
|
|
905
911
|
const header = {
|
|
906
912
|
sessionId,
|
|
@@ -912,7 +918,7 @@ async function buildSnapshot(params) {
|
|
|
912
918
|
totalTurns: detail.totalTurns,
|
|
913
919
|
userMsgs: detail.userMsgs,
|
|
914
920
|
totalMsgs: detail.totalMsgs,
|
|
915
|
-
counts
|
|
921
|
+
counts,
|
|
916
922
|
inputTokens: detail.inputTokens,
|
|
917
923
|
outputTokens: detail.outputTokens,
|
|
918
924
|
contextTokens: detail.contextTokens,
|
|
@@ -944,6 +950,7 @@ export function subscribeCodex(params, push) {
|
|
|
944
950
|
const p = resolvePaths();
|
|
945
951
|
let dbWatcher = null;
|
|
946
952
|
let sessionWatcher = null;
|
|
953
|
+
let logWatcher = null;
|
|
947
954
|
let debounce = null;
|
|
948
955
|
const fire = () => {
|
|
949
956
|
if (debounce)
|
|
@@ -971,11 +978,22 @@ export function subscribeCodex(params, push) {
|
|
|
971
978
|
});
|
|
972
979
|
}
|
|
973
980
|
catch { }
|
|
981
|
+
if (params.sessionId) {
|
|
982
|
+
try {
|
|
983
|
+
logWatcher = fs.watch(p.logs, (_evt, filename) => {
|
|
984
|
+
if (!filename || /^events(?:-.+)?\.log$/.test(String(filename)))
|
|
985
|
+
fire();
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
catch { }
|
|
989
|
+
}
|
|
974
990
|
return () => {
|
|
975
991
|
if (dbWatcher)
|
|
976
992
|
dbWatcher.close();
|
|
977
993
|
if (sessionWatcher)
|
|
978
994
|
sessionWatcher.close();
|
|
995
|
+
if (logWatcher)
|
|
996
|
+
logWatcher.close();
|
|
979
997
|
if (debounce)
|
|
980
998
|
clearTimeout(debounce);
|
|
981
999
|
};
|