evolcore 0.0.20 → 0.0.22
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 +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +30 -6
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
import { AccessPolicyError, readAccessPolicy, } from '../../config/access-policy.js';
|
|
2
|
-
import { isAcceptedContact, resolveContactView, resolvePrincipal, } from '../../config/contact-book.js';
|
|
2
|
+
import { getContactSnapshot, isAcceptedContact, resolveContactView, resolvePrincipal, } from '../../config/contact-book.js';
|
|
3
|
+
import { isValidAid } from '../../aun/aid/validation.js';
|
|
4
|
+
import { expirePendingContactRequests } from '../../config/contact-request-service.js';
|
|
5
|
+
/** Validate managed private AUN sends against the sender's local contact policy. */
|
|
6
|
+
export async function evaluateOutboundContactAdmission(input) {
|
|
7
|
+
const selfAid = String(input.selfAid || '').trim();
|
|
8
|
+
const targetAid = String(input.targetAid || '').trim();
|
|
9
|
+
if (!isValidAid(selfAid) || !isValidAid(targetAid)) {
|
|
10
|
+
return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: 'A valid Agent AID is required for outbound contact admission' };
|
|
11
|
+
}
|
|
12
|
+
let policy;
|
|
13
|
+
try {
|
|
14
|
+
policy = readAccessPolicy(selfAid);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
|
|
18
|
+
}
|
|
19
|
+
if (policy.policyMode !== 'contacts') {
|
|
20
|
+
try {
|
|
21
|
+
if (getContactSnapshot(selfAid).contacts.get(targetAid)?.status === 'blocked') {
|
|
22
|
+
return { allow: false, code: 'CONTACT_BLOCKED', status: 'blocked', error: `目标联系人 ${targetAid} 已被拉黑。请先解除拉黑:ec contact unblock ${targetAid} --self ${selfAid}` };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
|
|
27
|
+
}
|
|
28
|
+
return { allow: true, status: policy.policyMode };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await expirePendingContactRequests(selfAid);
|
|
32
|
+
const snapshot = getContactSnapshot(selfAid);
|
|
33
|
+
const entry = snapshot.contacts.get(targetAid);
|
|
34
|
+
const view = resolveContactView(selfAid, targetAid);
|
|
35
|
+
if (view.isOwner)
|
|
36
|
+
return { allow: true, status: 'owner' };
|
|
37
|
+
if (entry && (entry.status === undefined || entry.status === 'active'))
|
|
38
|
+
return { allow: true, status: 'active' };
|
|
39
|
+
const status = entry?.status ?? 'absent';
|
|
40
|
+
if (status === 'blocked')
|
|
41
|
+
return { allow: false, code: 'CONTACT_BLOCKED', status, error: `目标联系人 ${targetAid} 已被拉黑。请先解除拉黑:ec contact unblock ${targetAid} --self ${selfAid}` };
|
|
42
|
+
const addCommand = `ec contact add ${targetAid} --self ${selfAid}`;
|
|
43
|
+
if (status === 'pending')
|
|
44
|
+
return { allow: false, code: 'CONTACT_PENDING', status, error: `目标联系人 ${targetAid} 的联系人申请正在审核中,请等待 Owner 通过;如需重新提交,执行:${addCommand}` };
|
|
45
|
+
if (status === 'declined')
|
|
46
|
+
return { allow: false, code: 'CONTACT_DECLINED', status, error: `目标联系人 ${targetAid} 的联系人申请已拒绝或过期,请重新申请:${addCommand}` };
|
|
47
|
+
return { allow: false, code: 'CONTACT_NOT_ACTIVE', status: 'absent', error: `目标 ${targetAid} 不是联系人,请先申请添加:${addCommand}` };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
3
53
|
export function evaluateInboundAdmissionPreflight(input) {
|
|
4
54
|
const selfAid = String(input.selfAid || '').trim();
|
|
5
55
|
const channelType = String(input.channelType || '').trim().toLowerCase();
|
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'crypto';
|
|
|
2
2
|
import { logger } from '../../utils/logger.js';
|
|
3
3
|
import { StreamDebouncer } from './stream-debouncer.js';
|
|
4
4
|
import { appendMessageLog, appendMessageLogStrict, buildInboundEntry, isTransientProtocolMessage } from './message-log.js';
|
|
5
|
-
import { buildEnvelope, sendInteractionPayload } from './message-utils.js';
|
|
5
|
+
import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from './message-utils.js';
|
|
6
6
|
import { chatDirPath } from '../session/session-fs-store.js';
|
|
7
7
|
import { formatChannelKey, tryParseChannelKey } from '../channel-loader.js';
|
|
8
8
|
import { agentDir, resolvePaths } from '../../paths.js';
|
|
@@ -25,7 +25,10 @@ import { createRootCausation, deriveCausation, normalizeCausation } from '../cau
|
|
|
25
25
|
import { recordCausationSpan } from '../causation/audit.js';
|
|
26
26
|
import { renderActionAsText } from '../interaction-router.js';
|
|
27
27
|
import { planApprovalRoute } from '../permission/approval-gateway.js';
|
|
28
|
-
import {
|
|
28
|
+
import { parseFullAccessCommand } from '../command/slash-gate.js';
|
|
29
|
+
import { isFullAccessEnabled } from '../../config-store.js';
|
|
30
|
+
import { auditFullAccessEvent } from '../auth/authorization-audit.js';
|
|
31
|
+
import { consumeAdmissionReplyBudget, clearAdmissionReplyBudgets, evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission, } from './message-admission.js';
|
|
29
32
|
const CONTACT_REJECTION_NOTICE = '[rejection] Not accepting messages from you.';
|
|
30
33
|
const REJECTABLE_PEER_TYPES = new Set(['ai', 'bot', 'agent', 'service']);
|
|
31
34
|
const REJECTION_MAX = 3;
|
|
@@ -98,6 +101,8 @@ export class MessageBridge {
|
|
|
98
101
|
processOwnersProvider;
|
|
99
102
|
debouncers = new Map();
|
|
100
103
|
defaultDebounce;
|
|
104
|
+
/** Serializes session selection/renewal with inbound log admission per route. */
|
|
105
|
+
admissionLocks = new Map();
|
|
101
106
|
agentRegistry;
|
|
102
107
|
bootstrapService;
|
|
103
108
|
menuDeduper = new MenuRequestDeduper();
|
|
@@ -431,6 +436,27 @@ export class MessageBridge {
|
|
|
431
436
|
await this.sendContactRequestProtocolResponse(adapter, msg, result, owningAgent?.name);
|
|
432
437
|
return true;
|
|
433
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Agent self-service contact add: create/update a pending request and send
|
|
441
|
+
* the same Owner action card used by inbound `/request`.
|
|
442
|
+
*/
|
|
443
|
+
async submitAgentContactRequest(input) {
|
|
444
|
+
const owningAgent = this.agentRegistry?.get(input.selfAid) ?? null;
|
|
445
|
+
const submission = await submitContactRequest({
|
|
446
|
+
selfAid: input.selfAid,
|
|
447
|
+
applicantAid: input.applicantAid,
|
|
448
|
+
sourceChannelKey: input.sourceChannelKey,
|
|
449
|
+
note: '通过 ec contact add 发起的联系人申请',
|
|
450
|
+
});
|
|
451
|
+
if ((submission.code !== 'submitted' && submission.code !== 'resubmitted') || !submission.requestId) {
|
|
452
|
+
return { ...submission, reviewCardSent: false };
|
|
453
|
+
}
|
|
454
|
+
const reviewCardSent = await this.deliverContactReviewCard(input.selfAid, input.applicantAid, submission, owningAgent);
|
|
455
|
+
if (!reviewCardSent) {
|
|
456
|
+
logger.warn(`[MessageBridge] Agent contact add request persisted without review card: self=${input.selfAid} applicant=${input.applicantAid} request=${submission.requestId}`);
|
|
457
|
+
}
|
|
458
|
+
return { ...submission, reviewCardSent };
|
|
459
|
+
}
|
|
434
460
|
contactRequestReply(result) {
|
|
435
461
|
switch (result.code) {
|
|
436
462
|
case 'submitted': return '联系人申请已提交,等待管理员审核。';
|
|
@@ -507,14 +533,17 @@ export class MessageBridge {
|
|
|
507
533
|
toolInput: { applicantAid, requestId: submission.requestId },
|
|
508
534
|
summary: `Review contact request from ${applicantAid}`,
|
|
509
535
|
grantable: true,
|
|
510
|
-
|
|
536
|
+
// Contact membership is an Agent-owner decision. Do not use the broader
|
|
537
|
+
// agent_manager policy here: that policy intentionally prefers an Admin,
|
|
538
|
+
// which made request cards invisible to the configured Owner.
|
|
539
|
+
approverPolicy: 'agent_owner',
|
|
511
540
|
createdAt: Date.parse(submission.submittedAt),
|
|
512
541
|
}, {
|
|
513
542
|
userId: applicantAid,
|
|
514
543
|
chatType: 'private',
|
|
515
544
|
selfAid,
|
|
516
545
|
approvalRouting: {
|
|
517
|
-
approverPolicy: '
|
|
546
|
+
approverPolicy: 'agent_owner',
|
|
518
547
|
owners,
|
|
519
548
|
admins,
|
|
520
549
|
resolveApproverCandidates,
|
|
@@ -602,10 +631,11 @@ export class MessageBridge {
|
|
|
602
631
|
chatmode: 'interactive',
|
|
603
632
|
replyContext: { delivery: { chatType: 'private' }, metadata: { source: 'handoff', chatmode: 'interactive' } },
|
|
604
633
|
});
|
|
605
|
-
const
|
|
606
|
-
|
|
634
|
+
const receipt = await sendInteractionPayload(route.adapter, envelope, interaction, renderActionAsText(interaction), envelope.replyContext);
|
|
635
|
+
const accepted = isInteractionSendAccepted(receipt);
|
|
636
|
+
if (!accepted)
|
|
607
637
|
await this.interactionRouter.cancel(submission.requestId, 'delivery_failed');
|
|
608
|
-
return
|
|
638
|
+
return accepted;
|
|
609
639
|
}
|
|
610
640
|
async sendContactReviewOutcome(adapter, ownerAid, applicantAid, code, agentName) {
|
|
611
641
|
const text = code === 'approved' ? `已通过 ${applicantAid} 的联系人申请。`
|
|
@@ -666,12 +696,23 @@ export class MessageBridge {
|
|
|
666
696
|
const available = this.availableBaseagentsFor(channelName);
|
|
667
697
|
if (available.length === 0 || available.includes(session.baseagent))
|
|
668
698
|
return session;
|
|
699
|
+
// A topic's backend binding is fenced by SessionManager and cannot be
|
|
700
|
+
// cleared as a side effect of routing alignment. Reassigning the
|
|
701
|
+
// baseagent while leaving the old provider's agentSessionId persisted
|
|
702
|
+
// would make the new runner resume an incompatible backend. Keep the
|
|
703
|
+
// binding intact and let normal baseagent-mismatch handling report it;
|
|
704
|
+
// an explicit boundary operation must perform any future migration.
|
|
705
|
+
if (session.threadId) {
|
|
706
|
+
logger.error(`[MessageBridge] Cannot align topic to another baseagent: `
|
|
707
|
+
+ `session=${session.id} thread=${session.threadId} current=${session.baseagent} preferred=${owningAgent.baseagent}`);
|
|
708
|
+
return session;
|
|
709
|
+
}
|
|
669
710
|
const preferred = available.includes(owningAgent.baseagent)
|
|
670
711
|
? owningAgent.baseagent
|
|
671
712
|
: available[0];
|
|
672
713
|
logger.warn(`[MessageBridge] Aligning unavailable session baseagent: session=${session.id} ${session.baseagent} -> ${preferred} agent=${owningAgent.name} available=${available.join(',')}`);
|
|
673
|
-
await this.sessionManager.updateSession(session.id, { baseagent: preferred
|
|
674
|
-
return { ...session, baseagent: preferred
|
|
714
|
+
await this.sessionManager.updateSession(session.id, { baseagent: preferred });
|
|
715
|
+
return { ...session, baseagent: preferred };
|
|
675
716
|
}
|
|
676
717
|
/**
|
|
677
718
|
* 为渠道注册消息桥梁:入站处理管线 + 出站命令响应
|
|
@@ -685,8 +726,15 @@ export class MessageBridge {
|
|
|
685
726
|
register(channelName, onMessage, sendReply, adapter, channelType) {
|
|
686
727
|
const effectiveChannelType = channelType || channelName;
|
|
687
728
|
onMessage(async (msg) => {
|
|
729
|
+
let releaseAdmission;
|
|
730
|
+
let releaseSessionIngress;
|
|
731
|
+
const releaseIngress = () => {
|
|
732
|
+
releaseSessionIngress?.();
|
|
733
|
+
releaseSessionIngress = undefined;
|
|
734
|
+
};
|
|
688
735
|
try {
|
|
689
736
|
let content = msg.content.trim();
|
|
737
|
+
let executionPermissionOverride;
|
|
690
738
|
// Channels that expose adapter receive time (notably AUN) must keep
|
|
691
739
|
// that timestamp through session resolution and queue processing.
|
|
692
740
|
// The fallback is only for legacy adapters that do not provide it.
|
|
@@ -709,6 +757,12 @@ export class MessageBridge {
|
|
|
709
757
|
const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
|
|
710
758
|
const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
|
|
711
759
|
const resolvedChannelType = msg.channelType || parsedChannelKey?.type || effectiveChannelType;
|
|
760
|
+
const admissionRoute = [
|
|
761
|
+
resolvedChannelType,
|
|
762
|
+
selfAid || '',
|
|
763
|
+
msg.channelId,
|
|
764
|
+
msg.threadId || '',
|
|
765
|
+
].join('#');
|
|
712
766
|
const trustedHandoffEcho = msg.source === 'handoff'
|
|
713
767
|
&& resolvedChannelType === 'aun'
|
|
714
768
|
&& !!selfAid
|
|
@@ -890,6 +944,61 @@ export class MessageBridge {
|
|
|
890
944
|
await this.sendAccessDenied(adapter, channelName, msg, sendReply);
|
|
891
945
|
return;
|
|
892
946
|
}
|
|
947
|
+
// /fa is an execution prefix, not a session-mode switch or a normal
|
|
948
|
+
// quick command. Only this trusted bridge path can construct the
|
|
949
|
+
// internal per-call authorization carried into ResponseEngine.
|
|
950
|
+
const fullAccessCommand = parseFullAccessCommand(content);
|
|
951
|
+
if (fullAccessCommand.matched) {
|
|
952
|
+
const reject = async (text) => {
|
|
953
|
+
logger.warn(`[MessageBridge] fullaccess command rejected: channel=${channelName} `
|
|
954
|
+
+ `actor=${authSubject.principalId ?? authSubject.actorId ?? '<none>'} reason=${text}`);
|
|
955
|
+
auditFullAccessEvent({
|
|
956
|
+
event: 'fullaccess.command.rejected',
|
|
957
|
+
source: 'fullaccess-command',
|
|
958
|
+
decision: 'deny',
|
|
959
|
+
actorId: authSubject.principalId ?? authSubject.actorId,
|
|
960
|
+
processRole: authSubject.processRole,
|
|
961
|
+
messageId: msg.messageId,
|
|
962
|
+
reason: text,
|
|
963
|
+
});
|
|
964
|
+
await sendReply(msg.channelId, `❌ ${text}`, msg.replyContext);
|
|
965
|
+
};
|
|
966
|
+
if (msg.source !== undefined && msg.source !== 'user') {
|
|
967
|
+
await reject('fullaccess 只能由用户直接发送的 /fa 或 /fullaccess 命令发起');
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (!fullAccessCommand.prompt) {
|
|
971
|
+
await reject('用法:/fa <提示词> 或 /fullaccess <提示词>');
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (!isFullAccessEnabled()) {
|
|
975
|
+
await reject('fullaccess 功能未启用,请先设置 daemon.json.fullaccess.enabled=true');
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (!authSubject.isDaemonOwner || !authSubject.principalId) {
|
|
979
|
+
await reject('仅可信 daemon-owner 可以使用 fullaccess');
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (!msg.messageLogContent)
|
|
983
|
+
msg.messageLogContent = content;
|
|
984
|
+
content = fullAccessCommand.prompt;
|
|
985
|
+
executionPermissionOverride = {
|
|
986
|
+
permissionMode: 'fullaccess',
|
|
987
|
+
processRole: 'fullaccess-run',
|
|
988
|
+
dataScope: 'daemon',
|
|
989
|
+
source: 'fullaccess-command',
|
|
990
|
+
authorizedBy: authSubject.principalId,
|
|
991
|
+
};
|
|
992
|
+
logger.warn(`[MessageBridge] fullaccess command accepted: channel=${channelName} `
|
|
993
|
+
+ `actor=${authSubject.principalId} message=${msg.messageId ?? '<none>'}`);
|
|
994
|
+
auditFullAccessEvent({
|
|
995
|
+
event: 'fullaccess.command.accepted',
|
|
996
|
+
source: 'fullaccess-command',
|
|
997
|
+
actorId: authSubject.principalId,
|
|
998
|
+
processRole: authSubject.processRole,
|
|
999
|
+
messageId: msg.messageId,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
893
1002
|
// 0. 仅阻止协议遥测进入 session / model / messages.jsonl;协议本身已在
|
|
894
1003
|
// channel/main/events/AUN/handoff 专用链路中处理和记录。
|
|
895
1004
|
// 注:menu.* 自定义消息已由上方 handleMenuControl 快速路径处理。
|
|
@@ -928,7 +1037,18 @@ export class MessageBridge {
|
|
|
928
1037
|
const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
|
|
929
1038
|
const cmdContent = contentForCmd || content;
|
|
930
1039
|
const isCmd = msg.source !== 'handoff'
|
|
1040
|
+
// Once /fa has been accepted, its remainder is always model input.
|
|
1041
|
+
// A prompt such as `/fa /status` must not be re-routed as the
|
|
1042
|
+
// ordinary `/status` quick command after the prefix is removed.
|
|
1043
|
+
&& !executionPermissionOverride
|
|
1044
|
+
&& !parseFullAccessCommand(cmdContent).matched
|
|
931
1045
|
&& this.cmdHandler.isCommand(cmdContent);
|
|
1046
|
+
if (isCmd && /^\/new(?:\s|$)/i.test(cmdContent)) {
|
|
1047
|
+
// /new changes the active main-session boundary. Serialize it with
|
|
1048
|
+
// ordinary session admission so an in-flight renew cannot append or
|
|
1049
|
+
// enqueue against a superseded session after /new commits.
|
|
1050
|
+
releaseAdmission = await this.acquireAdmission(admissionRoute);
|
|
1051
|
+
}
|
|
932
1052
|
if (isCmd) {
|
|
933
1053
|
logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
|
|
934
1054
|
// 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
|
|
@@ -966,6 +1086,7 @@ export class MessageBridge {
|
|
|
966
1086
|
}
|
|
967
1087
|
}
|
|
968
1088
|
if (msg.source !== 'handoff'
|
|
1089
|
+
&& !executionPermissionOverride
|
|
969
1090
|
&& await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
|
|
970
1091
|
const taskId = `cmd-${msg.messageId || Date.now()}`;
|
|
971
1092
|
logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId, correlationId: taskId, sessionId: msg.replyContext?.sessionId, agentAid: msg.selfAID, payload: { kind: 'command.result', text } });
|
|
@@ -1009,8 +1130,16 @@ export class MessageBridge {
|
|
|
1009
1130
|
// 通道找不到归属时退回到 globalConfig(一般是测试场景)
|
|
1010
1131
|
const effectiveProjectPath = owningAgent?.projectPath
|
|
1011
1132
|
?? this.defaultProjectPath;
|
|
1133
|
+
// Session renewal and inbound history append form one admission
|
|
1134
|
+
// transaction. Without this per-route single-flight, a second message
|
|
1135
|
+
// can observe the just-renewed empty session before the first message
|
|
1136
|
+
// has been appended and create another renewed session.
|
|
1137
|
+
releaseAdmission = await this.acquireAdmission(admissionRoute);
|
|
1012
1138
|
const hadMainSession = msg.threadId ? true : this.sessionManager.hasMainSession(channelName, msg.channelId, msg.channelType || effectiveChannelType, msg.selfAID || selfAid);
|
|
1013
1139
|
let session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, this.extractTopicName(msg), msg.peerId, chatType, owningAgent?.baseagent, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType, identity);
|
|
1140
|
+
if (session.threadId && typeof this.messageQueue.reserveSessionIngress === 'function') {
|
|
1141
|
+
releaseSessionIngress = this.messageQueue.reserveSessionIngress(session.id);
|
|
1142
|
+
}
|
|
1014
1143
|
session = await this.alignSessionBaseagent(channelName, session);
|
|
1015
1144
|
const replyToMessageId = typeof msg.replyContext?.metadata?.refMessageId === 'string'
|
|
1016
1145
|
? msg.replyContext.metadata.refMessageId
|
|
@@ -1065,6 +1194,7 @@ export class MessageBridge {
|
|
|
1065
1194
|
topicName: this.extractTopicName(msg),
|
|
1066
1195
|
replyContext: msg.replyContext,
|
|
1067
1196
|
source: msg.source,
|
|
1197
|
+
...(executionPermissionOverride ? { executionPermissionOverride } : {}),
|
|
1068
1198
|
boundSessionId: session.id,
|
|
1069
1199
|
resolvedIdentity: identity,
|
|
1070
1200
|
mentionMode: msg.mentionMode,
|
|
@@ -1134,6 +1264,12 @@ export class MessageBridge {
|
|
|
1134
1264
|
}
|
|
1135
1265
|
if (!handoffCandidate)
|
|
1136
1266
|
appendMessageLog(inboundEntry.chatDir, inboundEntry.entry);
|
|
1267
|
+
// The authoritative session choice is now paired with its first
|
|
1268
|
+
// conversation record. Release before waiting for queue completion so
|
|
1269
|
+
// later group messages can still exercise the normal interrupt/FIFO
|
|
1270
|
+
// policy instead of being serialized behind the whole model turn.
|
|
1271
|
+
releaseAdmission?.();
|
|
1272
|
+
releaseAdmission = undefined;
|
|
1137
1273
|
// 6. ACK + debounce/enqueue
|
|
1138
1274
|
// ACK 在到达时立即做(每条独立 ACK),不等合并
|
|
1139
1275
|
// Interrupt 模式(单聊)→ 入队前 debounce 合并
|
|
@@ -1158,6 +1294,7 @@ export class MessageBridge {
|
|
|
1158
1294
|
role: !isInterrupt ? (session.identity?.role ?? 'none') : undefined,
|
|
1159
1295
|
sessionKeyField: session.sessionKey,
|
|
1160
1296
|
selfAID, // ← 传递 selfAID,用于队列隔离
|
|
1297
|
+
onPersisted: releaseIngress,
|
|
1161
1298
|
});
|
|
1162
1299
|
}
|
|
1163
1300
|
catch (error) {
|
|
@@ -1193,8 +1330,29 @@ export class MessageBridge {
|
|
|
1193
1330
|
catch (error) {
|
|
1194
1331
|
logger.error(`[MessageBridge] Error in onMessage handler for ${channelName}:`, error);
|
|
1195
1332
|
}
|
|
1333
|
+
finally {
|
|
1334
|
+
releaseAdmission?.();
|
|
1335
|
+
releaseIngress();
|
|
1336
|
+
}
|
|
1196
1337
|
});
|
|
1197
1338
|
}
|
|
1339
|
+
async acquireAdmission(key) {
|
|
1340
|
+
const previous = this.admissionLocks.get(key) ?? Promise.resolve();
|
|
1341
|
+
let release;
|
|
1342
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
1343
|
+
const tail = previous.then(() => current);
|
|
1344
|
+
this.admissionLocks.set(key, tail);
|
|
1345
|
+
await previous;
|
|
1346
|
+
let released = false;
|
|
1347
|
+
return () => {
|
|
1348
|
+
if (released)
|
|
1349
|
+
return;
|
|
1350
|
+
released = true;
|
|
1351
|
+
release();
|
|
1352
|
+
if (this.admissionLocks.get(key) === tail)
|
|
1353
|
+
this.admissionLocks.delete(key);
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1198
1356
|
// ── Menu Protocol ──
|
|
1199
1357
|
extractTopicName(msg) {
|
|
1200
1358
|
const raw = msg.topicName
|
|
@@ -1541,7 +1699,7 @@ export class MessageBridge {
|
|
|
1541
1699
|
return;
|
|
1542
1700
|
logger.warn(`[MenuControl] category=${category} request=${parsed.id || '<missing>'}`
|
|
1543
1701
|
+ ` type=${parsed.type} name=${parsed.name || '<none>'} action=${parsed.action || '<none>'}`
|
|
1544
|
-
+ ` transport=${channel} scope=${
|
|
1702
|
+
+ ` transport=${channel} scope=${scope || 'none'} sender=${msg.peerId || 'none'}`
|
|
1545
1703
|
+ (category === 'request-replay' ? ' replay=true' : '')
|
|
1546
1704
|
+ (category === 'request-retry' ? ' retry=true' : '')
|
|
1547
1705
|
+ `${code ? ` code=${code}` : ''}${decision.suppressed ? ` suppressed=${decision.suppressed}` : ''}`);
|
|
@@ -1603,7 +1761,22 @@ export class MessageBridge {
|
|
|
1603
1761
|
if (!this.cmdHandler.isCommand(content))
|
|
1604
1762
|
return false;
|
|
1605
1763
|
logger.info(`[${channel}] ${channelId}: ${content}${source === 'card-trigger' ? ' [card]' : ''}`);
|
|
1606
|
-
const
|
|
1764
|
+
const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
|
|
1765
|
+
const cmdResult = await this.cmdHandler.handle(content, channel, channelId, async (_cid, text, opts, updatePayload) => {
|
|
1766
|
+
if (updatePayload && adapter?.send) {
|
|
1767
|
+
const envelope = buildEnvelope({
|
|
1768
|
+
taskId: `cmd-update-${randomBytes(5).toString('hex')}`,
|
|
1769
|
+
channel,
|
|
1770
|
+
channelId,
|
|
1771
|
+
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
1772
|
+
chatmode: 'interactive',
|
|
1773
|
+
replyContext: opts ?? replyContext,
|
|
1774
|
+
});
|
|
1775
|
+
await adapter.send(envelope, updatePayload);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
await sendReply(text, opts, updatePayload);
|
|
1779
|
+
}, userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject, replyContext);
|
|
1607
1780
|
logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
|
|
1608
1781
|
if (cmdResult === undefined)
|
|
1609
1782
|
return false;
|
|
@@ -1621,7 +1794,6 @@ export class MessageBridge {
|
|
|
1621
1794
|
return true;
|
|
1622
1795
|
}
|
|
1623
1796
|
// 出站走 adapter.send 统一入口
|
|
1624
|
-
const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
|
|
1625
1797
|
let responseSessionId;
|
|
1626
1798
|
try {
|
|
1627
1799
|
const responseSession = threadId
|
|
@@ -231,24 +231,64 @@ export function messageLogPath(chatDir) {
|
|
|
231
231
|
return path.join(chatDir, MESSAGE_LOG_FILE);
|
|
232
232
|
}
|
|
233
233
|
export function hasMessageLogOperation(chatDir, operationId) {
|
|
234
|
+
return findMessageLogOperationMessageId(chatDir, operationId) !== undefined;
|
|
235
|
+
}
|
|
236
|
+
/** Find the newest completed outbound operation whose ID starts with prefix. */
|
|
237
|
+
export function findLatestMessageLogOperation(chatDir, operationPrefix) {
|
|
234
238
|
const file = messageLogPath(chatDir);
|
|
235
239
|
if (!fs.existsSync(file))
|
|
236
|
-
return
|
|
240
|
+
return undefined;
|
|
241
|
+
let latest;
|
|
237
242
|
try {
|
|
238
|
-
|
|
243
|
+
for (const line of fs.readFileSync(file, 'utf-8').split('\n')) {
|
|
239
244
|
if (!line.trim())
|
|
240
|
-
|
|
245
|
+
continue;
|
|
241
246
|
try {
|
|
242
247
|
const entry = JSON.parse(line);
|
|
243
|
-
|
|
248
|
+
if (entry?.dir !== 'out'
|
|
249
|
+
|| typeof entry.operationId !== 'string'
|
|
250
|
+
|| (!entry.operationId.startsWith(operationPrefix))
|
|
251
|
+
|| typeof entry.msgId !== 'string'
|
|
252
|
+
|| !entry.msgId)
|
|
253
|
+
continue;
|
|
254
|
+
const ts = typeof entry.ts === 'number' && Number.isFinite(entry.ts) ? entry.ts : 0;
|
|
255
|
+
if (!latest || ts >= latest.ts) {
|
|
256
|
+
latest = { messageId: entry.msgId, operationId: entry.operationId, ts };
|
|
257
|
+
}
|
|
244
258
|
}
|
|
245
259
|
catch {
|
|
246
|
-
|
|
260
|
+
// skip malformed message log lines
|
|
247
261
|
}
|
|
248
|
-
}
|
|
262
|
+
}
|
|
249
263
|
}
|
|
250
264
|
catch {
|
|
251
|
-
return
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
return latest;
|
|
268
|
+
}
|
|
269
|
+
export function findMessageLogOperationMessageId(chatDir, operationId) {
|
|
270
|
+
const file = messageLogPath(chatDir);
|
|
271
|
+
if (!fs.existsSync(file))
|
|
272
|
+
return undefined;
|
|
273
|
+
try {
|
|
274
|
+
for (const line of fs.readFileSync(file, 'utf-8').split('\n')) {
|
|
275
|
+
if (!line.trim())
|
|
276
|
+
continue;
|
|
277
|
+
try {
|
|
278
|
+
const entry = JSON.parse(line);
|
|
279
|
+
if (entry?.dir === 'out' && entry.operationId === operationId) {
|
|
280
|
+
if (typeof entry.msgId === 'string' && entry.msgId)
|
|
281
|
+
return entry.msgId;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// skip malformed message log lines
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return undefined;
|
|
252
292
|
}
|
|
253
293
|
}
|
|
254
294
|
export function resolveChatDir(sessionsDir, channelType, channelId, selfAID) {
|