evolcore 0.0.1 → 0.0.3

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 (379) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION-0.5.0.md +378 -0
  4. package/README.md +318 -14
  5. package/ROLE_ACCESS_CONTROL.md +174 -0
  6. package/assets/.env.template +4 -0
  7. package/bin/ec-safe-output.js +161 -0
  8. package/bin/ec.js +29 -0
  9. package/dist/agents/baseagent.js +163 -0
  10. package/dist/agents/claude-runner.js +2565 -0
  11. package/dist/agents/codex-app-server-client.js +448 -0
  12. package/dist/agents/codex-runner.js +2682 -0
  13. package/dist/agents/gemini-runner.js +666 -0
  14. package/dist/agents/runner-types.js +75 -0
  15. package/dist/aun/aid/agentmd.js +216 -0
  16. package/dist/aun/aid/client.js +132 -0
  17. package/dist/aun/aid/control-aid.js +91 -0
  18. package/dist/aun/aid/identity.js +518 -0
  19. package/dist/aun/aid/index.js +4 -0
  20. package/dist/aun/aid/store.js +74 -0
  21. package/dist/aun/aid/types.js +1 -0
  22. package/dist/aun/aid/validation.js +21 -0
  23. package/dist/aun/group-identity.js +10 -0
  24. package/dist/aun/msg/group-index.js +6 -0
  25. package/dist/aun/msg/group.js +1231 -0
  26. package/dist/aun/msg/history.js +123 -0
  27. package/dist/aun/msg/index.js +5 -0
  28. package/dist/aun/msg/p2p.js +393 -0
  29. package/dist/aun/msg/payload-type.js +27 -0
  30. package/dist/aun/msg/upload.js +137 -0
  31. package/dist/aun/outbox.js +168 -0
  32. package/dist/aun/rpc/caller.js +42 -0
  33. package/dist/aun/rpc/connection.js +25 -0
  34. package/dist/aun/rpc/index.js +2 -0
  35. package/dist/aun/service-proxy.js +225 -0
  36. package/dist/aun/storage/download.js +29 -0
  37. package/dist/aun/storage/index.js +3 -0
  38. package/dist/aun/storage/manage.js +10 -0
  39. package/dist/aun/storage/upload.js +68 -0
  40. package/dist/channels/aun.js +4164 -0
  41. package/dist/channels/contact-bind-code.js +134 -0
  42. package/dist/channels/daemon.js +422 -0
  43. package/dist/channels/dingtalk.js +1479 -0
  44. package/dist/channels/feishu.js +1865 -0
  45. package/dist/channels/qqbot.js +409 -0
  46. package/dist/channels/wechat.js +817 -0
  47. package/dist/channels/wecom-card.js +101 -0
  48. package/dist/channels/wecom-onboarding.js +82 -0
  49. package/dist/channels/wecom-state.js +191 -0
  50. package/dist/channels/wecom.js +1157 -0
  51. package/dist/cli/agent-command.js +642 -0
  52. package/dist/cli/agent.js +1059 -0
  53. package/dist/cli/aun-commands.js +2003 -0
  54. package/dist/cli/bench.js +1228 -0
  55. package/dist/cli/cli-argv.js +66 -0
  56. package/dist/cli/code-stats.js +329 -0
  57. package/dist/cli/command-log.js +82 -0
  58. package/dist/cli/config-selector.js +69 -0
  59. package/dist/cli/config.js +261 -0
  60. package/dist/cli/contact.js +71 -0
  61. package/dist/cli/ctl-command.js +62 -0
  62. package/dist/cli/daemon-commands.js +2750 -0
  63. package/dist/cli/fs-command.js +1447 -0
  64. package/dist/cli/handoff-command.js +302 -0
  65. package/dist/cli/help.js +35 -0
  66. package/dist/cli/index.js +374 -0
  67. package/dist/cli/init-channel.js +1372 -0
  68. package/dist/cli/init.js +590 -0
  69. package/dist/cli/link-rules.js +240 -0
  70. package/dist/cli/model.js +591 -0
  71. package/dist/cli/net-check.js +723 -0
  72. package/dist/cli/queue-command.js +150 -0
  73. package/dist/cli/raw-key-input.js +25 -0
  74. package/dist/cli/response.js +344 -0
  75. package/dist/cli/restart-monitor.js +480 -0
  76. package/dist/cli/stats.js +609 -0
  77. package/dist/cli/task-context.js +80 -0
  78. package/dist/cli/trigger-command.js +545 -0
  79. package/dist/cli/version.js +93 -0
  80. package/dist/cli/watch-logs.js +33 -0
  81. package/dist/cli/watch-msg.js +673 -0
  82. package/dist/config/boot-log.js +266 -0
  83. package/dist/config/builtin-role-templates.js +42 -0
  84. package/dist/config/builtin-roles.js +91 -0
  85. package/dist/config/config-batch-get.js +11 -0
  86. package/dist/config/config-field-policy.js +261 -0
  87. package/dist/config/config-manager.js +1120 -0
  88. package/dist/config/config-operation-service.js +384 -0
  89. package/dist/config/contact-alias.js +68 -0
  90. package/dist/config/contact-book-store.js +454 -0
  91. package/dist/config/contact-book-v2-startup.js +35 -0
  92. package/dist/config/contact-book.js +224 -0
  93. package/dist/config/contact-operation-service.js +110 -0
  94. package/dist/config/gateway-config.js +858 -0
  95. package/dist/config/lifecycle.js +17 -0
  96. package/dist/config/mention-mode.js +27 -0
  97. package/dist/config/merge.js +161 -0
  98. package/dist/config/owner-policy.js +4 -0
  99. package/dist/config/peer-role-resolver.js +218 -0
  100. package/dist/config/resolved-config-op.js +483 -0
  101. package/dist/config/role-config-v4-startup.js +32 -0
  102. package/dist/config/role-config-v5-startup.js +27 -0
  103. package/dist/config/role-ranks.js +18 -0
  104. package/dist/config/role-schema.js +105 -0
  105. package/dist/config/role-service.js +156 -0
  106. package/dist/config/role-store.js +215 -0
  107. package/dist/config/roles.js +64 -0
  108. package/dist/config/schema-registry.js +154 -0
  109. package/dist/config/snapshot.js +598 -0
  110. package/dist/config-store.js +501 -0
  111. package/dist/core/auth/agent-delegation.js +111 -0
  112. package/dist/core/auth/auth-gateway.js +166 -0
  113. package/dist/core/auth/authenticated-actor.js +6 -0
  114. package/dist/core/auth/authorization-audit.js +119 -0
  115. package/dist/core/auth/operation-authorizer.js +720 -0
  116. package/dist/core/auth/operation-catalog.js +731 -0
  117. package/dist/core/baseagent-loader.js +54 -0
  118. package/dist/core/bootstrap-service.js +175 -0
  119. package/dist/core/capability/capability-manager.js +316 -0
  120. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  121. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  122. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  123. package/dist/core/capability/types.js +27 -0
  124. package/dist/core/causation/audit.js +103 -0
  125. package/dist/core/causation/aun-association.js +111 -0
  126. package/dist/core/causation/context.js +93 -0
  127. package/dist/core/causation/index.js +4 -0
  128. package/dist/core/causation/types.js +2 -0
  129. package/dist/core/channel-loader.js +277 -0
  130. package/dist/core/command/agent-control.js +616 -0
  131. package/dist/core/command/cli-intent-parser.js +225 -0
  132. package/dist/core/command/command-handler.js +1733 -0
  133. package/dist/core/command/connect-menu.js +374 -0
  134. package/dist/core/command/menu-handler.js +3452 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1623 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3066 -0
  139. package/dist/core/daemon-file-cache.js +222 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +810 -0
  142. package/dist/core/evolagent-registry.js +545 -0
  143. package/dist/core/evolagent.js +342 -0
  144. package/dist/core/handoff/dispatcher.js +229 -0
  145. package/dist/core/handoff/mutex.js +46 -0
  146. package/dist/core/handoff/runtime.js +324 -0
  147. package/dist/core/handoff/store.js +537 -0
  148. package/dist/core/handoff/types.js +12 -0
  149. package/dist/core/inference/text-inference.js +173 -0
  150. package/dist/core/interaction-registration.js +10 -0
  151. package/dist/core/interaction-router.js +278 -0
  152. package/dist/core/message/create-status.js +67 -0
  153. package/dist/core/message/im-renderer.js +659 -0
  154. package/dist/core/message/items-formatter.js +76 -0
  155. package/dist/core/message/logical-queue-bridge.js +123 -0
  156. package/dist/core/message/message-bridge.js +874 -0
  157. package/dist/core/message/message-cache.js +56 -0
  158. package/dist/core/message/message-log.js +339 -0
  159. package/dist/core/message/message-processor.js +4 -0
  160. package/dist/core/message/message-queue.js +1446 -0
  161. package/dist/core/message/message-utils.js +76 -0
  162. package/dist/core/message/peer-mode.js +105 -0
  163. package/dist/core/message/pending-hints.js +232 -0
  164. package/dist/core/message/response-depth.js +33 -0
  165. package/dist/core/message/response-engine.js +4020 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/send-receipt.js +24 -0
  168. package/dist/core/message/stream-debouncer.js +139 -0
  169. package/dist/core/message/stream-idle-monitor.js +124 -0
  170. package/dist/core/model/config-scope.js +162 -0
  171. package/dist/core/model/field-scope.js +78 -0
  172. package/dist/core/model/model-catalog.js +227 -0
  173. package/dist/core/model/model-diagnostics.js +182 -0
  174. package/dist/core/model/model-permission.js +90 -0
  175. package/dist/core/permission/approval-gateway.js +1017 -0
  176. package/dist/core/permission/ec-command-parser.js +347 -0
  177. package/dist/core/permission/execution-sandbox.js +16 -0
  178. package/dist/core/permission/index.js +6 -0
  179. package/dist/core/permission/mode.js +24 -0
  180. package/dist/core/permission/sandbox-runtime.js +265 -0
  181. package/dist/core/permission/tool-policy.js +1019 -0
  182. package/dist/core/permission/unix-socket-policy.js +99 -0
  183. package/dist/core/protected-paths.js +332 -0
  184. package/dist/core/relation/peer-identity.js +222 -0
  185. package/dist/core/relation/peer-key.js +1 -0
  186. package/dist/core/role/runtime-policy.js +141 -0
  187. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  188. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  189. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  190. package/dist/core/session/session-file-adapter.js +7 -0
  191. package/dist/core/session/session-file-health.js +45 -0
  192. package/dist/core/session/session-fs-store.js +273 -0
  193. package/dist/core/session/session-key.js +24 -0
  194. package/dist/core/session/session-manager.js +1643 -0
  195. package/dist/core/session/session-mapper.js +100 -0
  196. package/dist/core/session/session-renew.js +314 -0
  197. package/dist/core/session/session-title.js +128 -0
  198. package/dist/core/session/session-turn-coordinator.js +205 -0
  199. package/dist/core/session/session-turns.js +67 -0
  200. package/dist/core/system-channels.js +29 -0
  201. package/dist/eck/baseagent-caps.js +18 -0
  202. package/dist/eck/detect.js +47 -0
  203. package/dist/eck/group-rules-sync.js +345 -0
  204. package/dist/eck/init.js +77 -0
  205. package/dist/eck/kit-renderer.js +359 -0
  206. package/dist/eck/manifest-engine.js +446 -0
  207. package/dist/eck/message-renderer.js +199 -0
  208. package/dist/eck/rules-loader.js +28 -0
  209. package/dist/index.js +2926 -4
  210. package/dist/ipc.js +777 -0
  211. package/dist/paths.js +262 -0
  212. package/dist/product.js +18 -0
  213. package/dist/response-system/context-builder.js +71 -0
  214. package/dist/response-system/coordinator.js +117 -0
  215. package/dist/response-system/decision-executor.js +86 -0
  216. package/dist/response-system/engines/v1/index.js +21 -0
  217. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  218. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  219. package/dist/response-system/engines/v1/types.js +1 -0
  220. package/dist/response-system/extensions.js +41 -0
  221. package/dist/response-system/index.js +6 -0
  222. package/dist/response-system/modes/index.js +7 -0
  223. package/dist/response-system/modes/single-session/index.js +72 -0
  224. package/dist/response-system/queues/fifo-queue.js +44 -0
  225. package/dist/response-system/queues/index.js +6 -0
  226. package/dist/response-system/queues/lifo-queue.js +42 -0
  227. package/dist/response-system/queues/priority-queue.js +63 -0
  228. package/dist/response-system/registry.js +97 -0
  229. package/dist/response-system/resolver.js +37 -0
  230. package/dist/response-system/selector.js +23 -0
  231. package/dist/response-system/types.js +7 -0
  232. package/dist/stats/billing.js +163 -0
  233. package/dist/stats/budget.js +93 -0
  234. package/dist/stats/db.js +403 -0
  235. package/dist/stats/eck-vars.js +89 -0
  236. package/dist/stats/index.js +11 -0
  237. package/dist/stats/normalizer.js +80 -0
  238. package/dist/stats/price-resolver.js +138 -0
  239. package/dist/stats/query.js +763 -0
  240. package/dist/stats/role-budget.js +168 -0
  241. package/dist/stats/writer.js +151 -0
  242. package/dist/trigger/anomaly-store.js +258 -0
  243. package/dist/trigger/audit.js +152 -0
  244. package/dist/trigger/event-source.js +119 -0
  245. package/dist/trigger/feedback.js +685 -0
  246. package/dist/trigger/history.js +290 -0
  247. package/dist/trigger/manager.js +294 -0
  248. package/dist/trigger/parser.js +595 -0
  249. package/dist/trigger/patch.js +155 -0
  250. package/dist/trigger/scheduler.js +1602 -0
  251. package/dist/trigger/script-executor.js +155 -0
  252. package/dist/trigger/state.js +145 -0
  253. package/dist/trigger/types.js +1 -0
  254. package/dist/trigger/validation.js +634 -0
  255. package/dist/types.js +12 -0
  256. package/dist/utils/aid-bind.js +313 -0
  257. package/dist/utils/atomic-write.js +95 -0
  258. package/dist/utils/avatar-upload.js +123 -0
  259. package/dist/utils/cross-platform.js +297 -0
  260. package/dist/utils/ecweb-utils.js +73 -0
  261. package/dist/utils/error-dict.json +153 -0
  262. package/dist/utils/error-utils.js +349 -0
  263. package/dist/utils/instance-registry.js +444 -0
  264. package/dist/utils/locale.js +21 -0
  265. package/dist/utils/log-writer.js +270 -0
  266. package/dist/utils/logger.js +89 -0
  267. package/dist/utils/markdown-to-plain-text.js +20 -0
  268. package/dist/utils/media-cache.js +274 -0
  269. package/dist/utils/model-prices.jsonl +20 -0
  270. package/dist/utils/npm-ops.js +210 -0
  271. package/dist/utils/process-introspect.js +133 -0
  272. package/dist/utils/process-tree-stats.js +271 -0
  273. package/dist/utils/project-path.js +74 -0
  274. package/dist/utils/restart-safety.js +31 -0
  275. package/dist/utils/stats.js +410 -0
  276. package/dist/utils/system-memory.js +62 -0
  277. package/dist/utils/tool-summary.js +284 -0
  278. package/dist/utils/welcome.js +268 -0
  279. package/kits/docs/GUIDE.md +20 -0
  280. package/kits/docs/INDEX.md +66 -0
  281. package/kits/docs/aun/CHEATSHEET.md +19 -0
  282. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  283. package/kits/docs/channels/aun.md +65 -0
  284. package/kits/docs/channels/feishu.md +56 -0
  285. package/kits/docs/context-assembly.md +366 -0
  286. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  287. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  288. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  289. package/kits/docs/eck_templates/runtime.template.md +19 -0
  290. package/kits/docs/evolcore/INDEX.md +68 -0
  291. package/kits/docs/evolcore/agent.md +77 -0
  292. package/kits/docs/evolcore/aid.md +52 -0
  293. package/kits/docs/evolcore/config.md +149 -0
  294. package/kits/docs/evolcore/contact.md +57 -0
  295. package/kits/docs/evolcore/ctl.md +46 -0
  296. package/kits/docs/evolcore/event.md +216 -0
  297. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  298. package/kits/docs/evolcore/fs.md +110 -0
  299. package/kits/docs/evolcore/group-fs.md +17 -0
  300. package/kits/docs/evolcore/group-rules.md +226 -0
  301. package/kits/docs/evolcore/group.md +150 -0
  302. package/kits/docs/evolcore/model.md +50 -0
  303. package/kits/docs/evolcore/msg.md +136 -0
  304. package/kits/docs/evolcore/response.md +75 -0
  305. package/kits/docs/evolcore/rpc.md +37 -0
  306. package/kits/docs/evolcore/self-summary.md +29 -0
  307. package/kits/docs/evolcore/stats.md +83 -0
  308. package/kits/docs/evolcore/storage.md +50 -0
  309. package/kits/docs/evolcore/trigger.md +539 -0
  310. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  311. package/kits/docs/identity/PATH_OPS.md +16 -0
  312. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  313. package/kits/docs/identity/identity-tools.md +26 -0
  314. package/kits/docs/path-registry.md +43 -0
  315. package/kits/docs/prompt-loading-architecture.md +266 -0
  316. package/kits/docs/venues/aun-group.md +45 -0
  317. package/kits/docs/venues/aun-private.md +10 -0
  318. package/kits/docs/venues/client-desktop.md +10 -0
  319. package/kits/docs/venues/client-mobile.md +10 -0
  320. package/kits/docs/venues/feishu-group.md +13 -0
  321. package/kits/docs/venues/feishu-private.md +9 -0
  322. package/kits/docs/venues/group.md +25 -0
  323. package/kits/docs/venues/private.md +10 -0
  324. package/kits/eck_manifest.auxiliary.json +43 -0
  325. package/kits/eck_manifest.json +203 -0
  326. package/kits/eck_message_manifest.json +63 -0
  327. package/kits/migrations/README-role-config-v4.md +32 -0
  328. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  329. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  330. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  331. package/kits/migrations/rename-config-file.mjs +99 -0
  332. package/kits/rules/01-overview.md +142 -0
  333. package/kits/rules/02-navigation.md +76 -0
  334. package/kits/rules/03-identity.md +34 -0
  335. package/kits/rules/04-relation.md +59 -0
  336. package/kits/rules/05-venue.md +44 -0
  337. package/kits/rules/06-channel.md +59 -0
  338. package/kits/schemas/_meta.json +32 -0
  339. package/kits/schemas/agent-config.schema.1.json +177 -0
  340. package/kits/schemas/agent-config.schema.2.json +239 -0
  341. package/kits/schemas/agent-config.schema.3.json +119 -0
  342. package/kits/schemas/agent-config.schema.4.json +208 -0
  343. package/kits/schemas/agent-config.schema.5.json +326 -0
  344. package/kits/schemas/agent-config.schema.6.json +322 -0
  345. package/kits/schemas/contact-book.schema.1.json +36 -0
  346. package/kits/schemas/contact-book.schema.2.json +43 -0
  347. package/kits/schemas/daemon.schema.1.json +90 -0
  348. package/kits/schemas/defaults.schema.1.json +81 -0
  349. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  350. package/kits/schemas/migrations/README.md +28 -0
  351. package/kits/schemas/relation-config.schema.1.json +158 -0
  352. package/kits/schemas/relation-config.schema.2.json +73 -0
  353. package/kits/schemas/relation-config.schema.3.json +50 -0
  354. package/kits/schemas/relation-config.schema.4.json +47 -0
  355. package/kits/schemas/relation-config.schema.5.json +47 -0
  356. package/kits/schemas/role-config.schema.1.json +201 -0
  357. package/kits/schemas/role-registry.schema.1.json +35 -0
  358. package/kits/schemas/single-session.schema.1.json +31 -0
  359. package/kits/templates/bootstrap-welcome.md +15 -0
  360. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  361. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  362. package/kits/templates/message-fragments/inject-default.md +2 -0
  363. package/kits/templates/message-fragments/item.md +2 -0
  364. package/kits/templates/roles/admin.json +9 -0
  365. package/kits/templates/roles/member.json +41 -0
  366. package/kits/templates/roles/owner.json +9 -0
  367. package/kits/templates/roles/visitor.json +40 -0
  368. package/kits/templates/system-fragments/baseagent.md +14 -0
  369. package/kits/templates/system-fragments/bootstrap.md +16 -0
  370. package/kits/templates/system-fragments/channel.md +48 -0
  371. package/kits/templates/system-fragments/commands.md +28 -0
  372. package/kits/templates/system-fragments/identity.md +11 -0
  373. package/kits/templates/system-fragments/relation.md +19 -0
  374. package/kits/templates/system-fragments/session.md +53 -0
  375. package/kits/templates/system-fragments/venue.md +31 -0
  376. package/package.json +50 -15
  377. package/dist/index.d.ts +0 -7
  378. package/dist/index.d.ts.map +0 -1
  379. 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
+ }