ec-web 0.0.25 → 0.0.27

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.
@@ -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('config', 'role-constraints.js');
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 role-constraints.js');
75
+ throw new Error('isModelAllowedByPatterns not found in model-permission.js');
73
76
  return {
74
77
  isModelAllowedByPatterns: mod.isModelAllowedByPatterns,
75
78
  };
@@ -105,8 +108,8 @@ function isReadMethod(method) {
105
108
  async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, operation, args) {
106
109
  const actor = auth.actorAid || '';
107
110
  const processOwners = Array.isArray(processConfig?.owners) ? processConfig.owners.map(String) : [];
108
- const syntheticOwner = !!auth.localDirect || (!!actor && processOwners.includes(actor));
109
- const actorId = auth.localDirect ? 'local-direct' : actor || undefined;
111
+ const syntheticOwner = isDaemonOwnerAuth(processConfig, auth);
112
+ const actorId = auth.localDirect ? 'local-direct' : actor || (syntheticOwner ? 'ecweb-remote' : undefined);
110
113
  const subject = modules.buildAuthSubject({
111
114
  selfAid: aid,
112
115
  actorId,
@@ -116,6 +119,7 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
116
119
  chatType: 'private',
117
120
  conversationId: actor || 'ecweb',
118
121
  processOwners,
122
+ trustedProcessRole: syntheticOwner ? 'daemon-owner' : undefined,
119
123
  ...(syntheticOwner ? {
120
124
  roleDetail: {
121
125
  effectiveRole: 'owner',
@@ -123,8 +127,8 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
123
127
  isAuthenticated: true,
124
128
  allowAccess: true,
125
129
  roleExists: true,
126
- // buildAuthSubject uses the authenticated actor separately from the
127
- // resolved relation role to determine daemon-owner provenance.
130
+ // Keep the token's actor provenance separate from the management
131
+ // authority established by the ECWeb authentication gate.
128
132
  actor: {
129
133
  operatorId: actorId || 'ecweb',
130
134
  operatorChannelKey: `ecweb#${aid}`,
@@ -155,7 +159,7 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
155
159
  });
156
160
  }
157
161
  function isDaemonOwnerAuth(processConfig, auth) {
158
- if (auth.localDirect)
162
+ if (auth.daemonOwner === true || auth.localDirect)
159
163
  return true;
160
164
  const actor = typeof auth.actorAid === 'string' ? auth.actorAid.trim() : '';
161
165
  const processOwners = Array.isArray(processConfig?.owners)
@@ -163,8 +167,16 @@ function isDaemonOwnerAuth(processConfig, auth) {
163
167
  : [];
164
168
  return !!actor && processOwners.includes(actor);
165
169
  }
166
- const MODEL_PERMISSION_FIELD = 'baseagents.claude.model';
167
- const MODEL_PATTERN_OPTIONS = ['*', 'claude-opus-*', 'claude-sonnet-*', 'claude-haiku-*'];
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
+ }
168
180
  const COMMAND_PERMISSION_SCOPES = new Set(['relation', 'role', 'agent', 'process', 'filesystem', 'control', 'raw-cli']);
169
181
  const COMMAND_CONSTRAINT_KEYS = new Set([
170
182
  'ownPeerOnly',
@@ -494,11 +506,11 @@ function normalizeRoleDefinitionForSave(roleDef) {
494
506
  return roleDef;
495
507
  return { ...roleDef, usageLimits: normalizeRoleUsageLimits(roleDef.usageLimits) };
496
508
  }
497
- function getModelPermission(roleDef) {
509
+ function getModelPermission(roleDef, field) {
498
510
  const permissions = roleDef?.permissions;
499
511
  if (!permissions || typeof permissions !== 'object')
500
512
  return {};
501
- const perm = permissions[MODEL_PERMISSION_FIELD];
513
+ const perm = permissions[field];
502
514
  return perm && typeof perm === 'object' ? perm : {};
503
515
  }
504
516
  function stripSelectionModeFromPermission(perm) {
@@ -543,38 +555,42 @@ async function validateRoleModelPermission(roleDef) {
543
555
  const permissions = roleDef?.permissions;
544
556
  if (!permissions || typeof permissions !== 'object')
545
557
  return { ok: true, errors: [] };
546
- const perm = permissions[MODEL_PERMISSION_FIELD];
547
- if (!perm || typeof perm !== 'object')
548
- return { ok: true, errors: [] };
549
- permissions[MODEL_PERMISSION_FIELD] = stripSelectionModeFromPermission(perm);
550
- const result = await validateModelPermissionInput({
551
- defaultModel: permissions[MODEL_PERMISSION_FIELD].default,
552
- allowOverride: permissions[MODEL_PERMISSION_FIELD].allowOverride,
553
- allowedModels: permissions[MODEL_PERMISSION_FIELD].allowedModels,
554
- });
555
- return { ok: result.ok, errors: result.errors };
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 };
556
571
  }
557
572
  async function filterAllowedCatalogModels(models, allowedModels) {
558
573
  const { isModelAllowedByPatterns } = await getRoleConstraintsModule();
559
574
  return models.filter(model => !model.isAlias && isModelAllowedByPatterns(model.id, allowedModels));
560
575
  }
561
- async function buildModelPermissionResponse(roleName, roleDef, override, aid) {
562
- const currentPerm = getModelPermission(roleDef);
576
+ async function buildModelPermissionResponse(roleName, roleDef, modelContext, override, aid) {
577
+ const currentPerm = getModelPermission(roleDef, modelContext.field);
563
578
  const defaultModel = override?.defaultModel ?? currentPerm.default ?? '';
564
579
  const allowOverride = override?.allowOverride ?? currentPerm.allowOverride ?? false;
565
580
  const allowedModels = override?.allowedModels ?? (Array.isArray(currentPerm.allowedModels) ? currentPerm.allowedModels : []);
566
- const catalog = await getModelCatalogSnapshot('claude', aid);
581
+ const catalog = await getModelCatalogSnapshot(modelContext.baseagent, aid);
567
582
  const matchingModels = await filterAllowedCatalogModels(catalog.models, allowedModels);
568
583
  return {
569
584
  role: roleName,
570
- field: MODEL_PERMISSION_FIELD,
585
+ baseagent: modelContext.baseagent,
586
+ field: modelContext.field,
571
587
  permission: {
572
588
  default: defaultModel,
573
589
  allowOverride,
574
590
  allowedModels,
575
591
  },
576
592
  selectionMode: inferSelectionMode(allowedModels),
577
- patternOptions: MODEL_PATTERN_OPTIONS,
593
+ patternOptions: modelContext.patternOptions,
578
594
  catalog,
579
595
  matchingModels,
580
596
  stats: {
@@ -612,10 +628,16 @@ async function buildSnapshot(params = {}, auth = {}) {
612
628
  };
613
629
  }
614
630
  const config = modules.readRolesConfig(aid);
631
+ const activeBaseagent = modules.resolveEffective({ self: aid }).active_baseagent;
632
+ const modelContext = activeBaseagent ? roleModelContext(modules, aid, activeBaseagent) : undefined;
615
633
  return {
616
634
  $schema_version: 1,
617
635
  aid,
618
636
  self: aid,
637
+ baseagent: modelContext?.baseagent ?? null,
638
+ modelPermissionField: modelContext?.field ?? null,
639
+ modelPatternOptions: modelContext?.patternOptions ?? [],
640
+ permissionFields: modules.rolePermissionFields(),
619
641
  agents: summarizeAgents(agents),
620
642
  defaultRoles: config.defaultRoles ?? { private: null, group: null },
621
643
  policyRevision: JSON.stringify(config),
@@ -865,7 +887,8 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
865
887
  sendJson(res, 404, { success: false, error: 'Role not found' });
866
888
  return;
867
889
  }
868
- const data = await buildModelPermissionResponse(roleName, roleDef, undefined, aid || undefined);
890
+ const modelContext = roleModelContext(modules, aid, parsedUrl.query.baseagent);
891
+ const data = await buildModelPermissionResponse(roleName, roleDef, modelContext, undefined, aid || undefined);
869
892
  sendJson(res, 200, { success: true, data });
870
893
  return;
871
894
  }
@@ -878,13 +901,14 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
878
901
  return;
879
902
  }
880
903
  const payload = await readJsonBody(req);
881
- const currentPerm = getModelPermission(roleDef);
904
+ const modelContext = roleModelContext(modules, aid, parsedUrl.query.baseagent);
905
+ const currentPerm = getModelPermission(roleDef, modelContext.field);
882
906
  const validation = await validateModelPermissionInput(extractModelPermissionPayload(payload, currentPerm));
883
907
  if (!validation.ok || !validation.value) {
884
908
  sendJson(res, 400, { success: false, error: 'Invalid model permissions', errors: validation.errors });
885
909
  return;
886
910
  }
887
- const data = await buildModelPermissionResponse(roleName, roleDef, validation.value, aid || undefined);
911
+ const data = await buildModelPermissionResponse(roleName, roleDef, modelContext, validation.value, aid || undefined);
888
912
  sendJson(res, 200, { success: true, data });
889
913
  return;
890
914
  }
@@ -897,7 +921,8 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
897
921
  return;
898
922
  }
899
923
  const payload = await readJsonBody(req);
900
- const currentPerm = getModelPermission(roleDef);
924
+ const modelContext = roleModelContext(modules, aid, parsedUrl.query.baseagent);
925
+ const currentPerm = getModelPermission(roleDef, modelContext.field);
901
926
  const validation = await validateModelPermissionInput(extractModelPermissionPayload(payload, currentPerm));
902
927
  if (!validation.ok || !validation.value) {
903
928
  sendJson(res, 400, { success: false, error: 'Invalid model permissions', errors: validation.errors });
@@ -906,15 +931,15 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
906
931
  roleDef.permissions = roleDef.permissions && typeof roleDef.permissions === 'object'
907
932
  ? roleDef.permissions
908
933
  : {};
909
- const existingPerm = stripSelectionModeFromPermission(getModelPermission(roleDef));
910
- roleDef.permissions[MODEL_PERMISSION_FIELD] = {
934
+ const existingPerm = stripSelectionModeFromPermission(getModelPermission(roleDef, modelContext.field));
935
+ roleDef.permissions[modelContext.field] = {
911
936
  ...existingPerm,
912
937
  default: validation.value.defaultModel,
913
938
  allowOverride: validation.value.allowOverride,
914
939
  allowedModels: validation.value.allowedModels,
915
940
  };
916
941
  modules.roleService.writeRoleDefinition(aid, roleName, roleDef);
917
- const data = await buildModelPermissionResponse(roleName, roleDef, validation.value, aid || undefined);
942
+ const data = await buildModelPermissionResponse(roleName, roleDef, modelContext, validation.value, aid || undefined);
918
943
  sendJson(res, 200, { success: true, data });
919
944
  return;
920
945
  }
@@ -1028,6 +1053,6 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
1028
1053
  }
1029
1054
  catch (err) {
1030
1055
  console.error('[role-definitions] API error:', err);
1031
- sendJson(res, 500, { error: err.message });
1056
+ sendJson(res, err.statusCode ?? 500, { error: err.message });
1032
1057
  }
1033
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; }
@@ -205,12 +205,12 @@ function listProjects() {
205
205
  if (!db)
206
206
  return [];
207
207
  try {
208
- const rows = db.prepare(`
209
- SELECT cwd, COUNT(*) as count, MAX(updated_at) as lastActivity
210
- FROM threads
211
- WHERE archived = 0
212
- GROUP BY cwd
213
- ORDER BY lastActivity DESC
208
+ const rows = db.prepare(`
209
+ SELECT cwd, COUNT(*) as count, MAX(updated_at) as lastActivity
210
+ FROM threads
211
+ WHERE archived = 0
212
+ GROUP BY cwd
213
+ ORDER BY lastActivity DESC
214
214
  `).all();
215
215
  const projects = rows.map(r => {
216
216
  const cwd = r.cwd || '';
@@ -382,11 +382,11 @@ function listTranscripts(encoded, cwd) {
382
382
  if (!db)
383
383
  return [];
384
384
  try {
385
- const rows = db.prepare(`
386
- SELECT id, title, first_user_message, updated_at, rollout_path, source, thread_source
387
- FROM threads
388
- WHERE cwd = ? AND archived = 0
389
- ORDER BY updated_at DESC
385
+ const rows = db.prepare(`
386
+ SELECT id, title, first_user_message, updated_at, rollout_path, source, thread_source
387
+ FROM threads
388
+ WHERE cwd = ? AND archived = 0
389
+ ORDER BY updated_at DESC
390
390
  `).all(cwd);
391
391
  const out = [];
392
392
  let parsed = 0, hitMem = 0, hitDisk = 0;
@@ -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 block = detail.turns[key[0]]?.blocks?.[key[1]];
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: detail.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
  };