evolcore 0.0.1 → 0.0.2
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 +813 -0
- package/LICENSE +21 -0
- package/MIGRATION-0.5.0.md +378 -0
- package/README.md +318 -14
- package/ROLE_ACCESS_CONTROL.md +174 -0
- package/assets/.env.template +4 -0
- package/assets/brand/evolcore/README.md +19 -0
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +13 -0
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +126 -0
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +14 -0
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +14 -0
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +10 -0
- package/bin/ec-safe-output.js +161 -0
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent.js +163 -0
- package/dist/agents/claude-runner.js +2385 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2639 -0
- package/dist/agents/gemini-runner.js +666 -0
- package/dist/agents/runner-types.js +75 -0
- package/dist/aun/aid/agentmd.js +216 -0
- package/dist/aun/aid/client.js +132 -0
- package/dist/aun/aid/control-aid.js +91 -0
- package/dist/aun/aid/identity.js +518 -0
- package/dist/aun/aid/index.js +4 -0
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/group-identity.js +10 -0
- package/dist/aun/msg/group-index.js +6 -0
- package/dist/aun/msg/group.js +1231 -0
- package/dist/aun/msg/history.js +123 -0
- package/dist/aun/msg/index.js +5 -0
- package/dist/aun/msg/p2p.js +393 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +137 -0
- package/dist/aun/outbox.js +160 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +25 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/service-proxy.js +225 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +68 -0
- package/dist/channels/aun.js +4147 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +649 -0
- package/dist/channels/feishu.js +1789 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom.js +565 -0
- package/dist/cli/agent-command.js +641 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +1948 -0
- package/dist/cli/bench.js +1228 -0
- package/dist/cli/cli-argv.js +66 -0
- package/dist/cli/code-stats.js +329 -0
- package/dist/cli/command-log.js +82 -0
- package/dist/cli/config-selector.js +69 -0
- package/dist/cli/config.js +261 -0
- package/dist/cli/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2887 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +31 -0
- package/dist/cli/index.js +358 -0
- package/dist/cli/init-channel.js +1377 -0
- package/dist/cli/init.js +553 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +590 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +126 -0
- package/dist/cli/response.js +345 -0
- package/dist/cli/restart-monitor.js +456 -0
- package/dist/cli/stats.js +607 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +505 -0
- package/dist/cli/version.js +88 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +673 -0
- package/dist/config/boot-log.js +266 -0
- package/dist/config/builtin-role-templates.js +30 -0
- package/dist/config/builtin-roles.js +85 -0
- package/dist/config/config-batch-get.js +11 -0
- package/dist/config/config-field-policy.js +261 -0
- package/dist/config/config-manager.js +916 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-book.js +371 -0
- package/dist/config/gateway-config.js +858 -0
- package/dist/config/lifecycle.js +17 -0
- package/dist/config/mention-mode.js +27 -0
- package/dist/config/merge.js +161 -0
- package/dist/config/owner-policy.js +4 -0
- package/dist/config/peer-role-resolver.js +139 -0
- package/dist/config/resolved-config-op.js +483 -0
- package/dist/config/role-config-v4-startup.js +32 -0
- package/dist/config/role-config-v5-startup.js +27 -0
- package/dist/config/role-schema.js +105 -0
- package/dist/config/role-service.js +159 -0
- package/dist/config/role-store.js +204 -0
- package/dist/config/roles.js +55 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +503 -0
- package/dist/core/auth/agent-delegation.js +111 -0
- package/dist/core/auth/auth-gateway.js +166 -0
- package/dist/core/auth/authenticated-actor.js +6 -0
- package/dist/core/auth/authorization-audit.js +110 -0
- package/dist/core/auth/operation-authorizer.js +718 -0
- package/dist/core/auth/operation-catalog.js +675 -0
- package/dist/core/baseagent-loader.js +54 -0
- package/dist/core/bootstrap-service.js +175 -0
- package/dist/core/capability/capability-manager.js +316 -0
- package/dist/core/capability/providers/claude-capability-provider.js +176 -0
- package/dist/core/capability/providers/codex-capability-provider.js +148 -0
- package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
- package/dist/core/capability/types.js +27 -0
- package/dist/core/causation/audit.js +103 -0
- package/dist/core/causation/aun-association.js +111 -0
- package/dist/core/causation/context.js +93 -0
- package/dist/core/causation/index.js +4 -0
- package/dist/core/causation/types.js +2 -0
- package/dist/core/channel-loader.js +277 -0
- package/dist/core/command/agent-control.js +616 -0
- package/dist/core/command/cli-intent-parser.js +225 -0
- package/dist/core/command/command-handler.js +1638 -0
- package/dist/core/command/menu-handler.js +3354 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1524 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3056 -0
- package/dist/core/daemon-file-cache.js +216 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +740 -0
- package/dist/core/evolagent-registry.js +546 -0
- package/dist/core/evolagent.js +331 -0
- package/dist/core/handoff/dispatcher.js +229 -0
- package/dist/core/handoff/mutex.js +46 -0
- package/dist/core/handoff/runtime.js +324 -0
- package/dist/core/handoff/store.js +537 -0
- package/dist/core/handoff/types.js +12 -0
- package/dist/core/inference/text-inference.js +173 -0
- package/dist/core/interaction-registration.js +10 -0
- package/dist/core/interaction-router.js +278 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +657 -0
- package/dist/core/message/items-formatter.js +76 -0
- package/dist/core/message/logical-queue-bridge.js +123 -0
- package/dist/core/message/message-bridge.js +803 -0
- package/dist/core/message/message-cache.js +56 -0
- package/dist/core/message/message-log.js +339 -0
- package/dist/core/message/message-processor.js +4 -0
- package/dist/core/message/message-queue.js +1436 -0
- package/dist/core/message/message-utils.js +70 -0
- package/dist/core/message/peer-mode.js +105 -0
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +33 -0
- package/dist/core/message/response-engine.js +3776 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/stream-debouncer.js +130 -0
- package/dist/core/message/stream-idle-monitor.js +124 -0
- package/dist/core/model/config-scope.js +162 -0
- package/dist/core/model/field-scope.js +78 -0
- package/dist/core/model/model-catalog.js +227 -0
- package/dist/core/model/model-diagnostics.js +182 -0
- package/dist/core/model/model-permission.js +90 -0
- package/dist/core/permission/approval-gateway.js +1017 -0
- package/dist/core/permission/ec-command-parser.js +347 -0
- package/dist/core/permission/execution-sandbox.js +16 -0
- package/dist/core/permission/index.js +6 -0
- package/dist/core/permission/mode.js +24 -0
- package/dist/core/permission/sandbox-runtime.js +265 -0
- package/dist/core/permission/tool-policy.js +987 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +330 -0
- package/dist/core/relation/peer-identity.js +222 -0
- package/dist/core/relation/peer-key.js +1 -0
- package/dist/core/role/runtime-policy.js +141 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-fs-store.js +232 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1587 -0
- package/dist/core/session/session-mapper.js +100 -0
- package/dist/core/session/session-renew.js +314 -0
- package/dist/core/session/session-title.js +128 -0
- package/dist/core/session/session-turn-coordinator.js +205 -0
- package/dist/core/session/session-turns.js +67 -0
- package/dist/core/system-channels.js +29 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/group-rules-sync.js +345 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/kit-renderer.js +359 -0
- package/dist/eck/manifest-engine.js +446 -0
- package/dist/eck/message-renderer.js +199 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +2870 -4
- package/dist/ipc.js +748 -0
- package/dist/paths.js +262 -0
- package/dist/product.js +18 -0
- package/dist/response-system/context-builder.js +71 -0
- package/dist/response-system/coordinator.js +117 -0
- package/dist/response-system/decision-executor.js +86 -0
- package/dist/response-system/engines/v1/index.js +21 -0
- package/dist/response-system/engines/v1/interactive-flow.js +27 -0
- package/dist/response-system/engines/v1/proactive-flow.js +137 -0
- package/dist/response-system/engines/v1/types.js +1 -0
- package/dist/response-system/extensions.js +41 -0
- package/dist/response-system/index.js +6 -0
- package/dist/response-system/modes/index.js +7 -0
- package/dist/response-system/modes/single-session/index.js +72 -0
- package/dist/response-system/queues/fifo-queue.js +44 -0
- package/dist/response-system/queues/index.js +6 -0
- package/dist/response-system/queues/lifo-queue.js +42 -0
- package/dist/response-system/queues/priority-queue.js +63 -0
- package/dist/response-system/registry.js +97 -0
- package/dist/response-system/resolver.js +37 -0
- package/dist/response-system/selector.js +23 -0
- package/dist/response-system/types.js +7 -0
- package/dist/stats/billing.js +151 -0
- package/dist/stats/budget.js +93 -0
- package/dist/stats/db.js +403 -0
- package/dist/stats/eck-vars.js +89 -0
- package/dist/stats/index.js +11 -0
- package/dist/stats/normalizer.js +80 -0
- package/dist/stats/price-resolver.js +138 -0
- package/dist/stats/query.js +763 -0
- package/dist/stats/role-budget.js +168 -0
- package/dist/stats/writer.js +151 -0
- package/dist/trigger/anomaly-store.js +258 -0
- package/dist/trigger/audit.js +152 -0
- package/dist/trigger/event-source.js +119 -0
- package/dist/trigger/feedback.js +685 -0
- package/dist/trigger/history.js +290 -0
- package/dist/trigger/manager.js +291 -0
- package/dist/trigger/parser.js +520 -0
- package/dist/trigger/patch.js +148 -0
- package/dist/trigger/scheduler.js +1600 -0
- package/dist/trigger/script-executor.js +155 -0
- package/dist/trigger/state.js +145 -0
- package/dist/trigger/types.js +1 -0
- package/dist/trigger/validation.js +621 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +299 -0
- package/dist/utils/atomic-write.js +95 -0
- package/dist/utils/avatar-upload.js +123 -0
- package/dist/utils/cross-platform.js +297 -0
- package/dist/utils/ecweb-utils.js +73 -0
- package/dist/utils/error-dict.json +153 -0
- package/dist/utils/error-utils.js +349 -0
- package/dist/utils/instance-registry.js +437 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +224 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +271 -0
- package/dist/utils/model-prices.jsonl +17 -0
- package/dist/utils/npm-ops.js +210 -0
- package/dist/utils/process-introspect.js +133 -0
- package/dist/utils/process-tree-stats.js +271 -0
- package/dist/utils/project-path.js +74 -0
- package/dist/utils/stats.js +410 -0
- package/dist/utils/tool-summary.js +284 -0
- package/dist/utils/welcome.js +268 -0
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +65 -0
- package/kits/docs/aun/CHEATSHEET.md +19 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +65 -0
- package/kits/docs/channels/feishu.md +56 -0
- package/kits/docs/context-assembly.md +366 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolcore/INDEX.md +66 -0
- package/kits/docs/evolcore/agent.md +69 -0
- package/kits/docs/evolcore/aid.md +49 -0
- package/kits/docs/evolcore/config.md +149 -0
- package/kits/docs/evolcore/ctl.md +46 -0
- package/kits/docs/evolcore/event.md +216 -0
- package/kits/docs/evolcore/fs-architecture.md +1215 -0
- package/kits/docs/evolcore/fs.md +101 -0
- package/kits/docs/evolcore/group-fs.md +17 -0
- package/kits/docs/evolcore/group-rules.md +226 -0
- package/kits/docs/evolcore/group.md +141 -0
- package/kits/docs/evolcore/model.md +47 -0
- package/kits/docs/evolcore/msg.md +130 -0
- package/kits/docs/evolcore/response.md +80 -0
- package/kits/docs/evolcore/rpc.md +35 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +70 -0
- package/kits/docs/evolcore/storage.md +49 -0
- package/kits/docs/evolcore/trigger.md +524 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +23 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/docs/prompt-loading-architecture.md +266 -0
- package/kits/docs/venues/aun-group.md +45 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +25 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.auxiliary.json +43 -0
- package/kits/eck_manifest.json +191 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -0
- package/kits/migrations/migrate-role-config-v4.mjs +623 -0
- package/kits/migrations/migrate-role-config-v5.mjs +346 -0
- package/kits/migrations/rename-config-file.mjs +99 -0
- package/kits/rules/01-overview.md +142 -0
- package/kits/rules/02-navigation.md +76 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +59 -0
- package/kits/rules/05-venue.md +44 -0
- package/kits/rules/06-channel.md +59 -0
- package/kits/schemas/_meta.json +29 -0
- package/kits/schemas/agent-config.schema.1.json +177 -0
- package/kits/schemas/agent-config.schema.2.json +239 -0
- package/kits/schemas/agent-config.schema.3.json +119 -0
- package/kits/schemas/agent-config.schema.4.json +208 -0
- package/kits/schemas/agent-config.schema.5.json +326 -0
- package/kits/schemas/contact-book.schema.1.json +36 -0
- package/kits/schemas/daemon.schema.1.json +90 -0
- package/kits/schemas/defaults.schema.1.json +81 -0
- package/kits/schemas/menu-exec-schema-commands.md +208 -0
- package/kits/schemas/migrations/README.md +28 -0
- package/kits/schemas/relation-config.schema.1.json +158 -0
- package/kits/schemas/relation-config.schema.2.json +73 -0
- package/kits/schemas/relation-config.schema.3.json +50 -0
- package/kits/schemas/relation-config.schema.4.json +47 -0
- package/kits/schemas/role-config.schema.1.json +200 -0
- package/kits/schemas/role-registry.schema.1.json +35 -0
- package/kits/schemas/single-session.schema.1.json +31 -0
- package/kits/templates/bootstrap-welcome.md +15 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
- package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/roles/admin.json +8 -0
- package/kits/templates/roles/member.json +40 -0
- package/kits/templates/roles/owner.json +8 -0
- package/kits/templates/roles/visitor.json +39 -0
- package/kits/templates/system-fragments/baseagent.md +14 -0
- package/kits/templates/system-fragments/bootstrap.md +16 -0
- package/kits/templates/system-fragments/channel.md +48 -0
- package/kits/templates/system-fragments/commands.md +26 -0
- package/kits/templates/system-fragments/identity.md +11 -0
- package/kits/templates/system-fragments/relation.md +19 -0
- package/kits/templates/system-fragments/session.md +53 -0
- package/kits/templates/system-fragments/venue.md +31 -0
- package/package.json +50 -15
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Agent Runner
|
|
3
|
+
*
|
|
4
|
+
* Integrates Google Gemini CLI as a backend via subprocess.
|
|
5
|
+
* Each runQuery spawns `gemini -p` with --output-format stream-json,
|
|
6
|
+
* parsing the JSONL event stream into EvolCore AgentEvent.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* GeminiRunner → spawn `gemini -p ...` → stdout JSONL stream
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { createInterface } from 'readline';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import crypto from 'crypto';
|
|
17
|
+
import { resolveGoogleConfig } from './baseagent.js';
|
|
18
|
+
import { commandExists } from '../utils/cross-platform.js';
|
|
19
|
+
import { GeminiSessionFileAdapter } from '../core/session/adapters/gemini-session-file-adapter.js';
|
|
20
|
+
import { logger } from '../utils/logger.js';
|
|
21
|
+
import { normalizePermissionMode } from '../core/permission/mode.js';
|
|
22
|
+
import { workspaceContainsHClassPaths } from '../core/protected-paths.js';
|
|
23
|
+
import { buildBubblewrapCommand } from '../core/permission/sandbox-runtime.js';
|
|
24
|
+
// Strip ANSI escape codes from Gemini CLI text output.
|
|
25
|
+
// Gemini embeds raw terminal colors from tool stdout (e.g. vitest, npm)
|
|
26
|
+
// into its assistant text, unlike Claude SDK which strips them internally.
|
|
27
|
+
// eslint-disable-next-line no-control-regex
|
|
28
|
+
const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x1b\\|\x07)/g;
|
|
29
|
+
function stripAnsi(text) {
|
|
30
|
+
return text.replace(ANSI_RE, '');
|
|
31
|
+
}
|
|
32
|
+
// ── MIME → 扩展名映射 ──
|
|
33
|
+
const MIME_EXT = {
|
|
34
|
+
'image/png': '.png',
|
|
35
|
+
'image/jpeg': '.jpg',
|
|
36
|
+
'image/gif': '.gif',
|
|
37
|
+
'image/webp': '.webp',
|
|
38
|
+
};
|
|
39
|
+
// ── Gemini 模型列表 ──
|
|
40
|
+
const GEMINI_MODELS = [
|
|
41
|
+
'gemini-2.5-pro',
|
|
42
|
+
'gemini-2.5-flash',
|
|
43
|
+
'gemini-2.5-flash-lite',
|
|
44
|
+
];
|
|
45
|
+
const GEMINI_READONLY_TOOLS = [
|
|
46
|
+
'read_file', 'list_directory', 'glob', 'grep_search', 'google_web_search',
|
|
47
|
+
'codebase_investigator', 'cli_help', 'get_internal_docs', 'update_topic', 'complete_task',
|
|
48
|
+
];
|
|
49
|
+
const GEMINI_AUTO_TOOLS = [
|
|
50
|
+
...GEMINI_READONLY_TOOLS,
|
|
51
|
+
'write_file', 'replace', 'web_fetch', 'activate_skill',
|
|
52
|
+
];
|
|
53
|
+
const GEMINI_UNATTENDED_INTERACTION_TOOLS = new Set([
|
|
54
|
+
'ask_user',
|
|
55
|
+
'ask_user_question',
|
|
56
|
+
'request_user_input',
|
|
57
|
+
'exit_plan_mode',
|
|
58
|
+
'plan_approval',
|
|
59
|
+
]);
|
|
60
|
+
function normalizeGeminiToolName(toolName) {
|
|
61
|
+
return toolName
|
|
62
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
63
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
64
|
+
.replace(/^_+|_+$/g, '')
|
|
65
|
+
.toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
function isGeminiUnattendedInteraction(toolName) {
|
|
68
|
+
return GEMINI_UNATTENDED_INTERACTION_TOOLS.has(normalizeGeminiToolName(toolName));
|
|
69
|
+
}
|
|
70
|
+
function isGeminiPermissionDenial(event) {
|
|
71
|
+
const status = typeof event.status === 'string' ? event.status.trim().toLowerCase() : '';
|
|
72
|
+
if (['denied', 'rejected', 'blocked', 'permission_denied', 'policy_denied', 'not_allowed'].includes(status)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const decisions = [
|
|
76
|
+
event.decision,
|
|
77
|
+
event.permissionDecision,
|
|
78
|
+
event.policyDecision,
|
|
79
|
+
event.metadata?.decision,
|
|
80
|
+
event.metadata?.permissionDecision,
|
|
81
|
+
event.metadata?.policyDecision,
|
|
82
|
+
];
|
|
83
|
+
if (decisions.some(value => typeof value === 'string'
|
|
84
|
+
&& ['deny', 'denied', 'reject', 'rejected', 'block', 'blocked', 'not_allowed']
|
|
85
|
+
.includes(value.trim().toLowerCase()))) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const codes = [event.code, event.reasonCode, event.error?.code, event.error?.type, event.error?.name]
|
|
89
|
+
.filter((value) => typeof value === 'string')
|
|
90
|
+
.map(value => value.toLowerCase());
|
|
91
|
+
if (codes.some(value => /(?:permission|policy|approval).*(?:deny|reject|block|not.?allowed)|(?:deny|reject|block).*(?:permission|policy|approval)/i.test(value))) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
const details = [event.output, event.message, event.reason, event.error?.message]
|
|
95
|
+
.filter((value) => typeof value === 'string');
|
|
96
|
+
if (details.some(value => /^permission denied[.!]?$/i.test(stripAnsi(value).trim()))) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const detail = details.join(' ');
|
|
100
|
+
return /(?:\btool\b.{0,80}\bis not (?:allowed|permitted|authorized)\b|not (?:allowed|permitted|authorized).{0,80}(?:policy|approval)|(?:denied|rejected|blocked).{0,80}(?:by|under|due to).{0,40}(?:policy|approval)|(?:policy|approval).{0,80}(?:denied|rejected|blocked)|(?:approval|confirmation).{0,80}required.{0,80}(?:headless|non[- ]interactive))/i.test(detail);
|
|
101
|
+
}
|
|
102
|
+
const GEMINI_H_CLASS_PATTERNS = [
|
|
103
|
+
String.raw `evolcore\.json`,
|
|
104
|
+
String.raw `agents[/\\]defaults\.json`,
|
|
105
|
+
String.raw `agents[/\\]defaults_\d+\.json`,
|
|
106
|
+
String.raw `agents[/\\][^/\\"]+[/\\]config\.json`,
|
|
107
|
+
String.raw `agents[/\\][^/\\"]+[/\\]contact\.json`,
|
|
108
|
+
String.raw `agents[/\\][^/\\"]+[/\\]relations[/\\][^/\\"]+[/\\]config\.json`,
|
|
109
|
+
String.raw `backups[/\\]config`,
|
|
110
|
+
String.raw `\.snapshots`,
|
|
111
|
+
String.raw `(?:^|[/\\"])CA(?:[/\\"]|$)`,
|
|
112
|
+
String.raw `(?:AIDs|aids)[/\\][^/\\"]+[/\\](?:cert|keys)`,
|
|
113
|
+
String.raw `(?:^|[/\\"])\.device_id(?:[/\\"]|$)`,
|
|
114
|
+
String.raw `(?:^|[/\\"])\.env(?:[/\\"]|$)`,
|
|
115
|
+
String.raw `(?:^|[/\\"])\.seed(?:[/\\"]|$)`,
|
|
116
|
+
String.raw `(?:^|[/\\"])\.seed\.[^/\\"]+`,
|
|
117
|
+
String.raw `(?:^|[/\\"])\.migrated-[^/\\"]+`,
|
|
118
|
+
String.raw `(?:^|[/\\"])\.lock(?:[/\\"]|$)`,
|
|
119
|
+
String.raw `(?:^|[/\\"])daemon\.pid(?:[/\\"]|$)`,
|
|
120
|
+
String.raw `\.json_`,
|
|
121
|
+
String.raw `\.json\.migrated`,
|
|
122
|
+
];
|
|
123
|
+
const GEMINI_L_CLASS_PATTERNS = [
|
|
124
|
+
String.raw `data[/\\]triggers(?:[/\\]|$)`,
|
|
125
|
+
];
|
|
126
|
+
const GEMINI_L_CLASS_WRITE_TOOLS = [
|
|
127
|
+
'write_file', 'replace', 'delete_file', 'move_file', 'rename_file',
|
|
128
|
+
];
|
|
129
|
+
const GEMINI_EC_COMMAND_PATTERNS = [
|
|
130
|
+
String.raw `"command":"[ ]*ec[ ]*"`,
|
|
131
|
+
// This is only a shell-process boundary. It deliberately does not inspect
|
|
132
|
+
// subcommands or arguments. Shell composition remains excluded because the
|
|
133
|
+
// Gemini policy exception applies to the whole run_shell_command call.
|
|
134
|
+
String.raw `"command":"[ ]*ec[ ]+(?:[^"\\;&|$()<>\x60\r\n]|\\")*"`,
|
|
135
|
+
];
|
|
136
|
+
export function resolveGeminiPermissionProfile(value) {
|
|
137
|
+
const normalized = normalizePermissionMode(value);
|
|
138
|
+
if (normalized.mode === 'auto')
|
|
139
|
+
return { mode: 'auto', approvalMode: 'auto_edit' };
|
|
140
|
+
if (normalized.mode === 'request' || normalized.mode === 'bypass') {
|
|
141
|
+
return { mode: 'readonly', approvalMode: 'plan', degradedFrom: normalized.mode };
|
|
142
|
+
}
|
|
143
|
+
return { mode: 'readonly', approvalMode: 'plan' };
|
|
144
|
+
}
|
|
145
|
+
function appendGeminiPolicyRule(lines, toolName, decision, priority, argsPattern) {
|
|
146
|
+
lines.push('[[rule]]');
|
|
147
|
+
lines.push(`toolName = ${Array.isArray(toolName) ? JSON.stringify(toolName) : JSON.stringify(toolName)}`);
|
|
148
|
+
if (argsPattern)
|
|
149
|
+
lines.push(`argsPattern = '${argsPattern}'`);
|
|
150
|
+
lines.push(`decision = "${decision}"`);
|
|
151
|
+
lines.push(`priority = ${priority}`, '');
|
|
152
|
+
}
|
|
153
|
+
export function buildGeminiAdminPolicy(profile) {
|
|
154
|
+
const lines = [];
|
|
155
|
+
// EC/daemon owns all command semantics and authorization. The Runner only
|
|
156
|
+
// admits a single EC process through the otherwise mode-specific shell
|
|
157
|
+
// policy. Keep this above path denies so `--file` is not reinterpreted here.
|
|
158
|
+
for (const pattern of GEMINI_EC_COMMAND_PATTERNS) {
|
|
159
|
+
appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 1000, pattern);
|
|
160
|
+
}
|
|
161
|
+
// H-class protection is mode-independent and applies to built-in and MCP tools.
|
|
162
|
+
for (const pattern of GEMINI_H_CLASS_PATTERNS) {
|
|
163
|
+
appendGeminiPolicyRule(lines, '*', 'deny', 999, pattern);
|
|
164
|
+
}
|
|
165
|
+
for (const pattern of GEMINI_L_CLASS_PATTERNS) {
|
|
166
|
+
appendGeminiPolicyRule(lines, GEMINI_L_CLASS_WRITE_TOOLS, 'deny', 998, pattern);
|
|
167
|
+
}
|
|
168
|
+
if (profile.mode === 'readonly') {
|
|
169
|
+
appendGeminiPolicyRule(lines, GEMINI_READONLY_TOOLS, 'allow', 950);
|
|
170
|
+
appendGeminiPolicyRule(lines, '*', 'deny', 900);
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
}
|
|
173
|
+
appendGeminiPolicyRule(lines, 'run_shell_command', 'deny', 970);
|
|
174
|
+
appendGeminiPolicyRule(lines, 'ask_user', 'deny', 960);
|
|
175
|
+
appendGeminiPolicyRule(lines, GEMINI_AUTO_TOOLS, 'allow', 950);
|
|
176
|
+
// Gemini headless policy does not expose a stable MCP-only matcher. Keep
|
|
177
|
+
// explicit shell/interaction and protected-path denies above, then inherit
|
|
178
|
+
// unconfigured and bundled MCP tools instead of rejecting unknown names.
|
|
179
|
+
appendGeminiPolicyRule(lines, '*', 'allow', 900);
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
export function buildGeminiPermissionArgs(profile, policyPath, externallySandboxed = false) {
|
|
183
|
+
return [
|
|
184
|
+
'--admin-policy', policyPath,
|
|
185
|
+
...(!externallySandboxed ? ['--sandbox'] : []),
|
|
186
|
+
`--approval-mode=${profile.approvalMode}`,
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
function standardGeminiAdminPolicyDirectory() {
|
|
190
|
+
if (process.platform === 'win32') {
|
|
191
|
+
const programData = process.env.ProgramData;
|
|
192
|
+
return programData ? path.join(programData, 'gemini-cli', 'policies') : undefined;
|
|
193
|
+
}
|
|
194
|
+
if (process.platform === 'darwin') {
|
|
195
|
+
return '/Library/Application Support/GeminiCli/policies';
|
|
196
|
+
}
|
|
197
|
+
return '/etc/gemini-cli/policies';
|
|
198
|
+
}
|
|
199
|
+
export function hasStandardGeminiAdminPolicy() {
|
|
200
|
+
const directory = standardGeminiAdminPolicyDirectory();
|
|
201
|
+
if (!directory)
|
|
202
|
+
return false;
|
|
203
|
+
try {
|
|
204
|
+
return fs.readdirSync(directory, { withFileTypes: true })
|
|
205
|
+
.some(entry => entry.isFile() && entry.name.endsWith('.toml'));
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// ── Gemini Runner ──
|
|
212
|
+
export class GeminiRunner {
|
|
213
|
+
name = 'gemini';
|
|
214
|
+
capabilities = { clear: true, compact: false, fork: false, forkAtTurn: false, askUserQuestion: false, planApproval: false, fileRewind: 'unsupported' };
|
|
215
|
+
resolved;
|
|
216
|
+
model;
|
|
217
|
+
activeProcesses = new Map();
|
|
218
|
+
activeStreams = new Map();
|
|
219
|
+
activeSessions = new Map(); // sessionId → geminiSessionId
|
|
220
|
+
onSessionIdUpdate;
|
|
221
|
+
currentMode = 'readonly';
|
|
222
|
+
permissionContexts = new Map();
|
|
223
|
+
constructor(config, callbacks) {
|
|
224
|
+
this.resolved = resolveGoogleConfig(config);
|
|
225
|
+
this.model = this.resolved.model;
|
|
226
|
+
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
227
|
+
}
|
|
228
|
+
// ── ModelSwitcher ──
|
|
229
|
+
setModel(model) { this.model = model; }
|
|
230
|
+
getModel() { return this.model; }
|
|
231
|
+
listModels() { return GEMINI_MODELS; }
|
|
232
|
+
// ── Effort (not applicable) ──
|
|
233
|
+
setEffort(_effort) { }
|
|
234
|
+
getEffort() { return undefined; }
|
|
235
|
+
// ── Permission ──
|
|
236
|
+
setMode(mode) { this.currentMode = normalizePermissionMode(mode).mode; }
|
|
237
|
+
getMode() { return this.currentMode; }
|
|
238
|
+
listModes() {
|
|
239
|
+
return [
|
|
240
|
+
{ key: 'readonly', nameZh: '只读', description: '只读操作自动执行,写入直接拒绝', available: true },
|
|
241
|
+
{ key: 'auto', nameZh: '自动', description: '常规操作自动执行,危险操作自动拒绝', available: true },
|
|
242
|
+
{ key: 'request', nameZh: '审批', description: '默认能力自动执行,缺失能力进入人工审批', available: false, unavailableReason: 'Gemini headless 不提供审批回调;请使用 ACP 集成' },
|
|
243
|
+
{ key: 'bypass', nameZh: '放行', description: '沙箱内能力自动执行,仅硬边界不可绕过', available: false, unavailableReason: 'Gemini headless 不提供审批回调;请使用 ACP 集成' },
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
setSendPrompt(_fn) { }
|
|
247
|
+
setPermissionGateway(_gw) { }
|
|
248
|
+
setPermissionContext(sessionId, context) {
|
|
249
|
+
this.permissionContexts.set(sessionId, context);
|
|
250
|
+
}
|
|
251
|
+
buildAgentEnv(sessionId, runtimeEnv) {
|
|
252
|
+
const env = {
|
|
253
|
+
...process.env,
|
|
254
|
+
EVOLCORE_SESSION_ID: sessionId,
|
|
255
|
+
...(runtimeEnv ?? {}),
|
|
256
|
+
};
|
|
257
|
+
if (this.resolved.apiKey)
|
|
258
|
+
env.GOOGLE_API_KEY = this.resolved.apiKey;
|
|
259
|
+
return env;
|
|
260
|
+
}
|
|
261
|
+
// ── Stream management ──
|
|
262
|
+
registerStream(key, stream) {
|
|
263
|
+
this.activeStreams.set(key, stream);
|
|
264
|
+
}
|
|
265
|
+
cleanupStream(key) {
|
|
266
|
+
this.activeStreams.delete(key);
|
|
267
|
+
this.activeProcesses.delete(key);
|
|
268
|
+
}
|
|
269
|
+
hasActiveStream(key) {
|
|
270
|
+
return this.activeStreams.has(key) || this.activeProcesses.has(key);
|
|
271
|
+
}
|
|
272
|
+
// ── Core: runQuery ──
|
|
273
|
+
async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
|
|
274
|
+
let geminiSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
275
|
+
// per-call 权限模式/模型:优先 override,缺省回落实例级(多会话并发互不污染)
|
|
276
|
+
const requestedPermissionMode = modelOverride?.permissionMode || this.currentMode;
|
|
277
|
+
const permissionProfile = resolveGeminiPermissionProfile(requestedPermissionMode);
|
|
278
|
+
const callModel = modelOverride?.model || this.model;
|
|
279
|
+
const geminiHome = path.join(os.homedir(), '.gemini');
|
|
280
|
+
fs.mkdirSync(geminiHome, { recursive: true, mode: 0o700 });
|
|
281
|
+
const sandboxProbe = buildBubblewrapCommand(this.resolved.cliPath, [], {
|
|
282
|
+
projectPath,
|
|
283
|
+
writablePaths: [geminiHome],
|
|
284
|
+
});
|
|
285
|
+
if (!sandboxProbe && workspaceContainsHClassPaths(projectPath)) {
|
|
286
|
+
throw new Error('Gemini 缺少可用的路径级隔离运行时,且项目覆盖 EvolCore 受保护根;已拒绝启动本轮任务');
|
|
287
|
+
}
|
|
288
|
+
// Build CLI args
|
|
289
|
+
const args = [];
|
|
290
|
+
// Only inject system context on first turn (no resume).
|
|
291
|
+
// Resumed sessions already have the context from the first turn;
|
|
292
|
+
// repeating it pollutes the conversation history.
|
|
293
|
+
let fullPrompt = prompt;
|
|
294
|
+
if (systemPromptAppend && !geminiSessionId) {
|
|
295
|
+
fullPrompt = prompt + '\n\n--- [SYSTEM_PROMPT_END] ---\n' + systemPromptAppend;
|
|
296
|
+
}
|
|
297
|
+
// Handle images: write to temp files, prepend @file references
|
|
298
|
+
const tempFiles = [];
|
|
299
|
+
{
|
|
300
|
+
if (hasStandardGeminiAdminPolicy()) {
|
|
301
|
+
throw new Error('Gemini 系统级 admin policy 会忽略 EvolCore 的临时安全策略,已拒绝启动本轮任务');
|
|
302
|
+
}
|
|
303
|
+
const policyPath = path.join(os.tmpdir(), `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
|
|
304
|
+
fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile), { mode: 0o600, flag: 'wx' });
|
|
305
|
+
tempFiles.push(policyPath);
|
|
306
|
+
args.push(...buildGeminiPermissionArgs(permissionProfile, policyPath, !!sandboxProbe));
|
|
307
|
+
}
|
|
308
|
+
if (images?.length) {
|
|
309
|
+
const tmpDir = os.tmpdir();
|
|
310
|
+
const fileParts = [];
|
|
311
|
+
for (let i = 0; i < images.length; i++) {
|
|
312
|
+
const img = images[i];
|
|
313
|
+
const ext = MIME_EXT[img.mimeType || ''] || '.jpg';
|
|
314
|
+
const tmpPath = path.join(tmpDir, `evolcore-gemini-img-${Date.now()}-${i}${ext}`);
|
|
315
|
+
fs.writeFileSync(tmpPath, Buffer.from(img.data, 'base64'));
|
|
316
|
+
tempFiles.push(tmpPath);
|
|
317
|
+
fileParts.push(`@${tmpPath}`);
|
|
318
|
+
}
|
|
319
|
+
fullPrompt = fileParts.join(' ') + ' ' + fullPrompt;
|
|
320
|
+
logger.info(`[GeminiRunner] Attached ${images.length} image(s) via @file reference`);
|
|
321
|
+
}
|
|
322
|
+
args.push('-p', fullPrompt);
|
|
323
|
+
args.push('--output-format', 'stream-json');
|
|
324
|
+
args.push('-m', callModel);
|
|
325
|
+
// Permission mode
|
|
326
|
+
if (permissionProfile.degradedFrom) {
|
|
327
|
+
logger.warn(`[GeminiRunner] permission mode ${permissionProfile.degradedFrom} is unavailable in headless mode; degraded to readonly`);
|
|
328
|
+
}
|
|
329
|
+
// Resume session
|
|
330
|
+
if (geminiSessionId) {
|
|
331
|
+
args.push('-r', geminiSessionId);
|
|
332
|
+
}
|
|
333
|
+
// Spawn subprocess
|
|
334
|
+
const env = this.buildAgentEnv(sessionId, runtimeEnv);
|
|
335
|
+
const sandboxedCommand = buildBubblewrapCommand(this.resolved.cliPath, args, {
|
|
336
|
+
projectPath,
|
|
337
|
+
writablePaths: [geminiHome],
|
|
338
|
+
readonlyPaths: tempFiles,
|
|
339
|
+
});
|
|
340
|
+
const child = spawn(sandboxedCommand?.command ?? this.resolved.cliPath, sandboxedCommand?.args ?? args, {
|
|
341
|
+
cwd: projectPath,
|
|
342
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
343
|
+
env,
|
|
344
|
+
});
|
|
345
|
+
this.activeProcesses.set(sessionId, child);
|
|
346
|
+
// Log stderr
|
|
347
|
+
child.stderr?.on('data', (data) => {
|
|
348
|
+
const msg = data.toString().trim();
|
|
349
|
+
if (msg)
|
|
350
|
+
logger.debug(`[GeminiRunner:stderr] ${msg}`);
|
|
351
|
+
});
|
|
352
|
+
return this.transformStream(child, sessionId, tempFiles);
|
|
353
|
+
}
|
|
354
|
+
// ── Event stream transformation ──
|
|
355
|
+
async *transformStream(child, sessionId, tempFiles) {
|
|
356
|
+
const pendingToolNames = new Map(); // toolId → toolName
|
|
357
|
+
const recordedDeniedToolIds = new Set();
|
|
358
|
+
const startTime = Date.now();
|
|
359
|
+
const rl = createInterface({ input: child.stdout });
|
|
360
|
+
// Build async queue from readline
|
|
361
|
+
const queue = [];
|
|
362
|
+
let resolve = null;
|
|
363
|
+
let rlClosed = false;
|
|
364
|
+
let processExited = false;
|
|
365
|
+
let exitCode = null;
|
|
366
|
+
// We need both rl 'close' AND child 'exit' before considering stream done.
|
|
367
|
+
// rl 'close' guarantees all buffered lines are emitted.
|
|
368
|
+
// child 'exit' gives us the exit code.
|
|
369
|
+
const isStreamDone = () => rlClosed && processExited;
|
|
370
|
+
rl.on('line', (line) => {
|
|
371
|
+
queue.push(line);
|
|
372
|
+
resolve?.();
|
|
373
|
+
});
|
|
374
|
+
rl.on('close', () => {
|
|
375
|
+
rlClosed = true;
|
|
376
|
+
if (isStreamDone())
|
|
377
|
+
resolve?.();
|
|
378
|
+
});
|
|
379
|
+
child.on('exit', (code) => {
|
|
380
|
+
processExited = true;
|
|
381
|
+
exitCode = code;
|
|
382
|
+
if (isStreamDone())
|
|
383
|
+
resolve?.();
|
|
384
|
+
});
|
|
385
|
+
// Handle race: process may have already exited before we registered the listener.
|
|
386
|
+
// Real ChildProcess sets exitCode (number) on exit; null means still running.
|
|
387
|
+
if (!processExited && child.exitCode != null) {
|
|
388
|
+
processExited = true;
|
|
389
|
+
exitCode = child.exitCode;
|
|
390
|
+
}
|
|
391
|
+
child.on('error', (err) => {
|
|
392
|
+
logger.error(`[GeminiRunner] Process error: ${err.message}`);
|
|
393
|
+
rlClosed = true;
|
|
394
|
+
processExited = true;
|
|
395
|
+
resolve?.();
|
|
396
|
+
});
|
|
397
|
+
try {
|
|
398
|
+
let done = false;
|
|
399
|
+
let accumulatedText = '';
|
|
400
|
+
// TextBuffer: accumulate streaming text tokens, flush as a single
|
|
401
|
+
// text event on boundary signals (tool_use / result / error / exit).
|
|
402
|
+
// Prevents StreamFlusher from splitting a single reply into multiple messages.
|
|
403
|
+
let textBuffer = '';
|
|
404
|
+
const flushTextBuffer = function* () {
|
|
405
|
+
if (textBuffer) {
|
|
406
|
+
yield { type: 'text', text: stripAnsi(textBuffer) };
|
|
407
|
+
textBuffer = '';
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
while (!done) {
|
|
411
|
+
// Process queued lines
|
|
412
|
+
while (queue.length > 0) {
|
|
413
|
+
const line = queue.shift();
|
|
414
|
+
if (!line.trim())
|
|
415
|
+
continue;
|
|
416
|
+
let event;
|
|
417
|
+
try {
|
|
418
|
+
event = JSON.parse(line);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
logger.debug(`[GeminiRunner] Non-JSON line: ${line.substring(0, 200)}`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
switch (event.type) {
|
|
425
|
+
case 'init': {
|
|
426
|
+
// Extract session_id from init event
|
|
427
|
+
const geminiId = event.session_id;
|
|
428
|
+
if (geminiId) {
|
|
429
|
+
this.activeSessions.set(sessionId, geminiId);
|
|
430
|
+
this.onSessionIdUpdate?.(sessionId, geminiId);
|
|
431
|
+
yield { type: 'session_id', sessionId: geminiId };
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
case 'message': {
|
|
436
|
+
// Skip user message echo
|
|
437
|
+
if (event.role === 'user')
|
|
438
|
+
break;
|
|
439
|
+
// Assistant message (delta=true → streaming)
|
|
440
|
+
// Accumulate into textBuffer; will flush on boundary event
|
|
441
|
+
if (event.role === 'assistant' && event.content) {
|
|
442
|
+
accumulatedText += event.content;
|
|
443
|
+
textBuffer += event.content;
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
case 'tool_use': {
|
|
448
|
+
// Boundary: flush accumulated text before tool call
|
|
449
|
+
yield* flushTextBuffer();
|
|
450
|
+
const toolName = event.tool_name || 'unknown';
|
|
451
|
+
if (event.tool_id) {
|
|
452
|
+
pendingToolNames.set(event.tool_id, toolName);
|
|
453
|
+
}
|
|
454
|
+
yield {
|
|
455
|
+
type: 'tool_use',
|
|
456
|
+
name: toolName,
|
|
457
|
+
input: event.parameters || {},
|
|
458
|
+
};
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case 'tool_result': {
|
|
462
|
+
const toolName = (event.tool_id && pendingToolNames.get(event.tool_id))
|
|
463
|
+
|| (typeof event.tool_name === 'string' && event.tool_name)
|
|
464
|
+
|| 'unknown';
|
|
465
|
+
if (event.tool_id)
|
|
466
|
+
pendingToolNames.delete(event.tool_id);
|
|
467
|
+
const requestId = typeof event.tool_id === 'string' || typeof event.tool_id === 'number'
|
|
468
|
+
? String(event.tool_id)
|
|
469
|
+
: undefined;
|
|
470
|
+
if (event.status !== 'success'
|
|
471
|
+
&& isGeminiPermissionDenial(event)
|
|
472
|
+
&& (!requestId || !recordedDeniedToolIds.has(requestId))) {
|
|
473
|
+
if (requestId)
|
|
474
|
+
recordedDeniedToolIds.add(requestId);
|
|
475
|
+
await this.recordUnattendedPolicyDenial(sessionId, toolName, requestId);
|
|
476
|
+
}
|
|
477
|
+
yield {
|
|
478
|
+
type: 'tool_result',
|
|
479
|
+
name: toolName,
|
|
480
|
+
result: stripAnsi(event.output || ''),
|
|
481
|
+
isError: event.status !== 'success',
|
|
482
|
+
};
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case 'result': {
|
|
486
|
+
// Boundary: flush accumulated text before complete
|
|
487
|
+
yield* flushTextBuffer();
|
|
488
|
+
const durationMs = event.stats?.duration_ms || (Date.now() - startTime);
|
|
489
|
+
const isError = event.status !== 'success';
|
|
490
|
+
// Extract error message from event.error.message (Gemini CLI structure)
|
|
491
|
+
const errorMessage = event.error?.message || event.message;
|
|
492
|
+
yield {
|
|
493
|
+
type: 'complete',
|
|
494
|
+
result: accumulatedText || undefined,
|
|
495
|
+
isError,
|
|
496
|
+
errors: isError ? [errorMessage || event.status || '任务执行失败'] : undefined,
|
|
497
|
+
durationMs,
|
|
498
|
+
costUsd: undefined,
|
|
499
|
+
};
|
|
500
|
+
done = true;
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case 'error': {
|
|
504
|
+
// Boundary: flush accumulated text before error
|
|
505
|
+
yield* flushTextBuffer();
|
|
506
|
+
yield {
|
|
507
|
+
type: 'error',
|
|
508
|
+
error: event.message || 'Unknown Gemini error',
|
|
509
|
+
errorType: 'unknown',
|
|
510
|
+
};
|
|
511
|
+
if (event.fatal) {
|
|
512
|
+
yield { type: 'complete', result: '', isError: true, durationMs: Date.now() - startTime };
|
|
513
|
+
done = true;
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
default:
|
|
518
|
+
logger.debug(`[GeminiRunner] Unknown event type: ${event.type}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (!done) {
|
|
522
|
+
if (isStreamDone()) {
|
|
523
|
+
// Boundary: flush accumulated text before exit
|
|
524
|
+
yield* flushTextBuffer();
|
|
525
|
+
// Process exited without result event
|
|
526
|
+
if (exitCode !== 0) {
|
|
527
|
+
yield { type: 'error', error: `Gemini CLI exited with code ${exitCode}`, errorType: 'unknown' };
|
|
528
|
+
}
|
|
529
|
+
yield { type: 'complete', result: '', isError: exitCode !== 0, durationMs: Date.now() - startTime };
|
|
530
|
+
done = true;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// Wait for more data
|
|
534
|
+
await new Promise((r) => { resolve = r; });
|
|
535
|
+
resolve = null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
rl.close();
|
|
542
|
+
this.activeProcesses.delete(sessionId);
|
|
543
|
+
// Kill process if still running
|
|
544
|
+
if (!child.killed && !processExited) {
|
|
545
|
+
child.kill('SIGTERM');
|
|
546
|
+
}
|
|
547
|
+
// Cleanup temp image files
|
|
548
|
+
if (tempFiles?.length) {
|
|
549
|
+
for (const f of tempFiles) {
|
|
550
|
+
try {
|
|
551
|
+
fs.unlinkSync(f);
|
|
552
|
+
}
|
|
553
|
+
catch { /* ignore */ }
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async recordUnattendedPolicyDenial(sessionId, toolName, requestId) {
|
|
559
|
+
const context = this.permissionContexts.get(sessionId);
|
|
560
|
+
if (context?.approvalInteractionPolicy !== 'deny')
|
|
561
|
+
return;
|
|
562
|
+
if (isGeminiUnattendedInteraction(toolName)) {
|
|
563
|
+
logger.info(`[GeminiRunner] interaction.auto_resolved kind=${normalizeGeminiToolName(toolName)} session=${sessionId}`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
await context.recordExecutionAnomaly?.({
|
|
568
|
+
code: 'runtime_permission_denied',
|
|
569
|
+
severity: 'warning',
|
|
570
|
+
phase: 'execution',
|
|
571
|
+
occurredAt: Date.now(),
|
|
572
|
+
toolName,
|
|
573
|
+
...(requestId ? { requestId } : {}),
|
|
574
|
+
policy: context.approvalRouting?.approverPolicy ?? 'requester',
|
|
575
|
+
summary: `${toolName} runtime authorization denied by Gemini headless policy`,
|
|
576
|
+
effect: 'operation_skipped',
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
logger.warn(`[GeminiRunner] failed to record unattended policy denial: session=${sessionId} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
|
|
581
|
+
}
|
|
582
|
+
logger.info(`[GeminiRunner] unattended tool approval denied: session=${sessionId} tool=${toolName}`);
|
|
583
|
+
}
|
|
584
|
+
// ── Interrupt ──
|
|
585
|
+
async interrupt(sessionKey) {
|
|
586
|
+
const child = this.activeProcesses.get(sessionKey);
|
|
587
|
+
if (child && !child.killed) {
|
|
588
|
+
child.kill('SIGINT');
|
|
589
|
+
setTimeout(() => {
|
|
590
|
+
if (!child.killed) {
|
|
591
|
+
child.kill('SIGTERM');
|
|
592
|
+
logger.info(`[GeminiRunner] SIGTERM fallback for: ${sessionKey}`);
|
|
593
|
+
}
|
|
594
|
+
}, 3000);
|
|
595
|
+
logger.info(`[GeminiRunner] Interrupted session: ${sessionKey} (SIGINT, SIGTERM fallback in 3s)`);
|
|
596
|
+
}
|
|
597
|
+
this.activeProcesses.delete(sessionKey);
|
|
598
|
+
this.activeStreams.delete(sessionKey);
|
|
599
|
+
}
|
|
600
|
+
// ── Session commands ──
|
|
601
|
+
updateSessionId(sessionId, agentSessionId) {
|
|
602
|
+
if (agentSessionId) {
|
|
603
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
this.activeSessions.delete(sessionId);
|
|
607
|
+
}
|
|
608
|
+
this.onSessionIdUpdate?.(sessionId, agentSessionId);
|
|
609
|
+
}
|
|
610
|
+
async closeSession(sessionId) {
|
|
611
|
+
this.activeSessions.delete(sessionId);
|
|
612
|
+
this.activeStreams.delete(sessionId);
|
|
613
|
+
this.activeProcesses.delete(sessionId);
|
|
614
|
+
this.permissionContexts.delete(sessionId);
|
|
615
|
+
}
|
|
616
|
+
resolveSessionFile(agentSessionId, projectPath) {
|
|
617
|
+
const adapter = new GeminiSessionFileAdapter();
|
|
618
|
+
return adapter.findSessionFile(projectPath, agentSessionId);
|
|
619
|
+
}
|
|
620
|
+
async clearSession(sessionId, _agentSessionId, _projectPath) {
|
|
621
|
+
// Clear = don't pass -r next time → fresh session
|
|
622
|
+
this.activeSessions.delete(sessionId);
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
async compactSession(_sessionId, _agentSessionId, _projectPath) {
|
|
626
|
+
logger.info('[GeminiRunner] Compact not supported, Gemini CLI handles context internally');
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
async compact(_sessionId, _agentSessionId, _projectPath) {
|
|
630
|
+
return this.compactSession(_sessionId, _agentSessionId, _projectPath);
|
|
631
|
+
}
|
|
632
|
+
setCompactStartCallback(_callback) { }
|
|
633
|
+
// ── Cleanup ──
|
|
634
|
+
async dispose() {
|
|
635
|
+
for (const [, child] of this.activeProcesses) {
|
|
636
|
+
if (!child.killed)
|
|
637
|
+
child.kill('SIGTERM');
|
|
638
|
+
}
|
|
639
|
+
this.activeProcesses.clear();
|
|
640
|
+
this.activeStreams.clear();
|
|
641
|
+
this.activeSessions.clear();
|
|
642
|
+
this.permissionContexts.clear();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// ── Plugin ──
|
|
646
|
+
export class GeminiAgentPlugin {
|
|
647
|
+
name = 'gemini';
|
|
648
|
+
isEnabled(agent) {
|
|
649
|
+
const geminiCfg = agent.config.baseagents?.gemini;
|
|
650
|
+
if (!geminiCfg)
|
|
651
|
+
return false;
|
|
652
|
+
if (geminiCfg.cliPath)
|
|
653
|
+
return true;
|
|
654
|
+
if (geminiCfg.apiKey && !geminiCfg.apiKey.includes('your-') && !geminiCfg.apiKey.includes('placeholder'))
|
|
655
|
+
return true;
|
|
656
|
+
return commandExists('gemini');
|
|
657
|
+
}
|
|
658
|
+
createAgent(agent, callbacks) {
|
|
659
|
+
const override = agent.config.baseagents?.gemini;
|
|
660
|
+
const syntheticConfig = { agents: { gemini: override } };
|
|
661
|
+
const merged = {
|
|
662
|
+
agents: { gemini: { ...(override || {}) } },
|
|
663
|
+
};
|
|
664
|
+
return { evolagentName: agent.name, baseagent: 'gemini', agent: new GeminiRunner(merged, callbacks) };
|
|
665
|
+
}
|
|
666
|
+
}
|