evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -4,10 +4,17 @@ import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
4
4
  export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
5
5
  export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
6
6
  export class AgentDelegationRegistry {
7
+ runtimeEpoch = crypto.randomBytes(16).toString('hex');
7
8
  grantsByHash = new Map();
8
9
  activeHashBySession = new Map();
9
10
  approvedCommands = new Map();
11
+ getRuntimeEpoch() {
12
+ return this.runtimeEpoch;
13
+ }
10
14
  issue(input) {
15
+ if (input.executionIdentity && !isValidFullAccessExecutionIdentity(input.executionIdentity)) {
16
+ throw new Error('invalid fullaccess task execution identity');
17
+ }
11
18
  this.revokeSession(input.sessionId);
12
19
  const token = crypto.randomBytes(32).toString('base64url');
13
20
  const tokenHash = hashDelegationToken(token);
@@ -21,6 +28,7 @@ export class AgentDelegationRegistry {
21
28
  ok: false,
22
29
  code: 'DELEGATION_REQUIRED',
23
30
  reason: 'An active task delegation token is required',
31
+ diagnosticCause: 'missing_token',
24
32
  };
25
33
  }
26
34
  const tokenHash = hashDelegationToken(token);
@@ -39,6 +47,13 @@ export class AgentDelegationRegistry {
39
47
  ok: false,
40
48
  code: 'INVALID_DELEGATION',
41
49
  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',
42
57
  };
43
58
  }
44
59
  armApprovedCommand(input) {
@@ -134,7 +149,7 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
134
149
  if (!validation.ok)
135
150
  return validation;
136
151
  const grant = validation.grant;
137
- if (grant.selfAid !== input.aid) {
152
+ if (grant.selfAid !== input.aid && !hasTrustedFullAccessDelegation(grant)) {
138
153
  return { ok: false, code: 'INVALID_DELEGATION', reason: 'Delegation self agent does not match sender' };
139
154
  }
140
155
  if (grant.messageId && grant.messageId !== input.messageId) {
@@ -157,6 +172,7 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
157
172
  conversationId: channelId,
158
173
  processOwners: [],
159
174
  fromControlChannel: false,
175
+ ...trustedExecutionIdentityInput(grant),
160
176
  });
161
177
  const subject = { ...builtSubject, peerKey: grant.peerKey };
162
178
  const decision = authorizeOperation({
@@ -180,6 +196,32 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
180
196
  }
181
197
  return { ok: true, grant };
182
198
  }
199
+ /** Build the only AuthGateway input shape accepted for a task-held fullaccess claim. */
200
+ export function trustedExecutionIdentityInput(grant) {
201
+ return grant.executionIdentity && isValidFullAccessExecutionIdentity(grant.executionIdentity)
202
+ ? {
203
+ trustedProcessRole: 'fullaccess-run',
204
+ trustedFullAccessAuthorization: grant.executionIdentity,
205
+ }
206
+ : {};
207
+ }
208
+ export function hasTrustedFullAccessDelegation(grant) {
209
+ return !!grant.executionIdentity && isValidFullAccessExecutionIdentity(grant.executionIdentity);
210
+ }
211
+ function isValidFullAccessExecutionIdentity(identity) {
212
+ if (identity.permissionMode !== 'fullaccess'
213
+ || identity.processRole !== 'fullaccess-run'
214
+ || identity.dataScope !== 'daemon')
215
+ return false;
216
+ if (identity.source === 'fullaccess-command') {
217
+ return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0;
218
+ }
219
+ return identity.source === 'trigger'
220
+ && typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
221
+ && typeof identity.triggerId === 'string' && identity.triggerId.length > 0
222
+ && typeof identity.runId === 'string' && identity.runId.length > 0
223
+ && typeof identity.attemptId === 'string' && identity.attemptId.length > 0;
224
+ }
183
225
  function hashDelegationToken(token) {
184
226
  return crypto.createHash('sha256').update(token).digest('hex');
185
227
  }
@@ -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,9 +188,18 @@ 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,
195
+ errorCode: params.auditMetadata?.errorCode
196
+ ?? (decision.allow
197
+ ? undefined
198
+ : params.auditMetadata?.decisionSource === 'approval'
199
+ ? 'USER_DENIED'
200
+ : params.auditMetadata?.decisionSource === 'infrastructure'
201
+ ? 'EXECUTION_FAILED'
202
+ : 'POLICY_DENIED'),
168
203
  policyCode: params.auditMetadata?.policyCode ?? (decision.allow ? undefined : decision.code),
169
204
  decisionSource: params.auditMetadata?.decisionSource ?? (decision.allow ? 'command' : 'policy'),
170
205
  source: params.source,
@@ -178,6 +213,8 @@ function auditDecision(params, decision) {
178
213
  channelId: params.subject.channelId,
179
214
  role: decision.allow ? decision.role : params.subject.role,
180
215
  processRole: params.subject.processRole,
216
+ dataScope: params.subject.dataScope,
217
+ authorizedBy: params.subject.authorizedBy,
181
218
  isDaemonOwner: params.subject.isDaemonOwner,
182
219
  fromControlChannel: params.subject.fromControlChannel,
183
220
  decision: decision.allow ? 'allow' : 'deny',
@@ -2,13 +2,15 @@ import { logger } from '../../utils/logger.js';
2
2
  import { LogWriter } from '../../utils/log-writer.js';
3
3
  import { resolvePaths } from '../../paths.js';
4
4
  import crypto from 'node:crypto';
5
- import { buildAuthorizationEventKey } from '../audit/event-key.js';
5
+ import { buildAuthorizationEventKey, buildToolLifecycleEventKey } 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,16 +18,97 @@ 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
22
95
  * denials otherwise only reach `logger.warn` and `TriggerExecutionAnomaly`.
23
96
  */
24
97
  export function auditToolPreflightDenial(input) {
98
+ const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
25
99
  auditCommandAuthorization({
26
100
  ts: Date.now(),
27
- callId: input.requestId,
28
- correlationId: input.requestId,
101
+ ...(lifecycleId ? {
102
+ eventKey: buildToolLifecycleEventKey({
103
+ sessionId: input.sessionId,
104
+ callId: input.callId,
105
+ correlationId: input.correlationId,
106
+ requestId: input.requestId,
107
+ }),
108
+ eventPhase: 'preflight',
109
+ } : {}),
110
+ callId: input.callId ?? input.requestId,
111
+ correlationId: input.correlationId ?? input.callId ?? input.requestId,
29
112
  source: 'agent-tool',
30
113
  operation: 'tool.preflight.deny',
31
114
  scope: 'filesystem',
@@ -34,6 +117,7 @@ export function auditToolPreflightDenial(input) {
34
117
  executed: false,
35
118
  executionState: 'blocked',
36
119
  decisionSource: 'policy',
120
+ errorCode: 'POLICY_DENIED',
37
121
  toolName: input.toolName,
38
122
  policyCode: input.policyCode,
39
123
  protectionClass: input.protectionClass ?? protectionClassForPolicy(input.policyCode),
@@ -45,6 +129,7 @@ export function auditToolPreflightDenial(input) {
45
129
  argsSummary: input.summary ? { summary: input.summary } : undefined,
46
130
  sessionId: input.sessionId ?? 'unknown',
47
131
  agentAid: input.agentAid ?? 'unknown',
132
+ agentName: input.agentName,
48
133
  permissionMode: input.permissionMode ?? 'unknown',
49
134
  channel: input.channel,
50
135
  actorId: input.actorId,
@@ -61,9 +146,19 @@ export function auditToolPreflightDenial(input) {
61
146
  * decision source make policy, user, and infrastructure denials distinct.
62
147
  */
63
148
  export function auditCodexApprovalDecision(input) {
149
+ const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
64
150
  auditCommandAuthorization({
65
151
  ts: Date.now(),
66
- callId: input.correlationId ?? input.requestId,
152
+ ...(lifecycleId ? {
153
+ eventKey: buildToolLifecycleEventKey({
154
+ sessionId: input.sessionId,
155
+ callId: input.callId,
156
+ correlationId: input.correlationId,
157
+ requestId: input.requestId,
158
+ }),
159
+ eventPhase: 'approval',
160
+ } : {}),
161
+ callId: input.callId ?? input.correlationId ?? input.requestId,
67
162
  source: 'agent-tool',
68
163
  operation: 'codex.approval',
69
164
  scope: input.toolName === 'PermissionGrant' ? 'agent' : 'filesystem',
@@ -72,10 +167,14 @@ export function auditCodexApprovalDecision(input) {
72
167
  executed: false,
73
168
  executionState: input.decision === 'allow' ? 'authorized' : 'blocked',
74
169
  decisionSource: input.decisionSource,
170
+ errorCode: input.decision === 'deny'
171
+ ? input.decisionSource === 'approval' ? 'USER_DENIED' : input.decisionSource === 'policy' ? 'POLICY_DENIED' : infrastructureErrorCode(input.policyCode)
172
+ : undefined,
75
173
  requestId: input.requestId,
76
- correlationId: input.correlationId ?? input.requestId,
174
+ correlationId: input.correlationId ?? input.callId ?? input.requestId,
77
175
  sessionId: input.sessionId ?? 'unknown',
78
176
  agentAid: input.agentAid ?? 'unknown',
177
+ agentName: input.agentName,
79
178
  permissionMode: input.permissionMode ?? 'unknown',
80
179
  toolName: input.toolName,
81
180
  approvalMethod: input.method,
@@ -84,7 +183,45 @@ export function auditCodexApprovalDecision(input) {
84
183
  reason: input.reason,
85
184
  role: input.role ?? 'unknown',
86
185
  taskId: input.taskId,
87
- argsSummary: { method: input.method },
186
+ argsSummary: {
187
+ method: input.method,
188
+ ...(input.parseFailure ? { parseFailure: input.parseFailure } : {}),
189
+ },
190
+ });
191
+ }
192
+ /**
193
+ * Record a runtime permission decision outside a Codex app-server approval.
194
+ * The shared command-audit stream distinguishes policy, human, and
195
+ * infrastructure denials without parsing localized log text.
196
+ */
197
+ export function auditPermissionDecision(input) {
198
+ auditCommandAuthorization({
199
+ ts: Date.now(),
200
+ callId: input.requestId,
201
+ correlationId: input.requestId,
202
+ requestId: input.requestId,
203
+ sessionId: input.sessionId,
204
+ agentAid: input.agentAid,
205
+ agentName: input.agentName,
206
+ permissionMode: input.permissionMode,
207
+ toolName: input.toolName,
208
+ policyCode: input.policyCode,
209
+ decisionSource: input.decisionSource,
210
+ errorCode: input.decisionSource === 'approval'
211
+ ? 'USER_DENIED'
212
+ : input.decisionSource === 'policy'
213
+ ? 'POLICY_DENIED'
214
+ : infrastructureErrorCode(input.policyCode),
215
+ source: 'agent-tool',
216
+ operation: 'permission.runtime',
217
+ scope: 'filesystem',
218
+ dangerous: false,
219
+ role: input.role ?? 'unknown',
220
+ taskId: input.taskId,
221
+ decision: 'deny',
222
+ executed: false,
223
+ executionState: 'blocked',
224
+ reason: input.reason,
88
225
  });
89
226
  }
90
227
  /** Record a fail-closed infrastructure condition before a tool can execute. */
@@ -97,6 +234,7 @@ export function auditToolInfrastructureFailure(input) {
97
234
  dangerous: false,
98
235
  decision: 'deny',
99
236
  decisionSource: 'infrastructure',
237
+ errorCode: infrastructureErrorCode(input.policyCode),
100
238
  executed: false,
101
239
  executionState: 'blocked',
102
240
  callId: input.callId,
@@ -104,6 +242,7 @@ export function auditToolInfrastructureFailure(input) {
104
242
  requestId: input.callId,
105
243
  sessionId: input.sessionId ?? 'unknown',
106
244
  agentAid: input.agentAid ?? 'unknown',
245
+ agentName: input.agentName,
107
246
  permissionMode: input.permissionMode ?? 'unknown',
108
247
  toolName: input.toolName,
109
248
  policyCode: input.policyCode,
@@ -115,8 +254,26 @@ export function auditToolInfrastructureFailure(input) {
115
254
  : undefined,
116
255
  });
117
256
  }
257
+ function infrastructureErrorCode(policyCode) {
258
+ const code = String(policyCode ?? '').toLowerCase();
259
+ if (/delegat|carrier|not.?armed/.test(code))
260
+ return 'DELEGATION_FAILED';
261
+ if (/capability|sandbox|unavailable|workspace_unknown/.test(code))
262
+ return 'CAPABILITY_UNAVAILABLE';
263
+ if (/argument|canonical|quote|shell|parse|invalid/.test(code))
264
+ return 'INVALID_ARGUMENT';
265
+ return 'EXECUTION_FAILED';
266
+ }
118
267
  function buildAuditRecord(event) {
119
268
  const executed = event.executed ?? false;
269
+ const decisionSource = event.decisionSource ?? (event.decision === 'deny' ? 'policy' : 'command');
270
+ const errorCode = event.errorCode ?? (event.decision === 'deny'
271
+ ? decisionSource === 'approval'
272
+ ? 'USER_DENIED'
273
+ : decisionSource === 'policy'
274
+ ? 'POLICY_DENIED'
275
+ : 'EXECUTION_FAILED'
276
+ : undefined);
120
277
  const executionState = event.executionState
121
278
  ?? (event.decision === 'deny'
122
279
  ? 'blocked'
@@ -135,18 +292,25 @@ function buildAuditRecord(event) {
135
292
  operation: event.operation,
136
293
  })
137
294
  : undefined),
295
+ eventPhase: event.eventPhase,
138
296
  callId: event.callId,
139
297
  correlationId: event.correlationId ?? event.requestId,
140
298
  requestId: event.requestId,
141
299
  sessionId: event.sessionId,
142
300
  agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
143
- permissionMode: event.permissionMode,
301
+ agentName: resolveAgentName(event),
302
+ permissionMode: normalizeAuditPermissionMode(event.permissionMode),
144
303
  toolName: event.toolName,
145
304
  approvalMethod: event.approvalMethod,
305
+ errorCode,
146
306
  policyCode: event.policyCode ?? event.code,
147
307
  protectionClass: event.protectionClass,
148
308
  matchedPath: event.matchedPath,
149
- decisionSource: event.decisionSource,
309
+ decisionSource,
310
+ ecwebRequestId: event.ecwebRequestId,
311
+ targetAgentAid: redactIdentifier(event.targetAgentAid),
312
+ endpoint: event.endpoint,
313
+ logicalToolName: event.logicalToolName,
150
314
  source: event.source,
151
315
  operation: event.operation,
152
316
  scope: event.scope,
@@ -161,6 +325,8 @@ function buildAuditRecord(event) {
161
325
  channelId: redactIdentifier(event.channelId),
162
326
  role: event.role,
163
327
  processRole: event.processRole,
328
+ dataScope: event.dataScope,
329
+ authorizedBy: redactIdentifier(event.authorizedBy),
164
330
  isDaemonOwner: event.isDaemonOwner,
165
331
  fromControlChannel: event.fromControlChannel,
166
332
  taskId: event.taskId,
@@ -177,6 +343,26 @@ function buildAuditRecord(event) {
177
343
  exitCode: event.exitCode,
178
344
  };
179
345
  }
346
+ function resolveAgentName(event) {
347
+ const displayName = typeof event.agentName === 'string' ? event.agentName.trim() : '';
348
+ if (displayName && !isUnknownAgentLabel(displayName))
349
+ return displayName;
350
+ const aid = typeof event.agentAid === 'string' ? event.agentAid.trim() : '';
351
+ if (aid && aid !== 'unknown')
352
+ return aid;
353
+ const selfAid = typeof event.selfAid === 'string' ? event.selfAid.trim() : '';
354
+ if (selfAid && selfAid !== 'unknown')
355
+ return selfAid;
356
+ return aid === 'unknown' || selfAid === 'unknown' ? 'unknown' : undefined;
357
+ }
358
+ function isUnknownAgentLabel(value) {
359
+ return value === 'unknown' || value === '<unknown>' || value === '<none>' || value === 'undefined' || value === 'null';
360
+ }
361
+ function normalizeAuditPermissionMode(value) {
362
+ if (!value || value === 'unknown')
363
+ return value;
364
+ return normalizeExecutionPermissionMode(value);
365
+ }
180
366
  function redactIdentifier(value) {
181
367
  if (!value)
182
368
  return undefined;
@@ -211,10 +397,17 @@ function logAuditEvent(record) {
211
397
  record.callId ? `call=${record.callId}` : null,
212
398
  record.correlationId ? `correlation=${record.correlationId}` : null,
213
399
  record.eventKey ? `eventKey=${record.eventKey}` : null,
400
+ record.eventPhase ? `eventPhase=${record.eventPhase}` : null,
214
401
  record.sessionId ? `session=${record.sessionId}` : null,
402
+ record.agentName ? `agentName=${JSON.stringify(record.agentName)}` : null,
215
403
  record.agentAid ? `agent=${record.agentAid}` : null,
216
404
  record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
217
405
  record.toolName ? `tool=${record.toolName}` : null,
406
+ record.errorCode ? `errorCode=${record.errorCode}` : null,
407
+ record.ecwebRequestId ? `ecwebRequest=${record.ecwebRequestId}` : null,
408
+ record.targetAgentAid ? `targetAgent=${record.targetAgentAid}` : null,
409
+ record.endpoint ? `endpoint=${JSON.stringify(record.endpoint)}` : null,
410
+ record.logicalToolName ? `logicalTool=${record.logicalToolName}` : null,
218
411
  record.policyCode ? `policy=${record.policyCode}` : null,
219
412
  record.protectionClass ? `protectionClass=${record.protectionClass}` : null,
220
413
  record.matchedPath ? `matchedPath=${JSON.stringify(record.matchedPath)}` : null,
@@ -296,6 +489,21 @@ function summarizeAuthorizationArgs(operation, args) {
296
489
  for (const [key, value] of Object.entries(args)) {
297
490
  if (value === undefined)
298
491
  continue;
492
+ if (key === 'parseFailure' && value && typeof value === 'object' && !Array.isArray(value)) {
493
+ const diagnostic = value;
494
+ const issue = typeof diagnostic.issue === 'string' ? diagnostic.issue : undefined;
495
+ const offset = typeof diagnostic.offset === 'number' && Number.isFinite(diagnostic.offset)
496
+ ? Math.max(0, Math.floor(diagnostic.offset)) : undefined;
497
+ const tokenIndex = typeof diagnostic.tokenIndex === 'number' && Number.isFinite(diagnostic.tokenIndex)
498
+ ? Math.max(0, Math.floor(diagnostic.tokenIndex)) : undefined;
499
+ const dialect = typeof diagnostic.dialect === 'string' ? diagnostic.dialect : undefined;
500
+ const inputLength = typeof diagnostic.inputLength === 'number' && Number.isFinite(diagnostic.inputLength)
501
+ ? Math.max(0, Math.floor(diagnostic.inputLength)) : undefined;
502
+ if (issue && offset !== undefined && tokenIndex !== undefined && dialect && inputLength !== undefined) {
503
+ summary.parseFailure = { issue, offset, tokenIndex, dialect, inputLength };
504
+ }
505
+ continue;
506
+ }
299
507
  if (/^(?:self|peer|peerKey|target|targetId|session|sessionId|channelId)$/i.test(key)) {
300
508
  summary[key] = typeof value === 'string' ? redactIdentifier(value) : '[redacted]';
301
509
  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',
@@ -7,9 +7,17 @@ import { chatDirPath } from './session/session-fs-store.js';
7
7
  import { hasMessageLogOperation } from './message/message-log.js';
8
8
  import { isDeliveryTarget, sameDeliveryTarget } from './message/message-utils.js';
9
9
  export const BOOTSTRAP_MESSAGE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
10
+ export const BOOTSTRAP_NOTIFICATION_WINDOW_MS = 24 * 60 * 60 * 1000;
10
11
  export function bootstrapInitialMessageOperationId(aid) {
11
12
  return `bootstrap-initial:v1:${aid}`;
12
13
  }
14
+ export function bootstrapInitialMessageOperationPrefix(aid) {
15
+ return `bootstrap-initial:v1:${aid}`;
16
+ }
17
+ /** A new operation ID is required after the notification window expires. */
18
+ export function bootstrapInitialRetryOperationId(aid, at = Date.now()) {
19
+ return `${bootstrapInitialMessageOperationPrefix(aid)}:${at}`;
20
+ }
13
21
  export function postBootstrapWelcomeOperationId(aid) {
14
22
  return `bootstrap-complete:v1:${aid}`;
15
23
  }