evolcore 0.0.19 → 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 (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -8,6 +8,9 @@ export class AgentDelegationRegistry {
8
8
  activeHashBySession = new Map();
9
9
  approvedCommands = new Map();
10
10
  issue(input) {
11
+ if (input.executionIdentity && !isValidFullAccessExecutionIdentity(input.executionIdentity)) {
12
+ throw new Error('invalid fullaccess task execution identity');
13
+ }
11
14
  this.revokeSession(input.sessionId);
12
15
  const token = crypto.randomBytes(32).toString('base64url');
13
16
  const tokenHash = hashDelegationToken(token);
@@ -134,7 +137,7 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
134
137
  if (!validation.ok)
135
138
  return validation;
136
139
  const grant = validation.grant;
137
- if (grant.selfAid !== input.aid) {
140
+ if (grant.selfAid !== input.aid && !hasTrustedFullAccessDelegation(grant)) {
138
141
  return { ok: false, code: 'INVALID_DELEGATION', reason: 'Delegation self agent does not match sender' };
139
142
  }
140
143
  if (grant.messageId && grant.messageId !== input.messageId) {
@@ -157,6 +160,7 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
157
160
  conversationId: channelId,
158
161
  processOwners: [],
159
162
  fromControlChannel: false,
163
+ ...trustedExecutionIdentityInput(grant),
160
164
  });
161
165
  const subject = { ...builtSubject, peerKey: grant.peerKey };
162
166
  const decision = authorizeOperation({
@@ -180,6 +184,32 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
180
184
  }
181
185
  return { ok: true, grant };
182
186
  }
187
+ /** Build the only AuthGateway input shape accepted for a task-held fullaccess claim. */
188
+ export function trustedExecutionIdentityInput(grant) {
189
+ return grant.executionIdentity && isValidFullAccessExecutionIdentity(grant.executionIdentity)
190
+ ? {
191
+ trustedProcessRole: 'fullaccess-run',
192
+ trustedFullAccessAuthorization: grant.executionIdentity,
193
+ }
194
+ : {};
195
+ }
196
+ export function hasTrustedFullAccessDelegation(grant) {
197
+ return !!grant.executionIdentity && isValidFullAccessExecutionIdentity(grant.executionIdentity);
198
+ }
199
+ function isValidFullAccessExecutionIdentity(identity) {
200
+ if (identity.permissionMode !== 'fullaccess'
201
+ || identity.processRole !== 'fullaccess-run'
202
+ || identity.dataScope !== 'daemon')
203
+ return false;
204
+ if (identity.source === 'fullaccess-command') {
205
+ return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0;
206
+ }
207
+ return identity.source === 'trigger'
208
+ && typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
209
+ && typeof identity.triggerId === 'string' && identity.triggerId.length > 0
210
+ && typeof identity.runId === 'string' && identity.runId.length > 0
211
+ && typeof identity.attemptId === 'string' && identity.attemptId.length > 0;
212
+ }
183
213
  function hashDelegationToken(token) {
184
214
  return crypto.createHash('sha256').update(token).digest('hex');
185
215
  }
@@ -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
@@ -37,11 +110,15 @@ export function auditToolPreflightDenial(input) {
37
110
  toolName: input.toolName,
38
111
  policyCode: input.policyCode,
39
112
  protectionClass: input.protectionClass ?? protectionClassForPolicy(input.policyCode),
40
- matchedPath: input.matchedPath ?? extractAuditPath(input.summary),
113
+ // Path provenance must come from the policy parser. Do not infer it from
114
+ // localized summaries: words such as `daemon.json` or `.lock` may only be
115
+ // regex/string literals in an otherwise harmless diagnostic command.
116
+ matchedPath: input.matchedPath,
41
117
  reason: input.reason,
42
118
  argsSummary: input.summary ? { summary: input.summary } : undefined,
43
119
  sessionId: input.sessionId ?? 'unknown',
44
120
  agentAid: input.agentAid ?? 'unknown',
121
+ agentName: input.agentName,
45
122
  permissionMode: input.permissionMode ?? 'unknown',
46
123
  channel: input.channel,
47
124
  actorId: input.actorId,
@@ -73,6 +150,7 @@ export function auditCodexApprovalDecision(input) {
73
150
  correlationId: input.correlationId ?? input.requestId,
74
151
  sessionId: input.sessionId ?? 'unknown',
75
152
  agentAid: input.agentAid ?? 'unknown',
153
+ agentName: input.agentName,
76
154
  permissionMode: input.permissionMode ?? 'unknown',
77
155
  toolName: input.toolName,
78
156
  approvalMethod: input.method,
@@ -81,7 +159,40 @@ export function auditCodexApprovalDecision(input) {
81
159
  reason: input.reason,
82
160
  role: input.role ?? 'unknown',
83
161
  taskId: input.taskId,
84
- 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,
85
196
  });
86
197
  }
87
198
  /** Record a fail-closed infrastructure condition before a tool can execute. */
@@ -101,6 +212,7 @@ export function auditToolInfrastructureFailure(input) {
101
212
  requestId: input.callId,
102
213
  sessionId: input.sessionId ?? 'unknown',
103
214
  agentAid: input.agentAid ?? 'unknown',
215
+ agentName: input.agentName,
104
216
  permissionMode: input.permissionMode ?? 'unknown',
105
217
  toolName: input.toolName,
106
218
  policyCode: input.policyCode,
@@ -137,7 +249,8 @@ function buildAuditRecord(event) {
137
249
  requestId: event.requestId,
138
250
  sessionId: event.sessionId,
139
251
  agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
140
- permissionMode: event.permissionMode,
252
+ agentName: resolveAgentName(event),
253
+ permissionMode: normalizeAuditPermissionMode(event.permissionMode),
141
254
  toolName: event.toolName,
142
255
  approvalMethod: event.approvalMethod,
143
256
  policyCode: event.policyCode ?? event.code,
@@ -158,6 +271,8 @@ function buildAuditRecord(event) {
158
271
  channelId: redactIdentifier(event.channelId),
159
272
  role: event.role,
160
273
  processRole: event.processRole,
274
+ dataScope: event.dataScope,
275
+ authorizedBy: redactIdentifier(event.authorizedBy),
161
276
  isDaemonOwner: event.isDaemonOwner,
162
277
  fromControlChannel: event.fromControlChannel,
163
278
  taskId: event.taskId,
@@ -174,6 +289,26 @@ function buildAuditRecord(event) {
174
289
  exitCode: event.exitCode,
175
290
  };
176
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
+ }
177
312
  function redactIdentifier(value) {
178
313
  if (!value)
179
314
  return undefined;
@@ -209,6 +344,7 @@ function logAuditEvent(record) {
209
344
  record.correlationId ? `correlation=${record.correlationId}` : null,
210
345
  record.eventKey ? `eventKey=${record.eventKey}` : null,
211
346
  record.sessionId ? `session=${record.sessionId}` : null,
347
+ record.agentName ? `agentName=${JSON.stringify(record.agentName)}` : null,
212
348
  record.agentAid ? `agent=${record.agentAid}` : null,
213
349
  record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
214
350
  record.toolName ? `tool=${record.toolName}` : null,
@@ -293,6 +429,21 @@ function summarizeAuthorizationArgs(operation, args) {
293
429
  for (const [key, value] of Object.entries(args)) {
294
430
  if (value === undefined)
295
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
+ }
296
447
  if (/^(?:self|peer|peerKey|target|targetId|session|sessionId|channelId)$/i.test(key)) {
297
448
  summary[key] = typeof value === 'string' ? redactIdentifier(value) : '[redacted]';
298
449
  continue;
@@ -341,7 +492,7 @@ function protectionClassForPolicy(policyCode) {
341
492
  return 'L';
342
493
  return undefined;
343
494
  }
344
- function extractAuditPath(summary) {
495
+ function extractAuditPath_UNUSED(summary) {
345
496
  if (!summary)
346
497
  return undefined;
347
498
  const match = summary.match(/(?:^|\s)(\/[^\s'"`;]+|[A-Za-z]:[\\/][^\s'"`;]+|(?:src|ecagent|ecweb|scripts|tests?)\/[^\s'"`;]+)/);
@@ -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',
@@ -70,11 +70,11 @@ export function bindPostBootstrapWelcomeOutboxSession(aid, sessionId) {
70
70
  return false;
71
71
  if (entry.context?.sessionId === sessionId)
72
72
  return true;
73
- return outbox.replace(aid, {
73
+ return outbox.replaceIfRouteMatches(aid, entry, {
74
74
  ...entry,
75
75
  context: {
76
76
  ...(entry.context ?? {}),
77
77
  sessionId,
78
78
  },
79
- });
79
+ }) === 'replaced';
80
80
  }
@@ -2,10 +2,10 @@ 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
- import { normalizeAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
8
+ import { resolveAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
9
9
  import { renderTemplate } from '../eck/manifest-engine.js';
10
10
  import { activeBaseagent } from './model/config-scope.js';
11
11
  import { buildEnvelope } from './message/message-utils.js';
@@ -123,12 +123,18 @@ export class BootstrapService {
123
123
  this.inFlight.delete(key);
124
124
  return false;
125
125
  }
126
- const config = normalizeAgentLifecycle(loadedConfig);
127
- if (config.lifecycle === 'active') {
126
+ const lifecycle = resolveAgentLifecycle(loadedConfig);
127
+ if (lifecycle === 'active') {
128
128
  this.inFlight.delete(key);
129
129
  return false;
130
130
  }
131
- const starting = config.lifecycle === 'created';
131
+ if (lifecycle !== 'created' && lifecycle !== 'bootstrapping') {
132
+ this.inFlight.delete(key);
133
+ logger.warn(`[Bootstrap] Refusing invalid lifecycle for ${aid}: ${String(loadedConfig.lifecycle)}`);
134
+ return false;
135
+ }
136
+ const config = { ...loadedConfig, lifecycle };
137
+ const starting = lifecycle === 'created';
132
138
  const channelType = ctx.channelType || this.channelTypeFromKey(ctx.channelKey);
133
139
  const configuredRecipient = this.resolveConfiguredRecipient(config, ctx.channelKey, channelType);
134
140
  const recipientId = ctx.recipientId || configuredRecipient;
@@ -140,35 +146,14 @@ export class BootstrapService {
140
146
  this.inFlight.delete(key);
141
147
  return false;
142
148
  }
143
- // The bootstrap prompt is deliberately a private message to the configured
144
- // owner. An inbound owner message may have arrived from a group, in which
145
- // case ctx.channelId is the group AID and must not be paired with the
146
- // private delivery route below (that would call message.send(to=group)).
147
- // A configured AUN recipient may itself be a group AID. Ask the channel's
148
- // authoritative group endpoint before choosing the transport; never infer
149
- // group/private semantics from the AID string or from the inbound venue.
150
- let isGroupRecipient = false;
151
- try {
152
- isGroupRecipient = channelType === 'aun'
153
- ? (await ctx.adapter.isGroup?.(recipientId)) === true
154
- : false;
155
- }
156
- catch (error) {
157
- // Group/private routing is security-sensitive. If the authoritative
158
- // probe fails, do not guess a route, and release the single-flight key
159
- // so a later connection or retry can recover.
160
- this.inFlight.delete(key);
161
- logger.warn(`[Bootstrap] Failed to resolve recipient type for ${aid}: ${error instanceof Error ? error.message : String(error)}`);
162
- return false;
163
- }
164
- const delivery = isGroupRecipient === true
165
- ? { chatType: 'group', groupId: recipientId }
166
- : { chatType: 'private' };
167
- const channelId = delivery.chatType === 'group'
168
- ? delivery.groupId
169
- : channelType === 'aun'
170
- ? recipientId
171
- : ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
149
+ // Bootstrap is initiated by connection/owner binding, not by a reply to
150
+ // an existing group message. `owners[]` contains personal Owner AIDs, so
151
+ // the first prompt must always use the private message route. Inbound
152
+ // group context must never change this initial delivery target.
153
+ const delivery = { chatType: 'private' };
154
+ const channelId = channelType === 'aun'
155
+ ? recipientId
156
+ : ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
172
157
  if (!channelId) {
173
158
  this.inFlight.delete(key);
174
159
  return false;
@@ -234,14 +219,14 @@ export class BootstrapService {
234
219
  const loadedConfig = loadAgent(aid) || agent?.config;
235
220
  if (!loadedConfig)
236
221
  return { ok: false, error: `Agent "${aid}" not found` };
237
- const lifecycle = normalizeAgentLifecycle(loadedConfig).lifecycle;
222
+ const lifecycle = resolveAgentLifecycle(loadedConfig);
238
223
  if (lifecycle === 'active')
239
224
  return { ok: true, aid, transitioned: false };
240
225
  if (lifecycle !== 'bootstrapping') {
241
226
  return {
242
227
  ok: false,
243
228
  code: 'INVALID_LIFECYCLE',
244
- error: `Agent "${aid}" is ${lifecycle}; only a bootstrapping agent can become ready`,
229
+ error: `Agent "${aid}" is ${lifecycle ?? `invalid (${String(loadedConfig.lifecycle)})`}; only a bootstrapping agent can become ready`,
245
230
  };
246
231
  }
247
232
  this.setLifecycle(agent, aid, 'active');
@@ -253,7 +238,7 @@ export class BootstrapService {
253
238
  const agent = this.agentRegistry.get(aid);
254
239
  const loadedConfig = loadAgent(aid) || agent?.config;
255
240
  return loadedConfig
256
- ? normalizeAgentLifecycle(loadedConfig).lifecycle
241
+ ? resolveAgentLifecycle(loadedConfig)
257
242
  : null;
258
243
  }
259
244
  setLifecycle(agent, aid, lifecycle) {
@@ -307,7 +292,11 @@ export class BootstrapService {
307
292
  if (typeof adapter.uploadAgentMd !== 'function')
308
293
  return;
309
294
  const existing = fs.existsSync(agentMdPath(aid)) ? fs.readFileSync(agentMdPath(aid), 'utf-8') : '';
310
- 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
+ });
311
300
  try {
312
301
  await adapter.uploadAgentMd(content);
313
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
  })));