evolcore 0.0.11 → 0.0.13
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 +44 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +86 -7
- package/dist/agents/codex-app-server-client.js +174 -20
- package/dist/agents/codex-runner.js +375 -92
- package/dist/agents/ecagent-runner.js +39 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +91 -44
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +865 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +655 -240
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +108 -31
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +70 -55
- package/dist/cli/fs-command.js +79 -10
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/init.js +77 -8
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +53 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +7 -32
- package/dist/cli/trigger-command.js +11 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +9 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/mention-mode.js +8 -24
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -2
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +31 -7
- package/dist/core/auth/operation-catalog.js +196 -25
- package/dist/core/bootstrap-messages.js +25 -0
- package/dist/core/bootstrap-service.js +64 -4
- package/dist/core/capability/providers/claude-capability-provider.js +9 -31
- package/dist/core/capability/providers/codex-capability-provider.js +4 -15
- package/dist/core/capability/skill-discovery.js +55 -0
- package/dist/core/channel-loader.js +1 -1
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +650 -42
- package/dist/core/command/connect-menu.js +15 -0
- package/dist/core/command/group-menu.js +4 -4
- package/dist/core/command/menu-catalog.js +370 -0
- package/dist/core/command/menu-handler.js +230 -51
- package/dist/core/command/menu-protocol.js +94 -2
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +99 -31
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +157 -22
- package/dist/core/handoff/store.js +29 -1
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +298 -88
- package/dist/core/message/message-log.js +3 -0
- package/dist/core/message/message-queue.js +268 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +161 -77
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +243 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +43 -1
- package/dist/core/permission/tool-policy.js +322 -57
- package/dist/core/protected-paths.js +114 -4
- package/dist/core/session/session-manager.js +42 -4
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +541 -180
- package/dist/ipc.js +232 -10
- package/dist/paths.js +3 -0
- package/dist/response-system/coordinator.js +6 -4
- package/dist/response-system/engines/v1/proactive-flow.js +26 -8
- package/dist/response-system/modes/single-session/index.js +14 -1
- package/dist/response-system/selector.js +6 -5
- package/dist/trigger/anomaly-store.js +4 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/script-executor.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/codex-app-server-registry.js +90 -0
- package/dist/utils/codex-cli.js +5 -1
- package/dist/utils/cross-platform.js +15 -0
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/docs/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +34 -1
- package/kits/templates/roles/visitor.json +35 -3
- package/kits/templates/system-fragments/bootstrap.md +1 -1
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +2 -2
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/command/evol-menu-version-gate.js +0 -39
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
3
|
import { isValidAid } from '../aun/aid/validation.js';
|
|
5
4
|
import { agentContactOperations } from '../paths.js';
|
|
6
5
|
import { listStaticAgentAdmins, listStaticAgentOwners } from './peer-role-resolver.js';
|
|
@@ -12,6 +11,7 @@ export const CONTACT_REQUEST_MAX_PER_WINDOW = 3;
|
|
|
12
11
|
export const CONTACT_REQUEST_MAX_PENDING = 100;
|
|
13
12
|
export const CONTACT_REQUEST_MAX_RETAINED = 1000;
|
|
14
13
|
export const CONTACT_REQUEST_NOTE_MAX_LENGTH = 500;
|
|
14
|
+
export const CONTACT_REQUEST_ID_MAX_LENGTH = 128;
|
|
15
15
|
export async function mutateContactWithOperation(input) {
|
|
16
16
|
const at = new Date(input.now ?? Date.now()).toISOString();
|
|
17
17
|
const logFile = agentContactOperations(input.selfAid);
|
|
@@ -26,7 +26,7 @@ export async function mutateContactWithOperation(input) {
|
|
|
26
26
|
mutation: input.mutation,
|
|
27
27
|
actor: input.actorId,
|
|
28
28
|
value: undefined,
|
|
29
|
-
beforeCommit: ({ contactRevision: nextRevision }) =>
|
|
29
|
+
beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
|
|
30
30
|
at,
|
|
31
31
|
op: input.operation,
|
|
32
32
|
primaryId: input.primaryId,
|
|
@@ -73,7 +73,17 @@ export async function submitContactRequest(input) {
|
|
|
73
73
|
&& Date.parse(record.at) > windowStart
|
|
74
74
|
&& Date.parse(record.at) <= nowMs)).length;
|
|
75
75
|
if (recentCount >= CONTACT_REQUEST_MAX_PER_WINDOW) {
|
|
76
|
-
return preparedWithExpirations(expiration, {
|
|
76
|
+
return preparedWithExpirations(expiration, {
|
|
77
|
+
code: 'rate-limited',
|
|
78
|
+
...(effectiveEntry?.status === 'pending' && effectiveEntry.pendingRequest
|
|
79
|
+
? {
|
|
80
|
+
requestId: effectiveEntry.pendingRequest.id,
|
|
81
|
+
submittedAt: effectiveEntry.pendingRequest.submittedAt,
|
|
82
|
+
expiresAt: effectiveEntry.pendingRequest.expiresAt,
|
|
83
|
+
}
|
|
84
|
+
: {}),
|
|
85
|
+
contactRevision: '',
|
|
86
|
+
}, logFile);
|
|
77
87
|
}
|
|
78
88
|
const counts = countRequestRecordsAfterExpirations(contact, nowMs);
|
|
79
89
|
const isExistingPending = effectiveEntry?.status === 'pending';
|
|
@@ -103,13 +113,14 @@ export async function submitContactRequest(input) {
|
|
|
103
113
|
mutation: { type: 'batch', mutations },
|
|
104
114
|
actor: applicantAid,
|
|
105
115
|
value,
|
|
106
|
-
beforeCommit: ({ contactRevision }) =>
|
|
116
|
+
beforeCommit: ({ contactRevision }) => operationLogChange(logFile, [
|
|
107
117
|
...expiration.events.map(event => ({ ...event, contactRevision })),
|
|
108
118
|
operationRecord({
|
|
109
119
|
at: nowIso,
|
|
110
120
|
op,
|
|
111
121
|
primaryId: applicantAid,
|
|
112
122
|
requestId,
|
|
123
|
+
expiresAt,
|
|
113
124
|
sourceChannelKey: input.sourceChannelKey,
|
|
114
125
|
...(note ? { note } : {}),
|
|
115
126
|
contactRevision,
|
|
@@ -143,6 +154,10 @@ export async function reviewContactRequest(input) {
|
|
|
143
154
|
if (!entry || entry.status !== 'pending' || entry.pendingRequest?.id !== input.requestId) {
|
|
144
155
|
return preparedNoop({ code: 'request-stale', contactRevision });
|
|
145
156
|
}
|
|
157
|
+
const requestMetadata = {
|
|
158
|
+
requestId: entry.pendingRequest.id,
|
|
159
|
+
expiresAt: entry.pendingRequest.expiresAt,
|
|
160
|
+
};
|
|
146
161
|
if (Date.parse(entry.pendingRequest.expiresAt) <= nowMs) {
|
|
147
162
|
const mutation = {
|
|
148
163
|
type: 'expire-request',
|
|
@@ -153,8 +168,8 @@ export async function reviewContactRequest(input) {
|
|
|
153
168
|
return {
|
|
154
169
|
mutation,
|
|
155
170
|
actor: input.actorId,
|
|
156
|
-
value: { code: 'request-expired', contactRevision },
|
|
157
|
-
beforeCommit: ({ contactRevision: nextRevision }) =>
|
|
171
|
+
value: { code: 'request-expired', contactRevision, ...requestMetadata },
|
|
172
|
+
beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
|
|
158
173
|
at: nowIso,
|
|
159
174
|
op: 'expire',
|
|
160
175
|
primaryId: input.primaryId,
|
|
@@ -177,8 +192,8 @@ export async function reviewContactRequest(input) {
|
|
|
177
192
|
now: nowIso,
|
|
178
193
|
},
|
|
179
194
|
actor: input.actorId,
|
|
180
|
-
value: { code, contactRevision },
|
|
181
|
-
beforeCommit: ({ contactRevision: nextRevision }) =>
|
|
195
|
+
value: { code, contactRevision, ...requestMetadata },
|
|
196
|
+
beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
|
|
182
197
|
at: nowIso,
|
|
183
198
|
op: input.decision === 'approve' ? 'approve' : input.decision === 'reject' ? 'reject' : 'block',
|
|
184
199
|
primaryId: input.primaryId,
|
|
@@ -208,7 +223,7 @@ export async function expirePendingContactRequests(selfAid, now = Date.now()) {
|
|
|
208
223
|
mutation: { type: 'batch', mutations: expiration.mutations },
|
|
209
224
|
actor: 'contact-request-expiry',
|
|
210
225
|
value: expiration.mutations.length,
|
|
211
|
-
beforeCommit: ({ contactRevision }) =>
|
|
226
|
+
beforeCommit: ({ contactRevision }) => operationLogChange(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
|
|
212
227
|
};
|
|
213
228
|
});
|
|
214
229
|
return result.value;
|
|
@@ -226,6 +241,91 @@ export async function listPendingContactRequests(selfAid, now = Date.now()) {
|
|
|
226
241
|
}))));
|
|
227
242
|
return result.value;
|
|
228
243
|
}
|
|
244
|
+
/** Return only the authenticated applicant's own admission/request state. */
|
|
245
|
+
export async function queryOwnContactRequestStatus(input) {
|
|
246
|
+
const applicantAid = String(input.applicantAid || '').trim();
|
|
247
|
+
if (!isValidAid(applicantAid))
|
|
248
|
+
return protocolResult('NOT_ALLOWED');
|
|
249
|
+
const now = input.now ?? Date.now();
|
|
250
|
+
const rawRequestId = input.requestId;
|
|
251
|
+
if (rawRequestId !== undefined && typeof rawRequestId !== 'string')
|
|
252
|
+
return protocolResult('NOT_ALLOWED');
|
|
253
|
+
const requestedRequestId = rawRequestId?.trim();
|
|
254
|
+
if (requestedRequestId !== undefined
|
|
255
|
+
&& (!requestedRequestId || requestedRequestId.length > CONTACT_REQUEST_ID_MAX_LENGTH)) {
|
|
256
|
+
return protocolResult('NOT_ALLOWED');
|
|
257
|
+
}
|
|
258
|
+
const owners = listAgentOwners(input.selfAid);
|
|
259
|
+
const result = await mutateContactBookPrepared(input.selfAid, ({ contact }) => {
|
|
260
|
+
const logFile = agentContactOperations(input.selfAid);
|
|
261
|
+
const expiration = collectExpiredMutations(contact, now);
|
|
262
|
+
const entry = effectiveEntryAfterExpirations(contact.contacts[applicantAid], now);
|
|
263
|
+
const operations = readOperationLog(logFile).filter(record => record.primaryId === applicantAid);
|
|
264
|
+
const latestRequestOperation = [...operations].reverse().find(record => (!!record.requestId
|
|
265
|
+
&& (record.op === 'request' || record.op === 'resubmit' || record.op === 'approve'
|
|
266
|
+
|| record.op === 'reject' || record.op === 'expire' || record.op === 'block')));
|
|
267
|
+
if (requestedRequestId !== undefined && latestRequestOperation?.requestId !== requestedRequestId) {
|
|
268
|
+
return preparedNoop(protocolResult('NOT_ALLOWED'));
|
|
269
|
+
}
|
|
270
|
+
const requestMetadata = requestMetadataFor(operations, latestRequestOperation?.requestId);
|
|
271
|
+
let value;
|
|
272
|
+
if (owners.includes(applicantAid)
|
|
273
|
+
|| (entry && (entry.status === undefined || entry.status === 'active'))) {
|
|
274
|
+
value = protocolResult('APPROVED', requestMetadata);
|
|
275
|
+
}
|
|
276
|
+
else if (entry?.status === 'blocked') {
|
|
277
|
+
value = protocolResult('NOT_ALLOWED');
|
|
278
|
+
}
|
|
279
|
+
else if (entry?.status === 'pending' && entry.pendingRequest) {
|
|
280
|
+
value = protocolResult('PENDING', {
|
|
281
|
+
requestId: entry.pendingRequest.id,
|
|
282
|
+
expiresAt: entry.pendingRequest.expiresAt,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else if (entry?.status === 'declined') {
|
|
286
|
+
value = protocolResult('EXPIRED', {
|
|
287
|
+
requestId: latestRequestOperation?.op === 'expire' ? latestRequestOperation.requestId : requestMetadata.requestId,
|
|
288
|
+
expiresAt: entry.declinedAt ?? requestMetadata.expiresAt,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else if (latestRequestOperation?.op === 'reject') {
|
|
292
|
+
value = protocolResult('REJECTED', requestMetadata);
|
|
293
|
+
}
|
|
294
|
+
else if (latestRequestOperation?.op === 'expire') {
|
|
295
|
+
value = protocolResult('EXPIRED', requestMetadata);
|
|
296
|
+
}
|
|
297
|
+
else if (owners.length === 0) {
|
|
298
|
+
value = protocolResult('NOT_ALLOWED');
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
value = protocolResult('CONTACT_REQUIRED');
|
|
302
|
+
}
|
|
303
|
+
if (expiration.mutations.length === 0)
|
|
304
|
+
return preparedNoop(value);
|
|
305
|
+
return {
|
|
306
|
+
mutation: { type: 'batch', mutations: expiration.mutations },
|
|
307
|
+
actor: 'contact-request-expiry',
|
|
308
|
+
value,
|
|
309
|
+
beforeCommit: ({ contactRevision }) => operationLogChange(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
return result.value;
|
|
313
|
+
}
|
|
314
|
+
export function contactRequestSubmissionProtocolResult(result) {
|
|
315
|
+
const metadata = { requestId: result.requestId, expiresAt: result.expiresAt };
|
|
316
|
+
switch (result.code) {
|
|
317
|
+
case 'submitted':
|
|
318
|
+
case 'resubmitted':
|
|
319
|
+
return protocolResult('SUBMITTED', metadata);
|
|
320
|
+
case 'already-contact':
|
|
321
|
+
case 'owner':
|
|
322
|
+
return protocolResult('APPROVED', metadata);
|
|
323
|
+
case 'rate-limited':
|
|
324
|
+
return protocolResult('RATE_LIMITED', metadata);
|
|
325
|
+
default:
|
|
326
|
+
return protocolResult('NOT_ALLOWED', metadata);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
229
329
|
export function findContactRequestNote(selfAid, requestId) {
|
|
230
330
|
const records = readOperationLog(agentContactOperations(selfAid));
|
|
231
331
|
return [...records].reverse().find(record => (record.requestId === requestId
|
|
@@ -237,6 +337,24 @@ function normalizeNote(value) {
|
|
|
237
337
|
return null;
|
|
238
338
|
return note;
|
|
239
339
|
}
|
|
340
|
+
function requestMetadataFor(operations, requestId) {
|
|
341
|
+
if (!requestId)
|
|
342
|
+
return {};
|
|
343
|
+
const submitted = [...operations].reverse().find(record => (record.requestId === requestId && (record.op === 'request' || record.op === 'resubmit')));
|
|
344
|
+
const submittedAt = submitted ? Date.parse(submitted.at) : Number.NaN;
|
|
345
|
+
return {
|
|
346
|
+
requestId,
|
|
347
|
+
expiresAt: submitted?.expiresAt
|
|
348
|
+
?? (Number.isFinite(submittedAt) ? new Date(submittedAt + CONTACT_REQUEST_TTL_MS).toISOString() : undefined),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function protocolResult(status, metadata = {}) {
|
|
352
|
+
return {
|
|
353
|
+
status,
|
|
354
|
+
requestId: metadata.requestId ?? null,
|
|
355
|
+
expiresAt: metadata.expiresAt ?? null,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
240
358
|
function collectExpiredMutations(book, now) {
|
|
241
359
|
const mutations = [];
|
|
242
360
|
const events = [];
|
|
@@ -286,7 +404,7 @@ function preparedWithExpirations(expiration, value, logFile) {
|
|
|
286
404
|
mutation: { type: 'batch', mutations: expiration.mutations },
|
|
287
405
|
actor: 'contact-request-expiry',
|
|
288
406
|
value,
|
|
289
|
-
beforeCommit: ({ contactRevision }) =>
|
|
407
|
+
beforeCommit: ({ contactRevision }) => operationLogChange(logFile, expiration.events.map(event => ({ ...event, contactRevision }))),
|
|
290
408
|
};
|
|
291
409
|
}
|
|
292
410
|
function preparedNoop(value) {
|
|
@@ -310,7 +428,14 @@ function readOperationLog(file) {
|
|
|
310
428
|
const record = JSON.parse(line);
|
|
311
429
|
if (!record || typeof record !== 'object' || typeof record.at !== 'string'
|
|
312
430
|
|| !Number.isFinite(Date.parse(record.at)) || typeof record.op !== 'string'
|
|
313
|
-
||
|
|
431
|
+
|| !['request', 'resubmit', 'approve', 'reject', 'expire', 'block', 'unblock', 'manual-add'].includes(record.op)
|
|
432
|
+
|| typeof record.primaryId !== 'string' || !isValidAid(record.primaryId)
|
|
433
|
+
|| (record.requestId !== undefined && typeof record.requestId !== 'string')
|
|
434
|
+
|| (record.expiresAt !== undefined && (!isValidIsoDate(record.expiresAt)))
|
|
435
|
+
|| (record.actorId !== undefined && typeof record.actorId !== 'string')
|
|
436
|
+
|| (record.sourceChannelKey !== undefined && typeof record.sourceChannelKey !== 'string')
|
|
437
|
+
|| (record.note !== undefined && (typeof record.note !== 'string' || record.note.length > CONTACT_REQUEST_NOTE_MAX_LENGTH))
|
|
438
|
+
|| typeof record.contactRevision !== 'string')
|
|
314
439
|
throw new Error('invalid record');
|
|
315
440
|
records.push(record);
|
|
316
441
|
}
|
|
@@ -320,11 +445,32 @@ function readOperationLog(file) {
|
|
|
320
445
|
}
|
|
321
446
|
return records;
|
|
322
447
|
}
|
|
323
|
-
function
|
|
448
|
+
function isValidIsoDate(value) {
|
|
449
|
+
return Number.isFinite(Date.parse(value));
|
|
450
|
+
}
|
|
451
|
+
function operationLogChange(file, records) {
|
|
324
452
|
if (records.length === 0)
|
|
325
|
-
return;
|
|
326
|
-
|
|
327
|
-
|
|
453
|
+
return undefined;
|
|
454
|
+
// Validate the existing log while the Contact Book lock is held. The log is
|
|
455
|
+
// part of the same journal transaction as contact.json, so a malformed log
|
|
456
|
+
// must fail closed before either file is changed.
|
|
457
|
+
readOperationLog(file);
|
|
458
|
+
let before;
|
|
459
|
+
try {
|
|
460
|
+
before = fs.readFileSync(file, 'utf8');
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
if (error?.code === 'ENOENT')
|
|
464
|
+
before = null;
|
|
465
|
+
else
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
const prefix = before && !before.endsWith('\n') ? `${before}\n` : (before ?? '');
|
|
469
|
+
return {
|
|
470
|
+
file,
|
|
471
|
+
before,
|
|
472
|
+
after: `${prefix}${records.map(record => JSON.stringify(record)).join('\n')}\n`,
|
|
473
|
+
};
|
|
328
474
|
}
|
|
329
475
|
function operationRecord(input) {
|
|
330
476
|
return { id: `evt_${crypto.randomUUID().replace(/-/g, '')}`, ...input };
|
|
@@ -1,27 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* - disabled(关闭 mention 过滤) ≡ broadcast(全部响应)
|
|
8
|
-
*
|
|
9
|
-
* 仅在协议边界(AUN resolver、render fallback)翻译;配置层、命令层一律用
|
|
10
|
-
* mentionMode 词汇。
|
|
11
|
-
*/
|
|
12
|
-
/** config → AUN 协议词汇;未知/未设值返回 undefined(回退服务端 dispatch_mode)。 */
|
|
13
|
-
export function mentionModeToDispatch(mode) {
|
|
14
|
-
if (mode === 'mention-only')
|
|
15
|
-
return 'mention';
|
|
16
|
-
if (mode === 'disabled')
|
|
17
|
-
return 'broadcast';
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
/** AUN 协议词汇 → config;未知返回 undefined。 */
|
|
21
|
-
export function dispatchToMentionMode(dispatch) {
|
|
22
|
-
if (dispatch === 'mention')
|
|
23
|
-
return 'mention-only';
|
|
24
|
-
if (dispatch === 'broadcast')
|
|
1
|
+
/** AUN `mention_mode` and EvolCore `mentionMode` share these wire values. */
|
|
2
|
+
export function normalizeMentionMode(mode) {
|
|
3
|
+
if (typeof mode !== 'string')
|
|
4
|
+
return undefined;
|
|
5
|
+
const normalized = mode.trim().toLowerCase();
|
|
6
|
+
if (normalized === 'disabled')
|
|
25
7
|
return 'disabled';
|
|
8
|
+
if (normalized === 'mention-only' || normalized === 'mention_only')
|
|
9
|
+
return 'mention-only';
|
|
26
10
|
return undefined;
|
|
27
11
|
}
|
|
@@ -123,8 +123,12 @@ export function resolvePeerRoleDetail(ctx) {
|
|
|
123
123
|
const winner = selectHighestRank(candidates, ctx.selfAid);
|
|
124
124
|
if (winner)
|
|
125
125
|
return resultFor(winner.role, winner.source, actor, ctx.selfAid);
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
// Group members get the normal interactive member policy when no explicit
|
|
127
|
+
// group/actor/default assignment exists. Private conversations retain the
|
|
128
|
+
// read-only visitor fallback.
|
|
129
|
+
const fallbackRole = ctx.chatType === 'group' ? 'member' : 'visitor';
|
|
130
|
+
return roleRegistryExists(ctx.selfAid) && roleExists(fallbackRole, ctx.selfAid)
|
|
131
|
+
? resultFor(fallbackRole, 'builtin-fallback', actor, ctx.selfAid)
|
|
128
132
|
: resultFor(null, 'none', actor, ctx.selfAid);
|
|
129
133
|
}
|
|
130
134
|
export function roleToSessionIdentity(role) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { pathToFileURL } from 'url';
|
|
3
|
+
import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
4
|
+
import { assertRoleConfigV4Ready, assertRoleConfigV5Ready, ConfigError } from './config-manager.js';
|
|
5
|
+
/** Apply the one-off v4 role config migration before daemon startup. */
|
|
6
|
+
export async function ensureRoleConfigV4OnStartup() {
|
|
7
|
+
try {
|
|
8
|
+
assertRoleConfigV4Ready();
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (!(error instanceof ConfigError) || error.code !== 'ROLE_CONFIG_MIGRATION_REQUIRED')
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
const home = resolvePaths().root;
|
|
16
|
+
const script = path.join(getPackageRoot(), 'kits', 'migrations', 'migrate-role-config-v4.mjs');
|
|
17
|
+
try {
|
|
18
|
+
const migration = await import(pathToFileURL(script).href);
|
|
19
|
+
const result = migration.runMigration({ home, apply: true });
|
|
20
|
+
assertRoleConfigV4Ready();
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
25
|
+
throw new ConfigError('ROLE_CONFIG_MIGRATION_FAILED', `Automatic role config v4 migration failed: ${detail}\n` +
|
|
26
|
+
`Fix the reported config or run manually:\n` +
|
|
27
|
+
`node kits/migrations/migrate-role-config-v4.mjs --apply --home ${JSON.stringify(home)}`, { cause: error });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Apply the standalone v5 role registry migration before daemon startup. */
|
|
31
|
+
export async function ensureRoleConfigV5OnStartup() {
|
|
32
|
+
try {
|
|
33
|
+
assertRoleConfigV5Ready();
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (!(error instanceof ConfigError) || error.code !== 'ROLE_CONFIG_V5_MIGRATION_REQUIRED')
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
const home = resolvePaths().root;
|
|
41
|
+
const script = path.join(getPackageRoot(), 'kits', 'migrations', 'migrate-role-config-v5.mjs');
|
|
42
|
+
try {
|
|
43
|
+
const migration = await import(pathToFileURL(script).href);
|
|
44
|
+
const result = migration.runMigration({ home, apply: true });
|
|
45
|
+
assertRoleConfigV5Ready();
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new ConfigError('ROLE_CONFIG_V5_MIGRATION_FAILED', `Automatic standalone role migration failed: ${detail}\n` +
|
|
51
|
+
`node kits/migrations/migrate-role-config-v5.mjs --apply --home ${JSON.stringify(home)}`, { cause: error });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -113,7 +113,7 @@ function readRegistry(selfAid, diagnostics) {
|
|
|
113
113
|
code: 'INVALID_ROLE_ASSIGNMENT',
|
|
114
114
|
roleId: selected,
|
|
115
115
|
file: path.relative(resolvePaths().root, file),
|
|
116
|
-
message: `defaultRoles.${scene} references invalid or unregistered role ${selected}; using visitor fallback`,
|
|
116
|
+
message: `defaultRoles.${scene} references invalid or unregistered role ${selected}; using ${scene === 'group' ? 'member' : 'visitor'} fallback`,
|
|
117
117
|
});
|
|
118
118
|
defaultRoles[scene] = null;
|
|
119
119
|
}
|
|
@@ -2,7 +2,7 @@ import crypto from 'crypto';
|
|
|
2
2
|
import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
|
|
3
3
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
4
4
|
export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
|
|
5
|
-
export const AGENT_DELEGATION_COMMAND_TTL_MS = 60_000;
|
|
5
|
+
export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
|
|
6
6
|
export class AgentDelegationRegistry {
|
|
7
7
|
grantsByHash = new Map();
|
|
8
8
|
activeHashBySession = new Map();
|
|
@@ -155,7 +155,6 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
|
|
|
155
155
|
channelId,
|
|
156
156
|
chatType: grant.chatType,
|
|
157
157
|
conversationId: channelId,
|
|
158
|
-
identity: { role: grant.issuedRole, mode: 'interactive' },
|
|
159
158
|
processOwners: [],
|
|
160
159
|
fromControlChannel: false,
|
|
161
160
|
});
|
|
@@ -4,6 +4,22 @@ import { formatPeerKey } from '../relation/peer-identity.js';
|
|
|
4
4
|
import { authorizeCommand, authorizeResolvedConfigCommand } from './operation-authorizer.js';
|
|
5
5
|
import { auditCommandAuthorization } from './authorization-audit.js';
|
|
6
6
|
import { hasTrustedPrincipal } from './authenticated-actor.js';
|
|
7
|
+
import { resolveRuntimePermissionMode } from '../role/runtime-policy.js';
|
|
8
|
+
const CROSS_AGENT_READ_OPERATIONS = new Set([
|
|
9
|
+
'trigger.list',
|
|
10
|
+
'trigger.show',
|
|
11
|
+
'trigger.history',
|
|
12
|
+
]);
|
|
13
|
+
export function isCrossAgentTriggerOperationAllowed(input) {
|
|
14
|
+
if (input.control || input.taskAgentAid === input.targetAgentAid)
|
|
15
|
+
return true;
|
|
16
|
+
if (!input.taskAgentAid || !input.targetManagement)
|
|
17
|
+
return false;
|
|
18
|
+
return CROSS_AGENT_READ_OPERATIONS.has(input.operation);
|
|
19
|
+
}
|
|
20
|
+
export function isCrossAgentTriggerReadOperation(operation) {
|
|
21
|
+
return CROSS_AGENT_READ_OPERATIONS.has(operation);
|
|
22
|
+
}
|
|
7
23
|
export function buildAuthSubject(input) {
|
|
8
24
|
const chatType = input.chatType === 'group' ? 'group' : 'private';
|
|
9
25
|
const actorId = input.actorId;
|
|
@@ -27,6 +43,8 @@ export function buildAuthSubject(input) {
|
|
|
27
43
|
return {
|
|
28
44
|
selfAid: input.selfAid,
|
|
29
45
|
actorId,
|
|
46
|
+
requestId: input.requestId,
|
|
47
|
+
sessionId: input.sessionId,
|
|
30
48
|
channel: input.channel,
|
|
31
49
|
channelType: input.channelType,
|
|
32
50
|
channelId: input.channelId,
|
|
@@ -41,6 +59,8 @@ export function buildAuthSubject(input) {
|
|
|
41
59
|
allowAccess: isDaemonOwner || (suppliedRole
|
|
42
60
|
? checkRoleAccess(suppliedRole, input.selfAid)
|
|
43
61
|
: roleDetail.allowAccess && checkRoleAccess(role, input.selfAid)),
|
|
62
|
+
permissionMode: input.permissionMode
|
|
63
|
+
?? resolveRuntimePermissionMode({ selfAid: input.selfAid, role }).effectiveValue,
|
|
44
64
|
};
|
|
45
65
|
}
|
|
46
66
|
function isProcessOwner(input) {
|
|
@@ -144,6 +164,15 @@ function resolveRoleDetail(input) {
|
|
|
144
164
|
function auditDecision(params, decision) {
|
|
145
165
|
auditCommandAuthorization({
|
|
146
166
|
ts: Date.now(),
|
|
167
|
+
...params.auditMetadata,
|
|
168
|
+
requestId: params.auditMetadata?.requestId ?? params.subject.requestId,
|
|
169
|
+
sessionId: params.auditMetadata?.sessionId ?? params.subject.sessionId,
|
|
170
|
+
agentAid: params.auditMetadata?.agentAid ?? params.subject.selfAid,
|
|
171
|
+
permissionMode: params.auditMetadata?.permissionMode
|
|
172
|
+
?? (decision.allow ? decision.permissionMode ?? params.subject.permissionMode : params.subject.permissionMode),
|
|
173
|
+
toolName: params.auditMetadata?.toolName,
|
|
174
|
+
policyCode: params.auditMetadata?.policyCode ?? (decision.allow ? undefined : decision.code),
|
|
175
|
+
decisionSource: params.auditMetadata?.decisionSource ?? (decision.allow ? 'command' : 'policy'),
|
|
147
176
|
source: params.source,
|
|
148
177
|
operation: params.intent.operation,
|
|
149
178
|
scope: params.intent.scope,
|
|
@@ -161,6 +190,5 @@ function auditDecision(params, decision) {
|
|
|
161
190
|
reason: decision.allow ? undefined : decision.reason,
|
|
162
191
|
matchedRule: decision.matchedRule,
|
|
163
192
|
argsSummary: params.intent.args,
|
|
164
|
-
...params.auditMetadata,
|
|
165
193
|
});
|
|
166
194
|
}
|
|
@@ -17,6 +17,13 @@ export function auditCommandAuthorization(event) {
|
|
|
17
17
|
function buildAuditRecord(event) {
|
|
18
18
|
return {
|
|
19
19
|
ts: event.ts,
|
|
20
|
+
requestId: event.requestId,
|
|
21
|
+
sessionId: event.sessionId,
|
|
22
|
+
agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
|
|
23
|
+
permissionMode: event.permissionMode,
|
|
24
|
+
toolName: event.toolName,
|
|
25
|
+
policyCode: event.policyCode ?? event.code,
|
|
26
|
+
decisionSource: event.decisionSource,
|
|
20
27
|
source: event.source,
|
|
21
28
|
operation: event.operation,
|
|
22
29
|
scope: event.scope,
|
|
@@ -71,6 +78,13 @@ function logAuditEvent(record) {
|
|
|
71
78
|
`actor=${record.actorId || 'unknown'}`,
|
|
72
79
|
record.taskId ? `task=${record.taskId}` : null,
|
|
73
80
|
record.messageId ? `message=${record.messageId}` : null,
|
|
81
|
+
record.requestId ? `request=${record.requestId}` : null,
|
|
82
|
+
record.sessionId ? `session=${record.sessionId}` : null,
|
|
83
|
+
record.agentAid ? `agent=${record.agentAid}` : null,
|
|
84
|
+
record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
|
|
85
|
+
record.toolName ? `tool=${record.toolName}` : null,
|
|
86
|
+
record.policyCode ? `policy=${record.policyCode}` : null,
|
|
87
|
+
record.decisionSource ? `decisionSource=${record.decisionSource}` : null,
|
|
74
88
|
record.code ? `code=${record.code}` : null,
|
|
75
89
|
record.matchedRule ? `rule=${record.matchedRule}` : null,
|
|
76
90
|
record.dangerous ? 'dangerous=true' : null,
|
|
@@ -78,6 +92,7 @@ function logAuditEvent(record) {
|
|
|
78
92
|
.filter(Boolean)
|
|
79
93
|
.join(' ');
|
|
80
94
|
logger[level](message);
|
|
95
|
+
logger[level](`[CommandAuditRecord] ${JSON.stringify(record)}`);
|
|
81
96
|
if (record.reason)
|
|
82
97
|
logger[level](` reason: ${record.reason}`);
|
|
83
98
|
if (record.argvHash)
|
|
@@ -170,13 +170,13 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
170
170
|
dangerous: opMeta.dangerous,
|
|
171
171
|
matchedRule,
|
|
172
172
|
constraints: permission.constraints,
|
|
173
|
+
permissionMode: permission.permissionMode,
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
function matchCommandPermission(operation, category, commandPerms, isDangerous) {
|
|
176
|
-
const namespace = operation.split('.')[0];
|
|
177
177
|
const matches = [];
|
|
178
178
|
for (const [rule, permission] of Object.entries(commandPerms)) {
|
|
179
|
-
const rank = getRuleRank(rule, permission, operation,
|
|
179
|
+
const rank = getRuleRank(rule, permission, operation, category, isDangerous);
|
|
180
180
|
if (rank > 0) {
|
|
181
181
|
matches.push({ permission, matchedRule: rule, rank });
|
|
182
182
|
}
|
|
@@ -186,12 +186,17 @@ function matchCommandPermission(operation, category, commandPerms, isDangerous)
|
|
|
186
186
|
matches.sort((a, b) => b.rank - a.rank);
|
|
187
187
|
return matches[0];
|
|
188
188
|
}
|
|
189
|
-
function getRuleRank(rule, permission, operation,
|
|
189
|
+
function getRuleRank(rule, permission, operation, category, isDangerous) {
|
|
190
190
|
const denyOffset = permission.allow ? 0 : 1;
|
|
191
191
|
if (rule === operation)
|
|
192
192
|
return 1000 + denyOffset;
|
|
193
|
-
if (rule
|
|
194
|
-
|
|
193
|
+
if (rule.endsWith('.*')) {
|
|
194
|
+
const prefix = rule.slice(0, -1);
|
|
195
|
+
if (operation.startsWith(prefix)) {
|
|
196
|
+
const depth = rule.slice(0, -2).split('.').length;
|
|
197
|
+
return 800 + Math.min((depth - 1) * 20, 180) + denyOffset;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
195
200
|
if (rule.startsWith('category:') && rule.slice('category:'.length) === category)
|
|
196
201
|
return 600 + denyOffset;
|
|
197
202
|
if (rule === 'dangerous:*' && isDangerous)
|
|
@@ -230,7 +235,7 @@ function checkConstraints(ctx, constraints, resolvedConfigCommand) {
|
|
|
230
235
|
const sameGroup = constraints.currentRelationOnly ? ' and may only target that same group' : '';
|
|
231
236
|
return {
|
|
232
237
|
ok: false,
|
|
233
|
-
reason: `Denied ${ctx.intent.operation}: role=${ctx.role}, source_chat=${ctx.chatType ?? 'unknown'}, target_group=${target}. This role may
|
|
238
|
+
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.`,
|
|
234
239
|
};
|
|
235
240
|
}
|
|
236
241
|
if (constraints.requireDaemonOwner && !ctx.isDaemonOwner) {
|
|
@@ -401,7 +406,7 @@ function checkCurrentRelation(ctx, resolvedConfigCommand) {
|
|
|
401
406
|
const targetGroup = stringArg(ctx.intent.args.targetId) ?? peerIdFromArg(requestedPeer);
|
|
402
407
|
return {
|
|
403
408
|
ok: false,
|
|
404
|
-
reason: `Denied ${ctx.intent.operation}: role=${ctx.role}, current_group=${currentGroup}, target_group=${targetGroup}. This role may
|
|
409
|
+
reason: `Denied ${ctx.intent.operation}: role=${ctx.role}, current_group=${currentGroup}, target_group=${targetGroup}. This role may access only the current group; cross-group access requires admin or owner permission.`,
|
|
405
410
|
};
|
|
406
411
|
}
|
|
407
412
|
return { ok: false, reason: 'Only the current relation can be targeted' };
|
|
@@ -498,6 +503,25 @@ function checkOwnPeer(ctx) {
|
|
|
498
503
|
return { ok: true };
|
|
499
504
|
const requestedPeerId = argPeer ? peerIdFromArg(argPeer) : undefined;
|
|
500
505
|
const requestedPeerKeyId = argPeerKey ? peerIdFromArg(argPeerKey) : undefined;
|
|
506
|
+
if (ctx.chatType === 'group') {
|
|
507
|
+
if (!expectedPeerKey)
|
|
508
|
+
return { ok: false, reason: 'Cannot verify peer ownership' };
|
|
509
|
+
const candidateKeys = new Set();
|
|
510
|
+
if (argPeerKey)
|
|
511
|
+
candidateKeys.add(argPeerKey);
|
|
512
|
+
if (argPeer) {
|
|
513
|
+
candidateKeys.add(argPeer);
|
|
514
|
+
const channelType = parseChannelType(expectedPeerKey);
|
|
515
|
+
if (channelType)
|
|
516
|
+
candidateKeys.add(formatPeerKey(channelType, argPeer));
|
|
517
|
+
const legacyChannelType = parseLegacyChannelType(expectedPeerKey);
|
|
518
|
+
if (legacyChannelType)
|
|
519
|
+
candidateKeys.add(`${legacyChannelType}::${argPeer}`);
|
|
520
|
+
}
|
|
521
|
+
return candidateKeys.has(expectedPeerKey)
|
|
522
|
+
? { ok: true }
|
|
523
|
+
: { ok: false, reason: 'Only the current group relation can be targeted' };
|
|
524
|
+
}
|
|
501
525
|
if (ctx.actorId) {
|
|
502
526
|
if (requestedPeerId && requestedPeerId !== ctx.actorId) {
|
|
503
527
|
return { ok: false, reason: 'Only the actor peer can be targeted' };
|