evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -8,6 +8,11 @@ import { resolveRuntimePermissionMode } from '../role/runtime-policy.js';
8
8
  export function isCrossAgentTriggerOperationAllowed(input) {
9
9
  return input.control || input.daemonOwner === true || input.taskAgentAid === input.targetAgentAid;
10
10
  }
11
+ /** A delegated identity may authorize ordinary work, but it is not a human approval for persistent host execution. */
12
+ export function canApprovePersistentFullAccessTrigger(input) {
13
+ return input.daemonOwner
14
+ && (input.source === 'direct-user' || input.source === 'management-channel');
15
+ }
11
16
  export function buildAuthSubject(input) {
12
17
  const chatType = input.chatType === 'group' ? 'group' : 'private';
13
18
  const actorId = input.actorId;
@@ -22,17 +27,31 @@ export function buildAuthSubject(input) {
22
27
  peerType: input.peerType,
23
28
  });
24
29
  const processOwners = input.processOwners ?? [];
30
+ const fullAccessAuthorization = input.trustedFullAccessAuthorization;
31
+ const isFullAccessRun = input.trustedProcessRole === 'fullaccess-run'
32
+ && fullAccessAuthorization?.permissionMode === 'fullaccess'
33
+ && fullAccessAuthorization.processRole === 'fullaccess-run'
34
+ && fullAccessAuthorization.dataScope === 'daemon';
25
35
  const isDaemonService = input.trustedProcessRole === 'daemon-service';
26
- const isDaemonOwner = !isDaemonService && isProcessOwner({
36
+ const isDaemonOwner = !isDaemonService && !isFullAccessRun && isProcessOwner({
27
37
  actor: roleDetail.actor,
28
38
  processOwners,
29
39
  });
30
40
  const suppliedRole = input.identity?.role && input.identity.role !== 'none' ? input.identity.role : undefined;
31
41
  const role = suppliedRole ?? roleDetail.effectiveRole ?? 'none';
32
- const processRole = isDaemonService ? 'daemon-service' : isDaemonOwner ? 'daemon-owner' : 'none';
42
+ const processRole = isFullAccessRun
43
+ ? 'fullaccess-run'
44
+ : isDaemonService ? 'daemon-service' : isDaemonOwner ? 'daemon-owner' : 'none';
45
+ const dataScope = processRole === 'fullaccess-run'
46
+ || processRole === 'daemon-service'
47
+ || processRole === 'daemon-owner'
48
+ ? 'daemon'
49
+ : role === 'owner' || role === 'admin' ? 'agent' : 'relation';
33
50
  return {
34
51
  selfAid: input.selfAid,
52
+ agentName: input.agentName,
35
53
  actorId,
54
+ principalId: roleDetail.actor.principalId,
36
55
  requestId: input.requestId,
37
56
  sessionId: input.sessionId,
38
57
  channel: input.channel,
@@ -44,6 +63,8 @@ export function buildAuthSubject(input) {
44
63
  role,
45
64
  relationRole: role,
46
65
  processRole,
66
+ dataScope,
67
+ authorizedBy: isFullAccessRun ? fullAccessAuthorization.authorizedBy : undefined,
47
68
  roleSource: roleDetail.source,
48
69
  identity: input.identity ?? roleToSessionIdentity(role === 'none' ? null : role),
49
70
  isDaemonOwner,
@@ -51,8 +72,12 @@ export function buildAuthSubject(input) {
51
72
  allowAccess: processRole !== 'none' || (suppliedRole
52
73
  ? checkRoleAccess(suppliedRole, input.selfAid)
53
74
  : roleDetail.allowAccess && checkRoleAccess(role, input.selfAid)),
54
- permissionMode: input.permissionMode
55
- ?? resolveRuntimePermissionMode({ selfAid: input.selfAid, role }).effectiveValue,
75
+ canApprovePersistentFullAccess: input.canApprovePersistentFullAccess
76
+ ?? (isDaemonOwner && !isDaemonService),
77
+ permissionMode: isFullAccessRun
78
+ ? 'fullaccess'
79
+ : input.permissionMode
80
+ ?? resolveRuntimePermissionMode({ selfAid: input.selfAid, role }).effectiveValue,
56
81
  };
57
82
  }
58
83
  function isProcessOwner(input) {
@@ -97,6 +122,7 @@ export function authorizeOperation(params) {
97
122
  peerKey: params.subject.peerKey,
98
123
  role: params.subject.role,
99
124
  processRole: params.subject.processRole,
125
+ dataScope: params.subject.dataScope,
100
126
  isDaemonOwner: params.subject.isDaemonOwner,
101
127
  fromControlChannel: params.subject.fromControlChannel,
102
128
  allowExplicitRelationTarget: params.allowExplicitRelationTarget,
@@ -162,6 +188,7 @@ function auditDecision(params, decision) {
162
188
  requestId: params.auditMetadata?.requestId ?? params.subject.requestId,
163
189
  sessionId: params.auditMetadata?.sessionId ?? params.subject.sessionId,
164
190
  agentAid: params.auditMetadata?.agentAid ?? params.subject.selfAid,
191
+ agentName: params.auditMetadata?.agentName ?? params.subject.agentName ?? params.subject.selfAid,
165
192
  permissionMode: params.auditMetadata?.permissionMode
166
193
  ?? (decision.allow ? decision.permissionMode ?? params.subject.permissionMode : params.subject.permissionMode),
167
194
  toolName: params.auditMetadata?.toolName,
@@ -178,6 +205,8 @@ function auditDecision(params, decision) {
178
205
  channelId: params.subject.channelId,
179
206
  role: decision.allow ? decision.role : params.subject.role,
180
207
  processRole: params.subject.processRole,
208
+ dataScope: params.subject.dataScope,
209
+ authorizedBy: params.subject.authorizedBy,
181
210
  isDaemonOwner: params.subject.isDaemonOwner,
182
211
  fromControlChannel: params.subject.fromControlChannel,
183
212
  decision: decision.allow ? 'allow' : 'deny',
@@ -3,12 +3,14 @@ import { LogWriter } from '../../utils/log-writer.js';
3
3
  import { resolvePaths } from '../../paths.js';
4
4
  import crypto from 'node:crypto';
5
5
  import { buildAuthorizationEventKey } from '../audit/event-key.js';
6
+ import { normalizeExecutionPermissionMode } from '../permission/mode.js';
6
7
  export function auditCommandAuthorization(event) {
7
8
  const shouldAudit = event.source === 'menu.cli' ||
8
9
  event.decision === 'deny' ||
9
10
  (event.decision === 'allow' && event.dangerous) ||
10
11
  (event.source === 'agent-tool' && event.operation.startsWith('config.')) ||
11
12
  event.operation === 'codex.approval' ||
13
+ event.operation.startsWith('fullaccess.') ||
12
14
  event.operation.startsWith('role.') ||
13
15
  event.operation === 'cli.exec.raw';
14
16
  if (!shouldAudit)
@@ -16,6 +18,77 @@ export function auditCommandAuthorization(event) {
16
18
  const auditRecord = buildAuditRecord(event);
17
19
  logAuditEvent(auditRecord);
18
20
  }
21
+ export function auditFullAccessEvent(input) {
22
+ const configurationEvent = input.event === 'fullaccess.trigger.configured'
23
+ || input.event === 'fullaccess.trigger.deescalated';
24
+ const executionEvent = input.event === 'fullaccess.execution.started'
25
+ || input.event === 'fullaccess.execution.ended'
26
+ || input.event === 'fullaccess.trigger.execution.started'
27
+ || input.event === 'fullaccess.trigger.execution.ended';
28
+ // The scheduler triggers a run, but the executing subject is the task-bound
29
+ // fullaccess identity. Keep the original owner/configuration provenance in
30
+ // a separate field rather than reporting the model task as a human owner.
31
+ const processRole = input.processRole
32
+ ?? (executionEvent ? 'fullaccess-run' : input.source === 'trigger' ? 'daemon-service' : 'none');
33
+ const actorRole = processRole === 'daemon-owner' ? 'daemon-owner' : processRole;
34
+ const runnerProfileActive = executionEvent && input.executionState !== 'blocked';
35
+ const completedEvent = input.event.endsWith('.ended') || configurationEvent;
36
+ auditCommandAuthorization({
37
+ ts: Date.now(),
38
+ callId: input.attemptId ?? input.runId ?? input.taskId ?? input.messageId,
39
+ correlationId: input.runId ?? input.taskId,
40
+ source: input.source === 'trigger' ? 'agent-tool' : 'slash',
41
+ operation: input.event,
42
+ // CommandScope has no separate daemon value; process is the closest
43
+ // existing scope for daemon-owner authorization and keeps audit records
44
+ // compatible with the established command-audit schema.
45
+ scope: 'process',
46
+ dangerous: true,
47
+ decision: input.decision ?? 'allow',
48
+ executed: input.executed ?? completedEvent,
49
+ executionState: input.executionState
50
+ ?? (input.decision === 'deny' ? 'blocked' : completedEvent ? 'completed' : 'authorized'),
51
+ decisionSource: input.decision === 'deny'
52
+ ? 'policy'
53
+ : configurationEvent
54
+ ? processRole === 'daemon-owner' ? 'daemon-owner' : 'policy'
55
+ : processRole === 'fullaccess-run'
56
+ ? 'fullaccess-run'
57
+ : input.source === 'trigger'
58
+ ? 'trigger-definition'
59
+ : 'daemon-owner',
60
+ actorId: input.actorId,
61
+ processRole,
62
+ dataScope: input.dataScope ?? (executionEvent ? 'daemon' : undefined),
63
+ authorizedBy: input.authorizedBy,
64
+ isDaemonOwner: processRole === 'daemon-owner',
65
+ agentAid: input.agentAid,
66
+ agentName: input.agentName,
67
+ sessionId: input.sessionId,
68
+ taskId: input.taskId,
69
+ messageId: input.messageId,
70
+ permissionMode: 'fullaccess',
71
+ role: actorRole,
72
+ reason: input.reason ?? input.result ?? input.event,
73
+ durationMs: input.durationMs,
74
+ argsSummary: {
75
+ source: input.source,
76
+ triggerId: input.triggerId,
77
+ runId: input.runId,
78
+ attemptId: input.attemptId,
79
+ baseagent: input.baseagent,
80
+ model: input.model,
81
+ dataScope: input.dataScope ?? (executionEvent ? 'daemon' : undefined),
82
+ authorizedBy: input.authorizedBy,
83
+ ...(runnerProfileActive ? {
84
+ sandbox: 'off',
85
+ approval: 'bypassed',
86
+ applicationPolicy: 'bypassed',
87
+ } : {}),
88
+ result: input.result,
89
+ },
90
+ });
91
+ }
19
92
  /**
20
93
  * Record a tool-level preflight denial (e.g. protected-path block) in the
21
94
  * structured command-audit log. This covers interactive sessions whose
@@ -45,6 +118,7 @@ export function auditToolPreflightDenial(input) {
45
118
  argsSummary: input.summary ? { summary: input.summary } : undefined,
46
119
  sessionId: input.sessionId ?? 'unknown',
47
120
  agentAid: input.agentAid ?? 'unknown',
121
+ agentName: input.agentName,
48
122
  permissionMode: input.permissionMode ?? 'unknown',
49
123
  channel: input.channel,
50
124
  actorId: input.actorId,
@@ -76,6 +150,7 @@ export function auditCodexApprovalDecision(input) {
76
150
  correlationId: input.correlationId ?? input.requestId,
77
151
  sessionId: input.sessionId ?? 'unknown',
78
152
  agentAid: input.agentAid ?? 'unknown',
153
+ agentName: input.agentName,
79
154
  permissionMode: input.permissionMode ?? 'unknown',
80
155
  toolName: input.toolName,
81
156
  approvalMethod: input.method,
@@ -84,7 +159,40 @@ export function auditCodexApprovalDecision(input) {
84
159
  reason: input.reason,
85
160
  role: input.role ?? 'unknown',
86
161
  taskId: input.taskId,
87
- argsSummary: { method: input.method },
162
+ argsSummary: {
163
+ method: input.method,
164
+ ...(input.parseFailure ? { parseFailure: input.parseFailure } : {}),
165
+ },
166
+ });
167
+ }
168
+ /**
169
+ * Record a runtime permission decision outside a Codex app-server approval.
170
+ * The shared command-audit stream distinguishes policy, human, and
171
+ * infrastructure denials without parsing localized log text.
172
+ */
173
+ export function auditPermissionDecision(input) {
174
+ auditCommandAuthorization({
175
+ ts: Date.now(),
176
+ callId: input.requestId,
177
+ correlationId: input.requestId,
178
+ requestId: input.requestId,
179
+ sessionId: input.sessionId,
180
+ agentAid: input.agentAid,
181
+ agentName: input.agentName,
182
+ permissionMode: input.permissionMode,
183
+ toolName: input.toolName,
184
+ policyCode: input.policyCode,
185
+ decisionSource: input.decisionSource,
186
+ source: 'agent-tool',
187
+ operation: 'permission.runtime',
188
+ scope: 'filesystem',
189
+ dangerous: false,
190
+ role: input.role ?? 'unknown',
191
+ taskId: input.taskId,
192
+ decision: 'deny',
193
+ executed: false,
194
+ executionState: 'blocked',
195
+ reason: input.reason,
88
196
  });
89
197
  }
90
198
  /** Record a fail-closed infrastructure condition before a tool can execute. */
@@ -104,6 +212,7 @@ export function auditToolInfrastructureFailure(input) {
104
212
  requestId: input.callId,
105
213
  sessionId: input.sessionId ?? 'unknown',
106
214
  agentAid: input.agentAid ?? 'unknown',
215
+ agentName: input.agentName,
107
216
  permissionMode: input.permissionMode ?? 'unknown',
108
217
  toolName: input.toolName,
109
218
  policyCode: input.policyCode,
@@ -140,7 +249,8 @@ function buildAuditRecord(event) {
140
249
  requestId: event.requestId,
141
250
  sessionId: event.sessionId,
142
251
  agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
143
- permissionMode: event.permissionMode,
252
+ agentName: resolveAgentName(event),
253
+ permissionMode: normalizeAuditPermissionMode(event.permissionMode),
144
254
  toolName: event.toolName,
145
255
  approvalMethod: event.approvalMethod,
146
256
  policyCode: event.policyCode ?? event.code,
@@ -161,6 +271,8 @@ function buildAuditRecord(event) {
161
271
  channelId: redactIdentifier(event.channelId),
162
272
  role: event.role,
163
273
  processRole: event.processRole,
274
+ dataScope: event.dataScope,
275
+ authorizedBy: redactIdentifier(event.authorizedBy),
164
276
  isDaemonOwner: event.isDaemonOwner,
165
277
  fromControlChannel: event.fromControlChannel,
166
278
  taskId: event.taskId,
@@ -177,6 +289,26 @@ function buildAuditRecord(event) {
177
289
  exitCode: event.exitCode,
178
290
  };
179
291
  }
292
+ function resolveAgentName(event) {
293
+ const displayName = typeof event.agentName === 'string' ? event.agentName.trim() : '';
294
+ if (displayName && !isUnknownAgentLabel(displayName))
295
+ return displayName;
296
+ const aid = typeof event.agentAid === 'string' ? event.agentAid.trim() : '';
297
+ if (aid && aid !== 'unknown')
298
+ return aid;
299
+ const selfAid = typeof event.selfAid === 'string' ? event.selfAid.trim() : '';
300
+ if (selfAid && selfAid !== 'unknown')
301
+ return selfAid;
302
+ return aid === 'unknown' || selfAid === 'unknown' ? 'unknown' : undefined;
303
+ }
304
+ function isUnknownAgentLabel(value) {
305
+ return value === 'unknown' || value === '<unknown>' || value === '<none>' || value === 'undefined' || value === 'null';
306
+ }
307
+ function normalizeAuditPermissionMode(value) {
308
+ if (!value || value === 'unknown')
309
+ return value;
310
+ return normalizeExecutionPermissionMode(value);
311
+ }
180
312
  function redactIdentifier(value) {
181
313
  if (!value)
182
314
  return undefined;
@@ -212,6 +344,7 @@ function logAuditEvent(record) {
212
344
  record.correlationId ? `correlation=${record.correlationId}` : null,
213
345
  record.eventKey ? `eventKey=${record.eventKey}` : null,
214
346
  record.sessionId ? `session=${record.sessionId}` : null,
347
+ record.agentName ? `agentName=${JSON.stringify(record.agentName)}` : null,
215
348
  record.agentAid ? `agent=${record.agentAid}` : null,
216
349
  record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
217
350
  record.toolName ? `tool=${record.toolName}` : null,
@@ -296,6 +429,21 @@ function summarizeAuthorizationArgs(operation, args) {
296
429
  for (const [key, value] of Object.entries(args)) {
297
430
  if (value === undefined)
298
431
  continue;
432
+ if (key === 'parseFailure' && value && typeof value === 'object' && !Array.isArray(value)) {
433
+ const diagnostic = value;
434
+ const issue = typeof diagnostic.issue === 'string' ? diagnostic.issue : undefined;
435
+ const offset = typeof diagnostic.offset === 'number' && Number.isFinite(diagnostic.offset)
436
+ ? Math.max(0, Math.floor(diagnostic.offset)) : undefined;
437
+ const tokenIndex = typeof diagnostic.tokenIndex === 'number' && Number.isFinite(diagnostic.tokenIndex)
438
+ ? Math.max(0, Math.floor(diagnostic.tokenIndex)) : undefined;
439
+ const dialect = typeof diagnostic.dialect === 'string' ? diagnostic.dialect : undefined;
440
+ const inputLength = typeof diagnostic.inputLength === 'number' && Number.isFinite(diagnostic.inputLength)
441
+ ? Math.max(0, Math.floor(diagnostic.inputLength)) : undefined;
442
+ if (issue && offset !== undefined && tokenIndex !== undefined && dialect && inputLength !== undefined) {
443
+ summary.parseFailure = { issue, offset, tokenIndex, dialect, inputLength };
444
+ }
445
+ continue;
446
+ }
299
447
  if (/^(?:self|peer|peerKey|target|targetId|session|sessionId|channelId)$/i.test(key)) {
300
448
  summary[key] = typeof value === 'string' ? redactIdentifier(value) : '[redacted]';
301
449
  continue;
@@ -95,6 +95,20 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
95
95
  && (resolvedConfigCommand.operationId !== operation || resolvedConfigCommand.commandScope !== intent.scope)) {
96
96
  return denyDecision(ctx, 'NOT_ALLOWED', 'Resolved config operation does not match the command intent', operation, intent.scope, opMeta.dangerous);
97
97
  }
98
+ // A fullaccess task is deliberately a daemon-scoped runtime identity. It
99
+ // still has to name a known operation and pass structural command parsing,
100
+ // but relation/Agent role rules, source lists, and daemon-owner gates are
101
+ // not meaningful limits for code already approved for host-level execution.
102
+ if (ctx.processRole === 'fullaccess-run' && ctx.dataScope === 'daemon') {
103
+ return {
104
+ allow: true,
105
+ operation,
106
+ scope: intent.scope,
107
+ role: 'fullaccess-run',
108
+ dangerous: opMeta.dangerous,
109
+ matchedRule: 'fullaccess-run-daemon-scope',
110
+ };
111
+ }
98
112
  if (ctx.chatType === 'group'
99
113
  && (resolvedConfigCommand ? isResolvedConfigMutation(resolvedConfigCommand) : isConfigMutationOperation(operation))
100
114
  && intent.scope !== 'relation') {
@@ -175,7 +189,31 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
175
189
  if (roleDef.allowAccess === false) {
176
190
  return denyDecision(ctx, 'ROLE_ACCESS_DENIED', `Role ${role} is not allowed to access commands`, operation, intent.scope, opMeta.dangerous);
177
191
  }
178
- const matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
192
+ let matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
193
+ // Agent self-service contact add deliberately reuses the same low-trust
194
+ // relation boundary as ec.msg.send. Older per-Agent role files may still
195
+ // contain the historical contact.* deny without an explicit contact.add
196
+ // entry; preserve their safe current-peer/private-only send capability while
197
+ // allowing this new request workflow. An explicit contact.add rule wins.
198
+ if (operation === 'contact.add'
199
+ && !Object.prototype.hasOwnProperty.call(roleDef.commandPermissions || {}, 'contact.add')) {
200
+ const sendPermission = roleDef.commandPermissions?.['ec.msg.send'];
201
+ if (sendPermission?.allow) {
202
+ matchResult = {
203
+ permission: {
204
+ ...sendPermission,
205
+ scopes: ['relation'],
206
+ constraints: {
207
+ ...(sendPermission.constraints ?? {}),
208
+ ownPeerOnly: true,
209
+ privateOnly: true,
210
+ },
211
+ },
212
+ matchedRule: 'ec.msg.send',
213
+ rank: 1000,
214
+ };
215
+ }
216
+ }
179
217
  if (!matchResult) {
180
218
  return denyDecision(ctx, 'NO_PERMISSION', `Role ${role} has no permission for ${operation}`, operation, intent.scope, opMeta.dangerous);
181
219
  }
@@ -274,6 +312,8 @@ function requiresDaemonOwner(ctx, resolvedConfigCommand) {
274
312
  const targetAgentAid = stringArg(args.targetAgentAid) ?? stringArg(args.self);
275
313
  return !!taskAgentAid && !!targetAgentAid && taskAgentAid !== targetAgentAid;
276
314
  }
315
+ if (operation.startsWith('role.') && ctx.fromControlChannel)
316
+ return true;
277
317
  return false;
278
318
  }
279
319
  function hasDaemonOwnerRole(ctx) {
@@ -55,7 +55,7 @@ const GROUP_MANAGEMENT_OPERATIONS = [
55
55
  sources: ['agent-tool'],
56
56
  }));
57
57
  const MSG_READ_OPERATIONS = [
58
- ['ec.msg.history', 'Read message history for an AUN private relation'],
58
+ ['ec.msg.history', 'Read message history for a controlled private relation'],
59
59
  ['ec.msg.online', 'Read online status for an AUN private relation'],
60
60
  ].map(([id, description]) => ({
61
61
  id,
@@ -476,6 +476,14 @@ const OPERATIONS = [
476
476
  description: 'Read the Agent contact block list',
477
477
  sources: ['agent-tool'],
478
478
  },
479
+ {
480
+ id: 'contact.add',
481
+ category: 'write-agent',
482
+ dangerous: false,
483
+ defaultScopes: ['relation'],
484
+ description: 'Request adding the current private relation as an Agent contact',
485
+ sources: ['agent-tool'],
486
+ },
479
487
  {
480
488
  id: 'contact.block',
481
489
  category: 'write-agent',
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { kitsTemplatesDir, agentMdPath } from '../paths.js';
5
- import { resolveAgentDisplayName } from '../aun/aid/agentmd.js';
5
+ import { buildInitialAgentMd, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
6
6
  import { logger } from '../utils/logger.js';
7
7
  import { loadAgent, saveAgent } from '../config-store.js';
8
8
  import { resolveAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
@@ -292,7 +292,11 @@ export class BootstrapService {
292
292
  if (typeof adapter.uploadAgentMd !== 'function')
293
293
  return;
294
294
  const existing = fs.existsSync(agentMdPath(aid)) ? fs.readFileSync(agentMdPath(aid), 'utf-8') : '';
295
- const content = existing.trim() || `---\naid: "${aid}"\nname: "${fallbackName}"\ntype: "codeagent"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolcore\n - ai-agent\n---\n`;
295
+ const content = existing.trim() || buildInitialAgentMd({
296
+ aid,
297
+ name: fallbackName,
298
+ type: 'codeagent',
299
+ });
296
300
  try {
297
301
  await adapter.uploadAgentMd(content);
298
302
  logger.info(`[Bootstrap] Published agent.md for ${aid}`);
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { resolvePaths } from '../../paths.js';
4
- import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
4
+ import { atomicReadJson, stableWriteJson } from '../../utils/atomic-write.js';
5
5
  import { cloneCausation, normalizeCausation } from './context.js';
6
6
  const DEFAULT_TTL_MS = 30 * 60 * 1000;
7
7
  const associations = new Map();
@@ -44,9 +44,12 @@ function ensureLoaded() {
44
44
  }
45
45
  function persist() {
46
46
  try {
47
- // Keep the protected mount target present even when there are no pending
48
- // associations. Removing it races with Bubblewrap's H-class projection.
49
- atomicWriteJson(associationFile(), [...associations].map(([messageId, association]) => ({
47
+ // Keep the protected mount target's inode stable. Bubblewrap binds this
48
+ // H-class file by pathname while constructing its namespace; an atomic
49
+ // rename can otherwise replace the inode during bind/remount and make
50
+ // sandbox initialization fail. Causation state is best-effort and small,
51
+ // so an in-place write is preferable to changing the protected inode.
52
+ stableWriteJson(associationFile(), [...associations].map(([messageId, association]) => ({
50
53
  messageId,
51
54
  ...association,
52
55
  })));
@@ -8,6 +8,7 @@ import { agentProjectRootFromDefaults, deriveAgentProjectPath } from '../../util
8
8
  import { uploadAvatar } from '../../utils/avatar-upload.js';
9
9
  import { agentmdGet, agentmdPut, updateAgentMdFrontmatterName } from '../../aun/aid/agentmd.js';
10
10
  import { isValidAid } from '../../aun/aid/validation.js';
11
+ import { isSupportedBaseagent } from '../../agents/baseagent.js';
11
12
  const SUPPORTED_AGENT_PATCH_FIELDS = new Set(['aid', 'name', 'avatar', 'active_baseagent', 'baseagents', 'projects', 'owners', 'chatmode', 'channels', 'channelOwners']);
12
13
  const HIDDEN_VALUE = '[hidden]';
13
14
  const SENSITIVE_CONFIG_KEYS = new Set([
@@ -95,6 +96,9 @@ function normalizeBaseagentsPatch(value) {
95
96
  }
96
97
  const out = {};
97
98
  for (const [name, raw] of Object.entries(value)) {
99
+ if (!isSupportedBaseagent(name)) {
100
+ return { ok: false, error: `不支持的 baseagent: ${name}(可选: claude/codex/gemini/ecagent)` };
101
+ }
98
102
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
99
103
  return { ok: false, error: `baseagents.${name} 必须是对象` };
100
104
  }
@@ -286,11 +290,11 @@ function buildCreateProgressOnlyAgent(aid, progress) {
286
290
  * 失败仅写日志 + create-status,不回传(受理即返回)。
287
291
  * agentSet key 对照 cli/agent.ts 的 setNestedValue:
288
292
  * model/effort → 'baseagents.<baseagent>.*';chatmode → 'chatmode'(ChatmodeBlock 对象)。 */
289
- async function runCreateInBackground(opts, w) {
293
+ async function runCreateInBackground(opts, w, service) {
290
294
  let curPhase = 'validating'; // 跟踪当前环节,供 catch 兜底时标注正确 phase
291
295
  try {
292
296
  // onPhase 把 agentCreateNonInteractive 内部环节(0-3、5)映射到进度文件
293
- const res = await agentCreateNonInteractive({
297
+ const createOptions = {
294
298
  aid: opts.aid, name: opts.name, baseagent: opts.baseagent,
295
299
  project: opts.project, owner: opts.owner,
296
300
  onPhase: (phase, state, detail) => {
@@ -305,7 +309,10 @@ async function runCreateInBackground(opts, w) {
305
309
  else if (state === 'failed')
306
310
  w.finishFailed(phase, detail ?? 'failed');
307
311
  },
308
- });
312
+ };
313
+ const res = service
314
+ ? await service.create(createOptions)
315
+ : await agentCreateNonInteractive(createOptions);
309
316
  if (!('ok' in res) || res.ok !== true) {
310
317
  // 硬失败:onPhase('failed') 已写终态;这里仅兜底日志(防回调未覆盖的 return 路径)
311
318
  const err = res.error;
@@ -318,17 +325,23 @@ async function runCreateInBackground(opts, w) {
318
325
  w.begin('applying_config');
319
326
  let warned;
320
327
  if (opts.model) {
321
- const r = await agentSet(opts.aid, `baseagents.${opts.baseagent}.model`, opts.model);
328
+ const r = service
329
+ ? await service.setConfig(opts.aid, `baseagents.${opts.baseagent}.model`, opts.model)
330
+ : await agentSet(opts.aid, `baseagents.${opts.baseagent}.model`, opts.model);
322
331
  if (!('ok' in r) || !r.ok)
323
332
  warned = `model: ${r.error}`;
324
333
  }
325
334
  if (opts.effort) {
326
- const r = await agentSet(opts.aid, `baseagents.${opts.baseagent}.effort`, opts.effort);
335
+ const r = service
336
+ ? await service.setConfig(opts.aid, `baseagents.${opts.baseagent}.effort`, opts.effort)
337
+ : await agentSet(opts.aid, `baseagents.${opts.baseagent}.effort`, opts.effort);
327
338
  if (!('ok' in r) || !r.ok)
328
339
  warned = `${warned ? warned + '; ' : ''}effort: ${r.error}`;
329
340
  }
330
341
  if (opts.chatmode) {
331
- const r = await agentSet(opts.aid, 'chatmode', JSON.stringify(opts.chatmode));
342
+ const r = service
343
+ ? await service.setConfig(opts.aid, 'chatmode', JSON.stringify(opts.chatmode))
344
+ : await agentSet(opts.aid, 'chatmode', JSON.stringify(opts.chatmode));
332
345
  if (!('ok' in r) || !r.ok)
333
346
  warned = `${warned ? warned + '; ' : ''}chatmode: ${r.error}`;
334
347
  }
@@ -361,7 +374,7 @@ async function runCreateInBackground(opts, w) {
361
374
  /** name=agent 的 menu.action 执行。create 使用显式 owner AID;peerId 仅作为控制面鉴权主体。
362
375
  * create 受理即返回(D3);delete/enable/disable 同步等结果。
363
376
  * 调用方负责传入已兜底的 args.project(见 command-handler 装配)。 */
364
- export async function execAgentAction(action, args, peerId, eventBus) {
377
+ export async function execAgentAction(action, args, peerId, eventBus, service) {
365
378
  const a = args ?? {};
366
379
  if (action === 'create') {
367
380
  if (!peerId)
@@ -394,13 +407,15 @@ export async function execAgentAction(action, args, peerId, eventBus) {
394
407
  project: a.project, owner,
395
408
  model, effort, chatmode: a.chatmode,
396
409
  eventBus,
397
- }, progressWriter).catch(e => logger.error(`[agent-control] runCreateInBackground unhandled ${a.aid}: ${e?.message || e}`));
410
+ }, progressWriter, service).catch(e => logger.error(`[agent-control] runCreateInBackground unhandled ${a.aid}: ${e?.message || e}`));
398
411
  return { data: { accepted: true, aid: a.aid } };
399
412
  }
400
413
  if (action === 'delete') {
401
414
  if (!a.aid)
402
415
  return { error: '缺少 aid', code: 'INVALID_ARGS' };
403
- const res = await agentDelete(a.aid, false);
416
+ const res = service
417
+ ? await service.delete(a.aid, false)
418
+ : await agentDelete(a.aid, false);
404
419
  if (!('ok' in res) || res.ok !== true)
405
420
  return { error: res.error, code: classifyError(res.error) };
406
421
  eventBus?.publish({ type: 'agent:deleted', aid: res.aid, purged: res.purged, timestamp: Date.now() });
@@ -410,7 +425,13 @@ export async function execAgentAction(action, args, peerId, eventBus) {
410
425
  if (!a.aid)
411
426
  return { error: '缺少 aid', code: 'INVALID_ARGS' };
412
427
  const options = { force: a.force === true };
413
- const res = action === 'enable' ? await agentEnable(a.aid, options) : await agentDisable(a.aid, options);
428
+ const res = service
429
+ ? action === 'enable'
430
+ ? await service.enable(a.aid, options.force === true)
431
+ : await service.disable(a.aid, options.force === true)
432
+ : action === 'enable'
433
+ ? await agentEnable(a.aid, options)
434
+ : await agentDisable(a.aid, options);
414
435
  if (!('ok' in res) || res.ok !== true)
415
436
  return { error: res.error, code: res.code || classifyError(res.error) };
416
437
  eventBus?.publish({
@@ -424,7 +445,9 @@ export async function execAgentAction(action, args, peerId, eventBus) {
424
445
  if (action === 'reload') {
425
446
  if (!a.aid)
426
447
  return { error: '缺少 aid', code: 'INVALID_ARGS' };
427
- const res = await agentReload(a.aid, { force: a.force === true });
448
+ const res = service
449
+ ? await service.reload(a.aid, { force: a.force === true })
450
+ : await agentReload(a.aid, { force: a.force === true });
428
451
  if (!('ok' in res) || res.ok !== true)
429
452
  return { error: res.error, code: res.code || classifyError(res.error) };
430
453
  eventBus?.publish({ type: 'agent:reloaded', aid: a.aid, timestamp: Date.now() });
@@ -508,6 +531,9 @@ export async function execAgentUpdate(args) {
508
531
  if (typeof p.active_baseagent !== 'string' || !p.active_baseagent.trim()) {
509
532
  return { error: `无效 active_baseagent: ${JSON.stringify(p.active_baseagent)}(必须是非空字符串)`, code: 'INVALID_ARGS' };
510
533
  }
534
+ if (!isSupportedBaseagent(p.active_baseagent.trim())) {
535
+ return { error: `不支持的 active_baseagent: ${p.active_baseagent}(可选: claude/codex/gemini/ecagent)`, code: 'INVALID_ARGS' };
536
+ }
511
537
  config.active_baseagent = p.active_baseagent.trim();
512
538
  touched = true;
513
539
  }
@@ -576,7 +602,7 @@ export async function execAgentUpdate(args) {
576
602
  /**
577
603
  * project 解析与 ec agent create 保持一致:
578
604
  * 显式值 > 默认项目根目录 + AID 前缀。
579
- * 默认项目根目录由 defaults.projects.rootPath/defaultPath 或运行时根目录推导。
605
+ * 默认项目根目录由 defaults.projects.defaultPath 或运行时根目录推导。
580
606
  */
581
607
  export function resolveProjectPath(explicit, aid, defaults) {
582
608
  if (explicit && explicit.trim())
@@ -585,13 +611,15 @@ export function resolveProjectPath(explicit, aid, defaults) {
585
611
  return deriveAgentProjectPath(root, aid);
586
612
  }
587
613
  /** name=agent 的 menu.query:查单个 agent 详情,附构建进度(D3)。 */
588
- export async function execAgentQuery(args) {
614
+ export async function execAgentQuery(args, service) {
589
615
  const aid = args?.aid;
590
616
  if (!aid)
591
617
  return { error: '缺少 aid', code: 'INVALID_ARGS' };
592
618
  const agentDir = path.join(resolvePaths().agentsDir, aid);
593
619
  const progress = readCreateStatus(agentDir);
594
- const res = await agentShow(aid);
620
+ const res = service
621
+ ? await service.show(aid)
622
+ : await agentShow(aid);
595
623
  if (!('ok' in res) || res.ok !== true) {
596
624
  const code = classifyError(res.error);
597
625
  if (code === 'NOT_FOUND' && progress)
@@ -616,9 +644,21 @@ export async function execAgentQuery(args) {
616
644
  return { data: progress ? { ...data, createProgress: progress } : data };
617
645
  }
618
646
  /** name=agent 的 menu.options:列出 agent(enabled 默认 / all) */
619
- export async function execAgentOptions(args) {
647
+ export async function execAgentOptions(args, service) {
620
648
  const scope = args?.options === 'all' ? 'all' : 'enabled';
621
- const res = await agentList();
649
+ const res = service
650
+ ? { ok: true, agents: service.list().map(info => ({
651
+ aid: info.aid || info.name,
652
+ name: info.name,
653
+ personalName: info.personalName || null,
654
+ status: info.status || 'stopped',
655
+ channels: info.channels || [],
656
+ projectPath: info.projectPath ? toPosix(info.projectPath) : null,
657
+ baseagent: info.baseagent || null,
658
+ model: info.model || null,
659
+ lastActivity: info.lastActivity || null,
660
+ })) }
661
+ : await agentList();
622
662
  if (!('ok' in res) || res.ok !== true)
623
663
  return { error: res.error, code: classifyError(res.error) };
624
664
  const agents = scope === 'all'