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.
Files changed (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
@@ -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
- const base = process.env.TMPDIR?.trim();
257
- if (!base || !path.isAbsolute(base) || !sessionId)
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
- if (fs.existsSync(pidFile)) {
346
- candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
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: grant
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 isDaemonOwner = !isDaemonService && !isFullAccessRun && isProcessOwner({
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
- auditCommandAuthorization({
199
- ts: Date.now(),
200
- callId: input.requestId,
201
- correlationId: input.requestId,
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.decisionSource === 'approval'
211
- ? 'USER_DENIED'
212
- : input.decisionSource === 'policy'
213
- ? 'POLICY_DENIED'
214
- : infrastructureErrorCode(input.policyCode),
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.runtime',
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
- decision: 'deny',
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: 'blocked',
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
- : executed
281
- ? (event.exitCode === undefined || event.exitCode === 0 ? 'completed' : 'failed')
282
- : 'authorized');
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' ? 'DENY' : record.dangerous ? 'DANGEROUS_ALLOW' : 'ALLOW';
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 || ctx.processRole === 'daemon-owner';
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 `${operation} is a daemon-level operation and requires DaemonOwner. `
344
- + 'Agent owner/admin and permissionMode=bypass do not grant this permission.';
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 the current Agent public AID record',
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 the current Agent public agent.md',
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;