evolcore 0.0.29 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/agents/codex-runner.js +61 -14
  3. package/dist/aun/aid/agentmd.js +224 -19
  4. package/dist/aun/aid/index.js +2 -1
  5. package/dist/aun/aid/managed-operation.js +70 -4
  6. package/dist/aun/group/index.js +1 -0
  7. package/dist/{core → aun}/group/invite-service.js +4 -4
  8. package/dist/channels/daemon.js +1 -1
  9. package/dist/cli/agent-command.js +7 -7
  10. package/dist/cli/aun-commands.js +97 -37
  11. package/dist/cli/data-command.js +2 -1
  12. package/dist/cli/managed-command-guard.js +2 -2
  13. package/dist/cli/model.js +1 -1
  14. package/dist/cli/response.js +1 -1
  15. package/dist/cli/trigger-command.js +52 -29
  16. package/dist/config/boot-log.js +95 -1
  17. package/dist/config/builtin-roles.js +1 -0
  18. package/dist/config/peer-role-resolver.js +21 -0
  19. package/dist/core/agent-ipc-registration.js +409 -0
  20. package/dist/core/agent-runtime.js +435 -0
  21. package/dist/core/auth/delegation-authorization.js +58 -0
  22. package/dist/core/auth/operation-catalog.js +8 -0
  23. package/dist/core/bootstrap-service.js +65 -1
  24. package/dist/core/channel-runtime.js +435 -0
  25. package/dist/core/command/command-handler.js +4 -1
  26. package/dist/core/daemon-bootstrap.js +151 -0
  27. package/dist/core/daemon-control.js +205 -0
  28. package/dist/core/daemon-lifecycle.js +225 -0
  29. package/dist/core/data-migration.js +4 -1
  30. package/dist/core/handoff/ipc-registration.js +156 -0
  31. package/dist/core/ipc-registration.js +457 -0
  32. package/dist/core/memory/consolidation-trigger.js +8 -0
  33. package/dist/core/memory/consolidation.js +838 -0
  34. package/dist/core/message/aun-ipc-sender.js +226 -0
  35. package/dist/core/message/outbound.js +165 -0
  36. package/dist/core/permission/approval-gateway.js +13 -0
  37. package/dist/core/permission/ec-command-parser.js +80 -35
  38. package/dist/core/permission/sandbox-runtime.js +23 -2
  39. package/dist/core/permission/tool-error-code.js +1 -0
  40. package/dist/core/permission/tool-policy.js +61 -24
  41. package/dist/core/session/session-idle-monitor.js +17 -0
  42. package/dist/index.js +352 -4841
  43. package/dist/ipc.js +2 -1
  44. package/dist/stats/db.js +24 -0
  45. package/dist/stats/index.js +1 -1
  46. package/dist/stats/writer.js +25 -1
  47. package/dist/trigger/application-service.js +157 -0
  48. package/dist/trigger/authorization.js +371 -0
  49. package/dist/trigger/ipc-service.js +406 -0
  50. package/dist/trigger/local-files.js +133 -0
  51. package/dist/trigger/manager.js +235 -22
  52. package/dist/trigger/scheduler.js +30 -5
  53. package/dist/trigger/script-executor.js +176 -39
  54. package/dist/trigger/script-sandbox.js +458 -0
  55. package/dist/trigger/system-definitions.js +188 -0
  56. package/dist/trigger/validation.js +0 -3
  57. package/dist/utils/aid-bind.js +1 -1
  58. package/dist/utils/evolcore-version.js +74 -0
  59. package/dist/utils/logger.js +16 -0
  60. package/kits/docs/evolcore/INDEX.md +1 -1
  61. package/kits/docs/evolcore/aid.md +9 -1
  62. package/kits/docs/evolcore/model.md +3 -2
  63. package/kits/docs/evolcore/rpc.md +4 -0
  64. package/kits/docs/evolcore/trigger.md +15 -1
  65. package/kits/schemas/relation-config.schema.9.json +1 -1
  66. package/kits/templates/system-fragments/commands.md +0 -1
  67. package/package.json +1 -1
  68. package/dist/core/memory/launch-consolidation.js +0 -395
package/dist/ipc.js CHANGED
@@ -1556,7 +1556,7 @@ export class IpcServer {
1556
1556
  const ownerAid = taskCtx.selfAid
1557
1557
  ? this.agentRegistry?.get(taskCtx.selfAid)?.config?.owners?.[0]
1558
1558
  : undefined;
1559
- const { launchConsolidation } = await import('./core/memory/launch-consolidation.js');
1559
+ const { launchConsolidation } = await import('./core/memory/consolidation.js');
1560
1560
  if (!this.consolidationLaunchExecutor) {
1561
1561
  // daemon 未注入 launcher —— 无法直接创建整理会话,明确报错而不是静默失败
1562
1562
  return { ok: false, code: 'NOT_CONFIGURED', error: 'daemon 未注入整理会话 launcher' };
@@ -1868,6 +1868,7 @@ export class IpcServer {
1868
1868
  case 'trigger.show':
1869
1869
  case 'trigger.history':
1870
1870
  case 'trigger.eventCatalog':
1871
+ case 'trigger.authorizeLocalFiles':
1871
1872
  case 'trigger.create':
1872
1873
  case 'trigger.update':
1873
1874
  case 'trigger.setEnabled':
package/dist/stats/db.js CHANGED
@@ -69,6 +69,30 @@ export function getDb(evolcoreHome) {
69
69
  return null;
70
70
  }
71
71
  }
72
+ export function startDailyRollupMaintenance(evolcoreHome) {
73
+ const db = getDb(evolcoreHome);
74
+ if (!db)
75
+ return undefined;
76
+ try {
77
+ const daily = db.prepare('SELECT COUNT(*) AS n FROM usage_daily').get();
78
+ const events = db.prepare('SELECT COUNT(*) AS n FROM usage_events').get();
79
+ if (daily.n === 0 && events.n > 0) {
80
+ logger.info('[Stats] usage_daily 为空,回填历史数据…');
81
+ rebuildDailyRollup(evolcoreHome);
82
+ }
83
+ }
84
+ catch (error) {
85
+ logger.warn(`[Stats] usage_daily 回填检测失败(非致命): ${error}`);
86
+ }
87
+ return setInterval(() => {
88
+ try {
89
+ rebuildDailyRollup(evolcoreHome);
90
+ }
91
+ catch (error) {
92
+ logger.warn(`[Stats] usage_daily 自愈失败(非致命): ${error}`);
93
+ }
94
+ }, 24 * 60 * 60 * 1000);
95
+ }
72
96
  /** 只读连接(CLI / ecweb 调用)。每次返回新连接,调用方负责 close()。 */
73
97
  export function openReadonlyDb(dbPath) {
74
98
  const sqlite = loadSqlite();
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export { getDb, openReadonlyDb, getStatsDir, getDbPath, closeStatsDb, archiveOldData, listArchivePaths, rebuildDailyRollup } from './db.js';
5
5
  export { normalizeUsage } from './normalizer.js';
6
- export { insertUsageEvent, insertContextBreakdown, insertMessageEvent, insertModelCalls } from './writer.js';
6
+ export { initializeStatsPersistence, insertUsageEvent, insertContextBreakdown, insertMessageEvent, insertModelCalls } from './writer.js';
7
7
  export { calcCost, resolvePriceRow, resolveCanonicalModel, registerBillingFn } from './billing.js';
8
8
  export { queryAggregated, queryTodaySummary, querySessionTurns, queryContextBreakdown, queryTopPeers, queryTopModels, queryMessageAggregated, queryMessageTodaySummary, queryPeerList, querySummary, queryPeerDaily, queryTaskModelCalls, querySessionModelCalls } from './query.js';
9
9
  export { getBudgetStatus } from './budget.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * writer.ts — 写入 usage_events 和 context_breakdown。
3
3
  */
4
- import { getDb, resetStatsDb } from './db.js';
4
+ import { getDb, resetStatsDb, startDailyRollupMaintenance } from './db.js';
5
5
  import { resolvePrices } from './price-resolver.js';
6
6
  import { logger } from '../utils/logger.js';
7
7
  export function insertUsageEvent(evolcoreHome, event, gatewayPricing) {
@@ -105,6 +105,30 @@ export function insertContextBreakdown(evolcoreHome, bd) {
105
105
  import('../utils/logger.js').then(({ logger }) => logger.warn(`[StatsWriter] insertContextBreakdown failed: ${e}`));
106
106
  }
107
107
  }
108
+ export function initializeStatsPersistence(evolcoreHome, collector) {
109
+ const previousOnMessage = collector.onMessage;
110
+ const onMessage = (event) => {
111
+ void Promise.resolve()
112
+ .then(() => insertMessageEvent(evolcoreHome, event))
113
+ .catch(() => { });
114
+ };
115
+ collector.onMessage = onMessage;
116
+ let stopped = false;
117
+ let maintenanceTimer;
118
+ void Promise.resolve()
119
+ .then(() => {
120
+ if (!stopped)
121
+ maintenanceTimer = startDailyRollupMaintenance(evolcoreHome);
122
+ })
123
+ .catch(() => { });
124
+ return () => {
125
+ stopped = true;
126
+ if (collector.onMessage === onMessage)
127
+ collector.onMessage = previousOnMessage;
128
+ if (maintenanceTimer)
129
+ clearInterval(maintenanceTimer);
130
+ };
131
+ }
108
132
  export function insertMessageEvent(evolcoreHome, event) {
109
133
  const db = getDb(evolcoreHome);
110
134
  if (!db)
@@ -0,0 +1,157 @@
1
+ import { consolidationCheckCron, removeConsolidationCheckTrigger, seedConsolidationCheckTrigger } from '../core/memory/consolidation-trigger.js';
2
+ import { tryParseChannelKey } from '../core/channel-loader.js';
3
+ import { TriggerDefinitionManager } from './manager.js';
4
+ import { TriggerRunStateStore } from './state.js';
5
+ import { removeLegacyAgentUpgradeCheck, seedSessionIdleCheckTrigger, seedTaskRecoveryTrigger, seedUpgradeCheckTrigger, } from './system-definitions.js';
6
+ import { TriggerFeedbackDispatcher } from './feedback.js';
7
+ import { TriggerRuntimeScheduler } from './scheduler.js';
8
+ import { logger } from '../utils/logger.js';
9
+ export class TriggerSchedulerRegistry {
10
+ schedulers = new Map();
11
+ starts = new Map();
12
+ get(agentAid) {
13
+ return this.schedulers.get(agentAid);
14
+ }
15
+ values() {
16
+ return this.schedulers.values();
17
+ }
18
+ async replace(agentAid, createScheduler) {
19
+ await this.schedulers.get(agentAid)?.stop();
20
+ const scheduler = createScheduler();
21
+ this.schedulers.set(agentAid, scheduler);
22
+ try {
23
+ await scheduler.init();
24
+ }
25
+ catch (error) {
26
+ logger.error(`[Trigger] Scheduler init failed for ${agentAid}: ${error instanceof Error ? error.message : String(error)}`);
27
+ }
28
+ }
29
+ async ensureStarted(agentAid, start) {
30
+ const existing = this.starts.get(agentAid);
31
+ if (existing)
32
+ return await existing;
33
+ const starting = start();
34
+ this.starts.set(agentAid, starting);
35
+ try {
36
+ await starting;
37
+ }
38
+ catch (error) {
39
+ if (this.starts.get(agentAid) === starting)
40
+ this.starts.delete(agentAid);
41
+ throw error;
42
+ }
43
+ }
44
+ async remove(agentAid) {
45
+ await this.schedulers.get(agentAid)?.stop();
46
+ this.schedulers.delete(agentAid);
47
+ this.starts.delete(agentAid);
48
+ }
49
+ sessionIdleTriggerIds() {
50
+ return [...this.schedulers.values()].flatMap(scheduler => (scheduler.list({ all: true })
51
+ .filter(definition => definition.source.type === 'event' && definition.source.eventPattern === 'session:idle')
52
+ .map(definition => definition.id)));
53
+ }
54
+ async waitForIdle(timeoutMs) {
55
+ await Promise.all([...this.schedulers.values()].map(scheduler => scheduler.waitForIdle(timeoutMs)));
56
+ }
57
+ async suspendForDaemonExit() {
58
+ await Promise.all([...this.schedulers.values()].map(scheduler => scheduler.suspendForDaemonExit()));
59
+ }
60
+ }
61
+ export function createTriggerChannelResolver({ agentRegistry, channelInstances, daemonChannel, daemonTriggerOwner, }) {
62
+ return (agentAid, channelKey) => {
63
+ if (agentAid === daemonTriggerOwner.aid && channelKey === daemonChannel.channelKey) {
64
+ return { adapter: daemonChannel, agentAid, agentName: daemonTriggerOwner.aid, projectPath: daemonTriggerOwner.projectPath, baseagent: daemonTriggerOwner.baseagent };
65
+ }
66
+ const agent = agentRegistry.get(agentAid);
67
+ if (!agent)
68
+ return undefined;
69
+ const instance = channelInstances.find(candidate => candidate.adapter.channelKey === channelKey || candidate.adapter.channelName === channelKey);
70
+ const parsed = instance ? tryParseChannelKey(instance.adapter.channelKey) : null;
71
+ if (instance && parsed?.selfAID !== agentAid)
72
+ return undefined;
73
+ if (!instance)
74
+ return undefined;
75
+ return { adapter: instance.adapter, agentAid, agentName: agent.aid, projectPath: agent.projectPath, baseagent: agent.baseagent };
76
+ };
77
+ }
78
+ export function buildTriggerScheduler(owner, dependencies, options = {}) {
79
+ const manager = new TriggerDefinitionManager(owner.aid);
80
+ if (options.seedUpgradeCheck) {
81
+ seedUpgradeCheckTrigger(manager, owner);
82
+ seedTaskRecoveryTrigger(manager, owner);
83
+ seedSessionIdleCheckTrigger(manager, owner);
84
+ }
85
+ else {
86
+ removeLegacyAgentUpgradeCheck(manager);
87
+ }
88
+ const memoryConfig = dependencies.getMemoryConfig(owner.aid);
89
+ if (memoryConfig?.enableRelation !== false) {
90
+ seedConsolidationCheckTrigger(manager, {
91
+ aid: owner.aid,
92
+ cronExpression: consolidationCheckCron(memoryConfig?.checkIntervalHours),
93
+ });
94
+ }
95
+ else {
96
+ removeConsolidationCheckTrigger(manager);
97
+ }
98
+ const state = new TriggerRunStateStore(manager);
99
+ const agentTriggerAudit = dependencies.triggerAudit.withHistory(manager.history);
100
+ const dispatcher = new TriggerFeedbackDispatcher({
101
+ getChannel: dependencies.getTriggerChannel,
102
+ sessionManager: dependencies.sessionManager,
103
+ messageQueue: dependencies.messageQueue,
104
+ recoverTask: request => dependencies.responseEngine.recoverTask(request),
105
+ inspectTargetExecution: (sessionId, attemptId, selfAid) => (dependencies.responseEngine.inspectTargetExecution(sessionId, attemptId, selfAid)),
106
+ eventBus: dependencies.eventBus,
107
+ tryHandoff: async (input, text, target, binding) => {
108
+ if (input.signal?.aborted) {
109
+ throw input.signal.reason instanceof Error ? input.signal.reason : new Error('trigger feedback cancelled');
110
+ }
111
+ if (tryParseChannelKey(target.channelKey)?.type !== 'aun')
112
+ return null;
113
+ const inst = dependencies.channelInstances.find(candidate => (candidate.channelType === 'aun'
114
+ && (candidate.adapter.channelKey === target.channelKey || candidate.adapter.channelName === target.channelKey)
115
+ && tryParseChannelKey(candidate.adapter.channelKey)?.selfAID === input.trigger.agentAid));
116
+ const channel = inst?.channel;
117
+ if (!channel)
118
+ return null;
119
+ const targetChatType = target.chatType;
120
+ if (!targetChatType)
121
+ return null;
122
+ if (targetChatType === 'group' && typeof channel.sendDaemonGroupMsg !== 'function')
123
+ return null;
124
+ if (targetChatType === 'private' && typeof channel.sendDaemonMsg !== 'function')
125
+ return null;
126
+ const originSession = await dependencies.daemonChannel.getOrCreateConversationSession(input.trigger, binding.projectPath, binding.baseagent || owner.baseagent, input.runId);
127
+ if (input.signal?.aborted) {
128
+ throw input.signal.reason instanceof Error ? input.signal.reason : new Error('trigger feedback cancelled');
129
+ }
130
+ const payload = { type: 'text', text };
131
+ if (target.session === 'thread' && target.threadId)
132
+ payload.thread_id = target.threadId;
133
+ const created = await dependencies.handoffRuntime.createOutbound({
134
+ selfAid: input.trigger.agentAid,
135
+ to: target.channelId,
136
+ originSessionId: originSession.id,
137
+ originMessageId: input.runId,
138
+ payload,
139
+ encrypt: typeof channel.getDefaultEncrypt === 'function' ? channel.getDefaultEncrypt() : true,
140
+ thread: target.session === 'thread' ? target.threadId : undefined,
141
+ targetChatType,
142
+ explicitReturnPolicy: 'none',
143
+ causation: input.causation,
144
+ signal: input.signal,
145
+ });
146
+ return created.crossSession && created.handoff
147
+ ? { handoffId: created.handoff.handoff_id, targetSessionId: created.targetSession.id }
148
+ : null;
149
+ },
150
+ });
151
+ return new TriggerRuntimeScheduler(manager, state, agentTriggerAudit, dependencies.triggerScriptExecutor, dispatcher, dependencies.daemonChannel, {
152
+ projectPath: owner.projectPath,
153
+ baseagent: owner.baseagent,
154
+ getBaseagent: () => dependencies.getBaseagent?.(owner.aid) || owner.baseagent,
155
+ authorizeExecution: dependencies.authorizeExecution,
156
+ }, dependencies.eventBus, dependencies.targetSessionLocks);
157
+ }
@@ -0,0 +1,371 @@
1
+ import crypto from 'crypto';
2
+ import { eventPatternIncludesInternal } from '../core/event-catalog.js';
3
+ import { trustedExecutionIdentityInput } from '../core/auth/agent-delegation.js';
4
+ import { authorizeOperation, buildAuthSubject } from '../core/auth/auth-gateway.js';
5
+ import { isManagementRole } from '../config/builtin-roles.js';
6
+ import { checkRoleAccess, resolvePeerRoleDetail, roleToSessionIdentity } from '../config/peer-role-resolver.js';
7
+ import { validateModelSelectionForRole } from '../core/model/model-permission.js';
8
+ import { isValidAunDeliveryForChannel } from '../core/message/outbound.js';
9
+ import { tryParseChannelKey } from '../core/channel-loader.js';
10
+ import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from '../core/role/runtime-policy.js';
11
+ export function originFromActorSession(session, authenticatedPeerId, authenticatedChannelKey, authenticatedChatType) {
12
+ const peerId = authenticatedPeerId || session.metadata?.peerId;
13
+ if (!peerId)
14
+ return undefined;
15
+ const sessionChannelKey = session.metadata?.channelKey || session.channel;
16
+ const channelKey = authenticatedChannelKey || sessionChannelKey;
17
+ const sameConversation = channelKey === sessionChannelKey;
18
+ const isGroup = authenticatedChatType === 'group' || session.chatType === 'group';
19
+ return {
20
+ channelKey,
21
+ channelType: session.channelType || tryParseChannelKey(channelKey)?.type || channelKey,
22
+ channelId: peerId,
23
+ session: sameConversation && !isGroup && session.threadId ? 'thread' : 'main',
24
+ threadId: sameConversation && !isGroup ? (session.threadId || undefined) : undefined,
25
+ peerId,
26
+ sessionKey: sameConversation && !isGroup ? session.sessionKey : undefined,
27
+ delivery: { chatType: 'private' },
28
+ };
29
+ }
30
+ export function daemonPrivilegedTriggerReason(definition) {
31
+ if (definition.origin?.channelKey === 'daemon' || definition.origin?.channelType === 'daemon') {
32
+ return 'daemon/control-origin Trigger';
33
+ }
34
+ if (definition.source.type === 'event' && eventPatternIncludesInternal(definition.source.eventPattern)) {
35
+ return 'internal-event Trigger';
36
+ }
37
+ if (definition.feedback.strategy === 'target' && definition.feedback.target?.channelKey === 'daemon') {
38
+ return 'daemon-control feedback';
39
+ }
40
+ if (definition.execution.permissionMode === 'fullaccess')
41
+ return 'persistent fullaccess Trigger';
42
+ return undefined;
43
+ }
44
+ export function isScopedScriptTrigger(definition) {
45
+ return definition.execution.type === 'script'
46
+ && definition.origin?.channelKey !== 'daemon'
47
+ && definition.origin?.channelType !== 'daemon'
48
+ && definition.execution.permissionMode !== 'fullaccess';
49
+ }
50
+ export function assertTriggerLocalFileAuthority(actor) {
51
+ if (actor.permissionMode === 'readonly' && !actor.fullAccess && !actor.daemonPrivileged) {
52
+ throw new Error('readonly tasks cannot create or update Triggers');
53
+ }
54
+ if (actor.role !== 'owner' && !actor.daemonPrivileged) {
55
+ throw new Error('local Trigger file inputs require the current Agent owner');
56
+ }
57
+ }
58
+ export function validateTriggerFeedbackChannels(definition, getTriggerChannel) {
59
+ if (definition.feedback.strategy === 'silent' || definition.feedback.strategy === 'event_session')
60
+ return;
61
+ const target = definition.feedback.strategy === 'target'
62
+ ? definition.feedback.target
63
+ : definition.origin;
64
+ if (target && !getTriggerChannel(definition.agentAid, target.channelKey)) {
65
+ throw new Error(`agent ${definition.agentAid} has no configured channel ${target.channelKey}`);
66
+ }
67
+ const channelType = tryParseChannelKey(target?.channelKey ?? '')?.type
68
+ ?? (target?.channelKey?.split('#', 1)[0] || undefined);
69
+ const validAunRoute = definition.feedback.strategy === 'target'
70
+ ? definition.feedback.target?.chatType === 'private' || definition.feedback.target?.chatType === 'group'
71
+ : !!definition.origin && isValidAunDeliveryForChannel(definition.origin.delivery, definition.origin.channelId);
72
+ if (target && channelType === 'aun' && !validAunRoute) {
73
+ throw Object.assign(new Error('AUN trigger feedback target requires a valid explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
74
+ }
75
+ }
76
+ export function hasDaemonTriggerAuthority(subject) {
77
+ return subject.isDaemonOwner || subject.processRole === 'daemon-service' || subject.dataScope === 'daemon';
78
+ }
79
+ export function validateTriggerDefinitionForActor(definition, actor, dependencies) {
80
+ if (definition.execution.permissionMode === 'fullaccess') {
81
+ if (!actor.daemonOwner && !actor.fullAccess) {
82
+ throw new Error('persistent fullaccess Trigger requires DaemonOwner or fullaccess-run');
83
+ }
84
+ }
85
+ const privilegedReason = daemonPrivilegedTriggerReason(definition);
86
+ if (privilegedReason && !actor.daemonPrivileged)
87
+ throw new Error(`${privilegedReason} requires DaemonOwner`);
88
+ if (isScopedScriptTrigger(definition) && actor.role !== 'owner' && !actor.daemonPrivileged) {
89
+ throw new Error('Agent-scoped script Triggers require the current Agent owner');
90
+ }
91
+ if (definition.execution.permissionMode && !actor.daemonPrivileged) {
92
+ const permissionDecision = constrainRuntimePermissionMode({
93
+ selfAid: definition.agentAid,
94
+ role: actor.role,
95
+ requestedValue: definition.execution.permissionMode,
96
+ });
97
+ if (permissionDecision.constrained) {
98
+ throw new Error(`trigger permissionMode ${definition.execution.permissionMode} exceeds current role ${actor.role}`);
99
+ }
100
+ }
101
+ if (definition.execution.model && !actor.daemonPrivileged) {
102
+ const agent = dependencies.getAgent(definition.agentAid);
103
+ const modelDecision = validateModelSelectionForRole({
104
+ role: actor.role,
105
+ baseagent: definition.execution.baseagent ?? agent?.baseagent,
106
+ requestedModel: definition.execution.model,
107
+ selfAid: definition.agentAid,
108
+ });
109
+ if (!modelDecision.ok)
110
+ throw new Error(modelDecision.message || 'trigger model is not allowed by current role');
111
+ }
112
+ if (definition.execution.effort && !actor.daemonPrivileged) {
113
+ const baseagent = definition.execution.baseagent
114
+ ?? dependencies.getAgent(definition.agentAid)?.baseagent;
115
+ if (!baseagent)
116
+ throw new Error('cannot determine Trigger baseagent');
117
+ const effortDecision = validateRuntimeStringFieldOverride({
118
+ selfAid: definition.agentAid,
119
+ role: actor.role,
120
+ field: `baseagents.${baseagent}.effort`,
121
+ value: definition.execution.effort,
122
+ });
123
+ if (!effortDecision.ok)
124
+ throw new Error(effortDecision.message || 'trigger effort is not allowed by current role');
125
+ }
126
+ if (!actor.management && definition.feedback.strategy === 'event_session') {
127
+ throw new Error('event-session feedback requires owner/admin');
128
+ }
129
+ if (!actor.management && definition.feedback.strategy === 'target') {
130
+ const target = definition.feedback.target;
131
+ const origin = actor.origin;
132
+ if (!target || !origin
133
+ || target.channelKey !== origin.channelKey
134
+ || target.channelId !== origin.channelId
135
+ || target.session !== origin.session
136
+ || (target.threadId ?? '') !== (origin.threadId ?? '')) {
137
+ throw new Error('non-management trigger target must be the creator private conversation');
138
+ }
139
+ }
140
+ }
141
+ export function authorizeTriggerExecution(definition, dependencies) {
142
+ if (definition.execution.permissionMode === 'fullaccess') {
143
+ if (!definition.authorizedBy) {
144
+ return { allowed: false, reason: 'fullaccess Trigger approval provenance is missing' };
145
+ }
146
+ const owners = new Set(dependencies.getDaemonOwners());
147
+ if (!owners.has(definition.authorizedBy)) {
148
+ return { allowed: false, reason: 'fullaccess Trigger approver is no longer a current daemon-owner' };
149
+ }
150
+ }
151
+ const origin = definition.origin;
152
+ if (!origin?.peerId)
153
+ return { allowed: false, reason: 'trigger origin peer is missing' };
154
+ if (definition.execution.permissionMode === 'fullaccess') {
155
+ return { allowed: true, scope: 'daemon', permissionMode: 'fullaccess' };
156
+ }
157
+ if (origin.channelKey === 'daemon') {
158
+ const subject = buildAuthSubject({
159
+ selfAid: definition.agentAid,
160
+ actorId: origin.peerId,
161
+ channel: origin.channelKey,
162
+ channelType: origin.channelType || 'daemon',
163
+ channelId: origin.channelId,
164
+ chatType: 'private',
165
+ conversationId: origin.peerId,
166
+ trustedProcessRole: 'daemon-service',
167
+ fromControlChannel: true,
168
+ });
169
+ const decision = authorizeOperation({
170
+ source: 'control',
171
+ subject,
172
+ intent: {
173
+ operation: 'trigger.run',
174
+ scope: 'relation',
175
+ source: 'control',
176
+ args: { self: definition.agentAid, peer: origin.peerId, peerKey: subject.peerKey, triggerId: definition.id },
177
+ },
178
+ });
179
+ return decision.allow
180
+ ? {
181
+ allowed: true,
182
+ scope: 'daemon',
183
+ permissionMode: definition.execution.permissionMode ?? 'bypass',
184
+ }
185
+ : { allowed: false, reason: decision.reason };
186
+ }
187
+ const channelType = origin.channelType || tryParseChannelKey(origin.channelKey)?.type;
188
+ if (!channelType)
189
+ return { allowed: false, reason: 'trigger origin channel type is missing' };
190
+ const roleDetail = resolvePeerRoleDetail({
191
+ selfAid: definition.agentAid,
192
+ channelKey: origin.channelKey,
193
+ channelType,
194
+ chatType: 'private',
195
+ actorId: origin.peerId,
196
+ conversationId: origin.peerId,
197
+ });
198
+ const role = roleDetail.effectiveRole;
199
+ if (!checkRoleAccess(role, definition.agentAid)) {
200
+ return { allowed: false, reason: `trigger creator no longer has access (role=${role ?? 'none'})` };
201
+ }
202
+ const subject = buildAuthSubject({
203
+ selfAid: definition.agentAid,
204
+ actorId: origin.peerId,
205
+ channel: origin.channelKey,
206
+ channelType,
207
+ channelId: origin.channelId,
208
+ chatType: 'private',
209
+ conversationId: origin.peerId,
210
+ roleDetail,
211
+ identity: roleToSessionIdentity(role),
212
+ processOwners: dependencies.getDaemonOwners(),
213
+ fromControlChannel: false,
214
+ });
215
+ const operationDecision = authorizeOperation({
216
+ source: 'agent-tool',
217
+ subject,
218
+ intent: {
219
+ operation: 'trigger.run',
220
+ scope: 'relation',
221
+ source: 'agent-tool',
222
+ args: { self: definition.agentAid, peer: origin.peerId, peerKey: subject.peerKey, triggerId: definition.id },
223
+ },
224
+ });
225
+ if (!operationDecision.allow)
226
+ return { allowed: false, reason: operationDecision.reason };
227
+ if (isScopedScriptTrigger(definition) && role !== 'owner' && !hasDaemonTriggerAuthority(subject)) {
228
+ return { allowed: false, reason: 'Agent-scoped script Trigger requires current Agent owner authority' };
229
+ }
230
+ const privilegedReason = daemonPrivilegedTriggerReason(definition);
231
+ const persistedFullAccessApproval = privilegedReason === 'persistent fullaccess Trigger';
232
+ if (privilegedReason && !persistedFullAccessApproval && !hasDaemonTriggerAuthority(subject)) {
233
+ return { allowed: false, reason: `${privilegedReason} requires current DaemonOwner` };
234
+ }
235
+ if (definition.feedback.strategy === 'event_session' && !isManagementRole(role)) {
236
+ return { allowed: false, reason: 'event-session feedback requires current owner/admin role' };
237
+ }
238
+ if (definition.feedback.strategy === 'target' && !isManagementRole(role)) {
239
+ const target = definition.feedback.target;
240
+ if (!target
241
+ || target.channelKey !== origin.channelKey
242
+ || target.channelId !== origin.channelId
243
+ || target.session !== origin.session
244
+ || (target.threadId ?? '') !== (origin.threadId ?? '')) {
245
+ return { allowed: false, reason: `cross-conversation target requires current owner/admin role (role=${role ?? 'none'})` };
246
+ }
247
+ }
248
+ const permissionDecision = constrainRuntimePermissionMode({
249
+ selfAid: definition.agentAid,
250
+ role,
251
+ requestedValue: definition.execution.permissionMode,
252
+ });
253
+ if (definition.execution.permissionMode && permissionDecision.constrained) {
254
+ return {
255
+ allowed: false,
256
+ reason: `trigger permissionMode ${definition.execution.permissionMode} exceeds current role ${role ?? 'none'}`,
257
+ };
258
+ }
259
+ return {
260
+ allowed: true,
261
+ scope: privilegedReason && hasDaemonTriggerAuthority(subject) ? 'daemon' : 'agent',
262
+ permissionMode: permissionDecision.effectiveValue,
263
+ };
264
+ }
265
+ export async function authenticatedTriggerActor(dependencies, agentAid, actorSessionId, delegationToken, delegationCommandHash, controlToken, contextOnly = false) {
266
+ if (typeof actorSessionId !== 'string' || !actorSessionId) {
267
+ const providedToken = typeof controlToken === 'string' ? Buffer.from(controlToken) : undefined;
268
+ const expectedToken = Buffer.from(dependencies.triggerControlToken);
269
+ if (!providedToken
270
+ || providedToken.length !== expectedToken.length
271
+ || !crypto.timingSafeEqual(providedToken, expectedToken)) {
272
+ throw new Error('valid local control token is required');
273
+ }
274
+ const origin = {
275
+ channelKey: 'daemon',
276
+ channelType: 'daemon',
277
+ channelId: dependencies.daemonTriggerOwner.originPeerId,
278
+ session: 'main',
279
+ peerId: dependencies.daemonTriggerOwner.originPeerId,
280
+ sessionKey: `daemon#${dependencies.daemonTriggerOwner.originPeerId}#__control__`,
281
+ };
282
+ const subject = buildAuthSubject({
283
+ selfAid: agentAid,
284
+ actorId: origin.peerId,
285
+ channel: origin.channelKey,
286
+ channelType: origin.channelType,
287
+ channelId: origin.channelId,
288
+ chatType: 'private',
289
+ conversationId: origin.peerId,
290
+ trustedProcessRole: 'daemon-service',
291
+ fromControlChannel: true,
292
+ });
293
+ return {
294
+ origin,
295
+ management: true,
296
+ daemonOwner: false,
297
+ daemonPrivileged: true,
298
+ fullAccess: false,
299
+ control: true,
300
+ role: 'none',
301
+ subject,
302
+ };
303
+ }
304
+ const validation = (contextOnly
305
+ ? dependencies.agentDelegationRegistry.validateForContext.bind(dependencies.agentDelegationRegistry)
306
+ : dependencies.agentDelegationRegistry.validate.bind(dependencies.agentDelegationRegistry))(typeof delegationToken === 'string' ? delegationToken : undefined, actorSessionId, typeof delegationCommandHash === 'string' ? delegationCommandHash : undefined);
307
+ if (!validation.ok)
308
+ throw new Error(validation.reason);
309
+ const actorSession = await dependencies.sessionManager.getSessionById(actorSessionId);
310
+ const actorOrigin = actorSession ? originFromActorSession(actorSession, validation.grant.actorId, validation.grant.channel, validation.grant.chatType) : undefined;
311
+ if (!actorSession || !actorOrigin?.peerId)
312
+ throw new Error('trigger actor session has no authenticated peer');
313
+ if (validation.grant.actorId !== actorOrigin.peerId) {
314
+ throw new Error('trigger delegation actor does not match session peer');
315
+ }
316
+ const roleDetail = resolvePeerRoleDetail({
317
+ selfAid: agentAid,
318
+ channelKey: validation.grant.channel,
319
+ channelType: validation.grant.channelType,
320
+ chatType: 'private',
321
+ actorId: validation.grant.actorId,
322
+ conversationId: validation.grant.actorId,
323
+ });
324
+ const currentRole = roleDetail.effectiveRole;
325
+ const subject = buildAuthSubject({
326
+ selfAid: agentAid,
327
+ actorId: validation.grant.actorId,
328
+ channel: validation.grant.channel,
329
+ channelType: validation.grant.channelType,
330
+ channelId: actorOrigin.channelId,
331
+ chatType: 'private',
332
+ conversationId: validation.grant.actorId,
333
+ roleDetail,
334
+ identity: roleToSessionIdentity(currentRole),
335
+ processOwners: dependencies.getDaemonOwners(),
336
+ fromControlChannel: false,
337
+ ...trustedExecutionIdentityInput(validation.grant),
338
+ });
339
+ if (validation.grant.selfAid !== agentAid && subject.dataScope !== 'daemon') {
340
+ throw new Error('cross-agent Trigger operations require DaemonOwner');
341
+ }
342
+ if (subject.dataScope !== 'daemon' && !checkRoleAccess(currentRole, agentAid)) {
343
+ throw new Error('trigger actor no longer has access to this agent');
344
+ }
345
+ const permissionDecision = subject.processRole === 'fullaccess-run'
346
+ ? { effectiveValue: 'fullaccess' }
347
+ : subject.dataScope === 'daemon'
348
+ ? { effectiveValue: validation.grant.permissionMode ?? subject.permissionMode ?? 'bypass' }
349
+ : constrainRuntimePermissionMode({
350
+ selfAid: agentAid,
351
+ role: currentRole,
352
+ requestedValue: validation.grant.permissionMode ?? subject.permissionMode,
353
+ });
354
+ if (subject.processRole === 'fullaccess-run'
355
+ && (!subject.authorizedBy || !dependencies.getDaemonOwners().includes(subject.authorizedBy))) {
356
+ throw new Error('fullaccess Trigger caller approval is no longer owned');
357
+ }
358
+ return {
359
+ origin: actorOrigin,
360
+ management: isManagementRole(currentRole) || subject.dataScope === 'daemon',
361
+ daemonOwner: subject.isDaemonOwner,
362
+ daemonPrivileged: subject.isDaemonOwner || subject.dataScope === 'daemon',
363
+ fullAccess: subject.processRole === 'fullaccess-run',
364
+ selfAid: validation.grant.selfAid,
365
+ control: false,
366
+ role: currentRole || 'none',
367
+ subject,
368
+ permissionMode: permissionDecision.effectiveValue,
369
+ sessionRuntimeDir: validation.grant.sessionRuntimeDir,
370
+ };
371
+ }