evolcore 0.0.17 → 0.0.19

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 (66) hide show
  1. package/CHANGELOG.md +51 -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 +43 -24
  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 +82 -14
  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/data-migration.js +10 -4
  40. package/dist/core/event-catalog.js +18 -0
  41. package/dist/core/message/message-bridge.js +66 -8
  42. package/dist/core/message/response-engine.js +147 -10
  43. package/dist/core/permission/ec-command-parser.js +148 -22
  44. package/dist/core/permission/sandbox-runtime.js +79 -13
  45. package/dist/core/permission/tool-policy.js +19 -7
  46. package/dist/index.js +357 -48
  47. package/dist/ipc.js +81 -5
  48. package/dist/paths.js +0 -3
  49. package/dist/utils/atomic-write.js +45 -11
  50. package/dist/utils/logger.js +27 -0
  51. package/dist/utils/windows-autostart.js +740 -83
  52. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  53. package/ecagent/dist/harness/agent-harness.js +6 -4
  54. package/kits/docs/evolcore/config.md +1 -1
  55. package/kits/docs/evolcore/group-rules.md +2 -1
  56. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  57. package/kits/docs/path-registry.md +1 -1
  58. package/kits/eck_manifest.json +25 -16
  59. package/kits/rules/01-overview.md +5 -5
  60. package/kits/rules/02-navigation.md +2 -2
  61. package/kits/rules/03-identity.md +1 -1
  62. package/kits/rules/04-relation.md +4 -4
  63. package/kits/rules/05-venue.md +5 -5
  64. package/kits/templates/bootstrap-welcome.md +3 -1
  65. package/kits/templates/system-fragments/bootstrap.md +17 -9
  66. package/package.json +1 -1
@@ -2,6 +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 } from '../audit/event-key.js';
5
6
  export function auditCommandAuthorization(event) {
6
7
  const shouldAudit = event.source === 'menu.cli' ||
7
8
  event.decision === 'deny' ||
@@ -23,11 +24,15 @@ export function auditCommandAuthorization(event) {
23
24
  export function auditToolPreflightDenial(input) {
24
25
  auditCommandAuthorization({
25
26
  ts: Date.now(),
27
+ callId: input.requestId,
28
+ correlationId: input.requestId,
26
29
  source: 'agent-tool',
27
30
  operation: 'tool.preflight.deny',
28
31
  scope: 'filesystem',
29
32
  dangerous: false,
30
33
  decision: 'deny',
34
+ executed: false,
35
+ executionState: 'blocked',
31
36
  decisionSource: 'policy',
32
37
  toolName: input.toolName,
33
38
  policyCode: input.policyCode,
@@ -55,11 +60,14 @@ export function auditToolPreflightDenial(input) {
55
60
  export function auditCodexApprovalDecision(input) {
56
61
  auditCommandAuthorization({
57
62
  ts: Date.now(),
63
+ callId: input.correlationId ?? input.requestId,
58
64
  source: 'agent-tool',
59
65
  operation: 'codex.approval',
60
66
  scope: input.toolName === 'PermissionGrant' ? 'agent' : 'filesystem',
61
67
  dangerous: input.toolName === 'PermissionGrant',
62
68
  decision: input.decision,
69
+ executed: false,
70
+ executionState: input.decision === 'allow' ? 'authorized' : 'blocked',
63
71
  decisionSource: input.decisionSource,
64
72
  requestId: input.requestId,
65
73
  correlationId: input.correlationId ?? input.requestId,
@@ -73,12 +81,58 @@ export function auditCodexApprovalDecision(input) {
73
81
  reason: input.reason,
74
82
  role: input.role ?? 'unknown',
75
83
  taskId: input.taskId,
76
- argsSummary: { method: input.method, executed: false },
84
+ argsSummary: { method: input.method },
85
+ });
86
+ }
87
+ /** Record a fail-closed infrastructure condition before a tool can execute. */
88
+ export function auditToolInfrastructureFailure(input) {
89
+ auditCommandAuthorization({
90
+ ts: Date.now(),
91
+ source: 'agent-tool',
92
+ operation: 'tool.execution.blocked',
93
+ scope: 'filesystem',
94
+ dangerous: false,
95
+ decision: 'deny',
96
+ decisionSource: 'infrastructure',
97
+ executed: false,
98
+ executionState: 'blocked',
99
+ callId: input.callId,
100
+ correlationId: input.callId,
101
+ requestId: input.callId,
102
+ sessionId: input.sessionId ?? 'unknown',
103
+ agentAid: input.agentAid ?? 'unknown',
104
+ permissionMode: input.permissionMode ?? 'unknown',
105
+ toolName: input.toolName,
106
+ policyCode: input.policyCode,
107
+ reason: input.reason,
108
+ role: input.role ?? 'unknown',
109
+ taskId: input.taskId,
110
+ argsSummary: input.occurrences && input.occurrences > 1
111
+ ? { occurrences: input.occurrences }
112
+ : undefined,
77
113
  });
78
114
  }
79
115
  function buildAuditRecord(event) {
116
+ const executed = event.executed ?? false;
117
+ const executionState = event.executionState
118
+ ?? (event.decision === 'deny'
119
+ ? 'blocked'
120
+ : executed
121
+ ? (event.exitCode === undefined || event.exitCode === 0 ? 'completed' : 'failed')
122
+ : 'authorized');
123
+ const lifecycleId = event.callId ?? event.correlationId ?? event.requestId;
80
124
  return {
81
125
  ts: event.ts,
126
+ eventKey: event.eventKey ?? (lifecycleId
127
+ ? buildAuthorizationEventKey({
128
+ sessionId: event.sessionId,
129
+ callId: event.callId,
130
+ correlationId: event.correlationId,
131
+ requestId: event.requestId,
132
+ operation: event.operation,
133
+ })
134
+ : undefined),
135
+ callId: event.callId,
82
136
  correlationId: event.correlationId ?? event.requestId,
83
137
  requestId: event.requestId,
84
138
  sessionId: event.sessionId,
@@ -96,23 +150,26 @@ function buildAuditRecord(event) {
96
150
  dangerous: event.dangerous,
97
151
  name: event.name,
98
152
  action: event.action,
99
- args: event.args?.argv ? { argv: [...event.args.argv] } : undefined,
153
+ args: event.args?.argv ? { argv: sanitizeAuditArgv(event.args.argv, event.decision) } : undefined,
100
154
  actorId: redactIdentifier(event.actorId),
101
155
  selfAid: redactIdentifier(event.selfAid),
102
156
  peerKey: redactIdentifier(event.peerKey),
103
157
  channel: event.channel,
104
158
  channelId: redactIdentifier(event.channelId),
105
159
  role: event.role,
160
+ processRole: event.processRole,
106
161
  isDaemonOwner: event.isDaemonOwner,
107
162
  fromControlChannel: event.fromControlChannel,
108
163
  taskId: event.taskId,
109
164
  messageId: event.messageId,
110
165
  decision: event.decision,
166
+ executed,
167
+ executionState,
111
168
  code: event.code,
112
169
  reason: event.reason,
113
170
  matchedRule: event.matchedRule,
114
171
  argvHash: event.argvHash,
115
- argsSummary: event.argsSummary,
172
+ argsSummary: summarizeAuthorizationArgs(event.operation, event.argsSummary),
116
173
  durationMs: event.durationMs,
117
174
  exitCode: event.exitCode,
118
175
  };
@@ -148,7 +205,9 @@ function logAuditEvent(record) {
148
205
  record.taskId ? `task=${record.taskId}` : null,
149
206
  record.messageId ? `message=${record.messageId}` : null,
150
207
  record.requestId ? `request=${record.requestId}` : null,
208
+ record.callId ? `call=${record.callId}` : null,
151
209
  record.correlationId ? `correlation=${record.correlationId}` : null,
210
+ record.eventKey ? `eventKey=${record.eventKey}` : null,
152
211
  record.sessionId ? `session=${record.sessionId}` : null,
153
212
  record.agentAid ? `agent=${record.agentAid}` : null,
154
213
  record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
@@ -157,6 +216,8 @@ function logAuditEvent(record) {
157
216
  record.protectionClass ? `protectionClass=${record.protectionClass}` : null,
158
217
  record.matchedPath ? `matchedPath=${JSON.stringify(record.matchedPath)}` : null,
159
218
  record.decisionSource ? `decisionSource=${record.decisionSource}` : null,
219
+ `executed=${record.executed}`,
220
+ `executionState=${record.executionState}`,
160
221
  record.code ? `code=${record.code}` : null,
161
222
  record.matchedRule ? `rule=${record.matchedRule}` : null,
162
223
  record.dangerous ? 'dangerous=true' : null,
@@ -225,6 +286,52 @@ function writeRoleMutationAudit(payload) {
225
286
  }
226
287
  catch { }
227
288
  }
289
+ function summarizeAuthorizationArgs(operation, args) {
290
+ if (!args)
291
+ return undefined;
292
+ const summary = {};
293
+ for (const [key, value] of Object.entries(args)) {
294
+ if (value === undefined)
295
+ continue;
296
+ if (/^(?:self|peer|peerKey|target|targetId|session|sessionId|channelId)$/i.test(key)) {
297
+ summary[key] = typeof value === 'string' ? redactIdentifier(value) : '[redacted]';
298
+ continue;
299
+ }
300
+ if (/(?:value|token|secret|password|credential|content|payload|body|sql|query)/i.test(key)) {
301
+ summary[`${key}Type`] = Array.isArray(value) ? 'array' : typeof value;
302
+ summary[key] = '[redacted]';
303
+ continue;
304
+ }
305
+ if (typeof value === 'string') {
306
+ summary[key] = value.length <= 96 ? value : `${value.slice(0, 93)}...`;
307
+ }
308
+ else if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
309
+ summary[key] = value;
310
+ }
311
+ else {
312
+ summary[key] = `[${Array.isArray(value) ? 'array' : 'object'}]`;
313
+ }
314
+ }
315
+ if (operation === 'ec.msg.history')
316
+ summary.kind = 'history';
317
+ return Object.keys(summary).length > 0 ? summary : undefined;
318
+ }
319
+ function sanitizeAuditArgv(argv, decision) {
320
+ if (decision === 'allow')
321
+ return [...argv];
322
+ const safe = [...argv];
323
+ if (safe[0] === 'config' && safe[1] === 'set' && safe.length > 3) {
324
+ safe[3] = '[redacted]';
325
+ }
326
+ for (let index = 0; index < safe.length - 1; index++) {
327
+ const flag = safe[index];
328
+ if (/^--(?:self|peer|session|channel|channel-id|token)$/i.test(flag)) {
329
+ safe[index + 1] = redactIdentifier(safe[index + 1]) ?? '[redacted]';
330
+ index++;
331
+ }
332
+ }
333
+ return safe;
334
+ }
228
335
  function protectionClassForPolicy(policyCode) {
229
336
  if (!policyCode)
230
337
  return undefined;
@@ -0,0 +1,17 @@
1
+ const NEXT_STEPS = {
2
+ DAEMON_OWNER_REQUIRED: 'Ask an AID listed in daemon.json.owners to run this operation from the daemon control channel or ECWeb.',
3
+ TARGET_SELF_ONLY: 'Target the current Agent, or ask a DaemonOwner to perform the cross-Agent operation.',
4
+ MANAGED_NON_INTERACTIVE_REQUIRED: 'Use the structured non-interactive Agent command (for example `ec agent new <aid> --non-interactive`) or ECWeb.',
5
+ PURGE_CONFIRMATION_REQUIRED: 'Run the purge from the daemon control plane and add --confirm-purge after verifying the target Agent.',
6
+ };
7
+ export function authorizationNextStep(reasonCode) {
8
+ return NEXT_STEPS[reasonCode];
9
+ }
10
+ export function authorizationDenialData(reasonCode) {
11
+ return { reasonCode, nextStep: authorizationNextStep(reasonCode) };
12
+ }
13
+ export function formatAuthorizationDenial(reason, reasonCode) {
14
+ if (!reasonCode)
15
+ return reason;
16
+ return `${reason}\nNext step: ${authorizationNextStep(reasonCode)}`;
17
+ }
@@ -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
  }