evolcore 0.0.22 → 0.0.24
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/CHANGELOG.md +74 -14
- package/README.md +4 -0
- package/bin/codex-managed-acl.ps1 +160 -0
- package/bin/codex-managed-hook.mjs +127 -10
- package/bin/ec-safe-output.js +85 -3
- package/bin/install-codex-managed-hooks.mjs +384 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +30 -2
- package/dist/agents/claude-runner.js +179 -240
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +369 -0
- package/dist/agents/codex-runner.js +839 -192
- package/dist/agents/ecagent-runner.js +27 -34
- package/dist/agents/gemini-runner.js +62 -8
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/managed-operation.js +28 -6
- package/dist/aun/aid/store.js +43 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +57 -5
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +7 -1
- package/dist/channels/aun.js +178 -77
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wechat.js +81 -10
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +29 -3
- package/dist/cli/agent-create.js +538 -0
- package/dist/cli/agent.js +26 -675
- package/dist/cli/aun-commands.js +108 -24
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/ctl-command.js +2 -0
- package/dist/cli/daemon-commands.js +289 -61
- package/dist/cli/index.js +22 -2
- package/dist/cli/init.js +2 -10
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/restart-monitor.js +432 -8
- package/dist/cli/stats.js +110 -0
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +75 -18
- package/dist/core/auth/operation-authorizer.js +63 -3
- package/dist/core/auth/operation-catalog.js +14 -2
- package/dist/core/auth/remote-access-store.js +136 -0
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +7 -2
- package/dist/core/command/command-handler.js +128 -30
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +6 -1
- package/dist/core/command/menu-handler.js +410 -98
- package/dist/core/command/menu-protocol.js +123 -0
- package/dist/core/command/slash-gate.js +4 -6
- package/dist/core/command/slash-handler.js +199 -70
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +3 -0
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +147 -94
- package/dist/core/model/catalog-service.js +310 -0
- package/dist/core/model/model-catalog.js +134 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +5 -2
- package/dist/core/permission/approval-gateway.js +183 -32
- package/dist/core/permission/protected-paths.js +63 -4
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-policy.js +257 -31
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +214 -57
- package/dist/ipc.js +191 -102
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +16 -2
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +70 -15
- package/dist/utils/windows-autostart.js +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +13 -0
- package/kits/docs/evolcore/group-collaboration.md +7 -2
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +16 -4
- package/dist/utils/codex-cli.js +0 -43
package/dist/cli/task-context.js
CHANGED
|
@@ -253,8 +253,18 @@ export function getManagedTaskTempDir(runtimeEnv) {
|
|
|
253
253
|
* silently escape the session-owned temporary workspace.
|
|
254
254
|
*/
|
|
255
255
|
export function ensureSessionRuntimeDir(sessionId) {
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
if (!sessionId)
|
|
257
|
+
return undefined;
|
|
258
|
+
let base = process.env.TMPDIR?.trim();
|
|
259
|
+
try {
|
|
260
|
+
if (!base || !path.isAbsolute(base) || !isPrivateDirectory(path.resolve(base))) {
|
|
261
|
+
base = ensureProcessManagedTempDir();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
if (!base || !path.isAbsolute(base))
|
|
258
268
|
return undefined;
|
|
259
269
|
const root = path.resolve(base);
|
|
260
270
|
const digest = crypto.createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
|
|
@@ -121,12 +121,20 @@ Create 参数模式支持:
|
|
|
121
121
|
通用输出参数:
|
|
122
122
|
--json | --format json
|
|
123
123
|
|
|
124
|
-
Update
|
|
124
|
+
Update 参数:
|
|
125
125
|
--once | --delay | --at | --cron | --every | --event [--tz]
|
|
126
126
|
--prompt <文本> | --prompt-file <路径> --script-file <路径> --name <名称>
|
|
127
|
+
--description <文本> (空字符串删除)
|
|
127
128
|
--model <模型> --effort <强度> --permission <权限模式|inherit>(inherit 清除覆盖)
|
|
128
129
|
--max-runs <次数> --max-duration <时长>
|
|
129
130
|
--concurrency <forbid|replace|allow> --missed-policy <skip|run-once|run-all>
|
|
131
|
+
--feedback <origin|target|silent>
|
|
132
|
+
--target-channel <channelKey> --target-channel-id <ID> (必须同时指定)
|
|
133
|
+
--target-chat-type <private|group> --target-session <main|thread> [--target-thread-id <threadId>]
|
|
134
|
+
--trigger-thread <by-run|by-trigger> --baseagent <name> (仅 trigger_session 之间)
|
|
135
|
+
--on-error <fail|retry> --noop-sentinel <文本>
|
|
136
|
+
--retry-attempts <次数> --retry-backoff <毫秒>
|
|
137
|
+
--script-runtime <node|python|bash> --script-args <JSON> --script-timeout <时长>
|
|
130
138
|
|
|
131
139
|
省略 --agent 时使用当前 agent AID;无会话上下文或跨身份操作时需要显式指定。
|
|
132
140
|
任务会话中的跨身份操作仅允许目标 Agent 上具备 owner/admin 权限的调用者执行 list/show/history;创建、修改、启停、删除和运行仍只允许当前 Agent。`);
|
|
@@ -48,6 +48,8 @@ export function getManagementCommandPermissions(role) {
|
|
|
48
48
|
'config.unset': { allow: true, scopes: ['agent', 'relation'] },
|
|
49
49
|
'role.policy.write': { allow: true, dangerous: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
|
|
50
50
|
'ec.aid.show': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
|
|
51
|
+
'ec.aid.lookup': { allow: true, scopes: ['agent'] },
|
|
52
|
+
'ec.aid.agentmd.get': { allow: true, scopes: ['agent'] },
|
|
51
53
|
'ec.aid.agentmd.put': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
|
|
52
54
|
'ec.fs.mkdir': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
|
|
53
55
|
'ec.fs.upload': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
|
|
@@ -342,8 +342,20 @@ function processIsAlive(pid) {
|
|
|
342
342
|
function assertDaemonStopped(home) {
|
|
343
343
|
const candidates = [];
|
|
344
344
|
const pidFile = path.join(home, 'daemon.pid');
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
try {
|
|
346
|
+
// daemon.pid was retired in favor of the instance registry. It may still
|
|
347
|
+
// exist as a stale legacy file, but a Windows Codex sandbox can also leave
|
|
348
|
+
// a directory at this path when protecting a not-yet-created file. Never
|
|
349
|
+
// feed a directory (or symlink) to readFileSync: that turns harmless
|
|
350
|
+
// legacy residue into an EISDIR startup failure.
|
|
351
|
+
const stat = fs.lstatSync(pidFile);
|
|
352
|
+
if (stat.isFile()) {
|
|
353
|
+
candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (!isRecord(error) || error.code !== 'ENOENT')
|
|
358
|
+
throw error;
|
|
347
359
|
}
|
|
348
360
|
const instanceDir = path.join(home, 'data', 'instance');
|
|
349
361
|
if (fs.existsSync(instanceDir)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
|
|
3
3
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
4
5
|
export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
|
|
5
6
|
export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
|
|
6
7
|
export class AgentDelegationRegistry {
|
|
@@ -43,17 +44,19 @@ export class AgentDelegationRegistry {
|
|
|
43
44
|
: undefined;
|
|
44
45
|
if (approved)
|
|
45
46
|
return { ok: true, grant: approved };
|
|
47
|
+
const diagnosticCause = grant
|
|
48
|
+
? grant.sessionId !== sessionId
|
|
49
|
+
? 'session_mismatch'
|
|
50
|
+
: 'task_replaced'
|
|
51
|
+
: commandHash
|
|
52
|
+
? 'approved_command_unavailable'
|
|
53
|
+
: 'unknown_token';
|
|
54
|
+
logger.warn(`[AgentDelegation] validate failed: cause=${diagnosticCause} session=${sessionId} grantSession=${grant?.sessionId ?? 'none'} taskId=${grant?.taskId ?? 'none'} hasCommandHash=${!!commandHash} tokenKnown=${!!grant} activeForSession=${this.activeHashBySession.get(sessionId) ?? 'none'} grantTokenHash=${grant?.tokenHash ?? 'none'}`);
|
|
46
55
|
return {
|
|
47
56
|
ok: false,
|
|
48
57
|
code: 'INVALID_DELEGATION',
|
|
49
58
|
reason: 'The task delegation token is invalid, revoked, or belongs to another session',
|
|
50
|
-
diagnosticCause
|
|
51
|
-
? grant.sessionId !== sessionId
|
|
52
|
-
? 'session_mismatch'
|
|
53
|
-
: 'task_replaced'
|
|
54
|
-
: commandHash
|
|
55
|
-
? 'approved_command_unavailable'
|
|
56
|
-
: 'unknown_token',
|
|
59
|
+
diagnosticCause,
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
armApprovedCommand(input) {
|
|
@@ -33,10 +33,11 @@ export function buildAuthSubject(input) {
|
|
|
33
33
|
&& fullAccessAuthorization.processRole === 'fullaccess-run'
|
|
34
34
|
&& fullAccessAuthorization.dataScope === 'daemon';
|
|
35
35
|
const isDaemonService = input.trustedProcessRole === 'daemon-service';
|
|
36
|
-
const
|
|
36
|
+
const isTrustedDaemonOwner = input.trustedProcessRole === 'daemon-owner';
|
|
37
|
+
const isDaemonOwner = !isDaemonService && !isFullAccessRun && (isTrustedDaemonOwner || isProcessOwner({
|
|
37
38
|
actor: roleDetail.actor,
|
|
38
39
|
processOwners,
|
|
39
|
-
});
|
|
40
|
+
}));
|
|
40
41
|
const suppliedRole = input.identity?.role && input.identity.role !== 'none' ? input.identity.role : undefined;
|
|
41
42
|
const role = suppliedRole ?? roleDetail.effectiveRole ?? 'none';
|
|
42
43
|
const processRole = isFullAccessRun
|
|
@@ -10,6 +10,8 @@ export function auditCommandAuthorization(event) {
|
|
|
10
10
|
(event.decision === 'allow' && event.dangerous) ||
|
|
11
11
|
(event.source === 'agent-tool' && event.operation.startsWith('config.')) ||
|
|
12
12
|
event.operation === 'codex.approval' ||
|
|
13
|
+
event.operation === 'permission.approval' ||
|
|
14
|
+
event.operation === 'permission.runtime' ||
|
|
13
15
|
event.operation.startsWith('fullaccess.') ||
|
|
14
16
|
event.operation.startsWith('role.') ||
|
|
15
17
|
event.operation === 'cli.exec.raw';
|
|
@@ -185,6 +187,9 @@ export function auditCodexApprovalDecision(input) {
|
|
|
185
187
|
taskId: input.taskId,
|
|
186
188
|
argsSummary: {
|
|
187
189
|
method: input.method,
|
|
190
|
+
...(input.summary ? { summary: input.summary } : {}),
|
|
191
|
+
...(input.threadId ? { threadId: input.threadId } : {}),
|
|
192
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
188
193
|
...(input.parseFailure ? { parseFailure: input.parseFailure } : {}),
|
|
189
194
|
},
|
|
190
195
|
});
|
|
@@ -195,32 +200,78 @@ export function auditCodexApprovalDecision(input) {
|
|
|
195
200
|
* infrastructure denials without parsing localized log text.
|
|
196
201
|
*/
|
|
197
202
|
export function auditPermissionDecision(input) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
requestId: input.requestId,
|
|
203
|
+
const requestId = input.requestId ?? `perm-audit-${crypto.randomUUID()}`;
|
|
204
|
+
auditPermissionLifecycle({
|
|
205
|
+
phase: 'final',
|
|
206
|
+
requestId,
|
|
203
207
|
sessionId: input.sessionId,
|
|
204
208
|
agentAid: input.agentAid,
|
|
205
209
|
agentName: input.agentName,
|
|
206
210
|
permissionMode: input.permissionMode,
|
|
207
211
|
toolName: input.toolName,
|
|
212
|
+
decision: 'deny',
|
|
213
|
+
decisionSource: input.decisionSource,
|
|
214
|
+
policyCode: input.policyCode,
|
|
215
|
+
reason: input.reason,
|
|
216
|
+
role: input.role,
|
|
217
|
+
taskId: input.taskId,
|
|
218
|
+
summary: input.summary,
|
|
219
|
+
operation: 'permission.runtime',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Emit a canonical request/response/final lifecycle for runtime approvals.
|
|
224
|
+
* All three phases share one eventKey and complete correlation fields so
|
|
225
|
+
* daily analysis can group them without parsing runner-specific log text.
|
|
226
|
+
*/
|
|
227
|
+
export function auditPermissionLifecycle(input) {
|
|
228
|
+
const sessionId = input.sessionId ?? 'unknown';
|
|
229
|
+
auditCommandAuthorization({
|
|
230
|
+
ts: Date.now(),
|
|
231
|
+
eventKey: buildToolLifecycleEventKey({
|
|
232
|
+
sessionId,
|
|
233
|
+
callId: input.callId,
|
|
234
|
+
correlationId: input.correlationId,
|
|
235
|
+
requestId: input.requestId,
|
|
236
|
+
}),
|
|
237
|
+
eventPhase: input.phase,
|
|
238
|
+
callId: input.callId ?? input.requestId,
|
|
239
|
+
correlationId: input.correlationId ?? input.callId ?? input.requestId,
|
|
240
|
+
requestId: input.requestId,
|
|
241
|
+
sessionId,
|
|
242
|
+
agentAid: input.agentAid ?? 'unknown',
|
|
243
|
+
agentName: input.agentName,
|
|
244
|
+
permissionMode: input.permissionMode ?? 'unknown',
|
|
245
|
+
toolName: input.toolName,
|
|
246
|
+
approvalMethod: input.approvalMethod,
|
|
208
247
|
policyCode: input.policyCode,
|
|
209
248
|
decisionSource: input.decisionSource,
|
|
210
|
-
errorCode: input.
|
|
211
|
-
? '
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
249
|
+
errorCode: input.decision === 'deny'
|
|
250
|
+
? input.decisionSource === 'approval'
|
|
251
|
+
? 'USER_DENIED'
|
|
252
|
+
: input.decisionSource === 'policy'
|
|
253
|
+
? 'POLICY_DENIED'
|
|
254
|
+
: infrastructureErrorCode(input.policyCode)
|
|
255
|
+
: undefined,
|
|
215
256
|
source: 'agent-tool',
|
|
216
|
-
operation: 'permission.
|
|
257
|
+
operation: input.operation ?? 'permission.approval',
|
|
217
258
|
scope: 'filesystem',
|
|
218
259
|
dangerous: false,
|
|
219
260
|
role: input.role ?? 'unknown',
|
|
220
261
|
taskId: input.taskId,
|
|
221
|
-
|
|
262
|
+
argsSummary: input.summary || input.response
|
|
263
|
+
? {
|
|
264
|
+
...(input.summary ? { summary: input.summary } : {}),
|
|
265
|
+
...(input.response ? { response: input.response } : {}),
|
|
266
|
+
}
|
|
267
|
+
: undefined,
|
|
268
|
+
decision: input.decision,
|
|
222
269
|
executed: false,
|
|
223
|
-
executionState: '
|
|
270
|
+
executionState: input.phase === 'request'
|
|
271
|
+
? 'requested'
|
|
272
|
+
: input.phase === 'response'
|
|
273
|
+
? 'responded'
|
|
274
|
+
: input.decision === 'deny' ? 'blocked' : 'authorized',
|
|
224
275
|
reason: input.reason,
|
|
225
276
|
});
|
|
226
277
|
}
|
|
@@ -266,7 +317,7 @@ function infrastructureErrorCode(policyCode) {
|
|
|
266
317
|
}
|
|
267
318
|
function buildAuditRecord(event) {
|
|
268
319
|
const executed = event.executed ?? false;
|
|
269
|
-
const decisionSource = event.decisionSource ?? (event.decision === 'deny' ? 'policy' : 'command');
|
|
320
|
+
const decisionSource = event.decisionSource ?? (event.decision === 'deny' ? 'policy' : event.decision === 'pending' ? 'approval' : 'command');
|
|
270
321
|
const errorCode = event.errorCode ?? (event.decision === 'deny'
|
|
271
322
|
? decisionSource === 'approval'
|
|
272
323
|
? 'USER_DENIED'
|
|
@@ -277,9 +328,11 @@ function buildAuditRecord(event) {
|
|
|
277
328
|
const executionState = event.executionState
|
|
278
329
|
?? (event.decision === 'deny'
|
|
279
330
|
? 'blocked'
|
|
280
|
-
:
|
|
281
|
-
?
|
|
282
|
-
:
|
|
331
|
+
: event.decision === 'pending'
|
|
332
|
+
? 'requested'
|
|
333
|
+
: executed
|
|
334
|
+
? (event.exitCode === undefined || event.exitCode === 0 ? 'completed' : 'failed')
|
|
335
|
+
: 'authorized');
|
|
283
336
|
const lifecycleId = event.callId ?? event.correlationId ?? event.requestId;
|
|
284
337
|
return {
|
|
285
338
|
ts: event.ts,
|
|
@@ -382,7 +435,11 @@ function logAuditEvent(record) {
|
|
|
382
435
|
: routineEcwebRoleRead
|
|
383
436
|
? 'debug'
|
|
384
437
|
: 'info';
|
|
385
|
-
const marker = record.decision === 'deny'
|
|
438
|
+
const marker = record.decision === 'deny'
|
|
439
|
+
? 'DENY'
|
|
440
|
+
: record.decision === 'pending'
|
|
441
|
+
? 'PENDING'
|
|
442
|
+
: record.dangerous ? 'DANGEROUS_ALLOW' : 'ALLOW';
|
|
386
443
|
const message = [
|
|
387
444
|
`[CommandAudit:${marker}]`,
|
|
388
445
|
`operation=${record.operation}`,
|
|
@@ -34,6 +34,9 @@ const DAEMON_SERVICE_OPERATIONS = new Set([
|
|
|
34
34
|
'trigger.cancel',
|
|
35
35
|
'trigger.delete',
|
|
36
36
|
'trigger.run',
|
|
37
|
+
'agent.reload',
|
|
38
|
+
'system.restart',
|
|
39
|
+
'system.upgrade',
|
|
37
40
|
]);
|
|
38
41
|
export function authorizeCommand(ctx) {
|
|
39
42
|
return authorizeCommandInternal(ctx);
|
|
@@ -121,6 +124,47 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
121
124
|
return denyDecision(ctx, 'NOT_ALLOWED', `Operation ${operation} is not available from source ${ctx.source}`, operation, intent.scope, opMeta.dangerous);
|
|
122
125
|
}
|
|
123
126
|
if (ctx.processRole === 'daemon-service') {
|
|
127
|
+
// ECWeb uses the authenticated daemon service for its read-only menu
|
|
128
|
+
// bootstrap (model/effort, agent metadata, trigger views, etc.). These
|
|
129
|
+
// queries are safe to authorize by catalog category; mutating operations
|
|
130
|
+
// remain restricted to the explicit daemon-service allowlist below.
|
|
131
|
+
const ECWEB_SERVICE_READ_OPERATIONS = new Set([
|
|
132
|
+
'system.status', 'model.current', 'model.list', 'baseagent.models.list',
|
|
133
|
+
'model.effort.current', 'agent.baseagent.current', 'agent.baseagent.list',
|
|
134
|
+
'agent.show',
|
|
135
|
+
'agent.list',
|
|
136
|
+
'capability.read',
|
|
137
|
+
'gateway.read', 'stats.summary', 'stats.session', 'stats.context',
|
|
138
|
+
'role.policy.read', 'role.relation.read', 'config.read',
|
|
139
|
+
'model.info', 'model.check',
|
|
140
|
+
'trigger.list', 'trigger.show', 'trigger.history', 'trigger.eventCatalog',
|
|
141
|
+
]);
|
|
142
|
+
if (ECWEB_SERVICE_READ_OPERATIONS.has(operation)) {
|
|
143
|
+
return {
|
|
144
|
+
allow: true,
|
|
145
|
+
operation,
|
|
146
|
+
scope: intent.scope,
|
|
147
|
+
role: 'daemon-service',
|
|
148
|
+
dangerous: opMeta.dangerous,
|
|
149
|
+
matchedRule: 'daemon-service-read',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// The in-process service may perform system lifecycle actions only when
|
|
153
|
+
// the request came through the authenticated ECWeb management surface.
|
|
154
|
+
// A raw daemon-service subject on the control source is not a human owner.
|
|
155
|
+
if ((operation === 'system.restart' || operation === 'system.upgrade') && ctx.source === 'control') {
|
|
156
|
+
return denyDecision(ctx, 'NOT_ALLOWED', `Daemon service is not authorized for ${operation}`, operation, intent.scope, opMeta.dangerous, 'daemon-service');
|
|
157
|
+
}
|
|
158
|
+
if (ctx.source !== 'control' && (operation === 'observable.current' || operation === 'observable.update')) {
|
|
159
|
+
return {
|
|
160
|
+
allow: true,
|
|
161
|
+
operation,
|
|
162
|
+
scope: intent.scope,
|
|
163
|
+
role: 'daemon-service',
|
|
164
|
+
dangerous: opMeta.dangerous,
|
|
165
|
+
matchedRule: 'ecweb-daemon-service-agent-management',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
124
168
|
if (!DAEMON_SERVICE_OPERATIONS.has(operation)) {
|
|
125
169
|
return denyDecision(ctx, 'NOT_ALLOWED', `Daemon service is not authorized for ${operation}`, operation, intent.scope, opMeta.dangerous, 'daemon-service');
|
|
126
170
|
}
|
|
@@ -189,6 +233,20 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
189
233
|
if (roleDef.allowAccess === false) {
|
|
190
234
|
return denyDecision(ctx, 'ROLE_ACCESS_DENIED', `Role ${role} is not allowed to access commands`, operation, intent.scope, opMeta.dangerous);
|
|
191
235
|
}
|
|
236
|
+
// AID lookup and public agent.md reads are available to every accessible
|
|
237
|
+
// role and may target any AID that passed managed-command validation. The
|
|
238
|
+
// operation source is restricted to agent-tool by the catalog.
|
|
239
|
+
if ((operation === 'ec.aid.lookup' || operation === 'ec.aid.agentmd.get')
|
|
240
|
+
&& (intent.scope === 'relation' || intent.scope === 'agent')) {
|
|
241
|
+
return {
|
|
242
|
+
allow: true,
|
|
243
|
+
operation,
|
|
244
|
+
scope: intent.scope,
|
|
245
|
+
role,
|
|
246
|
+
dangerous: opMeta.dangerous,
|
|
247
|
+
matchedRule: 'public-aid-read',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
192
250
|
let matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
|
|
193
251
|
// Agent self-service contact add deliberately reuses the same low-trust
|
|
194
252
|
// relation boundary as ec.msg.send. Older per-Agent role files may still
|
|
@@ -317,7 +375,9 @@ function requiresDaemonOwner(ctx, resolvedConfigCommand) {
|
|
|
317
375
|
return false;
|
|
318
376
|
}
|
|
319
377
|
function hasDaemonOwnerRole(ctx) {
|
|
320
|
-
return ctx.isDaemonOwner === true
|
|
378
|
+
return ctx.isDaemonOwner === true
|
|
379
|
+
|| ctx.processRole === 'daemon-owner'
|
|
380
|
+
|| ctx.processRole === 'daemon-service';
|
|
321
381
|
}
|
|
322
382
|
function daemonOwnerDenialReasonCode(ctx) {
|
|
323
383
|
if ((ctx.intent.operation === 'agent.show' || ctx.intent.operation === 'agent.reload')
|
|
@@ -340,8 +400,8 @@ function isCurrentAgentTarget(ctx) {
|
|
|
340
400
|
return !!ctx.selfAid && !!targetAid && targetAid === ctx.selfAid;
|
|
341
401
|
}
|
|
342
402
|
function daemonOwnerRequiredMessage(operation) {
|
|
343
|
-
return
|
|
344
|
-
+ 'Agent owner/admin
|
|
403
|
+
return `无权限:${operation} 是 daemon 级操作,需要 DaemonOwner。`
|
|
404
|
+
+ 'Agent owner/admin 和 permissionMode=bypass 均不能授予此权限。';
|
|
345
405
|
}
|
|
346
406
|
function getRuleRank(rule, permission, operation, category, isDangerous) {
|
|
347
407
|
const denyOffset = permission.allow ? 0 : 1;
|
|
@@ -38,6 +38,10 @@ const GROUP_MANAGEMENT_OPERATIONS = [
|
|
|
38
38
|
['ec.group.join', 'write-agent', ['agent'], 'Request to join a group'],
|
|
39
39
|
['ec.group.leave', 'write-agent', ['agent'], 'Leave a group'],
|
|
40
40
|
['ec.group.invite', 'write-agent', ['agent'], 'Invite group members'],
|
|
41
|
+
['ec.group.invites', 'read', ['agent'], 'List pending targeted group invitations'],
|
|
42
|
+
['ec.group.accept-invite', 'write-agent', ['agent'], 'Accept a targeted group invitation'],
|
|
43
|
+
['ec.group.reject-invite', 'write-agent', ['agent'], 'Reject a targeted group invitation'],
|
|
44
|
+
['ec.group.revoke-invite', 'write-agent', ['agent'], 'Revoke a targeted group invitation'],
|
|
41
45
|
['ec.group.kick', 'write-agent', ['agent'], 'Remove a group member'],
|
|
42
46
|
['ec.group.role', 'write-agent', ['agent'], 'Change a group member role'],
|
|
43
47
|
['ec.group.owner', 'write-agent', ['agent'], 'Transfer group ownership'],
|
|
@@ -112,6 +116,14 @@ const QUEUE_OPERATIONS = [['read', 'Read the current Agent message queue'], ['cl
|
|
|
112
116
|
*/
|
|
113
117
|
const OPERATIONS = [
|
|
114
118
|
// ── Model Operations ──
|
|
119
|
+
{
|
|
120
|
+
id: 'baseagent.models.list',
|
|
121
|
+
category: 'read',
|
|
122
|
+
dangerous: false,
|
|
123
|
+
defaultScopes: ['process'],
|
|
124
|
+
description: '查询主机默认 Baseagent 模型目录',
|
|
125
|
+
sources: ['menu', 'ecweb', 'control'],
|
|
126
|
+
},
|
|
115
127
|
{
|
|
116
128
|
id: 'model.list',
|
|
117
129
|
category: 'read',
|
|
@@ -913,7 +925,7 @@ const OPERATIONS = [
|
|
|
913
925
|
category: 'read',
|
|
914
926
|
dangerous: false,
|
|
915
927
|
defaultScopes: ['relation', 'agent'],
|
|
916
|
-
description: 'Look up
|
|
928
|
+
description: 'Look up a public AID record',
|
|
917
929
|
sources: ['agent-tool'],
|
|
918
930
|
},
|
|
919
931
|
{
|
|
@@ -921,7 +933,7 @@ const OPERATIONS = [
|
|
|
921
933
|
category: 'read',
|
|
922
934
|
dangerous: false,
|
|
923
935
|
defaultScopes: ['relation', 'agent'],
|
|
924
|
-
description: 'Read and verify
|
|
936
|
+
description: 'Read and verify a public Agent agent.md',
|
|
925
937
|
sources: ['agent-tool'],
|
|
926
938
|
},
|
|
927
939
|
{
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
export const REMOTE_ACCESS_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6
|
+
// 与 owner 绑定二维码(BindService)的默认有效期保持一致:10 分钟。
|
|
7
|
+
export const REMOTE_ACCESS_PAIRING_TTL_MS = 10 * 60 * 1000;
|
|
8
|
+
export function remoteAccessTokenStorePath() {
|
|
9
|
+
return path.join(resolvePaths().instanceDir, 'control-access-tokens.json');
|
|
10
|
+
}
|
|
11
|
+
function loadStore() {
|
|
12
|
+
try {
|
|
13
|
+
const value = JSON.parse(fs.readFileSync(remoteAccessTokenStorePath(), 'utf8'));
|
|
14
|
+
if (Array.isArray(value.tokens))
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
return { tokens: [] };
|
|
19
|
+
}
|
|
20
|
+
function saveStore(store) {
|
|
21
|
+
const file = remoteAccessTokenStorePath();
|
|
22
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
23
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
24
|
+
try {
|
|
25
|
+
fs.writeFileSync(temporary, JSON.stringify(store, null, 2) + '\n', { mode: 0o600 });
|
|
26
|
+
fs.renameSync(temporary, file);
|
|
27
|
+
try {
|
|
28
|
+
fs.chmodSync(file, 0o600);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (fs.existsSync(temporary)) {
|
|
34
|
+
try {
|
|
35
|
+
fs.unlinkSync(temporary);
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function pruneExpired(store, now) {
|
|
42
|
+
const before = store.tokens.length;
|
|
43
|
+
store.tokens = store.tokens.filter(record => typeof record?.token === 'string'
|
|
44
|
+
&& typeof record.lastActive === 'number'
|
|
45
|
+
&& now - record.lastActive < REMOTE_ACCESS_TOKEN_TTL_MS);
|
|
46
|
+
return store.tokens.length !== before;
|
|
47
|
+
}
|
|
48
|
+
function validLabel(value) {
|
|
49
|
+
if (typeof value !== 'string')
|
|
50
|
+
return 'remote';
|
|
51
|
+
const normalized = value.trim().replace(/[\r\n]/g, ' ').slice(0, 256);
|
|
52
|
+
return normalized || 'remote';
|
|
53
|
+
}
|
|
54
|
+
function validOwner(value) {
|
|
55
|
+
if (typeof value !== 'string')
|
|
56
|
+
return undefined;
|
|
57
|
+
const normalized = value.trim();
|
|
58
|
+
if (!normalized || normalized.length > 512 || /[\r\n]/.test(normalized))
|
|
59
|
+
return undefined;
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
/** Validate and slide the expiry window of a daemon-owned remote token. */
|
|
63
|
+
export function validateAndRenewRemoteAccessToken(token, now = Date.now()) {
|
|
64
|
+
if (typeof token !== 'string' || !token || token.length > 512)
|
|
65
|
+
return { valid: false };
|
|
66
|
+
try {
|
|
67
|
+
const store = loadStore();
|
|
68
|
+
const changed = pruneExpired(store, now);
|
|
69
|
+
const record = store.tokens.find(candidate => candidate.token === token);
|
|
70
|
+
if (!record) {
|
|
71
|
+
if (changed)
|
|
72
|
+
saveStore(store);
|
|
73
|
+
return { valid: false };
|
|
74
|
+
}
|
|
75
|
+
record.lastActive = now;
|
|
76
|
+
saveStore(store);
|
|
77
|
+
const authorizedBy = validOwner(record.authorizedBy);
|
|
78
|
+
return { valid: true, ...(authorizedBy ? { authorizedBy } : {}) };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return { valid: false };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export class RemoteAccessService {
|
|
85
|
+
pairingCode = '';
|
|
86
|
+
pairingExpiresAt = 0;
|
|
87
|
+
pairingAuthorizedBy;
|
|
88
|
+
getPairingCode(authorizedBy, now = Date.now()) {
|
|
89
|
+
if (!this.pairingCode || now >= this.pairingExpiresAt) {
|
|
90
|
+
this.pairingCode = String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
|
|
91
|
+
this.pairingExpiresAt = now + REMOTE_ACCESS_PAIRING_TTL_MS;
|
|
92
|
+
this.pairingAuthorizedBy = undefined;
|
|
93
|
+
}
|
|
94
|
+
const owner = validOwner(authorizedBy);
|
|
95
|
+
if (!this.pairingAuthorizedBy && owner)
|
|
96
|
+
this.pairingAuthorizedBy = owner;
|
|
97
|
+
return { code: this.pairingCode, expiresAt: this.pairingExpiresAt };
|
|
98
|
+
}
|
|
99
|
+
redeemPairingCode(code, label, authorizedBy, now = Date.now()) {
|
|
100
|
+
if (typeof code !== 'string' || !/^\d{6}$/.test(code)) {
|
|
101
|
+
return { ok: false, code: 'INVALID_ARGUMENT', error: 'pairing code must be six digits' };
|
|
102
|
+
}
|
|
103
|
+
if (!this.pairingCode || now >= this.pairingExpiresAt) {
|
|
104
|
+
return { ok: false, code: 'PAIRING_EXPIRED', error: 'pairing code has expired' };
|
|
105
|
+
}
|
|
106
|
+
if (code !== this.pairingCode) {
|
|
107
|
+
return { ok: false, code: 'PAIRING_INVALID', error: 'pairing code is invalid' };
|
|
108
|
+
}
|
|
109
|
+
const createdAt = now;
|
|
110
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
111
|
+
const store = loadStore();
|
|
112
|
+
pruneExpired(store, now);
|
|
113
|
+
const owner = validOwner(authorizedBy) ?? this.pairingAuthorizedBy;
|
|
114
|
+
store.tokens.push({
|
|
115
|
+
token,
|
|
116
|
+
createdAt,
|
|
117
|
+
lastActive: createdAt,
|
|
118
|
+
label: validLabel(label),
|
|
119
|
+
...(owner ? { authorizedBy: owner } : {}),
|
|
120
|
+
});
|
|
121
|
+
saveStore(store);
|
|
122
|
+
return { ok: true, token, expiresAt: createdAt + REMOTE_ACCESS_TOKEN_TTL_MS };
|
|
123
|
+
}
|
|
124
|
+
issueLocalToken(label, now = Date.now()) {
|
|
125
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
126
|
+
const store = loadStore();
|
|
127
|
+
pruneExpired(store, now);
|
|
128
|
+
store.tokens.push({ token, createdAt: now, lastActive: now, label: validLabel(label) });
|
|
129
|
+
saveStore(store);
|
|
130
|
+
return { ok: true, token, expiresAt: now + REMOTE_ACCESS_TOKEN_TTL_MS };
|
|
131
|
+
}
|
|
132
|
+
validateToken(token, now = Date.now()) {
|
|
133
|
+
return validateAndRenewRemoteAccessToken(token, now);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export const remoteAccessService = new RemoteAccessService();
|
|
@@ -20,16 +20,16 @@ export class AgentLoader {
|
|
|
20
20
|
* Iterate over all EvolAgents in the registry × all registered plugins.
|
|
21
21
|
* Each successful (agent, plugin) pair yields one runner instance.
|
|
22
22
|
*/
|
|
23
|
-
createAll(registry, callbacks, creationErrors) {
|
|
23
|
+
async createAll(registry, callbacks, creationErrors) {
|
|
24
24
|
const instances = [];
|
|
25
25
|
const allAgents = registry.runnableAgents();
|
|
26
26
|
for (const agent of allAgents) {
|
|
27
|
-
instances.push(...this.createForAgent(agent, callbacks, creationErrors));
|
|
27
|
+
instances.push(...await this.createForAgent(agent, callbacks, creationErrors));
|
|
28
28
|
}
|
|
29
29
|
return instances;
|
|
30
30
|
}
|
|
31
31
|
/** Create runners for one EvolAgent, used by runtime hot-load. */
|
|
32
|
-
createForAgent(agent, callbacks, creationErrors) {
|
|
32
|
+
async createForAgent(agent, callbacks, creationErrors) {
|
|
33
33
|
const instances = [];
|
|
34
34
|
for (const [pluginName, plugin] of this.plugins) {
|
|
35
35
|
if (!plugin.isEnabled(agent)) {
|
|
@@ -37,7 +37,7 @@ export class AgentLoader {
|
|
|
37
37
|
continue;
|
|
38
38
|
}
|
|
39
39
|
try {
|
|
40
|
-
const instance = plugin.createAgent(agent, callbacks);
|
|
40
|
+
const instance = await plugin.createAgent(agent, callbacks);
|
|
41
41
|
if (!instance) {
|
|
42
42
|
logger.debug(`Plugin '${pluginName}' returned null for agent '${agent.name}', skipping`);
|
|
43
43
|
continue;
|