evolcore 0.0.11 → 0.0.12
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 +22 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/dist/agents/claude-runner.js +35 -2
- package/dist/agents/codex-app-server-client.js +120 -14
- package/dist/agents/codex-runner.js +273 -35
- package/dist/agents/ecagent-runner.js +36 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/msg/group.js +81 -12
- package/dist/aun/msg/managed-operation.js +804 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/channels/aun.js +288 -108
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +170 -6
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/daemon-commands.js +31 -31
- package/dist/cli/fs-command.js +60 -0
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +11 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +4 -18
- package/dist/cli/trigger-command.js +4 -0
- package/dist/config/builtin-roles.js +4 -0
- 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/core/auth/agent-delegation.js +0 -1
- package/dist/core/auth/operation-authorizer.js +30 -7
- package/dist/core/auth/operation-catalog.js +187 -25
- package/dist/core/bootstrap-messages.js +18 -0
- package/dist/core/bootstrap-service.js +63 -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 +467 -6
- 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 +366 -0
- package/dist/core/command/menu-handler.js +147 -35
- package/dist/core/command/menu-protocol.js +71 -0
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +41 -21
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +145 -21
- package/dist/core/handoff/store.js +24 -0
- package/dist/core/handoff/types.js +1 -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 +236 -78
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +1 -0
- package/dist/core/message/response-engine.js +54 -36
- package/dist/core/permission/ec-command-parser.js +156 -10
- package/dist/core/permission/sandbox-runtime.js +22 -1
- package/dist/core/permission/tool-policy.js +73 -30
- package/dist/core/protected-paths.js +7 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +384 -98
- package/dist/ipc.js +218 -9
- 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/script-executor.js +1 -0
- 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/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/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- 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 +22 -0
- package/kits/templates/roles/member.json +27 -0
- package/kits/templates/roles/visitor.json +33 -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/core/command/evol-menu-version-gate.js +0 -39
|
@@ -11,7 +11,7 @@ import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
|
11
11
|
import { logger } from '../../utils/logger.js';
|
|
12
12
|
import { isHostChinese } from '../../utils/locale.js';
|
|
13
13
|
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
14
|
-
import {
|
|
14
|
+
import { isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
|
|
15
15
|
import { summarizeToolInput } from '../../utils/tool-summary.js';
|
|
16
16
|
import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
17
17
|
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
@@ -33,7 +33,6 @@ import { eligibleFallbackModels } from '../model/model-fallback.js';
|
|
|
33
33
|
import { RetryScheduler } from './retry-scheduler.js';
|
|
34
34
|
import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
|
|
35
35
|
import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
|
|
36
|
-
import { dispatchToMentionMode } from '../../config/mention-mode.js';
|
|
37
36
|
import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
|
|
38
37
|
import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
|
|
39
38
|
import { normalizeUsage } from '../../stats/normalizer.js';
|
|
@@ -727,7 +726,19 @@ export class ResponseEngine {
|
|
|
727
726
|
const selfAid = runtime?.selfAid || session?.selfAID;
|
|
728
727
|
if (!selfAid)
|
|
729
728
|
return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'self agent not found' };
|
|
730
|
-
return this.
|
|
729
|
+
return this.returnAuthorizedHandoffResult({
|
|
730
|
+
sessionId: params.sessionId,
|
|
731
|
+
handoffId: params.handoffId,
|
|
732
|
+
content: params.content,
|
|
733
|
+
}, selfAid);
|
|
734
|
+
}
|
|
735
|
+
/** Execute a handoff return after the daemon IPC boundary has authorized the task delegation. */
|
|
736
|
+
async returnAuthorizedHandoffResult(params, selfAid) {
|
|
737
|
+
if (!this.handoffRuntime) {
|
|
738
|
+
return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff runtime not configured' };
|
|
739
|
+
}
|
|
740
|
+
const runtime = this.activeTaskRuntimeContexts.get(params.sessionId);
|
|
741
|
+
return await this.handoffRuntime.returnHandoff({
|
|
731
742
|
selfAid,
|
|
732
743
|
currentSessionId: params.sessionId,
|
|
733
744
|
handoffId: params.handoffId,
|
|
@@ -1152,6 +1163,14 @@ export class ResponseEngine {
|
|
|
1152
1163
|
async processMessage(message) {
|
|
1153
1164
|
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
1154
1165
|
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
1166
|
+
if (message.handoffDelivery
|
|
1167
|
+
&& this.handoffRuntime
|
|
1168
|
+
&& this.handoffRuntime.isDiscardedDelivery(message)) {
|
|
1169
|
+
logger.info(`[ResponseEngine] Skip stale handoff delivery before session resolution:`
|
|
1170
|
+
+ ` direction=${message.handoffDelivery.direction}`
|
|
1171
|
+
+ ` handoff=${message.handoffDelivery.handoffId}`);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1155
1174
|
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
1156
1175
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
1157
1176
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
@@ -1720,11 +1739,11 @@ export class ResponseEngine {
|
|
|
1720
1739
|
messageId: message.messageId, peerId: message.peerId, content: message.content,
|
|
1721
1740
|
peerType: message.peerType || session.metadata?.peerType,
|
|
1722
1741
|
chatType: chatType, isMentioned: message.isMentioned,
|
|
1723
|
-
mentionAids: message.mentionAids, source: message.source,
|
|
1742
|
+
mentions: message.mentions, mentionAids: message.mentionAids, source: message.source,
|
|
1724
1743
|
},
|
|
1725
1744
|
modeConfig: resolvedMode.context.modeConfig,
|
|
1726
1745
|
state: modeState,
|
|
1727
|
-
isSendCommand: (toolName, toolInput) =>
|
|
1746
|
+
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, session.selfAID || message.selfAID),
|
|
1728
1747
|
logger,
|
|
1729
1748
|
} : null;
|
|
1730
1749
|
if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
|
|
@@ -2118,11 +2137,21 @@ export class ResponseEngine {
|
|
|
2118
2137
|
},
|
|
2119
2138
|
};
|
|
2120
2139
|
})();
|
|
2140
|
+
let effectivePermissionMode = 'readonly';
|
|
2121
2141
|
const recordExecutionAnomaly = triggerRunId
|
|
2122
2142
|
? (anomaly) => {
|
|
2123
|
-
recordTriggerExecutionAnomaly(triggerRunId,
|
|
2143
|
+
recordTriggerExecutionAnomaly(triggerRunId, {
|
|
2144
|
+
...anomaly,
|
|
2145
|
+
agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
|
|
2146
|
+
sessionId: anomaly.sessionId ?? session.id,
|
|
2147
|
+
permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
|
|
2148
|
+
});
|
|
2124
2149
|
}
|
|
2125
2150
|
: undefined;
|
|
2151
|
+
const runModeConfig = resolvedMode?.mode.configureRun && modeProcessCtx
|
|
2152
|
+
? resolvedMode.mode.configureRun(modeProcessCtx)
|
|
2153
|
+
: undefined;
|
|
2154
|
+
const pureSessionPolicyHook = runModeConfig?.policyHook;
|
|
2126
2155
|
// 设置权限审批的交互上下文(支持交互卡片)
|
|
2127
2156
|
agent.setPermissionContext?.(session.id, {
|
|
2128
2157
|
sendPrompt: permissionPromptForOrigin,
|
|
@@ -2144,6 +2173,7 @@ export class ResponseEngine {
|
|
|
2144
2173
|
approvalRouting,
|
|
2145
2174
|
approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
|
|
2146
2175
|
recordExecutionAnomaly,
|
|
2176
|
+
preToolUsePolicyHook: pureSessionPolicyHook,
|
|
2147
2177
|
armApprovedDelegationCommand: this.agentDelegationRegistry
|
|
2148
2178
|
? (carrierToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
|
|
2149
2179
|
carrierToken,
|
|
@@ -2233,10 +2263,7 @@ export class ResponseEngine {
|
|
|
2233
2263
|
// [迁移点2] policyHook 由 ProactiveMode.configureRun 提供(插件决策);
|
|
2234
2264
|
// 引擎补充副作用:违规时注入提醒到模型上下文 + 行为探针。
|
|
2235
2265
|
policyHook: (() => {
|
|
2236
|
-
const
|
|
2237
|
-
? resolvedMode.mode.configureRun(modeProcessCtx)
|
|
2238
|
-
: undefined;
|
|
2239
|
-
const pluginHook = runConfig?.policyHook;
|
|
2266
|
+
const pluginHook = pureSessionPolicyHook;
|
|
2240
2267
|
if (!pluginHook)
|
|
2241
2268
|
return undefined;
|
|
2242
2269
|
return (toolName, toolInput) => {
|
|
@@ -2250,9 +2277,9 @@ export class ResponseEngine {
|
|
|
2250
2277
|
agent.injectUserMessage?.(session.id, errorMsg);
|
|
2251
2278
|
return result;
|
|
2252
2279
|
}
|
|
2253
|
-
//
|
|
2254
|
-
|
|
2255
|
-
if (wasFirstPending
|
|
2280
|
+
// 这里只记录审批结果。firstToolDone 必须等真实 tool_use 事件再推进,
|
|
2281
|
+
// 避免伪造 managed hook IPC 提前解除门禁。
|
|
2282
|
+
if (wasFirstPending) {
|
|
2256
2283
|
snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: false, toolName } });
|
|
2257
2284
|
}
|
|
2258
2285
|
return undefined;
|
|
@@ -2290,8 +2317,6 @@ export class ResponseEngine {
|
|
|
2290
2317
|
let handoffPromptRendered = false;
|
|
2291
2318
|
let runtimeEnv;
|
|
2292
2319
|
try {
|
|
2293
|
-
// 动态构建运行时上下文提示
|
|
2294
|
-
const contextParts = [];
|
|
2295
2320
|
// 通道能力
|
|
2296
2321
|
const supportsFileMarker = currentChannelType !== 'aun' && !isProactive && !!channelInfo.adapter.capabilities?.file;
|
|
2297
2322
|
const capParts = [];
|
|
@@ -2301,14 +2326,11 @@ export class ResponseEngine {
|
|
|
2301
2326
|
capParts.push('图片输出');
|
|
2302
2327
|
if (!isProactive && channelInfo.adapter.capabilities?.file)
|
|
2303
2328
|
capParts.push('文件发送');
|
|
2304
|
-
// Personal
|
|
2329
|
+
// Personal state is exposed to the manifest as vars. The manifest owns
|
|
2330
|
+
// loading persona.md and working.md into the system prompt.
|
|
2305
2331
|
const owningAgent = owningAgentForTask;
|
|
2306
2332
|
const persona = owningAgent?.getPersona?.() || undefined;
|
|
2307
2333
|
const working = owningAgent?.getWorkingMemory?.() || undefined;
|
|
2308
|
-
if (persona)
|
|
2309
|
-
contextParts.push(persona);
|
|
2310
|
-
if (working)
|
|
2311
|
-
contextParts.push(`[当前关注]\n${working}`);
|
|
2312
2334
|
// 计算 peerKey:群聊固定按 groupId/channelId,私聊按发送者 peerId。
|
|
2313
2335
|
// 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
|
|
2314
2336
|
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
@@ -2316,7 +2338,6 @@ export class ResponseEngine {
|
|
|
2316
2338
|
// Trigger 只能降低本次调用权限,不能突破当前角色的权限上限。
|
|
2317
2339
|
// 作为 per-call 入参随 modelOverride 传入 runQuery —— 与 model/effort 同构,
|
|
2318
2340
|
// 不写 AgentRunner 实例字段,多对端/多会话并发共享同一 runner 实例时互不污染。
|
|
2319
|
-
let effectivePermissionMode;
|
|
2320
2341
|
const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
|
|
2321
2342
|
try {
|
|
2322
2343
|
effectivePermissionMode = constrainRuntimePermissionMode({
|
|
@@ -2532,9 +2553,10 @@ export class ResponseEngine {
|
|
|
2532
2553
|
channel: currentChannelType || null,
|
|
2533
2554
|
venueUid: undefined,
|
|
2534
2555
|
// 群 @ 处理模式 / 客户端类型 / 权限模式
|
|
2535
|
-
// 优先 agent/relation mentionMode 配置,fallback 到服务器
|
|
2556
|
+
// 优先 agent/relation mentionMode 配置,fallback 到服务器 mention_mode 缓存。
|
|
2536
2557
|
mentionMode: effectiveAgentConfig?.mentionMode
|
|
2537
|
-
??
|
|
2558
|
+
?? session.metadata?.mentionMode
|
|
2559
|
+
?? message.mentionMode
|
|
2538
2560
|
?? undefined,
|
|
2539
2561
|
clientType: message.clientType || undefined,
|
|
2540
2562
|
permissionMode: effectivePermissionMode,
|
|
@@ -2583,17 +2605,13 @@ export class ResponseEngine {
|
|
|
2583
2605
|
const sessionManifests = this.agentRegistry?.resolveByChannel(channelKey)?.config?.sessionManifests;
|
|
2584
2606
|
const manifestFile = sessionManifests?.[sessionType] ?? 'eck_manifest.json';
|
|
2585
2607
|
const kitContext = renderKitSections(kitCtx, manifestFile);
|
|
2586
|
-
|
|
2587
|
-
contextParts.push(kitContext);
|
|
2588
|
-
effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
2608
|
+
effectiveSystemPrompt = [options?.systemPromptAppend, kitContext].filter(Boolean).join('\n') || undefined;
|
|
2589
2609
|
// ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
|
|
2590
2610
|
try {
|
|
2591
2611
|
const estTokens = (s) => s ? Math.ceil(s.length / 4) : 0;
|
|
2592
2612
|
const cbModel = effectiveModel || agentModel || 'unknown';
|
|
2593
2613
|
const cbMaxTokens = 200000; // 保守默认,后续可从 model-catalog 取
|
|
2594
2614
|
const systemPromptTokens = estTokens(options?.systemPromptAppend);
|
|
2595
|
-
const personaTokens = estTokens(persona);
|
|
2596
|
-
const workingTokens = estTokens(working);
|
|
2597
2615
|
const kitTokens = estTokens(kitContext);
|
|
2598
2616
|
const totalEst = estTokens(effectiveSystemPrompt);
|
|
2599
2617
|
insertContextBreakdown(resolveRoot(), {
|
|
@@ -2603,7 +2621,7 @@ export class ResponseEngine {
|
|
|
2603
2621
|
turn_count: 0, // 按 ts 排序得轮次
|
|
2604
2622
|
model: cbModel,
|
|
2605
2623
|
max_tokens: cbMaxTokens,
|
|
2606
|
-
system_prompt: systemPromptTokens
|
|
2624
|
+
system_prompt: systemPromptTokens,
|
|
2607
2625
|
system_tools: 0, // 工具 schema 不在此层,留 0(后续 runner 层补)
|
|
2608
2626
|
mcp_tools: 0,
|
|
2609
2627
|
custom_agents: 0,
|
|
@@ -2629,6 +2647,7 @@ export class ResponseEngine {
|
|
|
2629
2647
|
encrypted: message.encrypted,
|
|
2630
2648
|
content: message.content, timestamp: message.timestamp,
|
|
2631
2649
|
images: message.images,
|
|
2650
|
+
mentions: message.mentions,
|
|
2632
2651
|
mentionAids: message.mentionAids,
|
|
2633
2652
|
}];
|
|
2634
2653
|
const peerItems = (() => {
|
|
@@ -3916,12 +3935,10 @@ export class ResponseEngine {
|
|
|
3916
3935
|
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
3917
3936
|
}
|
|
3918
3937
|
}
|
|
3919
|
-
//
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
logger.info(`[ResponseEngine] dispatchMode sync: sessionId=${session.id} ${session.metadata?.dispatchMode ?? 'none'} -> ${message.dispatchMode}`);
|
|
3924
|
-
session.metadata = { ...(session.metadata || {}), dispatchMode: message.dispatchMode };
|
|
3938
|
+
// 同步服务端 mention_mode,供同一群会话的上下文和菜单展示使用。
|
|
3939
|
+
if (message.chatType === 'group' && message.mentionMode && session.metadata?.mentionMode !== message.mentionMode) {
|
|
3940
|
+
logger.info(`[ResponseEngine] mentionMode sync: sessionId=${session.id} ${session.metadata?.mentionMode ?? 'none'} -> ${message.mentionMode}`);
|
|
3941
|
+
session.metadata = { ...(session.metadata || {}), mentionMode: message.mentionMode };
|
|
3925
3942
|
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
3926
3943
|
}
|
|
3927
3944
|
// chatMode 策略由 agent/relation behavior 配置在处理阶段解析;此处不再写 session 级参数。
|
|
@@ -4293,6 +4310,7 @@ export class ResponseEngine {
|
|
|
4293
4310
|
}
|
|
4294
4311
|
// [迁移点4] 工具汇报提醒:由 ProactiveMode.onToolUse 实现
|
|
4295
4312
|
if (modeHooks?.mode?.onToolUse) {
|
|
4313
|
+
const proactiveState = modeHooks.state.get('proactive');
|
|
4296
4314
|
modeHooks.mode.onToolUse({
|
|
4297
4315
|
session,
|
|
4298
4316
|
state: modeHooks.state,
|
|
@@ -4300,7 +4318,7 @@ export class ResponseEngine {
|
|
|
4300
4318
|
toolInput: event.input || {},
|
|
4301
4319
|
injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
|
|
4302
4320
|
getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
|
|
4303
|
-
isSendCommand: (toolName, toolInput) =>
|
|
4321
|
+
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', session.selfAID),
|
|
4304
4322
|
logger,
|
|
4305
4323
|
});
|
|
4306
4324
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
|
|
2
|
+
// Codex app-server sends command executions through a fixed shell carrier.
|
|
3
|
+
// Treat the carrier as syntax only after parsing it as literal argv; never
|
|
4
|
+
// accept an arbitrary `sh -c` string or unwrap shell composition here.
|
|
5
|
+
const CODEX_SHELL_EXECUTABLES = new Set([
|
|
6
|
+
'bash', '/bin/bash', '/usr/bin/bash',
|
|
7
|
+
'sh', '/bin/sh', '/usr/bin/sh',
|
|
8
|
+
]);
|
|
2
9
|
const SHELL_UNQUOTED_META = new Set([
|
|
3
10
|
';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
|
|
4
11
|
]);
|
|
@@ -135,6 +142,42 @@ export function parseLiteralShellArgv(command) {
|
|
|
135
142
|
const result = parseLiteralShellCommand(command);
|
|
136
143
|
return result.ok ? result.argv : null;
|
|
137
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Return the command string carried by Codex's literal `/bin/bash -lc ...`
|
|
147
|
+
* wrapper. The wrapper is accepted only in its exact three-argv form and the
|
|
148
|
+
* caller must parse the returned string with the literal shell parser again.
|
|
149
|
+
*/
|
|
150
|
+
export function unwrapCodexShellCommandArgv(argv) {
|
|
151
|
+
if (argv.length !== 3 || argv[1] !== '-lc')
|
|
152
|
+
return undefined;
|
|
153
|
+
return CODEX_SHELL_EXECUTABLES.has(argv[0]) ? argv[2] : undefined;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Parse either a bare literal command or the exact shell carrier emitted by
|
|
157
|
+
* Codex app-server. This does not make shell composition safe: the nested
|
|
158
|
+
* command still has to pass `parseLiteralShellCommand`.
|
|
159
|
+
*/
|
|
160
|
+
export function parseCodexShellCommandArgv(command) {
|
|
161
|
+
const outer = parseLiteralShellArgv(command);
|
|
162
|
+
if (!outer)
|
|
163
|
+
return null;
|
|
164
|
+
const wrapped = unwrapCodexShellCommandArgv(outer);
|
|
165
|
+
return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve a Codex command input while preserving the raw `commandArgv` form
|
|
169
|
+
* supplied by app-server. Joining that argv with spaces loses the quoting of
|
|
170
|
+
* the inner `-lc` command and was the source of a false proactive denial.
|
|
171
|
+
*/
|
|
172
|
+
export function parseCodexToolCommandArgv(input) {
|
|
173
|
+
if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
|
|
174
|
+
const outer = input.commandArgv;
|
|
175
|
+
const wrapped = unwrapCodexShellCommandArgv(outer);
|
|
176
|
+
return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
|
|
177
|
+
}
|
|
178
|
+
const command = typeof input.command === 'string' ? input.command : '';
|
|
179
|
+
return parseCodexShellCommandArgv(command);
|
|
180
|
+
}
|
|
138
181
|
function scanBoundedOutputOperators(command) {
|
|
139
182
|
const operators = [];
|
|
140
183
|
let quote = null;
|
|
@@ -314,9 +357,18 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
314
357
|
export function classifyEvolcoreShellCommand(command) {
|
|
315
358
|
const literal = parseLiteralShellCommand(command);
|
|
316
359
|
if (literal.ok) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
360
|
+
if (literal.argv[0] === 'ec')
|
|
361
|
+
return { kind: 'literal' };
|
|
362
|
+
const wrapped = unwrapCodexShellCommandArgv(literal.argv);
|
|
363
|
+
if (wrapped !== undefined) {
|
|
364
|
+
const nested = parseLiteralShellCommand(wrapped);
|
|
365
|
+
if (nested.ok && nested.argv[0] === 'ec')
|
|
366
|
+
return { kind: 'literal' };
|
|
367
|
+
if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(wrapped)) {
|
|
368
|
+
return { kind: 'composite', issue: nested.issue };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { kind: 'none' };
|
|
320
372
|
}
|
|
321
373
|
const boundedOutput = parseBoundedOutputShellCommand(command);
|
|
322
374
|
if (boundedOutput && boundedOutput.argv[0] === 'ec') {
|
|
@@ -350,8 +402,81 @@ function findSingleFileOption(argv, startIndex) {
|
|
|
350
402
|
? { path: filePath, invalid: false }
|
|
351
403
|
: { invalid: true };
|
|
352
404
|
}
|
|
353
|
-
|
|
354
|
-
|
|
405
|
+
const PROACTIVE_SEND_VALUE_FLAGS = new Set([
|
|
406
|
+
'--format', '--app', '--file', '--text-from-file', '--link', '--payload',
|
|
407
|
+
'--title', '--description', '--text', '--transcript', '--as',
|
|
408
|
+
'--content-type', '--mention', '--aun-path', '--thread', '--return',
|
|
409
|
+
]);
|
|
410
|
+
const PROACTIVE_SEND_BOOLEAN_FLAGS = new Set([
|
|
411
|
+
'--encrypt', '--no-encrypt', '--mention-all',
|
|
412
|
+
]);
|
|
413
|
+
function hasExecutableProactiveSendPayload(argv, scope) {
|
|
414
|
+
if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
|
|
415
|
+
return false;
|
|
416
|
+
const positionals = [];
|
|
417
|
+
const modes = new Map();
|
|
418
|
+
let endOfOptions = false;
|
|
419
|
+
for (let index = 5; index < argv.length; index++) {
|
|
420
|
+
const value = argv[index];
|
|
421
|
+
if (endOfOptions) {
|
|
422
|
+
positionals.push(value);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (value === '--') {
|
|
426
|
+
endOfOptions = true;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (PROACTIVE_SEND_BOOLEAN_FLAGS.has(value))
|
|
430
|
+
continue;
|
|
431
|
+
if (!PROACTIVE_SEND_VALUE_FLAGS.has(value)) {
|
|
432
|
+
positionals.push(value);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const operand = argv[++index];
|
|
436
|
+
if (operand === undefined || operand.startsWith('--'))
|
|
437
|
+
return false;
|
|
438
|
+
if (value === '--file' || value === '--text-from-file')
|
|
439
|
+
return false;
|
|
440
|
+
if (value === '--app' || value === '--aun-path')
|
|
441
|
+
return false;
|
|
442
|
+
if (value === '--return' && operand !== 'none' && operand !== 'required')
|
|
443
|
+
return false;
|
|
444
|
+
if (value === '--format' && operand !== 'json')
|
|
445
|
+
return false;
|
|
446
|
+
if (value === '--link') {
|
|
447
|
+
if (scope !== 'msg' || modes.has('link'))
|
|
448
|
+
return false;
|
|
449
|
+
modes.set('link', operand);
|
|
450
|
+
}
|
|
451
|
+
if (value === '--payload') {
|
|
452
|
+
if (modes.has('payload'))
|
|
453
|
+
return false;
|
|
454
|
+
modes.set('payload', operand);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (modes.size > 1)
|
|
458
|
+
return false;
|
|
459
|
+
if (modes.has('link')) {
|
|
460
|
+
try {
|
|
461
|
+
const url = new URL(modes.get('link'));
|
|
462
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (modes.has('payload')) {
|
|
469
|
+
try {
|
|
470
|
+
const payload = JSON.parse(modes.get('payload'));
|
|
471
|
+
return !!payload && typeof payload === 'object' && !Array.isArray(payload);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return positionals.join(' ').length > 0;
|
|
478
|
+
}
|
|
479
|
+
function parseEvolcoreSendArgv(argv) {
|
|
355
480
|
if (!argv || argv[0] !== 'ec')
|
|
356
481
|
return null;
|
|
357
482
|
if (argv[1] === 'ctl') {
|
|
@@ -380,23 +505,44 @@ export function parseEvolcoreSendCommand(command) {
|
|
|
380
505
|
if (subcommand === 'file') {
|
|
381
506
|
if (argv.length !== 6 || !isDeterministicFilePath(argv[5]))
|
|
382
507
|
return null;
|
|
383
|
-
return { scope, action: 'file', targetId, filePath: argv[5] };
|
|
508
|
+
return { scope, action: 'file', sourceId: argv[3], targetId, filePath: argv[5] };
|
|
384
509
|
}
|
|
385
510
|
const fileOption = findSingleFileOption(argv, 5);
|
|
386
511
|
if (fileOption.invalid)
|
|
387
512
|
return null;
|
|
388
513
|
return fileOption.path
|
|
389
|
-
? { scope, action: 'file', targetId, filePath: fileOption.path }
|
|
390
|
-
: { scope, action: 'send', targetId };
|
|
514
|
+
? { scope, action: 'file', sourceId: argv[3], targetId, filePath: fileOption.path }
|
|
515
|
+
: { scope, action: 'send', sourceId: argv[3], targetId };
|
|
516
|
+
}
|
|
517
|
+
export function parseEvolcoreSendCommand(command) {
|
|
518
|
+
const argv = parseCodexShellCommandArgv(command);
|
|
519
|
+
return argv ? parseEvolcoreSendArgv(argv) : null;
|
|
391
520
|
}
|
|
392
521
|
export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
|
|
393
522
|
if (toolName !== 'Bash')
|
|
394
523
|
return false;
|
|
395
|
-
const
|
|
396
|
-
const parsed =
|
|
524
|
+
const argv = parseCodexToolCommandArgv(input);
|
|
525
|
+
const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
|
|
397
526
|
if (!parsed)
|
|
398
527
|
return false;
|
|
399
528
|
if (parsed.scope === 'ctl')
|
|
400
529
|
return true;
|
|
401
530
|
return parsed.targetId === channelId;
|
|
402
531
|
}
|
|
532
|
+
/** Proactive first-send guard: exact channel kind and current target only. */
|
|
533
|
+
export function isExactEvolcoreSendCommandForSession(toolName, input, channelId, chatType, selfAid) {
|
|
534
|
+
// Codex hooks use the canonical name `Bash`; the app-server event adapter
|
|
535
|
+
// exposes the same commandExecution items as `Shell` to the renderer.
|
|
536
|
+
if ((toolName !== 'Bash' && toolName !== 'Shell') || !selfAid)
|
|
537
|
+
return false;
|
|
538
|
+
const argv = parseCodexToolCommandArgv(input);
|
|
539
|
+
const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
|
|
540
|
+
if (!parsed || parsed.action !== 'send')
|
|
541
|
+
return false;
|
|
542
|
+
const expectedScope = chatType === 'group' ? 'group' : 'msg';
|
|
543
|
+
return parsed.scope === expectedScope
|
|
544
|
+
&& parsed.targetId === channelId
|
|
545
|
+
&& parsed.sourceId === selfAid
|
|
546
|
+
&& !!argv
|
|
547
|
+
&& hasExecutableProactiveSendPayload(argv, expectedScope);
|
|
548
|
+
}
|
|
@@ -210,7 +210,7 @@ function appendLClassReadOnlyBinds(args, root) {
|
|
|
210
210
|
args.push('--ro-bind', target.path, target.path);
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
-
export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot()) {
|
|
213
|
+
export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot(), options = {}) {
|
|
214
214
|
const bubblewrapPath = resolveBubblewrapPath();
|
|
215
215
|
if (!bubblewrapPath)
|
|
216
216
|
return undefined;
|
|
@@ -224,6 +224,27 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
|
|
|
224
224
|
];
|
|
225
225
|
appendLClassReadOnlyBinds(args, path.resolve(root));
|
|
226
226
|
appendHClassMasks(args, path.resolve(root), true);
|
|
227
|
+
if (options.managedCodexRequirements) {
|
|
228
|
+
const requirements = path.resolve(options.managedCodexRequirements);
|
|
229
|
+
if (!fs.statSync(requirements).isFile()) {
|
|
230
|
+
throw new Error(`Codex managed requirements is not a file: ${requirements}`);
|
|
231
|
+
}
|
|
232
|
+
const managedDirectory = path.dirname(requirements);
|
|
233
|
+
// Keep the copied hook immutable even when a session uses bypass mode.
|
|
234
|
+
args.push('--ro-bind', managedDirectory, managedDirectory);
|
|
235
|
+
// A host may not have /etc/codex yet. Overlay /etc inside this namespace so
|
|
236
|
+
// Bubblewrap can create the mountpoint without modifying the host, while
|
|
237
|
+
// retaining the complete host /etc as the read-only lower layer.
|
|
238
|
+
args.push('--overlay-src', '/etc');
|
|
239
|
+
args.push('--tmp-overlay', '/etc');
|
|
240
|
+
args.push('--dir', '/etc/codex');
|
|
241
|
+
// Replace the complete Codex system-config directory inside this app-server
|
|
242
|
+
// namespace so host system hooks cannot join EvolCore's managed hook set.
|
|
243
|
+
args.push('--tmpfs', '/etc/codex');
|
|
244
|
+
args.push('--ro-bind', requirements, '/etc/codex/requirements.toml');
|
|
245
|
+
args.push('--remount-ro', '/etc/codex');
|
|
246
|
+
args.push('--remount-ro', '/etc');
|
|
247
|
+
}
|
|
227
248
|
args.push('--', executable, ...executableArgs);
|
|
228
249
|
return { command: bubblewrapPath, args };
|
|
229
250
|
}
|