evolcore 0.0.26 → 0.0.27

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 (169) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/claude-runner.js +123 -22
  5. package/dist/agents/codex/app-server-client.js +63 -26
  6. package/dist/agents/codex-runner.js +316 -411
  7. package/dist/agents/ecagent-runner.js +170 -25
  8. package/dist/agents/gemini-runner.js +6 -0
  9. package/dist/agents/runner-types.js +50 -2
  10. package/dist/aun/aid/agentmd.js +73 -31
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/control-aid.js +180 -1
  13. package/dist/aun/aid/identity.js +3 -3
  14. package/dist/aun/group/history.js +303 -0
  15. package/dist/aun/group/identity.js +43 -0
  16. package/dist/aun/group/index.js +5 -0
  17. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  18. package/dist/aun/{menu-client.js → menu/client.js} +12 -7
  19. package/dist/aun/menu/index.js +2 -0
  20. package/dist/aun/menu/transport.js +49 -0
  21. package/dist/aun/msg/index.js +2 -1
  22. package/dist/aun/msg/managed-operation.js +84 -18
  23. package/dist/aun/msg/mention-schema.js +51 -0
  24. package/dist/aun/msg/p2p.js +13 -8
  25. package/dist/aun/msg/upload.js +43 -88
  26. package/dist/aun/outbox.js +29 -7
  27. package/dist/aun/rpc/client.js +1 -0
  28. package/dist/aun/storage/client.js +39 -50
  29. package/dist/channels/aun.js +522 -233
  30. package/dist/channels/daemon.js +10 -6
  31. package/dist/cli/agent-command.js +108 -6
  32. package/dist/cli/agent-create.js +515 -2
  33. package/dist/cli/agent.js +109 -8
  34. package/dist/cli/aun-commands.js +191 -42
  35. package/dist/cli/config.js +8 -5
  36. package/dist/cli/contact.js +2 -2
  37. package/dist/cli/ctl-command.js +2 -2
  38. package/dist/cli/daemon-commands.js +35 -23
  39. package/dist/cli/diag.js +2 -2
  40. package/dist/cli/fs-command.js +3 -3
  41. package/dist/cli/handoff-command.js +6 -6
  42. package/dist/cli/index.js +8 -2
  43. package/dist/cli/init.js +34 -1
  44. package/dist/cli/managed-command-guard.js +2 -1
  45. package/dist/cli/menu-client.js +9 -5
  46. package/dist/cli/model.js +3 -3
  47. package/dist/cli/queue-command.js +2 -2
  48. package/dist/cli/response.js +2 -2
  49. package/dist/cli/restart-monitor.js +16 -2
  50. package/dist/cli/task-command.js +59 -0
  51. package/dist/cli/task-context.js +97 -49
  52. package/dist/cli/trigger-command.js +20 -10
  53. package/dist/config/builtin-roles.js +10 -0
  54. package/dist/config/config-field-policy.js +11 -0
  55. package/dist/config/config-manager.js +32 -4
  56. package/dist/config/config-operation-service.js +20 -5
  57. package/dist/config/contact-book-store.js +1 -1
  58. package/dist/config/contact-book.js +1 -1
  59. package/dist/config/contact-operation-service.js +1 -1
  60. package/dist/config/peer-role-resolver.js +1 -1
  61. package/dist/config/resolved-config-op.js +3 -0
  62. package/dist/config-store.js +1 -1
  63. package/dist/core/agent-application-service.js +5 -0
  64. package/dist/core/audit/event-key.js +17 -2
  65. package/dist/core/audit/log-integrity.js +34 -8
  66. package/dist/core/auth/agent-delegation.js +48 -14
  67. package/dist/core/auth/auth-gateway.js +2 -0
  68. package/dist/core/auth/authorization-audit.js +34 -38
  69. package/dist/core/auth/operation-authorizer.js +67 -5
  70. package/dist/core/auth/operation-catalog.js +110 -0
  71. package/dist/core/auth/remote-access-store.js +115 -9
  72. package/dist/core/bootstrap-messages.js +66 -7
  73. package/dist/core/bootstrap-service.js +110 -19
  74. package/dist/core/command/agent-control.js +88 -7
  75. package/dist/core/command/agent-storage-menu.js +443 -0
  76. package/dist/core/command/cli-intent-parser.js +19 -0
  77. package/dist/core/command/command-handler.js +76 -39
  78. package/dist/core/command/data-root-resolver.js +273 -0
  79. package/dist/core/command/group-menu.js +7 -2
  80. package/dist/core/command/menu-catalog.js +34 -4
  81. package/dist/core/command/menu-handler.js +348 -77
  82. package/dist/core/command/menu-protocol.js +40 -6
  83. package/dist/core/command/role-menu.js +2 -2
  84. package/dist/core/command/slash-handler.js +37 -22
  85. package/dist/core/event-catalog.js +4 -0
  86. package/dist/core/evolagent-registry.js +28 -2
  87. package/dist/core/group/invite-service.js +924 -0
  88. package/dist/core/interaction-router.js +22 -8
  89. package/dist/core/message/im-renderer.js +76 -15
  90. package/dist/core/message/message-bridge.js +94 -11
  91. package/dist/core/message/message-log.js +5 -3
  92. package/dist/core/message/message-queue.js +73 -41
  93. package/dist/core/message/response-engine.js +709 -117
  94. package/dist/core/permission/approval-gateway.js +36 -3
  95. package/dist/core/permission/ec-command-parser.js +61 -29
  96. package/dist/core/permission/protected-paths.js +134 -16
  97. package/dist/core/permission/sandbox-runtime.js +72 -3
  98. package/dist/core/permission/tool-error-code.js +15 -0
  99. package/dist/core/permission/tool-policy.js +7 -7
  100. package/dist/core/relation/peer-identity.js +57 -6
  101. package/dist/core/relation/shared-data.js +106 -0
  102. package/dist/core/role/runtime-policy.js +64 -0
  103. package/dist/core/runtime-lock.js +10 -26
  104. package/dist/core/session/runner-task-history.js +170 -0
  105. package/dist/core/session/session-manager.js +34 -6
  106. package/dist/core/session/session-mapper.js +41 -0
  107. package/dist/core/session/session-turn-coordinator.js +37 -7
  108. package/dist/core/session/session-turns.js +48 -7
  109. package/dist/core/task/task-recovery-service.js +176 -0
  110. package/dist/core/task/task-resume-operation.js +32 -0
  111. package/dist/eck/group-rules-sync.js +5 -3
  112. package/dist/eck/kit-renderer.js +1 -0
  113. package/dist/index.js +308 -54
  114. package/dist/ipc.js +141 -15
  115. package/dist/paths.js +17 -4
  116. package/dist/product.js +2 -0
  117. package/dist/trigger/event-source.js +19 -0
  118. package/dist/trigger/feedback.js +233 -40
  119. package/dist/trigger/parser.js +55 -5
  120. package/dist/trigger/patch.js +9 -0
  121. package/dist/trigger/scheduler.js +30 -4
  122. package/dist/trigger/validation.js +20 -3
  123. package/dist/utils/ecweb-utils.js +1 -0
  124. package/dist/utils/error-utils.js +40 -2
  125. package/dist/utils/logger.js +23 -13
  126. package/dist/utils/tool-summary.js +59 -3
  127. package/dist/utils/welcome.js +174 -1
  128. package/ecagent/dist/harness/agent-harness.js +3 -0
  129. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  130. package/kits/docs/INDEX.md +0 -6
  131. package/kits/docs/channels/aun.md +15 -2
  132. package/kits/docs/channels/feishu.md +1 -1
  133. package/kits/docs/context-assembly.md +29 -17
  134. package/kits/docs/evolcore/INDEX.md +8 -3
  135. package/kits/docs/evolcore/agent-create.md +39 -7
  136. package/kits/docs/evolcore/agent.md +33 -1
  137. package/kits/docs/evolcore/aid.md +9 -1
  138. package/kits/docs/evolcore/config.md +40 -1
  139. package/kits/docs/evolcore/group-collaboration.md +1 -1
  140. package/kits/docs/evolcore/group.md +117 -2
  141. package/kits/docs/evolcore/menu-client.md +16 -0
  142. package/kits/docs/evolcore/msg.md +3 -0
  143. package/kits/docs/evolcore/storage.md +3 -0
  144. package/kits/docs/evolcore/task.md +14 -0
  145. package/kits/docs/evolcore/trigger.md +51 -3
  146. package/kits/docs/venues/group.md +5 -0
  147. package/kits/eck_manifest.json +27 -0
  148. package/kits/rules/01-overview.md +11 -7
  149. package/kits/rules/04-relation.md +2 -1
  150. package/kits/rules/05-venue.md +1 -1
  151. package/kits/rules/06-channel.md +6 -4
  152. package/kits/schemas/agent-config.schema.12.json +25 -1
  153. package/kits/schemas/daemon.schema.6.json +2 -2
  154. package/kits/schemas/defaults.schema.5.json +1 -1
  155. package/kits/schemas/relation-config.schema.1.json +1 -1
  156. package/kits/schemas/relation-config.schema.5.json +1 -1
  157. package/kits/schemas/role-config.schema.1.json +14 -0
  158. package/kits/templates/roles/member.json +5 -0
  159. package/kits/templates/roles/visitor.json +5 -0
  160. package/kits/templates/system-fragments/baseagent.md +1 -1
  161. package/kits/templates/system-fragments/bootstrap.md +26 -9
  162. package/kits/templates/system-fragments/channel.md +8 -44
  163. package/kits/templates/system-fragments/commands.md +10 -2
  164. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  165. package/kits/templates/system-fragments/reply.md +56 -0
  166. package/package.json +7 -3
  167. package/dist/aun/group-identity.js +0 -9
  168. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  169. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { buildToolLifecycleEventKey, normalizeToolCorrelation } from './event-key.js';
4
5
  // Logger rotation is hourly/daily, but the suffix is intentionally generic so
5
6
  // this check also covers future rotation formats without changing the audit.
6
7
  const STRUCTURED_LOG_RE = /^(?:daemon|events|channel-in|channel-out|messages|command-audit)(?:-[^.]+)?\.log$/;
@@ -33,6 +34,7 @@ export function inspectStructuredLogs(logDir, options = {}) {
33
34
  // daemon logs mix human-readable entries with structured audit lines.
34
35
  // Ignore the former, but still validate malformed structured payloads.
35
36
  if (MIXED_TEXT_LOG_RE.test(file)) {
37
+ raw = raw.replace(/^\[[^\]]+\] \[(?:DEBUG|INFO|WARN|ERROR)\] /, '');
36
38
  if (raw.startsWith('[CommandAuditRecord] '))
37
39
  raw = raw.slice('[CommandAuditRecord] '.length);
38
40
  else if (!raw.startsWith('{'))
@@ -57,8 +59,9 @@ export function inspectStructuredLogs(logDir, options = {}) {
57
59
  issues.push({ file, line: index + 1, reason: 'missing_agent', key });
58
60
  if (isLifecycleRecord(value) && !hasContextValue(lifecycleField(value, 'permissionMode')))
59
61
  issues.push({ file, line: index + 1, reason: 'missing_permission_mode', key });
60
- if (isCorrelatableRecord(value) && !key)
62
+ if (isCorrelatableRecord(value) && (!key || value.correlationMissing === true || key.startsWith('tool:unlinked:'))) {
61
63
  issues.push({ file, line: index + 1, reason: 'missing_correlation' });
64
+ }
62
65
  });
63
66
  }
64
67
  const seen = new Set();
@@ -79,16 +82,33 @@ export function inspectStructuredLogs(logDir, options = {}) {
79
82
  function structuredRecordKey(value) {
80
83
  const nested = nestedRecord(value);
81
84
  const eventKey = firstDefined(value, nested, ['eventKey', 'event_key']);
82
- const eventPhase = firstDefined(value, nested, ['eventPhase', 'event_phase']);
85
+ const eventPhase = recordPhase(value);
83
86
  if (eventKey)
84
87
  return `${String(eventKey)}${eventPhase ? `:${String(eventPhase)}` : ''}`;
88
+ if (isLifecycleRecord(value)) {
89
+ const correlation = normalizeToolCorrelation({
90
+ callId: firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
91
+ correlationId: firstDefined(value, nested, ['correlationId', 'correlation_id']),
92
+ requestId: firstDefined(value, nested, ['requestId', 'request_id']),
93
+ });
94
+ if (!correlation.callId)
95
+ return undefined;
96
+ return `${buildToolLifecycleEventKey({
97
+ sessionId: lifecycleField(value, 'sessionId'),
98
+ ...correlation,
99
+ })}:${eventPhase}`;
100
+ }
85
101
  const correlation = firstDefined(value, nested, [
86
102
  'correlationId', 'correlation_id', 'callId', 'call_id', 'toolUseId', 'tool_use_id',
87
103
  'requestId', 'request_id', 'msgId', 'operationId',
88
104
  ]);
89
105
  if (!correlation)
90
106
  return undefined;
91
- return `${canonicalEventType(value)}:${String(correlation)}`;
107
+ return JSON.stringify([lifecycleField(value, 'sessionId'), canonicalEventType(value), correlation, eventPhase]);
108
+ }
109
+ function recordPhase(value) {
110
+ return firstDefined(value, nestedRecord(value), ['eventPhase', 'event_phase'])
111
+ ?? (canonicalEventType(value) === 'tool_use' ? 'use' : canonicalEventType(value) === 'tool_result' ? 'result' : undefined);
92
112
  }
93
113
  function isLifecycleRecord(value) {
94
114
  const type = canonicalEventType(value);
@@ -101,11 +121,13 @@ function isCorrelatableRecord(value) {
101
121
  }
102
122
  function stableHash(value) {
103
123
  const nested = nestedRecord(value);
104
- const eventKey = firstDefined(value, nested, ['eventKey', 'event_key']);
124
+ const eventKey = structuredRecordKey(value);
105
125
  const canonical = {
106
126
  eventKey,
107
- eventPhase: firstDefined(value, nested, ['eventPhase', 'event_phase']),
127
+ eventPhase: recordPhase(value),
108
128
  type: canonicalEventType(value),
129
+ operation: firstDefined(value, nested, ['operation']),
130
+ targetAgentAid: firstDefined(value, nested, ['targetAgentAid']),
109
131
  toolName: firstDefined(value, nested, ['toolName', 'tool', 'name']),
110
132
  callId: eventKey ? undefined : firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
111
133
  correlationId: eventKey ? undefined : firstDefined(value, nested, ['correlationId', 'correlation_id']),
@@ -116,11 +138,15 @@ function stableHash(value) {
116
138
  isError: firstDefined(value, nested, ['isError', 'is_error']) ?? (value.ok === false || nested?.ok === false ? true : undefined),
117
139
  errorCode: firstDefined(value, nested, ['errorCode', 'error_code']),
118
140
  decision: firstDefined(value, nested, ['decision']),
141
+ decisionSource: firstDefined(value, nested, ['decisionSource', 'decision_source']),
142
+ policyCode: firstDefined(value, nested, ['policyCode', 'policy_code']),
119
143
  executed: firstDefined(value, nested, ['executed']),
120
144
  executionState: firstDefined(value, nested, ['executionState', 'execution_state']),
121
- input: firstDefined(value, nested, ['input']),
122
- result: firstDefined(value, nested, ['result', 'content']),
123
- error: firstDefined(value, nested, ['error', 'errorMessage']),
145
+ // A lifecycle stage may be mirrored without its large input/output body.
146
+ // Keep decision/classification differences, not projection payload size.
147
+ input: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['input']),
148
+ result: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['result', 'content']),
149
+ error: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['error', 'errorMessage']),
124
150
  };
125
151
  return crypto.createHash('sha256').update(JSON.stringify(canonical)).digest('hex').slice(0, 16);
126
152
  }
@@ -1,9 +1,22 @@
1
+ import { assertAgentPolicyAuthorization } from '../role/runtime-policy.js';
1
2
  import crypto from 'crypto';
2
3
  import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
3
4
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
4
5
  import { logger } from '../../utils/logger.js';
5
6
  export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
7
+ export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
6
8
  export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
9
+ /**
10
+ * Resolve the credential carried by an EC CLI process. A live task token has
11
+ * priority; persistent Codex threads fall back to their session credential.
12
+ */
13
+ export function readDelegationCredential(env = process.env) {
14
+ const taskToken = env[AGENT_DELEGATION_TOKEN_ENV]?.trim();
15
+ if (taskToken)
16
+ return taskToken;
17
+ const sessionToken = env[CODEX_SESSION_TOKEN_ENV]?.trim();
18
+ return sessionToken || undefined;
19
+ }
7
20
  export class AgentDelegationRegistry {
8
21
  runtimeEpoch = crypto.randomBytes(16).toString('hex');
9
22
  grantsByHash = new Map();
@@ -24,6 +37,13 @@ export class AgentDelegationRegistry {
24
37
  return token;
25
38
  }
26
39
  validate(token, sessionId, commandHash) {
40
+ return this.validateToken(token, sessionId, commandHash, true);
41
+ }
42
+ /** Authenticate a CLI context lookup without spending its approved operation. */
43
+ validateForContext(token, sessionId, commandHash) {
44
+ return this.validateToken(token, sessionId, commandHash, false);
45
+ }
46
+ validateToken(token, sessionId, commandHash, consumeApproval) {
27
47
  if (!token) {
28
48
  return {
29
49
  ok: false,
@@ -37,10 +57,19 @@ export class AgentDelegationRegistry {
37
57
  if (grant
38
58
  && grant.sessionId === sessionId
39
59
  && this.activeHashBySession.get(sessionId) === tokenHash) {
60
+ if (grant.executionIdentity?.source === 'agent-policy') {
61
+ try {
62
+ assertAgentPolicyAuthorization(grant.executionIdentity);
63
+ }
64
+ catch {
65
+ this.revokeSession(sessionId);
66
+ return { ok: false, code: 'INVALID_DELEGATION', reason: 'Agent fullaccess policy was revoked', diagnosticCause: 'task_replaced' };
67
+ }
68
+ }
40
69
  return { ok: true, grant };
41
70
  }
42
71
  const approved = commandHash
43
- ? this.consumeApprovedCommand(tokenHash, sessionId, commandHash)
72
+ ? this.readApprovedCommand(tokenHash, sessionId, commandHash, consumeApproval)
44
73
  : undefined;
45
74
  if (approved)
46
75
  return { ok: true, grant: approved };
@@ -66,11 +95,12 @@ export class AgentDelegationRegistry {
66
95
  const grant = activeTokenHash ? this.grantsByHash.get(activeTokenHash) : undefined;
67
96
  if (!grant || grant.taskId !== input.taskId)
68
97
  return false;
69
- if (!input.carrierToken || !isDelegationCommandHash(input.commandHash))
98
+ const sessionToken = input.sessionToken ?? input.carrierToken;
99
+ if (!sessionToken || !isDelegationCommandHash(input.commandHash))
70
100
  return false;
71
101
  this.deleteExpiredApprovedCommands();
72
- const carrierTokenHash = hashDelegationToken(input.carrierToken);
73
- const key = approvedCommandKey(carrierTokenHash, input.sessionId, input.commandHash);
102
+ const sessionTokenHash = hashDelegationToken(sessionToken);
103
+ const key = approvedCommandKey(sessionTokenHash, input.sessionId, input.commandHash);
74
104
  const existing = this.approvedCommands.get(key);
75
105
  const ttlMs = Math.max(1, Math.min(input.ttlMs ?? AGENT_DELEGATION_COMMAND_TTL_MS, AGENT_DELEGATION_COMMAND_TTL_MS));
76
106
  this.approvedCommands.set(key, {
@@ -105,10 +135,10 @@ export class AgentDelegationRegistry {
105
135
  this.activeHashBySession.delete(sessionId);
106
136
  this.deleteApprovedCommandsForSession(sessionId);
107
137
  }
108
- consumeApprovedCommand(carrierTokenHash, sessionId, commandHash) {
138
+ readApprovedCommand(sessionTokenHash, sessionId, commandHash, consume) {
109
139
  if (!isDelegationCommandHash(commandHash))
110
140
  return undefined;
111
- const key = approvedCommandKey(carrierTokenHash, sessionId, commandHash);
141
+ const key = approvedCommandKey(sessionTokenHash, sessionId, commandHash);
112
142
  const approved = this.approvedCommands.get(key);
113
143
  if (!approved)
114
144
  return undefined;
@@ -124,10 +154,12 @@ export class AgentDelegationRegistry {
124
154
  this.approvedCommands.delete(key);
125
155
  return undefined;
126
156
  }
127
- if (approved.remainingUses <= 1)
128
- this.approvedCommands.delete(key);
129
- else
130
- approved.remainingUses--;
157
+ if (consume) {
158
+ if (approved.remainingUses <= 1)
159
+ this.approvedCommands.delete(key);
160
+ else
161
+ approved.remainingUses--;
162
+ }
131
163
  return grant;
132
164
  }
133
165
  deleteExpiredApprovedCommands() {
@@ -216,8 +248,10 @@ function isValidFullAccessExecutionIdentity(identity) {
216
248
  || identity.processRole !== 'fullaccess-run'
217
249
  || identity.dataScope !== 'daemon')
218
250
  return false;
219
- if (identity.source === 'fullaccess-command') {
220
- return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0;
251
+ if (identity.source === 'fullaccess-command' || identity.source === 'agent-policy') {
252
+ return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
253
+ && (identity.source !== 'agent-policy'
254
+ || (typeof identity.agentAid === 'string' && identity.agentAid.length > 0));
221
255
  }
222
256
  return identity.source === 'trigger'
223
257
  && typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
@@ -228,8 +262,8 @@ function isValidFullAccessExecutionIdentity(identity) {
228
262
  function hashDelegationToken(token) {
229
263
  return crypto.createHash('sha256').update(token).digest('hex');
230
264
  }
231
- function approvedCommandKey(carrierTokenHash, sessionId, commandHash) {
232
- return JSON.stringify([carrierTokenHash, sessionId, commandHash]);
265
+ function approvedCommandKey(sessionTokenHash, sessionId, commandHash) {
266
+ return JSON.stringify([sessionTokenHash, sessionId, commandHash]);
233
267
  }
234
268
  function isDelegationCommandHash(value) {
235
269
  return /^[a-f0-9]{64}$/.test(value);
@@ -70,6 +70,7 @@ export function buildAuthSubject(input) {
70
70
  identity: input.identity ?? roleToSessionIdentity(role === 'none' ? null : role),
71
71
  isDaemonOwner,
72
72
  fromControlChannel: !!input.fromControlChannel,
73
+ ecwebManagement: input.ecwebManagement === true && isTrustedDaemonOwner && input.fromControlChannel === true,
73
74
  allowAccess: processRole !== 'none' || (suppliedRole
74
75
  ? checkRoleAccess(suppliedRole, input.selfAid)
75
76
  : roleDetail.allowAccess && checkRoleAccess(role, input.selfAid)),
@@ -114,6 +115,7 @@ export function authorizeOperation(params) {
114
115
  return access;
115
116
  }
116
117
  const ctx = {
118
+ principalId: params.subject.principalId,
117
119
  intent: params.intent,
118
120
  actorId: params.subject.actorId,
119
121
  channel: params.subject.channel,
@@ -2,7 +2,7 @@ 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, buildToolLifecycleEventKey } from '../audit/event-key.js';
5
+ import { buildAuthorizationEventKey, buildToolLifecycleEventKey, normalizeToolCorrelation } from '../audit/event-key.js';
6
6
  import { normalizeExecutionPermissionMode } from '../permission/mode.js';
7
7
  export function auditCommandAuthorization(event) {
8
8
  const shouldAudit = event.source === 'menu.cli' ||
@@ -14,6 +14,8 @@ export function auditCommandAuthorization(event) {
14
14
  event.operation === 'permission.runtime' ||
15
15
  event.operation.startsWith('fullaccess.') ||
16
16
  event.operation.startsWith('role.') ||
17
+ event.operation === 'file.root.storage' ||
18
+ event.operation.startsWith('file.storage.') ||
17
19
  event.operation === 'cli.exec.raw';
18
20
  if (!shouldAudit)
19
21
  return;
@@ -25,6 +27,8 @@ export function auditFullAccessEvent(input) {
25
27
  || input.event === 'fullaccess.trigger.deescalated';
26
28
  const executionEvent = input.event === 'fullaccess.execution.started'
27
29
  || input.event === 'fullaccess.execution.ended'
30
+ || input.event === 'fullaccess.agent-policy.execution.started'
31
+ || input.event === 'fullaccess.agent-policy.execution.ended'
28
32
  || input.event === 'fullaccess.trigger.execution.started'
29
33
  || input.event === 'fullaccess.trigger.execution.ended';
30
34
  // The scheduler triggers a run, but the executing subject is the task-bound
@@ -39,7 +43,7 @@ export function auditFullAccessEvent(input) {
39
43
  ts: Date.now(),
40
44
  callId: input.attemptId ?? input.runId ?? input.taskId ?? input.messageId,
41
45
  correlationId: input.runId ?? input.taskId,
42
- source: input.source === 'trigger' ? 'agent-tool' : 'slash',
46
+ source: input.source === 'fullaccess-command' ? 'slash' : 'agent-tool',
43
47
  operation: input.event,
44
48
  // CommandScope has no separate daemon value; process is the closest
45
49
  // existing scope for daemon-owner authorization and keeps audit records
@@ -97,20 +101,11 @@ export function auditFullAccessEvent(input) {
97
101
  * denials otherwise only reach `logger.warn` and `TriggerExecutionAnomaly`.
98
102
  */
99
103
  export function auditToolPreflightDenial(input) {
100
- const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
101
104
  auditCommandAuthorization({
102
105
  ts: Date.now(),
103
- ...(lifecycleId ? {
104
- eventKey: buildToolLifecycleEventKey({
105
- sessionId: input.sessionId,
106
- callId: input.callId,
107
- correlationId: input.correlationId,
108
- requestId: input.requestId,
109
- }),
110
- eventPhase: 'preflight',
111
- } : {}),
112
- callId: input.callId ?? input.requestId,
113
- correlationId: input.correlationId ?? input.callId ?? input.requestId,
106
+ eventKey: buildToolLifecycleEventKey(input),
107
+ eventPhase: 'preflight',
108
+ ...normalizeToolCorrelation(input),
114
109
  source: 'agent-tool',
115
110
  operation: 'tool.preflight.deny',
116
111
  scope: 'filesystem',
@@ -138,29 +133,20 @@ export function auditToolPreflightDenial(input) {
138
133
  role: input.role ?? 'unknown',
139
134
  selfAid: input.selfAid,
140
135
  peerKey: input.peerKey,
141
- requestId: input.requestId,
142
136
  taskId: input.taskId,
143
137
  });
144
138
  }
145
139
  /**
146
140
  * Emit one canonical, structured record for every Codex app-server approval
147
- * decision. The app-server's request id is the lifecycle key; method and
141
+ * decision. Prefer the tool call id over the approval request id; method and
148
142
  * decision source make policy, user, and infrastructure denials distinct.
149
143
  */
150
144
  export function auditCodexApprovalDecision(input) {
151
- const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
152
145
  auditCommandAuthorization({
153
146
  ts: Date.now(),
154
- ...(lifecycleId ? {
155
- eventKey: buildToolLifecycleEventKey({
156
- sessionId: input.sessionId,
157
- callId: input.callId,
158
- correlationId: input.correlationId,
159
- requestId: input.requestId,
160
- }),
161
- eventPhase: 'approval',
162
- } : {}),
163
- callId: input.callId ?? input.correlationId ?? input.requestId,
147
+ eventKey: buildToolLifecycleEventKey(input),
148
+ eventPhase: 'approval',
149
+ ...normalizeToolCorrelation(input),
164
150
  source: 'agent-tool',
165
151
  operation: 'codex.approval',
166
152
  scope: input.toolName === 'PermissionGrant' ? 'agent' : 'filesystem',
@@ -172,8 +158,6 @@ export function auditCodexApprovalDecision(input) {
172
158
  errorCode: input.decision === 'deny'
173
159
  ? input.decisionSource === 'approval' ? 'USER_DENIED' : input.decisionSource === 'policy' ? 'POLICY_DENIED' : infrastructureErrorCode(input.policyCode)
174
160
  : undefined,
175
- requestId: input.requestId,
176
- correlationId: input.correlationId ?? input.callId ?? input.requestId,
177
161
  sessionId: input.sessionId ?? 'unknown',
178
162
  agentAid: input.agentAid ?? 'unknown',
179
163
  agentName: input.agentName,
@@ -204,6 +188,8 @@ export function auditPermissionDecision(input) {
204
188
  auditPermissionLifecycle({
205
189
  phase: 'final',
206
190
  requestId,
191
+ callId: input.callId,
192
+ correlationId: input.correlationId,
207
193
  sessionId: input.sessionId,
208
194
  agentAid: input.agentAid,
209
195
  agentName: input.agentName,
@@ -235,9 +221,7 @@ export function auditPermissionLifecycle(input) {
235
221
  requestId: input.requestId,
236
222
  }),
237
223
  eventPhase: input.phase,
238
- callId: input.callId ?? input.requestId,
239
- correlationId: input.correlationId ?? input.callId ?? input.requestId,
240
- requestId: input.requestId,
224
+ ...normalizeToolCorrelation(input),
241
225
  sessionId,
242
226
  agentAid: input.agentAid ?? 'unknown',
243
227
  agentName: input.agentName,
@@ -279,6 +263,8 @@ export function auditPermissionLifecycle(input) {
279
263
  export function auditToolInfrastructureFailure(input) {
280
264
  auditCommandAuthorization({
281
265
  ts: Date.now(),
266
+ eventKey: buildToolLifecycleEventKey(input),
267
+ eventPhase: 'final',
282
268
  source: 'agent-tool',
283
269
  operation: 'tool.execution.blocked',
284
270
  scope: 'filesystem',
@@ -288,9 +274,7 @@ export function auditToolInfrastructureFailure(input) {
288
274
  errorCode: infrastructureErrorCode(input.policyCode),
289
275
  executed: false,
290
276
  executionState: 'blocked',
291
- callId: input.callId,
292
- correlationId: input.callId,
293
- requestId: input.callId,
277
+ ...normalizeToolCorrelation(input),
294
278
  sessionId: input.sessionId ?? 'unknown',
295
279
  agentAid: input.agentAid ?? 'unknown',
296
280
  agentName: input.agentName,
@@ -300,8 +284,20 @@ export function auditToolInfrastructureFailure(input) {
300
284
  reason: input.reason,
301
285
  role: input.role ?? 'unknown',
302
286
  taskId: input.taskId,
303
- argsSummary: input.occurrences && input.occurrences > 1
304
- ? { occurrences: input.occurrences }
287
+ argsSummary: input.occurrences || input.sandboxFailure
288
+ ? {
289
+ ...(input.occurrences ? { occurrences: input.occurrences } : {}),
290
+ // Flat primitive fields survive the audit argument redactor and can
291
+ // be aggregated without reparsing (possibly truncated) stderr.
292
+ ...(input.sandboxFailure ? {
293
+ sandboxFailureKind: input.sandboxFailure.kind,
294
+ sandboxFailureObservation: input.sandboxFailure.observation,
295
+ sandboxInitializationFailures: input.sandboxFailure.counters.total,
296
+ sandboxApplySeccompFailures: input.sandboxFailure.counters.applySeccomp,
297
+ ...Object.fromEntries(Object.entries(input.sandboxFailure.counters.byKind)
298
+ .map(([kind, count]) => [`sandboxFailures_${kind}`, count])),
299
+ } : {}),
300
+ }
305
301
  : undefined,
306
302
  });
307
303
  }
@@ -335,7 +331,7 @@ function buildAuditRecord(event) {
335
331
  : 'authorized');
336
332
  const ecwebRequestId = event.source === 'ecweb' ? event.ecwebRequestId : undefined;
337
333
  const requestId = event.requestId ?? ecwebRequestId;
338
- const correlationId = event.correlationId ?? requestId;
334
+ const correlationId = event.correlationId ?? event.callId ?? requestId;
339
335
  const lifecycleId = event.callId ?? correlationId ?? requestId;
340
336
  return {
341
337
  ts: event.ts,
@@ -1,13 +1,15 @@
1
1
  import { getRoleDefinition } from '../../config/roles.js';
2
2
  import { isManagementRole } from '../../config/builtin-roles.js';
3
- import { relationFieldWriteOperation, resolveRoleFieldPermission, } from '../../config/config-field-policy.js';
3
+ import { isUnsupportedBaseagentConfigField, relationFieldWriteOperation, resolveRoleFieldPermission, } from '../../config/config-field-policy.js';
4
4
  import { isResolvedConfigMutation, } from '../../config/resolved-config-op.js';
5
5
  import { normalizePeer } from '../model/config-scope.js';
6
6
  import { getOperationMeta } from './operation-catalog.js';
7
7
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
8
+ import { sameGroupAid } from '../../aun/group/index.js';
8
9
  const DAEMON_OWNER_PROCESS_OPERATIONS = new Set([
9
10
  'agent.list',
10
11
  'agent.create',
12
+ 'agent.initialize',
11
13
  'agent.delete',
12
14
  'agent.enable',
13
15
  'agent.disable',
@@ -99,6 +101,12 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
99
101
  && (resolvedConfigCommand.operationId !== operation || resolvedConfigCommand.commandScope !== intent.scope)) {
100
102
  return denyDecision(ctx, 'NOT_ALLOWED', 'Resolved config operation does not match the command intent', operation, intent.scope, opMeta.dangerous);
101
103
  }
104
+ if (resolvedConfigCommand?.kind === 'field'
105
+ && resolvedConfigCommand.configScope === 'agent'
106
+ && (resolvedConfigCommand.field === 'fullaccess' || resolvedConfigCommand.field.startsWith('fullaccess.'))
107
+ && resolvedConfigCommand.subcommand !== 'get') {
108
+ return denyDecision(ctx, 'NOT_ALLOWED', 'Agent fullaccess can only be configured in ECWeb Agent settings', operation, intent.scope, true);
109
+ }
102
110
  // A fullaccess task is deliberately a daemon-scoped runtime identity. It
103
111
  // still has to name a known operation and pass structural command parsing,
104
112
  // but relation/Agent role rules, source lists, and daemon-owner gates are
@@ -224,11 +232,30 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
224
232
  matchedRule: role === 'owner' ? 'agent-owner-persona' : 'daemon-owner-persona',
225
233
  };
226
234
  }
235
+ if (operation === 'agent.welcome') {
236
+ if (!isCurrentAgentTarget(ctx) || ctx.chatType !== 'private'
237
+ || (role !== 'owner' && !(role === 'admin' && intent.args.action === 'show'))) {
238
+ return denyDecision(ctx, 'NOT_ALLOWED', 'welcome requires the current Agent Owner in private (admin may read)', operation, intent.scope, opMeta.dangerous);
239
+ }
240
+ return {
241
+ allow: true,
242
+ operation,
243
+ scope: intent.scope,
244
+ role,
245
+ dangerous: opMeta.dangerous,
246
+ matchedRule: 'self-agent-welcome',
247
+ };
248
+ }
227
249
  if (resolvedConfigCommand) {
228
250
  const targetCheck = checkResolvedConfigTarget(ctx, resolvedConfigCommand);
229
251
  if (!targetCheck.ok) {
230
252
  return denyDecision(ctx, intent.scope === 'relation' ? 'ARGUMENT_MISMATCH' : 'SCOPE_MISMATCH', targetCheck.reason || 'Config target is outside the current task context', operation, intent.scope, opMeta.dangerous);
231
253
  }
254
+ if (resolvedConfigCommand.kind === 'field'
255
+ && resolvedConfigCommand.subcommand === 'get'
256
+ && isUnsupportedBaseagentConfigField(resolvedConfigCommand.field)) {
257
+ return denyDecision(ctx, 'UNSUPPORTED_FIELD', `Config field ${resolvedConfigCommand.field} is not supported by this version`, operation, intent.scope, opMeta.dangerous);
258
+ }
232
259
  const identityMutationCheck = checkManagementIdentityMutation(ctx, resolvedConfigCommand);
233
260
  if (!identityMutationCheck.ok) {
234
261
  return denyDecision(ctx, 'NOT_ALLOWED', identityMutationCheck.reason || 'Static management identities cannot be changed by this role', operation, intent.scope, opMeta.dangerous);
@@ -236,7 +263,9 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
236
263
  if (!isManagementRole(role) && intent.scope !== 'relation') {
237
264
  return denyDecision(ctx, 'SCOPE_MISMATCH', `Role ${role} may only access relation-scoped config`, operation, intent.scope, opMeta.dangerous);
238
265
  }
239
- if (role === 'owner' && isCurrentAgentTarget(ctx)) {
266
+ if (role === 'owner'
267
+ && isCurrentAgentTarget(ctx)
268
+ && !isCrossRelationConfigRead(ctx, resolvedConfigCommand)) {
240
269
  return {
241
270
  allow: true,
242
271
  operation,
@@ -636,6 +665,19 @@ function checkConfigFieldPolicy(ctx, policy, resolvedConfigOp) {
636
665
  }
637
666
  return { ok: true };
638
667
  }
668
+ function isCrossRelationConfigRead(ctx, command) {
669
+ if (!command || command.kind !== 'field' || command.subcommand !== 'get' || command.configScope !== 'relation') {
670
+ return false;
671
+ }
672
+ if (!command.peerKey || !ctx.peerKey)
673
+ return true;
674
+ try {
675
+ return normalizeRelationPeer(command.peerKey) !== normalizeRelationPeer(ctx.peerKey);
676
+ }
677
+ catch {
678
+ return true;
679
+ }
680
+ }
639
681
  function checkResolvedConfigTarget(ctx, command) {
640
682
  if (command.kind === 'global' || command.configScope === 'process' || command.configScope === 'defaults') {
641
683
  return { ok: true };
@@ -652,8 +694,12 @@ function checkResolvedConfigTarget(ctx, command) {
652
694
  return { ok: true };
653
695
  }
654
696
  if (ctx.allowExplicitRelationTarget
655
- && (ctx.role === 'owner' || ctx.role === 'admin')
656
- && (command.kind === 'field' && command.operationId.startsWith('role.'))) {
697
+ && isManagementRole(ctx.role)
698
+ && command.kind === 'field'
699
+ && (command.operationId.startsWith('role.') || command.subcommand === 'get')) {
700
+ // Management reads may select another relation of this Agent. This only
701
+ // relaxes target matching; admin field grants/constraints are checked later.
702
+ // Ordinary config writes and whole-config commands remain current-relation only.
657
703
  if (!ctx.selfAid || !command.self || command.self !== ctx.selfAid) {
658
704
  return { ok: false, reason: 'Only the current agent relation can be targeted' };
659
705
  }
@@ -693,7 +739,8 @@ function checkCurrentRelation(ctx, resolvedConfigCommand) {
693
739
  return { ok: false, reason: 'Current relation context is required' };
694
740
  }
695
741
  try {
696
- if (normalizeRelationPeer(requestedPeer) !== normalizeRelationPeer(ctx.peerKey)) {
742
+ if (normalizeRelationPeer(requestedPeer) !== normalizeRelationPeer(ctx.peerKey)
743
+ && !sameAunGroupRelation(ctx, requestedPeer, ctx.peerKey)) {
697
744
  if (ctx.intent.operation.startsWith('ec.group.')) {
698
745
  const currentGroup = peerIdFromArg(ctx.peerKey);
699
746
  const targetGroup = stringArg(ctx.intent.args.targetId) ?? peerIdFromArg(requestedPeer);
@@ -788,6 +835,20 @@ function collectStringValues(value, out) {
788
835
  out.add(item);
789
836
  }
790
837
  }
838
+ function sameAunGroupRelation(ctx, requested, current) {
839
+ if (ctx.chatType !== 'group'
840
+ || (!ctx.intent.operation.startsWith('ec.group.') && !ctx.intent.operation.startsWith('ec.fs.')))
841
+ return false;
842
+ try {
843
+ const target = parsePeerKey(requested);
844
+ const relation = parsePeerKey(current);
845
+ return target.channelType === 'aun' && relation.channelType === 'aun'
846
+ && sameGroupAid(target.channelId, relation.channelId);
847
+ }
848
+ catch {
849
+ return false;
850
+ }
851
+ }
791
852
  function checkOwnPeer(ctx) {
792
853
  const argPeer = stringArg(ctx.intent.args.peer);
793
854
  const argPeerKey = stringArg(ctx.intent.args.peerKey);
@@ -812,6 +873,7 @@ function checkOwnPeer(ctx) {
812
873
  candidateKeys.add(`${legacyChannelType}::${argPeer}`);
813
874
  }
814
875
  return candidateKeys.has(expectedPeerKey)
876
+ || [...candidateKeys].some(key => sameAunGroupRelation(ctx, key, expectedPeerKey))
815
877
  ? { ok: true }
816
878
  : { ok: false, reason: 'Only the current group relation can be targeted' };
817
879
  }