evolcore 0.0.16 → 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.
- package/CHANGELOG.md +46 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +132 -14
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +21 -6
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/group.js +3 -1
- package/dist/aun/msg/p2p.js +23 -9
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +53 -12
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/task-context.js +46 -1
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +72 -9
- package/dist/core/message/pause-controller.js +53 -0
- package/dist/core/message/response-engine.js +98 -11
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/index.js +357 -48
- package/dist/ipc.js +75 -4
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/error-utils.js +38 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- 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
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
160
|
+
if (role === 'owner') {
|
|
97
161
|
return {
|
|
98
162
|
allow: true,
|
|
99
163
|
operation,
|
|
100
164
|
scope: intent.scope,
|
|
101
|
-
role
|
|
165
|
+
role,
|
|
102
166
|
dangerous: opMeta.dangerous,
|
|
103
|
-
matchedRule:
|
|
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
|
|
219
|
-
const targetAid = stringArg(ctx.intent.args.
|
|
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
|
|
242
|
-
return {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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
|
}
|