evolcore 0.0.17 → 0.0.18

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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. package/package.json +1 -1
@@ -5,6 +5,36 @@ 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
+ const DAEMON_OWNER_PROCESS_OPERATIONS = new Set([
9
+ 'agent.list',
10
+ 'agent.create',
11
+ 'agent.delete',
12
+ 'agent.enable',
13
+ 'agent.disable',
14
+ 'system.restart',
15
+ 'system.upgrade',
16
+ 'gateway.read',
17
+ 'gateway.write',
18
+ 'stats.peers',
19
+ 'stats.groups',
20
+ 'stats.sqlReadonly',
21
+ 'stats.rebuild',
22
+ 'aid.listLocal',
23
+ 'aid.showLocal',
24
+ 'aid.lookupRemote',
25
+ ]);
26
+ const DAEMON_SERVICE_OPERATIONS = new Set([
27
+ 'trigger.list',
28
+ 'trigger.show',
29
+ 'trigger.history',
30
+ 'trigger.eventCatalog',
31
+ 'trigger.create',
32
+ 'trigger.update',
33
+ 'trigger.setEnabled',
34
+ 'trigger.cancel',
35
+ 'trigger.delete',
36
+ 'trigger.run',
37
+ ]);
8
38
  export function authorizeCommand(ctx) {
9
39
  return authorizeCommandInternal(ctx);
10
40
  }
@@ -42,7 +72,8 @@ export function evaluateRoleOperationCapability(params) {
42
72
  peerKey: capabilityPeer,
43
73
  role: params.role,
44
74
  chatType: params.chatType,
45
- isDaemonOwner: params.fromControlChannel && params.role === 'owner',
75
+ processRole: 'none',
76
+ isDaemonOwner: false,
46
77
  fromControlChannel: params.fromControlChannel,
47
78
  source: 'menu',
48
79
  });
@@ -75,6 +106,45 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
75
106
  if (!sourceAllowed) {
76
107
  return denyDecision(ctx, 'NOT_ALLOWED', `Operation ${operation} is not available from source ${ctx.source}`, operation, intent.scope, opMeta.dangerous);
77
108
  }
109
+ if (ctx.processRole === 'daemon-service') {
110
+ if (!DAEMON_SERVICE_OPERATIONS.has(operation)) {
111
+ return denyDecision(ctx, 'NOT_ALLOWED', `Daemon service is not authorized for ${operation}`, operation, intent.scope, opMeta.dangerous, 'daemon-service');
112
+ }
113
+ return {
114
+ allow: true,
115
+ operation,
116
+ scope: intent.scope,
117
+ role: 'daemon-service',
118
+ dangerous: opMeta.dangerous,
119
+ matchedRule: 'daemon-service',
120
+ };
121
+ }
122
+ if (requiresDaemonOwner(ctx, resolvedConfigCommand)) {
123
+ if (!hasDaemonOwnerRole(ctx)) {
124
+ const reasonCode = daemonOwnerDenialReasonCode(ctx);
125
+ return denyDecision(ctx, 'NOT_ALLOWED', daemonOwnerDenialMessage(ctx), operation, intent.scope, opMeta.dangerous, 'daemon-owner', reasonCode);
126
+ }
127
+ return {
128
+ allow: true,
129
+ operation,
130
+ scope: intent.scope,
131
+ role: 'daemon-owner',
132
+ dangerous: opMeta.dangerous,
133
+ matchedRule: 'daemon-owner',
134
+ };
135
+ }
136
+ // Any authenticated role may inspect the current Agent through the daemon's
137
+ // redacted view. Cross-Agent inspection is handled by requiresDaemonOwner().
138
+ if (operation === 'agent.show' && isCurrentAgentTarget(ctx)) {
139
+ return {
140
+ allow: true,
141
+ operation,
142
+ scope: intent.scope,
143
+ role,
144
+ dangerous: opMeta.dangerous,
145
+ matchedRule: 'self-agent-read',
146
+ };
147
+ }
78
148
  if (resolvedConfigCommand) {
79
149
  const targetCheck = checkResolvedConfigTarget(ctx, resolvedConfigCommand);
80
150
  if (!targetCheck.ok) {
@@ -84,23 +154,17 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
84
154
  if (!identityMutationCheck.ok) {
85
155
  return denyDecision(ctx, 'NOT_ALLOWED', identityMutationCheck.reason || 'Static management identities cannot be changed by this role', operation, intent.scope, opMeta.dangerous);
86
156
  }
87
- if (resolvedConfigCommand.kind === 'global' && !ctx.isDaemonOwner) {
88
- return denyDecision(ctx, 'NOT_ALLOWED', `Global config command ${operation} requires daemon owner permission`, operation, intent.scope, opMeta.dangerous);
89
- }
90
- if (role === 'owner' && !ctx.isDaemonOwner && intent.scope === 'process') {
91
- return denyDecision(ctx, 'SCOPE_MISMATCH', 'Agent owners cannot access process, defaults, or global config commands', operation, intent.scope, opMeta.dangerous);
92
- }
93
157
  if (!isManagementRole(role) && intent.scope !== 'relation') {
94
158
  return denyDecision(ctx, 'SCOPE_MISMATCH', `Role ${role} may only access relation-scoped config`, operation, intent.scope, opMeta.dangerous);
95
159
  }
96
- if (ctx.isDaemonOwner || role === 'owner') {
160
+ if (role === 'owner') {
97
161
  return {
98
162
  allow: true,
99
163
  operation,
100
164
  scope: intent.scope,
101
- role: ctx.isDaemonOwner ? 'owner' : role,
165
+ role,
102
166
  dangerous: opMeta.dangerous,
103
- matchedRule: ctx.isDaemonOwner ? 'daemon-owner' : 'agent-owner',
167
+ matchedRule: 'agent-owner',
104
168
  };
105
169
  }
106
170
  }
@@ -159,7 +223,7 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
159
223
  if (permission.constraints) {
160
224
  const constraintCheck = checkConstraints(ctx, permission.constraints, resolvedConfigCommand);
161
225
  if (!constraintCheck.ok) {
162
- return denyDecision(ctx, constraintCheck.code ?? 'ARGUMENT_MISMATCH', constraintCheck.reason || 'Command arguments do not satisfy permission constraints', operation, intent.scope, opMeta.dangerous, matchedRule);
226
+ return denyDecision(ctx, constraintCheck.code ?? 'ARGUMENT_MISMATCH', constraintCheck.reason || 'Command arguments do not satisfy permission constraints', operation, intent.scope, opMeta.dangerous, matchedRule, constraintCheck.reasonCode);
163
227
  }
164
228
  }
165
229
  return {
@@ -186,6 +250,59 @@ function matchCommandPermission(operation, category, commandPerms, isDangerous)
186
250
  matches.sort((a, b) => b.rank - a.rank);
187
251
  return matches[0];
188
252
  }
253
+ function requiresDaemonOwner(ctx, resolvedConfigCommand) {
254
+ const { operation, scope, args } = ctx.intent;
255
+ if (scope === 'process' || scope === 'control')
256
+ return true;
257
+ if (DAEMON_OWNER_PROCESS_OPERATIONS.has(operation))
258
+ return true;
259
+ if (resolvedConfigCommand
260
+ && (resolvedConfigCommand.kind === 'global'
261
+ || resolvedConfigCommand.configScope === 'process'
262
+ || resolvedConfigCommand.configScope === 'defaults'))
263
+ return true;
264
+ if (operation === 'agent.reload' && hasDaemonOwnerRole(ctx)) {
265
+ return true;
266
+ }
267
+ if (operation === 'agent.show' || operation === 'agent.reload') {
268
+ return !isCurrentAgentTarget(ctx);
269
+ }
270
+ if (operation.startsWith('trigger.')) {
271
+ if (ctx.fromControlChannel)
272
+ return true;
273
+ const taskAgentAid = stringArg(args.taskAgentAid);
274
+ const targetAgentAid = stringArg(args.targetAgentAid) ?? stringArg(args.self);
275
+ return !!taskAgentAid && !!targetAgentAid && taskAgentAid !== targetAgentAid;
276
+ }
277
+ return false;
278
+ }
279
+ function hasDaemonOwnerRole(ctx) {
280
+ return ctx.isDaemonOwner === true || ctx.processRole === 'daemon-owner';
281
+ }
282
+ function daemonOwnerDenialReasonCode(ctx) {
283
+ if ((ctx.intent.operation === 'agent.show' || ctx.intent.operation === 'agent.reload')
284
+ && ctx.intent.scope !== 'control'
285
+ && !ctx.fromControlChannel
286
+ && !isCurrentAgentTarget(ctx)) {
287
+ return 'TARGET_SELF_ONLY';
288
+ }
289
+ return 'DAEMON_OWNER_REQUIRED';
290
+ }
291
+ function daemonOwnerDenialMessage(ctx) {
292
+ return daemonOwnerDenialReasonCode(ctx) === 'TARGET_SELF_ONLY'
293
+ ? `${ctx.intent.operation} may only target the current Agent from a managed Agent task.`
294
+ : daemonOwnerRequiredMessage(ctx.intent.operation);
295
+ }
296
+ function isCurrentAgentTarget(ctx) {
297
+ const targetAid = stringArg(ctx.intent.args.targetAgentAid)
298
+ ?? stringArg(ctx.intent.args.aid)
299
+ ?? stringArg(ctx.intent.args.self);
300
+ return !!ctx.selfAid && !!targetAid && targetAid === ctx.selfAid;
301
+ }
302
+ function daemonOwnerRequiredMessage(operation) {
303
+ return `${operation} is a daemon-level operation and requires DaemonOwner. `
304
+ + 'Agent owner/admin and permissionMode=bypass do not grant this permission.';
305
+ }
189
306
  function getRuleRank(rule, permission, operation, category, isDangerous) {
190
307
  const denyOffset = permission.allow ? 0 : 1;
191
308
  if (rule === operation)
@@ -215,8 +332,10 @@ function checkConstraints(ctx, constraints, resolvedConfigCommand) {
215
332
  return peerCheck;
216
333
  }
217
334
  if (constraints.ownAgentOnly || constraints.targetCurrentAgentOnly) {
218
- const controlOwnerOverride = ctx.fromControlChannel && ctx.isDaemonOwner;
219
- const targetAid = stringArg(ctx.intent.args.self) ?? stringArg(ctx.intent.args.aid);
335
+ const controlOwnerOverride = ctx.fromControlChannel && hasDaemonOwnerRole(ctx);
336
+ const targetAid = stringArg(ctx.intent.args.targetAgentAid)
337
+ ?? stringArg(ctx.intent.args.aid)
338
+ ?? stringArg(ctx.intent.args.self);
220
339
  if (!controlOwnerOverride && (!ctx.selfAid || !targetAid || targetAid !== ctx.selfAid)) {
221
340
  return { ok: false, reason: 'Only the current agent can be targeted' };
222
341
  }
@@ -238,8 +357,13 @@ function checkConstraints(ctx, constraints, resolvedConfigCommand) {
238
357
  reason: `Denied ${ctx.intent.operation}: role=${ctx.role}, source_chat=${ctx.chatType ?? 'unknown'}, target_group=${target}. This role may access group operations only from a group-chat task${sameGroup}; the target being a group is not sufficient.`,
239
358
  };
240
359
  }
241
- if (constraints.requireDaemonOwner && !ctx.isDaemonOwner) {
242
- return { ok: false, reason: 'This command requires daemon owner permission' };
360
+ if (constraints.requireDaemonOwner && !hasDaemonOwnerRole(ctx)) {
361
+ return {
362
+ ok: false,
363
+ code: 'NOT_ALLOWED',
364
+ reason: daemonOwnerRequiredMessage(ctx.intent.operation),
365
+ reasonCode: 'DAEMON_OWNER_REQUIRED',
366
+ };
243
367
  }
244
368
  if (constraints.requireControlChannel && !ctx.fromControlChannel) {
245
369
  return { ok: false, reason: 'This command requires the control channel' };
@@ -310,7 +434,7 @@ function checkConfigFieldPolicy(ctx, policy, resolvedConfigOp) {
310
434
  if (policy === 'role-overridable-write'
311
435
  && resolvedConfigOp.fieldRule.access === 'owner-only'
312
436
  && ctx.role !== 'owner'
313
- && !ctx.isDaemonOwner) {
437
+ && !hasDaemonOwnerRole(ctx)) {
314
438
  return { ok: false, reason: `Config field ${field} can only be changed by an owner` };
315
439
  }
316
440
  const roleDef = getRoleDefinition(ctx.role, ctx.selfAid);
@@ -386,7 +510,7 @@ function checkManagementIdentityMutation(ctx, command) {
386
510
  if (command.configScope !== 'agent') {
387
511
  return { ok: false, reason: `${field} requires an explicit current-agent target` };
388
512
  }
389
- if (ctx.isDaemonOwner || ctx.role === 'owner')
513
+ if (hasDaemonOwnerRole(ctx) || ctx.role === 'owner')
390
514
  return { ok: true };
391
515
  return { ok: false, reason: `Only an Agent owner can modify ${field}` };
392
516
  }
@@ -625,7 +749,7 @@ function globMatches(pattern, value) {
625
749
  function stringArg(value) {
626
750
  return typeof value === 'string' && value.length > 0 ? value : undefined;
627
751
  }
628
- function denyDecision(ctx, code, reason, operation, scope, dangerous, matchedRule) {
752
+ function denyDecision(ctx, code, reason, operation, scope, dangerous, matchedRule, reasonCode) {
629
753
  return {
630
754
  allow: false,
631
755
  code,
@@ -635,5 +759,6 @@ function denyDecision(ctx, code, reason, operation, scope, dangerous, matchedRul
635
759
  role: ctx.role,
636
760
  dangerous,
637
761
  matchedRule,
762
+ ...(reasonCode ? { reasonCode } : {}),
638
763
  };
639
764
  }
@@ -539,7 +539,7 @@ const OPERATIONS = [
539
539
  dangerous: false,
540
540
  defaultScopes: ['control'],
541
541
  description: '列出所有 agent(敏感信息)',
542
- sources: ['menu', 'menu.cli', 'control'],
542
+ sources: ['menu', 'menu.cli', 'agent-tool', 'control'],
543
543
  },
544
544
  {
545
545
  id: 'agent.show',
@@ -547,7 +547,7 @@ const OPERATIONS = [
547
547
  dangerous: false,
548
548
  defaultScopes: ['control', 'agent'],
549
549
  description: '查看 agent 详情(敏感信息)',
550
- sources: ['menu', 'menu.cli', 'control'],
550
+ sources: ['menu', 'menu.cli', 'agent-tool', 'control'],
551
551
  },
552
552
  {
553
553
  id: 'agent.baseagent.current',
@@ -587,7 +587,23 @@ const OPERATIONS = [
587
587
  dangerous: true,
588
588
  defaultScopes: ['control'],
589
589
  description: '创建新 agent',
590
- sources: ['menu', 'menu.cli', 'control'],
590
+ sources: ['menu', 'menu.cli', 'agent-tool', 'control'],
591
+ },
592
+ {
593
+ id: 'agent.enable',
594
+ category: 'process',
595
+ dangerous: true,
596
+ defaultScopes: ['control'],
597
+ description: '启用 agent',
598
+ sources: ['menu', 'menu.cli', 'agent-tool', 'control'],
599
+ },
600
+ {
601
+ id: 'agent.disable',
602
+ category: 'process',
603
+ dangerous: true,
604
+ defaultScopes: ['control'],
605
+ description: '停用 agent',
606
+ sources: ['menu', 'menu.cli', 'agent-tool', 'control'],
591
607
  },
592
608
  {
593
609
  id: 'agent.reload',
@@ -595,7 +611,7 @@ const OPERATIONS = [
595
611
  dangerous: true,
596
612
  defaultScopes: ['control', 'agent'],
597
613
  description: '重载 agent 配置',
598
- sources: ['slash', 'menu', 'control'],
614
+ sources: ['slash', 'menu', 'agent-tool', 'control'],
599
615
  },
600
616
  {
601
617
  id: 'agent.bootstrapComplete',
@@ -611,7 +627,7 @@ const OPERATIONS = [
611
627
  dangerous: true,
612
628
  defaultScopes: ['control'],
613
629
  description: '删除 agent(危险操作)',
614
- sources: ['menu', 'menu.cli', 'control'],
630
+ sources: ['menu', 'menu.cli', 'agent-tool', 'control'],
615
631
  },
616
632
  // ── System Operations ──
617
633
  {
@@ -5,6 +5,7 @@ import { generateWelcomeMessage } from '../utils/welcome.js';
5
5
  import * as outbox from '../aun/outbox.js';
6
6
  import { chatDirPath } from './session/session-fs-store.js';
7
7
  import { hasMessageLogOperation } from './message/message-log.js';
8
+ import { isDeliveryTarget, sameDeliveryTarget } from './message/message-utils.js';
8
9
  export const BOOTSTRAP_MESSAGE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
9
10
  export function bootstrapInitialMessageOperationId(aid) {
10
11
  return `bootstrap-initial:v1:${aid}`;
@@ -26,26 +27,29 @@ export function renderPostBootstrapWelcome(aid, owner, ownerName) {
26
27
  includeBindingNote: true,
27
28
  });
28
29
  }
29
- export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName) {
30
+ export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName, delivery = { chatType: 'private' }) {
30
31
  const operationId = postBootstrapWelcomeOperationId(aid);
31
32
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
32
33
  if (hasMessageLogOperation(chatDir, operationId))
33
34
  return;
34
35
  const existing = outbox.findByDedupeKey(aid, operationId);
35
- if (existing?.delivery?.chatType === 'private' && existing.channelId === owner)
36
+ if (existing
37
+ && existing.channelId === owner
38
+ && isDeliveryTarget(existing.delivery)
39
+ && sameDeliveryTarget(existing.delivery, delivery))
36
40
  return;
37
41
  if (existing)
38
42
  outbox.remove(aid, existing.id);
39
43
  outbox.enqueue(aid, {
40
44
  channelId: owner,
41
- delivery: { chatType: 'private' },
45
+ delivery,
42
46
  dedupeKey: operationId,
43
47
  critical: true,
44
48
  type: 'text',
45
49
  text: renderPostBootstrapWelcome(aid, owner, ownerName),
46
50
  ttl: BOOTSTRAP_MESSAGE_TTL_MS,
47
51
  context: {
48
- delivery: { chatType: 'private' },
52
+ delivery,
49
53
  metadata: {
50
54
  source: 'daemon',
51
55
  chatmode: 'interactive',
@@ -56,12 +60,13 @@ export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName) {
56
60
  });
57
61
  }
58
62
  export function hasPendingPostBootstrapWelcomeOutbox(aid) {
59
- return outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid))?.delivery?.chatType === 'private';
63
+ const entry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid));
64
+ return !!entry && outbox.isDeliveryForChannel(entry.delivery, entry.channelId);
60
65
  }
61
66
  /** Bind the durable completion welcome to the fresh post-bootstrap session. */
62
67
  export function bindPostBootstrapWelcomeOutboxSession(aid, sessionId) {
63
68
  const entry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid));
64
- if (!entry || entry.delivery?.chatType !== 'private')
69
+ if (!entry || !outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
65
70
  return false;
66
71
  if (entry.context?.sessionId === sessionId)
67
72
  return true;
@@ -144,9 +144,31 @@ export class BootstrapService {
144
144
  // owner. An inbound owner message may have arrived from a group, in which
145
145
  // case ctx.channelId is the group AID and must not be paired with the
146
146
  // private delivery route below (that would call message.send(to=group)).
147
- const channelId = channelType === 'aun'
148
- ? recipientId
149
- : ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
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);
150
172
  if (!channelId) {
151
173
  this.inFlight.delete(key);
152
174
  return false;
@@ -172,7 +194,7 @@ export class BootstrapService {
172
194
  channelId,
173
195
  agentName: aid,
174
196
  replyContext: {
175
- delivery: { chatType: 'private' },
197
+ delivery,
176
198
  metadata: {
177
199
  source: 'daemon',
178
200
  persistRequired: true,
@@ -44,14 +44,13 @@ function ensureLoaded() {
44
44
  }
45
45
  function persist() {
46
46
  try {
47
- if (associations.size === 0) {
48
- removeAssociationFiles();
49
- return;
50
- }
47
+ // Keep the protected mount target present even when there are no pending
48
+ // associations. Removing it races with Bubblewrap's H-class projection.
51
49
  atomicWriteJson(associationFile(), [...associations].map(([messageId, association]) => ({
52
50
  messageId,
53
51
  ...association,
54
52
  })));
53
+ removeAssociationBackups();
55
54
  }
56
55
  catch {
57
56
  }
@@ -106,6 +105,10 @@ export function clearAunCausationForTests() {
106
105
  function removeAssociationFiles() {
107
106
  const file = associationFile();
108
107
  fs.rmSync(file, { force: true });
108
+ removeAssociationBackups();
109
+ }
110
+ function removeAssociationBackups() {
111
+ const file = associationFile();
109
112
  fs.rmSync(`${file}_`, { force: true });
110
113
  fs.rmSync(`${file}__`, { force: true });
111
114
  }
@@ -11,22 +11,18 @@ import { isValidAid } from '../../aun/aid/validation.js';
11
11
  const SUPPORTED_AGENT_PATCH_FIELDS = new Set(['aid', 'name', 'avatar', 'active_baseagent', 'baseagents', 'projects', 'owners', 'chatmode', 'channels', 'channelOwners']);
12
12
  const HIDDEN_VALUE = '[hidden]';
13
13
  const SENSITIVE_CONFIG_KEYS = new Set([
14
- 'apiKey',
15
14
  'apikey',
16
- 'appId',
17
- 'appSecret',
18
- 'botId',
19
- 'clientId',
20
- 'clientSecret',
21
- 'secret',
22
- 'token',
23
- 'accessToken',
15
+ 'appid',
16
+ 'appsecret',
17
+ 'botid',
24
18
  'authorization',
19
+ 'clientid',
25
20
  'credential',
26
21
  'credentials',
27
22
  'password',
28
- 'privateKey',
29
- 'encryptionSeed',
23
+ 'privatekey',
24
+ 'encryptionseed',
25
+ 'aeskey',
30
26
  ]);
31
27
  const HIDDEN_SETTINGS_KEYS = new Set([
32
28
  'pathToClaudeCodeExecutable',
@@ -225,7 +221,7 @@ function sanitizeConfigValue(value) {
225
221
  if (out.effort === undefined)
226
222
  out.effort = sanitizeConfigValue(raw);
227
223
  }
228
- else if (SENSITIVE_CONFIG_KEYS.has(key)) {
224
+ else if (isSensitiveConfigKey(key)) {
229
225
  out[key] = raw == null || raw === '' ? raw : HIDDEN_VALUE;
230
226
  }
231
227
  else if (key === 'projects') {
@@ -237,6 +233,18 @@ function sanitizeConfigValue(value) {
237
233
  }
238
234
  return out;
239
235
  }
236
+ function isSensitiveConfigKey(key) {
237
+ const normalized = key.replace(/[-_\s]/g, '').toLowerCase();
238
+ return SENSITIVE_CONFIG_KEYS.has(normalized)
239
+ || normalized.endsWith('token')
240
+ || normalized.endsWith('secret')
241
+ || normalized.endsWith('password')
242
+ || normalized.endsWith('credential')
243
+ || normalized.endsWith('credentials')
244
+ || normalized.endsWith('privatekey')
245
+ || normalized.endsWith('apikey')
246
+ || normalized.endsWith('authorization');
247
+ }
240
248
  function buildSafeAgentConfig(config) {
241
249
  if (!config)
242
250
  return null;
@@ -592,14 +600,15 @@ export async function execAgentQuery(args) {
592
600
  }
593
601
  const config = cfgRead(ConfigTarget.Agent, { self: aid });
594
602
  const safeConfig = buildSafeAgentConfig(config);
603
+ const safeAgentConfig = safeConfig ?? {};
595
604
  const data = {
596
605
  ...res,
597
606
  config: {
598
607
  ...(res.config ?? {}),
599
- active_baseagent: config?.active_baseagent ?? res.config?.baseagent ?? null,
600
- projects: stripUnsupportedProjectFields(config?.projects) ?? {},
601
- baseagents: config?.baseagents ?? {},
602
- owners: config?.owners ?? res.config?.owners ?? [],
608
+ active_baseagent: safeAgentConfig.active_baseagent ?? res.config?.baseagent ?? null,
609
+ projects: safeAgentConfig.projects ?? {},
610
+ baseagents: safeAgentConfig.baseagents ?? {},
611
+ owners: safeAgentConfig.owners ?? res.config?.owners ?? [],
603
612
  },
604
613
  safeConfig,
605
614
  };
@@ -19,7 +19,7 @@ import { buildSessionTurnList } from '../session/session-turns.js';
19
19
  import { resolveConfigCommand } from '../../config/resolved-config-op.js';
20
20
  import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
21
21
  import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
22
- import { hashArgv } from '../auth/authorization-audit.js';
22
+ import { auditCommandAuthorization, hashArgv } from '../auth/authorization-audit.js';
23
23
  import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
24
24
  import { executeManagedAidOperation, ManagedAidOperationError, resolveManagedAidOperation, } from '../../aun/aid/managed-operation.js';
25
25
  import { executeManagedGroupOperation, isManagedGroupRelationRead, ManagedGroupOperationError, resolveManagedGroupOperation, executeManagedMsgOperation, ManagedMsgOperationError, resolveManagedMsgOperation, executeManagedFsOperation, ManagedFsOperationError, resolveManagedFsOperation, } from '../../aun/msg/managed-operation.js';
@@ -27,7 +27,7 @@ import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
27
27
  import { isHClassPath, isLClassPath, isSameOrDescendant } from '../protected-paths.js';
28
28
  import { validateModelSelectionForRole } from '../model/model-permission.js';
29
29
  import { getModelInfo } from '../model/model-catalog.js';
30
- import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride } from '../role/runtime-policy.js';
30
+ import { constrainRuntimePermissionMode, resolveRuntimePermissionMode, validateRuntimeStringFieldOverride } from '../role/runtime-policy.js';
31
31
  import { parsePeerKey } from '../relation/peer-identity.js';
32
32
  import { isQuickCommand } from './slash-gate.js';
33
33
  import { handleSlashCommand } from './slash-handler.js';
@@ -1002,7 +1002,13 @@ export class CommandHandler {
1002
1002
  }
1003
1003
  const payload = result;
1004
1004
  if (payload.kind === 'command.error') {
1005
- return { error: payload.text || '执行失败', code: 'EXEC_FAILED' };
1005
+ return {
1006
+ error: payload.text || '执行失败',
1007
+ code: payload.reason || 'EXEC_FAILED',
1008
+ ...(payload.reasonCode ? {
1009
+ data: { reasonCode: payload.reasonCode, nextStep: payload.nextStep },
1010
+ } : {}),
1011
+ };
1006
1012
  }
1007
1013
  const data = payload.structured && typeof payload.structured === 'object'
1008
1014
  ? { ...payload.structured }
@@ -1771,6 +1777,7 @@ export class CommandHandler {
1771
1777
  },
1772
1778
  },
1773
1779
  auditMetadata: {
1780
+ correlationId: grant.taskId,
1774
1781
  argvHash: hashArgv(parsed.command.canonicalArgv),
1775
1782
  taskId: grant.taskId,
1776
1783
  messageId: grant.messageId,
@@ -1854,8 +1861,41 @@ export class CommandHandler {
1854
1861
  if (!session)
1855
1862
  return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1856
1863
  const parsed = resolveManagedMsgOperation(argv);
1857
- if (!parsed.ok)
1864
+ if (!parsed.ok) {
1865
+ const grant = delegation.grant;
1866
+ auditCommandAuthorization({
1867
+ ts: Date.now(),
1868
+ source: 'agent-tool',
1869
+ operation: 'ec.msg.managed.parse',
1870
+ scope: 'relation',
1871
+ dangerous: false,
1872
+ decision: 'deny',
1873
+ executed: false,
1874
+ executionState: 'blocked',
1875
+ decisionSource: 'policy',
1876
+ policyCode: parsed.code,
1877
+ code: parsed.code,
1878
+ reason: parsed.reason,
1879
+ correlationId: grant.taskId,
1880
+ requestId: grant.taskId,
1881
+ sessionId,
1882
+ agentAid: grant.selfAid,
1883
+ permissionMode: resolveRuntimePermissionMode({ selfAid: grant.selfAid, role: grant.issuedRole }).effectiveValue,
1884
+ actorId: grant.actorId,
1885
+ selfAid: grant.selfAid,
1886
+ peerKey: grant.peerKey,
1887
+ channel: grant.channel,
1888
+ role: grant.issuedRole,
1889
+ taskId: grant.taskId,
1890
+ messageId: grant.messageId,
1891
+ argvHash: hashArgv(argv),
1892
+ argsSummary: {
1893
+ subcommand: String(argv[1] ?? argv[0] ?? '<missing>'),
1894
+ providedOptions: argv.filter(value => typeof value === 'string' && value.startsWith('--')).join(','),
1895
+ },
1896
+ });
1858
1897
  return { ok: false, code: parsed.code, error: parsed.reason };
1898
+ }
1859
1899
  const grant = delegation.grant;
1860
1900
  const grantSelfAid = grant.selfAid.replace(/^@/, '');
1861
1901
  const sessionSelfAid = session.selfAID?.replace(/^@/, '');
@@ -1897,9 +1937,15 @@ export class CommandHandler {
1897
1937
  targetId: parsed.command.target,
1898
1938
  peer: parsed.command.target,
1899
1939
  peerKey: formatPeerKey('aun', parsed.command.target),
1940
+ ...(parsed.command.session ? { session: parsed.command.session } : {}),
1941
+ ...(parsed.command.limit !== undefined ? { limit: parsed.command.limit } : {}),
1942
+ ...(parsed.command.before !== undefined ? { before: parsed.command.before } : {}),
1943
+ ...(parsed.command.after !== undefined ? { after: parsed.command.after } : {}),
1944
+ ...(parsed.command.direction ? { direction: parsed.command.direction } : {}),
1900
1945
  },
1901
1946
  },
1902
1947
  auditMetadata: {
1948
+ correlationId: grant.taskId,
1903
1949
  argvHash: hashArgv(parsed.command.canonicalArgv),
1904
1950
  taskId: grant.taskId,
1905
1951
  messageId: grant.messageId,
@@ -225,7 +225,7 @@ function updateSetting(key, target, value, args, context) {
225
225
  if (!allowed.has(raw)) {
226
226
  throw new GroupMenuError('INVALID_ARGUMENT', `Invalid ${key} value: ${raw || '<empty>'}`);
227
227
  }
228
- if (key === 'rulesPolicy' && context.role !== 'owner' && !context.isDaemonOwner) {
228
+ if (key === 'rulesPolicy' && context.role !== 'owner') {
229
229
  throw new GroupMenuError('PERMISSION_DENIED', 'rulesPolicy can only be changed by an Agent owner');
230
230
  }
231
231
  const expectedRevision = stringArg(args?.expectedRevision);