ec-web 0.0.25 → 0.0.26
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/LICENSE +21 -21
- package/README.md +17 -17
- package/dist/index.js +0 -0
- package/dist/paths.js +8 -1
- package/dist/server.js +7 -5
- package/dist/sources/aid.js +28 -13
- package/dist/sources/basic-settings.js +7 -27
- package/dist/sources/relation-roles.js +23 -9
- package/dist/sources/role-definitions.js +6 -5
- package/dist/sources/session-codex.js +11 -11
- package/dist/sources/stats.js +92 -92
- package/dist/sources/types.js +1 -0
- package/dist/static/app.js +11691 -11691
- package/dist/static/index.html +427 -427
- package/dist/static/style.css +4899 -4899
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -45
- package/ecweb-wrapper.cjs +67 -67
- package/package.json +35 -35
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 EvolCore contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EvolCore contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# EC Web
|
|
2
|
-
|
|
3
|
-
Web console and monitoring dashboard for EvolCore.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g ec-web
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Run
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
ec-web --home /path/to/runtime-home
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Use `ec watch web` when EC Web is managed by the main daemon.
|
|
1
|
+
# EC Web
|
|
2
|
+
|
|
3
|
+
Web console and monitoring dashboard for EvolCore.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ec-web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Run
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ec-web --home /path/to/runtime-home
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use `ec watch web` when EC Web is managed by the main daemon.
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/paths.js
CHANGED
|
@@ -48,7 +48,14 @@ function resolveInstanceSocketPath(root) {
|
|
|
48
48
|
const hash = crypto.createHash('sha1').update(canonicalRoot).digest('hex').slice(0, 12);
|
|
49
49
|
return `\\\\.\\pipe\\daemon-${hash}`;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
const filesystemPath = path.join(root, 'data', 'instance', 'daemon.sock');
|
|
52
|
+
if (Buffer.byteLength(filesystemPath) < 100)
|
|
53
|
+
return filesystemPath;
|
|
54
|
+
const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 20);
|
|
55
|
+
const shortFilesystemPath = path.join(os.tmpdir(), `.ec-${hash.slice(0, 15)}.sock`);
|
|
56
|
+
if (Buffer.byteLength(shortFilesystemPath) < 100)
|
|
57
|
+
return shortFilesystemPath;
|
|
58
|
+
return `\0evolcore-${hash}`;
|
|
52
59
|
}
|
|
53
60
|
export function resolvePaths() {
|
|
54
61
|
const root = resolveRoot();
|
package/dist/server.js
CHANGED
|
@@ -40,6 +40,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
40
40
|
const STATIC_DIR = path.join(__dirname, 'static');
|
|
41
41
|
const DEFAULT_PORT = 42705;
|
|
42
42
|
const PROTOCOL_VERSION = 1; // 与 evolcore ping response 对齐的软校验版本
|
|
43
|
+
const ECWEB_TRIGGER_IPC_TYPES = new Set(['trigger.history', 'trigger.eventCatalog']);
|
|
43
44
|
function daemonOwnerMenuAuth(localDirect, actorAid) {
|
|
44
45
|
return {
|
|
45
46
|
localDirect,
|
|
@@ -48,6 +49,7 @@ function daemonOwnerMenuAuth(localDirect, actorAid) {
|
|
|
48
49
|
// authentication gate (local-direct exemption or a valid bearer token).
|
|
49
50
|
// ECWeb is the daemon-owner management surface, so remote tokens receive
|
|
50
51
|
// the same persistent fullaccess approval capability as local requests.
|
|
52
|
+
daemonOwner: true,
|
|
51
53
|
canApprovePersistentFullAccess: true,
|
|
52
54
|
};
|
|
53
55
|
}
|
|
@@ -614,7 +616,7 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
614
616
|
return;
|
|
615
617
|
}
|
|
616
618
|
if (msg.type === 'ipc' && msg.payload) {
|
|
617
|
-
if (typeof msg.payload.type !== 'string' || !msg.payload.type
|
|
619
|
+
if (typeof msg.payload.type !== 'string' || !ECWEB_TRIGGER_IPC_TYPES.has(msg.payload.type)) {
|
|
618
620
|
send({ type: 'ipc.response', requestId: msg.requestId, data: { ok: false, error: 'unsupported ipc payload' } });
|
|
619
621
|
return;
|
|
620
622
|
}
|
|
@@ -622,9 +624,7 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
622
624
|
const timeoutMs = Number.isFinite(Number(msg.timeoutMs)) && Number(msg.timeoutMs) > 0
|
|
623
625
|
? Math.min(Number(msg.timeoutMs), 120_000)
|
|
624
626
|
: 10000;
|
|
625
|
-
const payload = msg.payload
|
|
626
|
-
? { ...msg.payload, controlToken: daemonTriggerControlToken() }
|
|
627
|
-
: msg.payload;
|
|
627
|
+
const payload = { ...msg.payload, controlToken: daemonTriggerControlToken() };
|
|
628
628
|
const resp = await ipcQuery(p.socket, payload, timeoutMs);
|
|
629
629
|
send({ type: 'ipc.response', requestId: msg.requestId, data: resp ?? { ok: false, error: 'daemon unreachable' } });
|
|
630
630
|
return;
|
|
@@ -796,7 +796,9 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
796
796
|
res.end(JSON.stringify({ success: false, error: 'unauthorized' }));
|
|
797
797
|
return;
|
|
798
798
|
}
|
|
799
|
-
|
|
799
|
+
// Pairing grants daemon-owner management authority, just like local
|
|
800
|
+
// access. Authorize above; do not downgrade settings by network origin.
|
|
801
|
+
void handleBasicSettingsApi(req, res);
|
|
800
802
|
}
|
|
801
803
|
else if (req.method === 'GET' && (req.url || '').startsWith('/api/stats/')) {
|
|
802
804
|
// Stats API — 本地直连自动发 token(免鉴权),远程需鉴权
|
package/dist/sources/aid.js
CHANGED
|
@@ -65,28 +65,43 @@ function scanAidActivity() {
|
|
|
65
65
|
}
|
|
66
66
|
return result;
|
|
67
67
|
}
|
|
68
|
-
// evolcore
|
|
69
|
-
//
|
|
68
|
+
// evolcore 版本:使用 /system 的轻量 version 视图。daemon 进程级、整轮 watch 不变,
|
|
69
|
+
// 故缓存一次并合并 in-flight 请求,避免每秒重复发 IPC。
|
|
70
70
|
const versionCache = new Map();
|
|
71
|
+
const versionInFlight = new Map();
|
|
71
72
|
function authContextKey(params = {}) {
|
|
72
73
|
const auth = watchMenuRequestAuth(params);
|
|
73
74
|
return `${auth.localDirect ? 'local' : 'remote'}:${auth.actorAid || ''}`;
|
|
74
75
|
}
|
|
75
76
|
async function fetchVersion(socket, params = {}) {
|
|
76
|
-
const key = authContextKey(params)
|
|
77
|
+
const key = `${socket}\0${authContextKey(params)}`;
|
|
77
78
|
const cached = versionCache.get(key);
|
|
78
79
|
if (cached)
|
|
79
80
|
return cached;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
const pending = versionInFlight.get(key);
|
|
82
|
+
if (pending)
|
|
83
|
+
return pending;
|
|
84
|
+
const request = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const r = await ipcQuery(socket, {
|
|
87
|
+
type: 'menu.exec',
|
|
88
|
+
payload: { type: 'menu.query', id: 'aid-sys-q', name: 'system', args: { view: 'version' } },
|
|
89
|
+
auth: watchMenuRequestAuth(params),
|
|
90
|
+
}, 4000);
|
|
91
|
+
const v = r?.ok ? r.response?.data?.version : null;
|
|
92
|
+
if (v)
|
|
93
|
+
versionCache.set(key, v);
|
|
94
|
+
return v ?? null;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
versionInFlight.delete(key);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
versionInFlight.set(key, request);
|
|
104
|
+
return request;
|
|
90
105
|
}
|
|
91
106
|
async function buildSnapshot(fallback = null, params = {}) {
|
|
92
107
|
const p = resolvePaths();
|
|
@@ -580,7 +580,7 @@ export function restartRequiredForChanges(file, changes) {
|
|
|
580
580
|
const prefixes = file === 'daemon' ? RESTART_DAEMON_PREFIXES : RESTART_DEFAULTS_PREFIXES;
|
|
581
581
|
return changes.map(change => change.path).filter(fieldPath => prefixes.some(prefix => fieldPath === prefix || fieldPath.startsWith(prefix)));
|
|
582
582
|
}
|
|
583
|
-
function redactSecrets(config, file, secrets, secretRefs
|
|
583
|
+
function redactSecrets(config, file, secrets, secretRefs) {
|
|
584
584
|
const redacted = clone(config);
|
|
585
585
|
if (file !== 'defaults' || !isRecord(redacted.baseagents))
|
|
586
586
|
return redacted;
|
|
@@ -600,26 +600,6 @@ function redactSecrets(config, file, secrets, secretRefs, options = {}) {
|
|
|
600
600
|
raw.apiKey = '******';
|
|
601
601
|
}
|
|
602
602
|
}
|
|
603
|
-
// Headers and query parameters are also common credential channels. They
|
|
604
|
-
// remain visible to local callers for editing, but remote snapshots must
|
|
605
|
-
// not disclose any map value, including values under custom key names.
|
|
606
|
-
if (options.redactConnectionMaps) {
|
|
607
|
-
for (const field of ['headers', 'queryParams']) {
|
|
608
|
-
if (!Object.prototype.hasOwnProperty.call(raw, field))
|
|
609
|
-
continue;
|
|
610
|
-
const fieldPath = `defaults.baseagents.${name}.${field}`;
|
|
611
|
-
if (isRecord(raw[field])) {
|
|
612
|
-
raw[field] = Object.fromEntries(Object.keys(raw[field]).map(key => {
|
|
613
|
-
secrets[`${fieldPath}.${key}`] = true;
|
|
614
|
-
return [key, '******'];
|
|
615
|
-
}));
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
secrets[fieldPath] = true;
|
|
619
|
-
raw[field] = '******';
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
603
|
}
|
|
624
604
|
return redacted;
|
|
625
605
|
}
|
|
@@ -670,7 +650,7 @@ export function collectLogTypeStats(configured, logsDir = resolvePaths().logs) {
|
|
|
670
650
|
export function collectLogTypes(configured, logsDir = resolvePaths().logs) {
|
|
671
651
|
return collectLogTypeStats(configured, logsDir).types;
|
|
672
652
|
}
|
|
673
|
-
async function readSnapshot(
|
|
653
|
+
async function readSnapshot() {
|
|
674
654
|
const modules = await getConfigManager();
|
|
675
655
|
const daemonFile = modules.read(modules.ConfigTarget.Process, undefined, { cache: false, expand: false });
|
|
676
656
|
const defaultsFile = modules.read(modules.ConfigTarget.Defaults, undefined, { cache: false, expand: false });
|
|
@@ -682,8 +662,8 @@ async function readSnapshot(options = {}) {
|
|
|
682
662
|
const secretRefs = {};
|
|
683
663
|
const logTypeStats = collectLogTypeStats(rawDaemon?.watch?.logTypes);
|
|
684
664
|
return {
|
|
685
|
-
daemon: redactSecrets(rawDaemon, 'daemon', secrets, secretRefs
|
|
686
|
-
defaults: redactSecrets(rawDefaults, 'defaults', secrets, secretRefs
|
|
665
|
+
daemon: redactSecrets(rawDaemon, 'daemon', secrets, secretRefs),
|
|
666
|
+
defaults: redactSecrets(rawDefaults, 'defaults', secrets, secretRefs),
|
|
687
667
|
revisions: {
|
|
688
668
|
daemon: basicSettingsRevision(rawDaemon),
|
|
689
669
|
defaults: basicSettingsRevision(rawDefaults),
|
|
@@ -758,12 +738,12 @@ function basicSettingsPath(rawUrl) {
|
|
|
758
738
|
return rawUrl.split('?')[0].replace(/\/+$/, '') || '/';
|
|
759
739
|
}
|
|
760
740
|
}
|
|
761
|
-
export async function handleBasicSettingsApi(req, res
|
|
741
|
+
export async function handleBasicSettingsApi(req, res) {
|
|
762
742
|
try {
|
|
763
743
|
const url = new URL(req.url || '', 'http://localhost');
|
|
764
744
|
const pathname = basicSettingsPath(req.url || '');
|
|
765
745
|
if (req.method === 'GET' && pathname === '/api/basic-settings') {
|
|
766
|
-
sendJson(res, 200, { success: true, data: await readSnapshot(
|
|
746
|
+
sendJson(res, 200, { success: true, data: await readSnapshot() });
|
|
767
747
|
return;
|
|
768
748
|
}
|
|
769
749
|
if (req.method === 'GET' && pathname === '/api/basic-settings/secret') {
|
|
@@ -816,7 +796,7 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
|
|
|
816
796
|
changed: mutation.changed,
|
|
817
797
|
restartRequired: mutation.changed && restartPaths.length > 0,
|
|
818
798
|
restartPaths: mutation.changed ? restartPaths : [],
|
|
819
|
-
data: await readSnapshot(
|
|
799
|
+
data: await readSnapshot(),
|
|
820
800
|
});
|
|
821
801
|
return;
|
|
822
802
|
}
|
|
@@ -24,9 +24,11 @@ async function getParentModules() {
|
|
|
24
24
|
async function getCommandModules() {
|
|
25
25
|
const permissionPath = resolveParentDistModule('core', 'auth', 'operation-authorizer.js');
|
|
26
26
|
const auditPath = resolveParentDistModule('core', 'auth', 'authorization-audit.js');
|
|
27
|
+
const runtimePolicyPath = resolveParentDistModule('core', 'role', 'runtime-policy.js');
|
|
27
28
|
const permission = await import(toFileUrl(permissionPath));
|
|
28
29
|
const audit = await import(toFileUrl(auditPath));
|
|
29
|
-
|
|
30
|
+
const runtimePolicy = await import(toFileUrl(runtimePolicyPath));
|
|
31
|
+
return { permission, audit, runtimePolicy };
|
|
30
32
|
}
|
|
31
33
|
export const ROLE_ASSIGNMENT_POLICY = {
|
|
32
34
|
owner: {
|
|
@@ -38,15 +40,17 @@ export const ROLE_ASSIGNMENT_POLICY = {
|
|
|
38
40
|
revoke: 'user-roles',
|
|
39
41
|
},
|
|
40
42
|
};
|
|
41
|
-
function isManagementRoleName(role) {
|
|
42
|
-
return role === 'owner' || role === 'admin';
|
|
43
|
-
}
|
|
44
43
|
function isAssignableUserRole(role) {
|
|
45
|
-
|
|
44
|
+
// `admin` is a management role, but it is still a valid relation role.
|
|
45
|
+
// `owner` and `daemon-owner` remain reserved for process/Agent config and
|
|
46
|
+
// can never be assigned through a relation.
|
|
47
|
+
return typeof role === 'string' && !!role && role !== 'owner' && role !== 'daemon-owner';
|
|
46
48
|
}
|
|
47
49
|
export function canAssignRole(actorRole, targetRole) {
|
|
48
50
|
if (!isAssignableUserRole(targetRole))
|
|
49
51
|
return false;
|
|
52
|
+
if (targetRole === 'admin')
|
|
53
|
+
return actorRole === 'owner';
|
|
50
54
|
return actorRole === 'owner' || actorRole === 'admin';
|
|
51
55
|
}
|
|
52
56
|
export function canRevokeRole(actorRole, targetRole) {
|
|
@@ -54,6 +58,8 @@ export function canRevokeRole(actorRole, targetRole) {
|
|
|
54
58
|
return true;
|
|
55
59
|
if (actorRole !== 'admin' || !targetRole)
|
|
56
60
|
return false;
|
|
61
|
+
if (targetRole === 'admin')
|
|
62
|
+
return false;
|
|
57
63
|
return isAssignableUserRole(targetRole);
|
|
58
64
|
}
|
|
59
65
|
async function getAgentsFromIpc() {
|
|
@@ -74,7 +80,8 @@ function resolveEcwebActor(modules, aid, auth) {
|
|
|
74
80
|
const processOwners = Array.isArray(processConfig?.owners)
|
|
75
81
|
? processConfig.owners.map((value) => String(value || '').trim()).filter(Boolean)
|
|
76
82
|
: [];
|
|
77
|
-
const daemonOwner =
|
|
83
|
+
const daemonOwner = auth.daemonOwner === true || !!auth.localDirect
|
|
84
|
+
|| (!!auth.actorAid && processOwners.includes(auth.actorAid));
|
|
78
85
|
const actorRole = daemonOwner
|
|
79
86
|
? 'owner'
|
|
80
87
|
: auth.actorAid
|
|
@@ -89,11 +96,11 @@ function resolveEcwebActor(modules, aid, auth) {
|
|
|
89
96
|
return {
|
|
90
97
|
daemonOwner,
|
|
91
98
|
actorRole,
|
|
92
|
-
actorId: auth.localDirect ? 'local-direct' : auth.actorAid || undefined,
|
|
99
|
+
actorId: auth.localDirect ? 'local-direct' : auth.actorAid || (daemonOwner ? 'ecweb-remote' : undefined),
|
|
93
100
|
};
|
|
94
101
|
}
|
|
95
102
|
async function authorizeRoleAssignmentRead(modules, aid, auth) {
|
|
96
|
-
const { permission, audit } = await getCommandModules();
|
|
103
|
+
const { permission, audit, runtimePolicy } = await getCommandModules();
|
|
97
104
|
const actor = resolveEcwebActor(modules, aid, auth);
|
|
98
105
|
const decision = permission.authorizeCommand({
|
|
99
106
|
intent: {
|
|
@@ -118,6 +125,7 @@ async function authorizeRoleAssignmentRead(modules, aid, auth) {
|
|
|
118
125
|
actorId: actor.actorId,
|
|
119
126
|
selfAid: aid,
|
|
120
127
|
role: actor.actorRole,
|
|
128
|
+
permissionMode: runtimePolicy.resolveRuntimePermissionMode({ selfAid: aid, role: actor.actorRole }).effectiveValue,
|
|
121
129
|
processRole: actor.daemonOwner ? 'daemon-owner' : 'none',
|
|
122
130
|
isDaemonOwner: actor.daemonOwner,
|
|
123
131
|
decision: decision.allow ? 'allow' : 'deny',
|
|
@@ -696,11 +704,17 @@ async function buildSnapshot(auth = {}) {
|
|
|
696
704
|
if (!readDecision.allow)
|
|
697
705
|
continue;
|
|
698
706
|
const roleConfig = modules.roles.readRolesConfig(aid);
|
|
707
|
+
const availableRoles = Object.keys(roleConfig.roles || {});
|
|
708
|
+
// Management roles are resolved separately from user-role definitions.
|
|
709
|
+
// Include the assignable management role in the ECWeb snapshot so relation
|
|
710
|
+
// editors can offer it; owner remains reserved for config.json owners[].
|
|
711
|
+
if (roleExists(modules.roles, aid, 'admin'))
|
|
712
|
+
availableRoles.push('admin');
|
|
699
713
|
snapshotAgents.push({
|
|
700
714
|
aid,
|
|
701
715
|
displayName: agent.displayName ?? agent.personalName,
|
|
702
716
|
name: agent.name,
|
|
703
|
-
roles:
|
|
717
|
+
roles: [...new Set(availableRoles)].filter(role => isAssignableUserRole(role)),
|
|
704
718
|
defaultRoles: roleConfig.defaultRoles ?? { private: null, group: null },
|
|
705
719
|
assignments: listRelationRoleAssignments(modules.config, aid),
|
|
706
720
|
});
|
|
@@ -105,8 +105,8 @@ function isReadMethod(method) {
|
|
|
105
105
|
async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, operation, args) {
|
|
106
106
|
const actor = auth.actorAid || '';
|
|
107
107
|
const processOwners = Array.isArray(processConfig?.owners) ? processConfig.owners.map(String) : [];
|
|
108
|
-
const syntheticOwner =
|
|
109
|
-
const actorId = auth.localDirect ? 'local-direct' : actor || undefined;
|
|
108
|
+
const syntheticOwner = isDaemonOwnerAuth(processConfig, auth);
|
|
109
|
+
const actorId = auth.localDirect ? 'local-direct' : actor || (syntheticOwner ? 'ecweb-remote' : undefined);
|
|
110
110
|
const subject = modules.buildAuthSubject({
|
|
111
111
|
selfAid: aid,
|
|
112
112
|
actorId,
|
|
@@ -116,6 +116,7 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
116
116
|
chatType: 'private',
|
|
117
117
|
conversationId: actor || 'ecweb',
|
|
118
118
|
processOwners,
|
|
119
|
+
trustedProcessRole: syntheticOwner ? 'daemon-owner' : undefined,
|
|
119
120
|
...(syntheticOwner ? {
|
|
120
121
|
roleDetail: {
|
|
121
122
|
effectiveRole: 'owner',
|
|
@@ -123,8 +124,8 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
123
124
|
isAuthenticated: true,
|
|
124
125
|
allowAccess: true,
|
|
125
126
|
roleExists: true,
|
|
126
|
-
//
|
|
127
|
-
//
|
|
127
|
+
// Keep the token's actor provenance separate from the management
|
|
128
|
+
// authority established by the ECWeb authentication gate.
|
|
128
129
|
actor: {
|
|
129
130
|
operatorId: actorId || 'ecweb',
|
|
130
131
|
operatorChannelKey: `ecweb#${aid}`,
|
|
@@ -155,7 +156,7 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
function isDaemonOwnerAuth(processConfig, auth) {
|
|
158
|
-
if (auth.localDirect)
|
|
159
|
+
if (auth.daemonOwner === true || auth.localDirect)
|
|
159
160
|
return true;
|
|
160
161
|
const actor = typeof auth.actorAid === 'string' ? auth.actorAid.trim() : '';
|
|
161
162
|
const processOwners = Array.isArray(processConfig?.owners)
|
|
@@ -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;
|