agim-cli 1.2.21 → 1.2.34
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 +172 -0
- package/README.md +8 -8
- package/README.zh-CN.md +6 -6
- package/dist/cli-ui/cmd-handlers.d.ts.map +1 -1
- package/dist/cli-ui/cmd-handlers.js +172 -13
- package/dist/cli-ui/cmd-handlers.js.map +1 -1
- package/dist/cli-ui/config-wizard.d.ts.map +1 -1
- package/dist/cli-ui/config-wizard.js +134 -24
- package/dist/cli-ui/config-wizard.js.map +1 -1
- package/dist/cli-ui/i18n.d.ts +33 -4
- package/dist/cli-ui/i18n.d.ts.map +1 -1
- package/dist/cli-ui/i18n.js +66 -8
- package/dist/cli-ui/i18n.js.map +1 -1
- package/dist/cli-ui/service.d.ts +44 -0
- package/dist/cli-ui/service.d.ts.map +1 -1
- package/dist/cli-ui/service.js +176 -8
- package/dist/cli-ui/service.js.map +1 -1
- package/dist/cli.js +193 -33
- package/dist/cli.js.map +1 -1
- package/dist/core/access-token.d.ts +51 -5
- package/dist/core/access-token.d.ts.map +1 -1
- package/dist/core/access-token.js +114 -15
- package/dist/core/access-token.js.map +1 -1
- package/dist/core/acp-server.d.ts.map +1 -1
- package/dist/core/acp-server.js +44 -3
- package/dist/core/acp-server.js.map +1 -1
- package/dist/core/admin-allowlist.d.ts +4 -1
- package/dist/core/admin-allowlist.d.ts.map +1 -1
- package/dist/core/admin-allowlist.js +14 -1
- package/dist/core/admin-allowlist.js.map +1 -1
- package/dist/core/admin-bootstrap.d.ts.map +1 -1
- package/dist/core/admin-bootstrap.js +8 -5
- package/dist/core/admin-bootstrap.js.map +1 -1
- package/dist/core/agent-base.d.ts.map +1 -1
- package/dist/core/agent-base.js +25 -0
- package/dist/core/agent-base.js.map +1 -1
- package/dist/core/agent-cwd.d.ts +1 -1
- package/dist/core/agent-cwd.d.ts.map +1 -1
- package/dist/core/agent-cwd.js +73 -2
- package/dist/core/agent-cwd.js.map +1 -1
- package/dist/core/agent-helper.d.ts +1 -1
- package/dist/core/agent-helper.js +4 -4
- package/dist/core/agent-helper.js.map +1 -1
- package/dist/core/approval-bus.d.ts +10 -0
- package/dist/core/approval-bus.d.ts.map +1 -1
- package/dist/core/approval-bus.js +130 -0
- package/dist/core/approval-bus.js.map +1 -1
- package/dist/core/approval-router.d.ts.map +1 -1
- package/dist/core/approval-router.js +18 -0
- package/dist/core/approval-router.js.map +1 -1
- package/dist/core/artifacts.d.ts +9 -1
- package/dist/core/artifacts.d.ts.map +1 -1
- package/dist/core/artifacts.js +22 -5
- package/dist/core/artifacts.js.map +1 -1
- package/dist/core/audit-log.d.ts +28 -0
- package/dist/core/audit-log.d.ts.map +1 -1
- package/dist/core/audit-log.js +100 -2
- package/dist/core/audit-log.js.map +1 -1
- package/dist/core/commands/service.d.ts +6 -5
- package/dist/core/commands/service.d.ts.map +1 -1
- package/dist/core/commands/service.js +59 -8
- package/dist/core/commands/service.js.map +1 -1
- package/dist/core/feature-flags.d.ts +10 -0
- package/dist/core/feature-flags.d.ts.map +1 -0
- package/dist/core/feature-flags.js +30 -0
- package/dist/core/feature-flags.js.map +1 -0
- package/dist/core/intent.d.ts.map +1 -1
- package/dist/core/intent.js +0 -4
- package/dist/core/intent.js.map +1 -1
- package/dist/core/logger.d.ts +31 -0
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +41 -1
- package/dist/core/logger.js.map +1 -1
- package/dist/core/memory-distill.js +1 -1
- package/dist/core/memory-distill.js.map +1 -1
- package/dist/core/memory-rpc.d.ts.map +1 -1
- package/dist/core/memory-rpc.js +55 -12
- package/dist/core/memory-rpc.js.map +1 -1
- package/dist/core/memory.d.ts +5 -0
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +151 -1
- package/dist/core/memory.js.map +1 -1
- package/dist/core/onboarding.d.ts +6 -0
- package/dist/core/onboarding.d.ts.map +1 -1
- package/dist/core/onboarding.js +21 -10
- package/dist/core/onboarding.js.map +1 -1
- package/dist/core/outbox.d.ts +8 -1
- package/dist/core/outbox.d.ts.map +1 -1
- package/dist/core/outbox.js +46 -2
- package/dist/core/outbox.js.map +1 -1
- package/dist/core/persona.d.ts.map +1 -1
- package/dist/core/persona.js +1 -18
- package/dist/core/persona.js.map +1 -1
- package/dist/core/prompt-injection-guard.d.ts +23 -0
- package/dist/core/prompt-injection-guard.d.ts.map +1 -0
- package/dist/core/prompt-injection-guard.js +94 -0
- package/dist/core/prompt-injection-guard.js.map +1 -0
- package/dist/core/registry.d.ts +8 -0
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +41 -7
- package/dist/core/registry.js.map +1 -1
- package/dist/core/restart-flow.d.ts.map +1 -1
- package/dist/core/restart-flow.js +27 -0
- package/dist/core/restart-flow.js.map +1 -1
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +4 -0
- package/dist/core/router.js.map +1 -1
- package/dist/core/sensitive-paths.d.ts +28 -0
- package/dist/core/sensitive-paths.d.ts.map +1 -0
- package/dist/core/sensitive-paths.js +234 -0
- package/dist/core/sensitive-paths.js.map +1 -0
- package/dist/core/session.d.ts +9 -0
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +35 -4
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +6 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/viewer-local.d.ts +4 -0
- package/dist/core/viewer-local.d.ts.map +1 -1
- package/dist/core/viewer-local.js +74 -0
- package/dist/core/viewer-local.js.map +1 -1
- package/dist/plugins/agents/antigravity/ensure-mcp-config.d.ts +49 -0
- package/dist/plugins/agents/antigravity/ensure-mcp-config.d.ts.map +1 -0
- package/dist/plugins/agents/antigravity/ensure-mcp-config.js +110 -0
- package/dist/plugins/agents/antigravity/ensure-mcp-config.js.map +1 -0
- package/dist/plugins/agents/antigravity/index.d.ts +34 -0
- package/dist/plugins/agents/antigravity/index.d.ts.map +1 -0
- package/dist/plugins/agents/antigravity/index.js +330 -0
- package/dist/plugins/agents/antigravity/index.js.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js +5 -5
- package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -1
- package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -1
- package/dist/plugins/messengers/feishu/card-builder.js +0 -1
- package/dist/plugins/messengers/feishu/card-builder.js.map +1 -1
- package/dist/utils/cross-platform.d.ts +0 -5
- package/dist/utils/cross-platform.d.ts.map +1 -1
- package/dist/utils/cross-platform.js +1 -21
- package/dist/utils/cross-platform.js.map +1 -1
- package/dist/web/public/assets/{a2a-Dk2fSs33.js → a2a-BFM2Ojvs.js} +2 -2
- package/dist/web/public/assets/{a2a-Dk2fSs33.js.map → a2a-BFM2Ojvs.js.map} +1 -1
- package/dist/web/public/assets/{activity-eiIPshcV.js → activity-DNe1vn9s.js} +2 -2
- package/dist/web/public/assets/{activity-eiIPshcV.js.map → activity-DNe1vn9s.js.map} +1 -1
- package/dist/web/public/assets/{admins-DlbQYdW_.js → admins-DtIF8yji.js} +2 -2
- package/dist/web/public/assets/{admins-DlbQYdW_.js.map → admins-DtIF8yji.js.map} +1 -1
- package/dist/web/public/assets/agents-BJ3jyH1u.js +12 -0
- package/dist/web/public/assets/agents-BJ3jyH1u.js.map +1 -0
- package/dist/web/public/assets/{approvals-DlXS_sKD.js → approvals-8ifx3_0b.js} +2 -2
- package/dist/web/public/assets/{approvals-DlXS_sKD.js.map → approvals-8ifx3_0b.js.map} +1 -1
- package/dist/web/public/assets/{audit-C8I8xC_6.js → audit-BiFOljMy.js} +2 -2
- package/dist/web/public/assets/{audit-C8I8xC_6.js.map → audit-BiFOljMy.js.map} +1 -1
- package/dist/web/public/assets/{bgjobs-PFYinH7D.js → bgjobs-Dve01ab0.js} +2 -2
- package/dist/web/public/assets/{bgjobs-PFYinH7D.js.map → bgjobs-Dve01ab0.js.map} +1 -1
- package/dist/web/public/assets/{brain-DEEJttEL.js → brain-BI78EY6_.js} +2 -2
- package/dist/web/public/assets/{brain-DEEJttEL.js.map → brain-BI78EY6_.js.map} +1 -1
- package/dist/web/public/assets/{briefcase-BlMy8gI6.js → briefcase-CuwoOW31.js} +2 -2
- package/dist/web/public/assets/{briefcase-BlMy8gI6.js.map → briefcase-CuwoOW31.js.map} +1 -1
- package/dist/web/public/assets/{chevron-right-DmABPvoA.js → chevron-right-D5JanEQ3.js} +2 -2
- package/dist/web/public/assets/{chevron-right-DmABPvoA.js.map → chevron-right-D5JanEQ3.js.map} +1 -1
- package/dist/web/public/assets/{circle-check-C0Qpg1vL.js → circle-check-DXMzdFtD.js} +2 -2
- package/dist/web/public/assets/{circle-check-C0Qpg1vL.js.map → circle-check-DXMzdFtD.js.map} +1 -1
- package/dist/web/public/assets/{circle-check-big-C8LG3beV.js → circle-check-big-CyJIIhiq.js} +2 -2
- package/dist/web/public/assets/{circle-check-big-C8LG3beV.js.map → circle-check-big-CyJIIhiq.js.map} +1 -1
- package/dist/web/public/assets/{circle-x-D_cRHcHK.js → circle-x-DTwfNpvp.js} +2 -2
- package/dist/web/public/assets/{circle-x-D_cRHcHK.js.map → circle-x-DTwfNpvp.js.map} +1 -1
- package/dist/web/public/assets/{confirm-dialog-Baz_xFle.js → confirm-dialog-F0sdcLGN.js} +2 -2
- package/dist/web/public/assets/{confirm-dialog-Baz_xFle.js.map → confirm-dialog-F0sdcLGN.js.map} +1 -1
- package/dist/web/public/assets/{data-table--I_ktDF4.js → data-table-DN_-VLbp.js} +2 -2
- package/dist/web/public/assets/{data-table--I_ktDF4.js.map → data-table-DN_-VLbp.js.map} +1 -1
- package/dist/web/public/assets/{dialog-DZpoEskO.js → dialog-CM16nfWK.js} +2 -2
- package/dist/web/public/assets/{dialog-DZpoEskO.js.map → dialog-CM16nfWK.js.map} +1 -1
- package/dist/web/public/assets/{download-DbFGHwZ5.js → download-tFqY3Zj6.js} +2 -2
- package/dist/web/public/assets/{download-DbFGHwZ5.js.map → download-tFqY3Zj6.js.map} +1 -1
- package/dist/web/public/assets/{email-BB1Hq8eE.js → email-By2-ARPV.js} +2 -2
- package/dist/web/public/assets/{email-BB1Hq8eE.js.map → email-By2-ARPV.js.map} +1 -1
- package/dist/web/public/assets/{empty-state-DXNa90pP.js → empty-state-Dq9_8t-M.js} +2 -2
- package/dist/web/public/assets/{empty-state-DXNa90pP.js.map → empty-state-Dq9_8t-M.js.map} +1 -1
- package/dist/web/public/assets/{external-link-nhnJN0qg.js → external-link-amfAWSUX.js} +2 -2
- package/dist/web/public/assets/{external-link-nhnJN0qg.js.map → external-link-amfAWSUX.js.map} +1 -1
- package/dist/web/public/assets/{eye-IKkn_oUo.js → eye-BfgN09Lx.js} +2 -2
- package/dist/web/public/assets/{eye-IKkn_oUo.js.map → eye-BfgN09Lx.js.map} +1 -1
- package/dist/web/public/assets/{facts-C7Qy9vTw.js → facts-uvL__ilQ.js} +2 -2
- package/dist/web/public/assets/{facts-C7Qy9vTw.js.map → facts-uvL__ilQ.js.map} +1 -1
- package/dist/web/public/assets/{health-CMRdeNEW.js → health-fiU-ueEw.js} +2 -2
- package/dist/web/public/assets/{health-CMRdeNEW.js.map → health-fiU-ueEw.js.map} +1 -1
- package/dist/web/public/assets/{hot-Bh5Nrc7i.js → hot-Dt4V3jM_.js} +2 -2
- package/dist/web/public/assets/{hot-Bh5Nrc7i.js.map → hot-Dt4V3jM_.js.map} +1 -1
- package/dist/web/public/assets/{index-CpGWCLE5.js → index-6GMwymev.js} +8 -8
- package/dist/web/public/assets/index-6GMwymev.js.map +1 -0
- package/dist/web/public/assets/{index-GpceOxum.css → index-CDYTPZH0.css} +1 -1
- package/dist/web/public/assets/{installed-FYLkPij2.js → installed-B_x6no76.js} +2 -2
- package/dist/web/public/assets/{installed-FYLkPij2.js.map → installed-B_x6no76.js.map} +1 -1
- package/dist/web/public/assets/{jobs-BmqLUzHp.js → jobs-LqWH3CIe.js} +2 -2
- package/dist/web/public/assets/{jobs-BmqLUzHp.js.map → jobs-LqWH3CIe.js.map} +1 -1
- package/dist/web/public/assets/layout-CvxcdPD9.js +2 -0
- package/dist/web/public/assets/layout-CvxcdPD9.js.map +1 -0
- package/dist/web/public/assets/{layout-BZaHqf69.js → layout-DHUzlXrd.js} +2 -2
- package/dist/web/public/assets/{layout-BZaHqf69.js.map → layout-DHUzlXrd.js.map} +1 -1
- package/dist/web/public/assets/{layout-CXsUyEpG.js → layout-MNk0bLGe.js} +2 -2
- package/dist/web/public/assets/{layout-CXsUyEpG.js.map → layout-MNk0bLGe.js.map} +1 -1
- package/dist/web/public/assets/{layout-DFxtpNut.js → layout-WcrkE0es.js} +2 -2
- package/dist/web/public/assets/{layout-DFxtpNut.js.map → layout-WcrkE0es.js.map} +1 -1
- package/dist/web/public/assets/{layout-d8qxPKQk.js → layout-apvyE2JN.js} +2 -2
- package/dist/web/public/assets/{layout-d8qxPKQk.js.map → layout-apvyE2JN.js.map} +1 -1
- package/dist/web/public/assets/{loader-circle-JaKY-xMt.js → loader-circle-hxNy7hSm.js} +2 -2
- package/dist/web/public/assets/{loader-circle-JaKY-xMt.js.map → loader-circle-hxNy7hSm.js.map} +1 -1
- package/dist/web/public/assets/{map-pin-hFFSWZ3B.js → map-pin-CCmA7ke2.js} +2 -2
- package/dist/web/public/assets/{map-pin-hFFSWZ3B.js.map → map-pin-CCmA7ke2.js.map} +1 -1
- package/dist/web/public/assets/{memos-EhjMUvVZ.js → memos-pEjDfEj3.js} +2 -2
- package/dist/web/public/assets/{memos-EhjMUvVZ.js.map → memos-pEjDfEj3.js.map} +1 -1
- package/dist/web/public/assets/messengers-Ba7opEc1.js +7 -0
- package/dist/web/public/assets/messengers-Ba7opEc1.js.map +1 -0
- package/dist/web/public/assets/{network-DtCI2ZUU.js → network-DJw-ei_k.js} +2 -2
- package/dist/web/public/assets/{network-DtCI2ZUU.js.map → network-DJw-ei_k.js.map} +1 -1
- package/dist/web/public/assets/{outbox-CxUbMp6o.js → outbox-Ckq-VT5C.js} +2 -2
- package/dist/web/public/assets/{outbox-CxUbMp6o.js.map → outbox-Ckq-VT5C.js.map} +1 -1
- package/dist/web/public/assets/{pagination-CkZY8YNa.js → pagination-DGS-TnI5.js} +2 -2
- package/dist/web/public/assets/{pagination-CkZY8YNa.js.map → pagination-DGS-TnI5.js.map} +1 -1
- package/dist/web/public/assets/{persona-B6TFMSnI.js → persona--LsrhCVU.js} +2 -2
- package/dist/web/public/assets/{persona-B6TFMSnI.js.map → persona--LsrhCVU.js.map} +1 -1
- package/dist/web/public/assets/{play-BxRcWaH5.js → play-Cb7co2DX.js} +2 -2
- package/dist/web/public/assets/{play-BxRcWaH5.js.map → play-Cb7co2DX.js.map} +1 -1
- package/dist/web/public/assets/{policy-ndE1Y8zD.js → policy-CbCotzr6.js} +2 -2
- package/dist/web/public/assets/{policy-ndE1Y8zD.js.map → policy-CbCotzr6.js.map} +1 -1
- package/dist/web/public/assets/{refresh-ccw-Bx817_KW.js → refresh-ccw-CINxCmwV.js} +2 -2
- package/dist/web/public/assets/{refresh-ccw-Bx817_KW.js.map → refresh-ccw-CINxCmwV.js.map} +1 -1
- package/dist/web/public/assets/{reminders-XynkGQc5.js → reminders-CSKrWre3.js} +2 -2
- package/dist/web/public/assets/{reminders-XynkGQc5.js.map → reminders-CSKrWre3.js.map} +1 -1
- package/dist/web/public/assets/{save-CqMcATrh.js → save-Bib9iAA-.js} +2 -2
- package/dist/web/public/assets/{save-CqMcATrh.js.map → save-Bib9iAA-.js.map} +1 -1
- package/dist/web/public/assets/{schedules-VM02w_Om.js → schedules-DUD_FfEX.js} +2 -2
- package/dist/web/public/assets/{schedules-VM02w_Om.js.map → schedules-DUD_FfEX.js.map} +1 -1
- package/dist/web/public/assets/{search-Ba-e1t1P.js → search-DZOHNA81.js} +2 -2
- package/dist/web/public/assets/{search-Ba-e1t1P.js.map → search-DZOHNA81.js.map} +1 -1
- package/dist/web/public/assets/{service-C-wnwJ-b.js → service-Cf7EQ4Sj.js} +3 -3
- package/dist/web/public/assets/{service-C-wnwJ-b.js.map → service-Cf7EQ4Sj.js.map} +1 -1
- package/dist/web/public/assets/{status-badge-CsdJ6k8Q.js → status-badge-RpKtiHgj.js} +2 -2
- package/dist/web/public/assets/{status-badge-CsdJ6k8Q.js.map → status-badge-RpKtiHgj.js.map} +1 -1
- package/dist/web/public/assets/{subtasks-mGRKpF0G.js → subtasks-xCHP5uI6.js} +2 -2
- package/dist/web/public/assets/{subtasks-mGRKpF0G.js.map → subtasks-xCHP5uI6.js.map} +1 -1
- package/dist/web/public/assets/{table-vmLMgj6_.js → table-S43AHY-3.js} +2 -2
- package/dist/web/public/assets/{table-vmLMgj6_.js.map → table-S43AHY-3.js.map} +1 -1
- package/dist/web/public/assets/{topn-nu66Fotx.js → topn-C2tcvmnB.js} +2 -2
- package/dist/web/public/assets/{topn-nu66Fotx.js.map → topn-C2tcvmnB.js.map} +1 -1
- package/dist/web/public/assets/{trash-2-ZIitN_U3.js → trash-2-Ct7YJmZO.js} +2 -2
- package/dist/web/public/assets/{trash-2-ZIitN_U3.js.map → trash-2-Ct7YJmZO.js.map} +1 -1
- package/dist/web/public/assets/{use-memory-DgEqHEca.js → use-memory-CCn0h8EP.js} +2 -2
- package/dist/web/public/assets/{use-memory-DgEqHEca.js.map → use-memory-CCn0h8EP.js.map} +1 -1
- package/dist/web/public/assets/{use-observability-CQev_A8e.js → use-observability-Dbal-WXR.js} +2 -2
- package/dist/web/public/assets/{use-observability-CQev_A8e.js.map → use-observability-Dbal-WXR.js.map} +1 -1
- package/dist/web/public/assets/{use-settings-CU-UcrVD.js → use-settings-erlkhPqn.js} +2 -2
- package/dist/web/public/assets/{use-settings-CU-UcrVD.js.map → use-settings-erlkhPqn.js.map} +1 -1
- package/dist/web/public/assets/{use-skills-Dr77CXLA.js → use-skills-C8Ukv8B4.js} +2 -2
- package/dist/web/public/assets/{use-skills-Dr77CXLA.js.map → use-skills-C8Ukv8B4.js.map} +1 -1
- package/dist/web/public/assets/{use-workspace-PNv9Z4de.js → use-workspace-8VZDPppc.js} +2 -2
- package/dist/web/public/assets/{use-workspace-PNv9Z4de.js.map → use-workspace-8VZDPppc.js.map} +1 -1
- package/dist/web/public/assets/{useQuery-BTyugXYV.js → useQuery-BnCHQlxq.js} +2 -2
- package/dist/web/public/assets/{useQuery-BTyugXYV.js.map → useQuery-BnCHQlxq.js.map} +1 -1
- package/dist/web/public/assets/{vector-w-Ea3pg6.js → vector-DDnIidyp.js} +2 -2
- package/dist/web/public/assets/{vector-w-Ea3pg6.js.map → vector-DDnIidyp.js.map} +1 -1
- package/dist/web/public/assets/{viewer-DKA7QP9U.js → viewer-BV0Cux3V.js} +2 -2
- package/dist/web/public/assets/{viewer-DKA7QP9U.js.map → viewer-BV0Cux3V.js.map} +1 -1
- package/dist/web/public/assets/{workspace-DVLZca7t.js → workspace-DbLMyUTn.js} +2 -2
- package/dist/web/public/assets/{workspace-DVLZca7t.js.map → workspace-DbLMyUTn.js.map} +1 -1
- package/dist/web/public/assets/{workspaces-DYZsMmY-.js → workspaces-BhF0IAJg.js} +2 -2
- package/dist/web/public/assets/{workspaces-DYZsMmY-.js.map → workspaces-BhF0IAJg.js.map} +1 -1
- package/dist/web/public/assets/{x-Ru3rHT82.js → x-BmgfwQRl.js} +2 -2
- package/dist/web/public/assets/{x-Ru3rHT82.js.map → x-BmgfwQRl.js.map} +1 -1
- package/dist/web/public/index.html +2 -2
- package/dist/web/public/settings.html +0 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +613 -28
- package/dist/web/server.js.map +1 -1
- package/package.json +3 -2
- package/dist/plugins/agents/copilot/index.d.ts +0 -35
- package/dist/plugins/agents/copilot/index.d.ts.map +0 -1
- package/dist/plugins/agents/copilot/index.js +0 -182
- package/dist/plugins/agents/copilot/index.js.map +0 -1
- package/dist/web/public/assets/agents-BMI1WbZj.js +0 -12
- package/dist/web/public/assets/agents-BMI1WbZj.js.map +0 -1
- package/dist/web/public/assets/env-Bqrb9XkC.js +0 -2
- package/dist/web/public/assets/env-Bqrb9XkC.js.map +0 -1
- package/dist/web/public/assets/index-CpGWCLE5.js.map +0 -1
- package/dist/web/public/assets/layout-9Gp_myEd.js +0 -2
- package/dist/web/public/assets/layout-9Gp_myEd.js.map +0 -1
- package/dist/web/public/assets/messengers-BRV1IVGX.js +0 -7
- package/dist/web/public/assets/messengers-BRV1IVGX.js.map +0 -1
package/dist/web/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { parseMessage, routeMessage } from '../core/router.js';
|
|
|
10
10
|
import { sessionManager } from '../core/session.js';
|
|
11
11
|
import { registry } from '../core/registry.js';
|
|
12
12
|
import { sink, resolveMessenger } from '../core/message-sink.js';
|
|
13
|
-
import { generateTraceId, createLogger, logger as rootLogger } from '../core/logger.js';
|
|
13
|
+
import { generateTraceId, createLogger, logger as rootLogger, sanitizeUserText } from '../core/logger.js';
|
|
14
14
|
import { validateConfig } from '../core/config-schema.js';
|
|
15
15
|
import { isMasked, maskSecret } from './env-mask.js';
|
|
16
16
|
import { consumeToken, peekToken } from '../core/location-token.js';
|
|
@@ -66,6 +66,11 @@ function isLoopbackPeer(req) {
|
|
|
66
66
|
const ip = (req.socket.remoteAddress || '').replace(/^::ffff:/, '');
|
|
67
67
|
return ip === '127.0.0.1' || ip === '::1';
|
|
68
68
|
}
|
|
69
|
+
/** R13 A5 — track once-per-process whether the deprecated `?token=`
|
|
70
|
+
* URL fallback has been used, so we warn at most once per service
|
|
71
|
+
* lifetime instead of spamming the journal. Cleared by tests via
|
|
72
|
+
* re-importing the module fresh. */
|
|
73
|
+
let _queryTokenWarned = false;
|
|
69
74
|
function extractToken(req, url) {
|
|
70
75
|
// 1. Authorization: Bearer <token>
|
|
71
76
|
const auth = req.headers['authorization'];
|
|
@@ -83,10 +88,44 @@ function extractToken(req, url) {
|
|
|
83
88
|
return decodeURIComponent(rest.join('=').trim());
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
|
-
// 3. ?token=... (
|
|
91
|
+
// 3. ?token=... (deprecated — kept for WS upgrade / iframe compat)
|
|
92
|
+
//
|
|
93
|
+
// R13 A5 — query-string tokens leak to:
|
|
94
|
+
// * browser history / bookmarks
|
|
95
|
+
// * Referer headers when the page links to a third party
|
|
96
|
+
// * any HTTP-proxy / WAF / CDN access log between client and agim
|
|
97
|
+
//
|
|
98
|
+
// We warn loudly the first time it's used per process lifetime
|
|
99
|
+
// (rate-limited so a misbehaving client doesn't fill the journal)
|
|
100
|
+
// + emit a one-shot audit-event row. Operators should migrate
|
|
101
|
+
// callers to Authorization: Bearer or the agim_token cookie.
|
|
87
102
|
const q = url.searchParams.get('token');
|
|
88
|
-
if (q)
|
|
103
|
+
if (q) {
|
|
104
|
+
if (!_queryTokenWarned) {
|
|
105
|
+
_queryTokenWarned = true;
|
|
106
|
+
rootLogger.warn({
|
|
107
|
+
component: 'web',
|
|
108
|
+
event: 'web.auth.query_token_used',
|
|
109
|
+
path: url.pathname,
|
|
110
|
+
msg: '?token= URL fallback used; prefer Authorization: Bearer or agim_token cookie '
|
|
111
|
+
+ '(query token leaks to browser history / Referer / proxy logs)',
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
void (async () => {
|
|
115
|
+
const { logAuditEvent } = await import('../core/audit-log.js');
|
|
116
|
+
logAuditEvent({
|
|
117
|
+
eventType: 'config.put',
|
|
118
|
+
actor: 'system',
|
|
119
|
+
target: 'web.auth.query_token',
|
|
120
|
+
outcome: 'ok',
|
|
121
|
+
details: { reason: 'query_token_used', path: url.pathname },
|
|
122
|
+
});
|
|
123
|
+
})();
|
|
124
|
+
}
|
|
125
|
+
catch { /* best-effort */ }
|
|
126
|
+
}
|
|
89
127
|
return q;
|
|
128
|
+
}
|
|
90
129
|
return null;
|
|
91
130
|
}
|
|
92
131
|
/** Returns true if the request is authenticated (or auth not required). */
|
|
@@ -143,6 +182,80 @@ function verifyTokenSync(raw) {
|
|
|
143
182
|
}
|
|
144
183
|
return _tokenModule.verifyToken(raw);
|
|
145
184
|
}
|
|
185
|
+
/** Resolve a stable actor string for audit-event rows. Mirrors checkAuth's
|
|
186
|
+
* identity model:
|
|
187
|
+
* - auth disabled → 'web:auth-off'
|
|
188
|
+
* - loopback peer (no auth needed) → 'web:loopback'
|
|
189
|
+
* - verified token → 'web:<tokenId>'
|
|
190
|
+
* - otherwise → 'web:unknown' (request should already have been rejected
|
|
191
|
+
* by checkAuth — this branch exists for defensive logging). */
|
|
192
|
+
function getRequestActor(req) {
|
|
193
|
+
if ((process.env.IMHUB_WEB_AUTH || '').toLowerCase() === 'off')
|
|
194
|
+
return 'web:auth-off';
|
|
195
|
+
if (isLoopbackPeer(req))
|
|
196
|
+
return 'web:loopback';
|
|
197
|
+
let url;
|
|
198
|
+
try {
|
|
199
|
+
url = new URL(req.url || '', 'http://localhost');
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return 'web:unknown';
|
|
203
|
+
}
|
|
204
|
+
const tok = extractToken(req, url);
|
|
205
|
+
if (tok) {
|
|
206
|
+
const id = verifyTokenSync(tok);
|
|
207
|
+
if (id)
|
|
208
|
+
return `web:${id}`;
|
|
209
|
+
}
|
|
210
|
+
return 'web:unknown';
|
|
211
|
+
}
|
|
212
|
+
/** Resolve whether the request's actor has admin role. Used to gate
|
|
213
|
+
* mutation + privileged-read endpoints so a stolen viewer-role token
|
|
214
|
+
* can't elevate to control plane (R13 A1).
|
|
215
|
+
*
|
|
216
|
+
* Trust order:
|
|
217
|
+
* 1. IMHUB_WEB_AUTH=off → admin (operator explicitly disabled auth)
|
|
218
|
+
* 2. Loopback peer → admin (operator on the host)
|
|
219
|
+
* 3. Bearer token → token.role === 'admin'
|
|
220
|
+
* 4. Otherwise → not admin
|
|
221
|
+
*
|
|
222
|
+
* Note: when no token has been created yet (pre-bootstrap), the
|
|
223
|
+
* loopback-peer branch still grants admin so the CLI bootstrap flow
|
|
224
|
+
* works. */
|
|
225
|
+
function isRequestAdmin(req) {
|
|
226
|
+
if ((process.env.IMHUB_WEB_AUTH || '').toLowerCase() === 'off')
|
|
227
|
+
return true;
|
|
228
|
+
if (isLoopbackPeer(req))
|
|
229
|
+
return true;
|
|
230
|
+
let url;
|
|
231
|
+
try {
|
|
232
|
+
url = new URL(req.url || '', 'http://localhost');
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const tok = extractToken(req, url);
|
|
238
|
+
if (!tok)
|
|
239
|
+
return false;
|
|
240
|
+
const id = verifyTokenSync(tok);
|
|
241
|
+
if (!id)
|
|
242
|
+
return false;
|
|
243
|
+
if (!_tokenModule)
|
|
244
|
+
return false;
|
|
245
|
+
// isAdminTokenId returns true for legacy tokens missing the role
|
|
246
|
+
// field (back-compat) so existing deployments stay functional after
|
|
247
|
+
// upgrade — see access-token.ts:getTokenRole.
|
|
248
|
+
return _tokenModule.isAdminTokenId(id);
|
|
249
|
+
}
|
|
250
|
+
/** Send 403 and return false when the actor isn't admin. Use as guard:
|
|
251
|
+
* `if (!requireAdmin(req, res)) return` */
|
|
252
|
+
function requireAdmin(req, res) {
|
|
253
|
+
if (isRequestAdmin(req))
|
|
254
|
+
return true;
|
|
255
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
256
|
+
res.end(JSON.stringify({ error: 'forbidden', message: 'admin role required' }));
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
146
259
|
export function createSerialQueue() {
|
|
147
260
|
let queue = Promise.resolve();
|
|
148
261
|
return (fn) => {
|
|
@@ -187,6 +300,50 @@ export async function startWebServer(options) {
|
|
|
187
300
|
bind: bindHost,
|
|
188
301
|
}, 'IMHUB_WEB_AUTH=off + public bind — auth deliberately off, ensure your reverse proxy handles access control');
|
|
189
302
|
}
|
|
303
|
+
// R13 C2 — when binding to a non-loopback address, the operator
|
|
304
|
+
// almost certainly intends to put a TLS-terminating reverse proxy
|
|
305
|
+
// in front of agim. We can't reliably detect from inside whether
|
|
306
|
+
// that's been done (X-Forwarded-Proto could be forged), so we
|
|
307
|
+
// surface a one-time banner + audit row at boot. Suppress with
|
|
308
|
+
// IMHUB_WEB_TLS_ACK=1 for the operator who's already done the
|
|
309
|
+
// checklist and wants quiet logs.
|
|
310
|
+
if (isPublicBind && process.env.IMHUB_WEB_TLS_ACK !== '1') {
|
|
311
|
+
const banner = [
|
|
312
|
+
'',
|
|
313
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
|
314
|
+
`⚠️ WEB SERVER BOUND TO ${bindHost}:${port} (non-loopback)`,
|
|
315
|
+
'',
|
|
316
|
+
' Tokens travel as Bearer headers / cookies — if any leg between',
|
|
317
|
+
' client and agim is HTTP, they go on the wire in cleartext.',
|
|
318
|
+
' Confirm a TLS-terminating reverse proxy fronts this listener',
|
|
319
|
+
' (nginx, caddy, traefik, k8s ingress, …) before exposing it to',
|
|
320
|
+
' any network you do not control.',
|
|
321
|
+
'',
|
|
322
|
+
' Silence this banner once your terminator is verified:',
|
|
323
|
+
' IMHUB_WEB_TLS_ACK=1',
|
|
324
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
|
325
|
+
'',
|
|
326
|
+
].join('\n');
|
|
327
|
+
process.stdout.write(banner);
|
|
328
|
+
webLog.warn({
|
|
329
|
+
event: 'web.public_bind_no_tls_ack',
|
|
330
|
+
bind: bindHost,
|
|
331
|
+
port,
|
|
332
|
+
}, `non-loopback bind without IMHUB_WEB_TLS_ACK=1 — confirm reverse-proxy TLS termination`);
|
|
333
|
+
try {
|
|
334
|
+
void (async () => {
|
|
335
|
+
const { logAuditEvent } = await import('../core/audit-log.js');
|
|
336
|
+
logAuditEvent({
|
|
337
|
+
eventType: 'config.put',
|
|
338
|
+
actor: 'system',
|
|
339
|
+
target: 'web.bind',
|
|
340
|
+
outcome: 'ok',
|
|
341
|
+
details: { bind: bindHost, port, tls_ack: false },
|
|
342
|
+
});
|
|
343
|
+
})();
|
|
344
|
+
}
|
|
345
|
+
catch { /* best-effort */ }
|
|
346
|
+
}
|
|
190
347
|
webLog.info({
|
|
191
348
|
event: 'web.auth_mode',
|
|
192
349
|
bind: bindHost,
|
|
@@ -200,19 +357,20 @@ export async function startWebServer(options) {
|
|
|
200
357
|
// Loopback peers + the public paths below bypass.
|
|
201
358
|
if (!checkAuth(req, res, url))
|
|
202
359
|
return;
|
|
203
|
-
// v2 SPA fallback —
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// src/web/public/`.
|
|
360
|
+
// v2 SPA fallback — default ON since 1.2.21 (the v2 SPA shell is now
|
|
361
|
+
// the only `index.html` we ship; the v1 monolithic HTML was retired
|
|
362
|
+
// during M1 / R10). This branch serves the SPA chunks under
|
|
363
|
+
// `/assets/*.js`, the favicon / manifest, and falls back to
|
|
364
|
+
// index.html for any client-side route. Without this branch a fresh
|
|
365
|
+
// install hits a white screen — `<script src="/assets/…js">` 404s
|
|
366
|
+
// because no other handler claims that path.
|
|
211
367
|
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
368
|
+
// Operators on a deeply-customised v1 layout can opt out with
|
|
369
|
+
// `IMHUB_WEB_V2=0`; that drops back into the v1 handler block below,
|
|
370
|
+
// which still serves the legacy `tasks.html` / `reminders.html` /
|
|
371
|
+
// `memos.html` / `settings.html` directly. The default-off shape
|
|
372
|
+
// shipped through 1.2.20.
|
|
373
|
+
if (process.env.IMHUB_WEB_V2 !== '0' && req.method === 'GET') {
|
|
216
374
|
const p = url.pathname;
|
|
217
375
|
const isApi = p.startsWith('/api/');
|
|
218
376
|
const isWs = p.startsWith('/ws');
|
|
@@ -472,15 +630,21 @@ export async function startWebServer(options) {
|
|
|
472
630
|
return handleGetConfig(req, res);
|
|
473
631
|
}
|
|
474
632
|
if (url.pathname === '/api/config' && req.method === 'PUT') {
|
|
633
|
+
if (!requireAdmin(req, res))
|
|
634
|
+
return;
|
|
475
635
|
return handlePutConfig(req, res);
|
|
476
636
|
}
|
|
477
637
|
if (url.pathname === '/api/agents/status' && req.method === 'GET') {
|
|
478
638
|
return handleAgentsStatus(req, res);
|
|
479
639
|
}
|
|
480
640
|
if (url.pathname === '/api/agents/acp/test' && req.method === 'POST') {
|
|
641
|
+
if (!requireAdmin(req, res))
|
|
642
|
+
return;
|
|
481
643
|
return handleAcpTest(req, res);
|
|
482
644
|
}
|
|
483
645
|
if (url.pathname === '/api/agents/acp/discover' && req.method === 'POST') {
|
|
646
|
+
if (!requireAdmin(req, res))
|
|
647
|
+
return;
|
|
484
648
|
return handleAcpDiscover(req, res);
|
|
485
649
|
}
|
|
486
650
|
// Jobs
|
|
@@ -550,30 +714,51 @@ export async function startWebServer(options) {
|
|
|
550
714
|
return handleGetEnv(req, res, url);
|
|
551
715
|
}
|
|
552
716
|
if (url.pathname === '/api/env' && req.method === 'PUT') {
|
|
717
|
+
if (!requireAdmin(req, res))
|
|
718
|
+
return;
|
|
553
719
|
return handlePutEnv(req, res);
|
|
554
720
|
}
|
|
555
721
|
if (url.pathname === '/api/messengers/email/test' && req.method === 'POST') {
|
|
722
|
+
if (!requireAdmin(req, res))
|
|
723
|
+
return;
|
|
556
724
|
return handleEmailTest(req, res);
|
|
557
725
|
}
|
|
558
726
|
if (url.pathname === '/api/workspaces' && req.method === 'GET') {
|
|
559
727
|
return handleListWorkspaces(req, res, url);
|
|
560
728
|
}
|
|
561
729
|
if (url.pathname === '/api/workspaces' && req.method === 'POST') {
|
|
730
|
+
if (!requireAdmin(req, res))
|
|
731
|
+
return;
|
|
562
732
|
return handleCreateOrUpdateWorkspace(req, res);
|
|
563
733
|
}
|
|
564
734
|
const workspaceIdMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)$/);
|
|
565
735
|
if (workspaceIdMatch && req.method === 'PATCH') {
|
|
736
|
+
if (!requireAdmin(req, res))
|
|
737
|
+
return;
|
|
566
738
|
return handleCreateOrUpdateWorkspace(req, res, workspaceIdMatch[1]);
|
|
567
739
|
}
|
|
568
740
|
if (workspaceIdMatch && req.method === 'DELETE') {
|
|
741
|
+
if (!requireAdmin(req, res))
|
|
742
|
+
return;
|
|
569
743
|
return handleDeleteWorkspace(req, res, workspaceIdMatch[1]);
|
|
570
744
|
}
|
|
571
745
|
if (url.pathname === '/api/metrics' && req.method === 'GET') {
|
|
572
746
|
return handleMetrics(req, res, url);
|
|
573
747
|
}
|
|
574
748
|
if (url.pathname === '/api/audit' && req.method === 'GET') {
|
|
749
|
+
if (!requireAdmin(req, res))
|
|
750
|
+
return;
|
|
575
751
|
return handleAudit(req, res, url);
|
|
576
752
|
}
|
|
753
|
+
// R12 ⑤ — governance audit events (approvals, admin changes, config /
|
|
754
|
+
// env / token / workspace mutations). Separate retention from
|
|
755
|
+
// invocations (default 180d). Admin-only — these rows can expose
|
|
756
|
+
// who promoted whom and which config keys were rotated when.
|
|
757
|
+
if (url.pathname === '/api/audit/events' && req.method === 'GET') {
|
|
758
|
+
if (!requireAdmin(req, res))
|
|
759
|
+
return;
|
|
760
|
+
return handleAuditEvents(req, res, url);
|
|
761
|
+
}
|
|
577
762
|
// v1.1.2 — Outbox tab. List rows by status, plus aggregate stats and
|
|
578
763
|
// a retry endpoint for the giving_up row state.
|
|
579
764
|
if (url.pathname === '/api/outbox' && req.method === 'GET') {
|
|
@@ -584,6 +769,8 @@ export async function startWebServer(options) {
|
|
|
584
769
|
}
|
|
585
770
|
const outboxRetryMatch = url.pathname.match(/^\/api\/outbox\/(\d+)\/retry$/);
|
|
586
771
|
if (outboxRetryMatch && req.method === 'POST') {
|
|
772
|
+
if (!requireAdmin(req, res))
|
|
773
|
+
return;
|
|
587
774
|
return handleOutboxRetry(req, res, parseInt(outboxRetryMatch[1], 10));
|
|
588
775
|
}
|
|
589
776
|
// v1.1.3 — A2A tab. Stats over inline rows with parent_id; recent
|
|
@@ -649,6 +836,8 @@ export async function startWebServer(options) {
|
|
|
649
836
|
return handleMemoryFacts(req, res, url);
|
|
650
837
|
}
|
|
651
838
|
if (url.pathname === '/api/memory/facts' && req.method === 'DELETE') {
|
|
839
|
+
if (!requireAdmin(req, res))
|
|
840
|
+
return;
|
|
652
841
|
return handleMemoryBulkDelete(req, res, url);
|
|
653
842
|
}
|
|
654
843
|
const memFactIdMatch = url.pathname.match(/^\/api\/memory\/facts\/(\d+)$/);
|
|
@@ -659,9 +848,13 @@ export async function startWebServer(options) {
|
|
|
659
848
|
return handleMemoryPersona(req, res, url);
|
|
660
849
|
}
|
|
661
850
|
if (url.pathname === '/api/memory/persona' && req.method === 'PUT') {
|
|
851
|
+
if (!requireAdmin(req, res))
|
|
852
|
+
return;
|
|
662
853
|
return handleMemoryPersonaPut(req, res, url);
|
|
663
854
|
}
|
|
664
855
|
if (url.pathname === '/api/memory/persona' && req.method === 'DELETE') {
|
|
856
|
+
if (!requireAdmin(req, res))
|
|
857
|
+
return;
|
|
665
858
|
return handleMemoryPersonaDelete(req, res, url);
|
|
666
859
|
}
|
|
667
860
|
if (url.pathname === '/api/memory/export' && req.method === 'GET') {
|
|
@@ -672,20 +865,30 @@ export async function startWebServer(options) {
|
|
|
672
865
|
return handleVectorStatus(req, res, url);
|
|
673
866
|
}
|
|
674
867
|
if (url.pathname === '/api/memory/vector/test' && req.method === 'POST') {
|
|
868
|
+
if (!requireAdmin(req, res))
|
|
869
|
+
return;
|
|
675
870
|
return handleVectorTest(req, res);
|
|
676
871
|
}
|
|
677
872
|
if (url.pathname === '/api/memory/vector/download' && req.method === 'POST') {
|
|
873
|
+
if (!requireAdmin(req, res))
|
|
874
|
+
return;
|
|
678
875
|
return handleVectorDownload(req, res);
|
|
679
876
|
}
|
|
680
877
|
if (url.pathname === '/api/memory/vector/backfill' && req.method === 'POST') {
|
|
878
|
+
if (!requireAdmin(req, res))
|
|
879
|
+
return;
|
|
681
880
|
return handleVectorBackfill(req, res, url);
|
|
682
881
|
}
|
|
683
882
|
if (url.pathname === '/api/memory/vector/clear' && req.method === 'POST') {
|
|
883
|
+
if (!requireAdmin(req, res))
|
|
884
|
+
return;
|
|
684
885
|
return handleVectorClear(req, res, url);
|
|
685
886
|
}
|
|
686
887
|
// v1.2.2 — manual trigger for the daily consolidation. Useful when the
|
|
687
888
|
// user just wants a persona summary now without waiting 24h.
|
|
688
889
|
if (url.pathname === '/api/memory/consolidate' && req.method === 'POST') {
|
|
890
|
+
if (!requireAdmin(req, res))
|
|
891
|
+
return;
|
|
689
892
|
return handleMemoryConsolidate(req, res);
|
|
690
893
|
}
|
|
691
894
|
if (url.pathname === '/api/memory/consolidate/status' && req.method === 'GET') {
|
|
@@ -719,15 +922,21 @@ export async function startWebServer(options) {
|
|
|
719
922
|
return handleWorkspaceFiles(req, res, url);
|
|
720
923
|
}
|
|
721
924
|
if (url.pathname === '/api/workspace-files' && req.method === 'PUT') {
|
|
925
|
+
if (!requireAdmin(req, res))
|
|
926
|
+
return;
|
|
722
927
|
return handleWorkspaceFileWrite(req, res, url);
|
|
723
928
|
}
|
|
724
929
|
// PR-D: Job batch operations. Same semantics as /api/jobs/:id/cancel
|
|
725
930
|
// and /run but accepts an array of ids in one request — saves N
|
|
726
931
|
// round-trips when the user multi-selects a long list.
|
|
727
932
|
if (url.pathname === '/api/jobs/batch-cancel' && req.method === 'POST') {
|
|
933
|
+
if (!requireAdmin(req, res))
|
|
934
|
+
return;
|
|
728
935
|
return handleBatchJob(req, res, 'cancel');
|
|
729
936
|
}
|
|
730
937
|
if (url.pathname === '/api/jobs/batch-run' && req.method === 'POST') {
|
|
938
|
+
if (!requireAdmin(req, res))
|
|
939
|
+
return;
|
|
731
940
|
return handleBatchJob(req, res, 'run', options.defaultAgent);
|
|
732
941
|
}
|
|
733
942
|
// PR-C: SSE event stream — audit / approval / job / metrics events
|
|
@@ -737,9 +946,13 @@ export async function startWebServer(options) {
|
|
|
737
946
|
return handleEventsSSE(req, res);
|
|
738
947
|
}
|
|
739
948
|
if (url.pathname === '/api/notify' && req.method === 'POST') {
|
|
949
|
+
if (!requireAdmin(req, res))
|
|
950
|
+
return;
|
|
740
951
|
return handleNotify(req, res);
|
|
741
952
|
}
|
|
742
953
|
if (url.pathname === '/api/invoke' && req.method === 'POST') {
|
|
954
|
+
if (!requireAdmin(req, res))
|
|
955
|
+
return;
|
|
743
956
|
return handleInvoke(req, res, options.defaultAgent);
|
|
744
957
|
}
|
|
745
958
|
// WeChat QR login — drives the "扫码登录" button in the web settings
|
|
@@ -748,6 +961,8 @@ export async function startWebServer(options) {
|
|
|
748
961
|
// credentials to disk AND adds 'wechat-ilink' into config.messengers
|
|
749
962
|
// so the next service restart picks the channel up.
|
|
750
963
|
if (url.pathname === '/api/messengers/wechat/qr-start' && req.method === 'POST') {
|
|
964
|
+
if (!requireAdmin(req, res))
|
|
965
|
+
return;
|
|
751
966
|
return handleWechatQrStart(res);
|
|
752
967
|
}
|
|
753
968
|
if (url.pathname === '/api/messengers/wechat/qr-status' && req.method === 'GET') {
|
|
@@ -763,12 +978,18 @@ export async function startWebServer(options) {
|
|
|
763
978
|
return handleServiceStatus(res);
|
|
764
979
|
}
|
|
765
980
|
if (url.pathname === '/api/service/start' && req.method === 'POST') {
|
|
981
|
+
if (!requireAdmin(req, res))
|
|
982
|
+
return;
|
|
766
983
|
return handleServiceStart(res);
|
|
767
984
|
}
|
|
768
985
|
if (url.pathname === '/api/service/stop' && req.method === 'POST') {
|
|
986
|
+
if (!requireAdmin(req, res))
|
|
987
|
+
return;
|
|
769
988
|
return handleServiceStop(res);
|
|
770
989
|
}
|
|
771
990
|
if (url.pathname === '/api/service/restart' && req.method === 'POST') {
|
|
991
|
+
if (!requireAdmin(req, res))
|
|
992
|
+
return;
|
|
772
993
|
return handleServiceRestart(res);
|
|
773
994
|
}
|
|
774
995
|
// Admin allowlist — list + add/remove via the Safety card. Locally
|
|
@@ -779,9 +1000,13 @@ export async function startWebServer(options) {
|
|
|
779
1000
|
return handleAdminAllowlistGet(res);
|
|
780
1001
|
}
|
|
781
1002
|
if (url.pathname === '/api/admin-allowlist' && req.method === 'POST') {
|
|
1003
|
+
if (!requireAdmin(req, res))
|
|
1004
|
+
return;
|
|
782
1005
|
return handleAdminAllowlistAdd(req, res, bindHost);
|
|
783
1006
|
}
|
|
784
1007
|
if (url.pathname === '/api/admin-allowlist' && req.method === 'DELETE') {
|
|
1008
|
+
if (!requireAdmin(req, res))
|
|
1009
|
+
return;
|
|
785
1010
|
return handleAdminAllowlistRemove(req, res, bindHost);
|
|
786
1011
|
}
|
|
787
1012
|
res.writeHead(404);
|
|
@@ -796,6 +1021,18 @@ export async function startWebServer(options) {
|
|
|
796
1021
|
const wss = new WebSocketServer({
|
|
797
1022
|
server: httpServer,
|
|
798
1023
|
verifyClient: (info, cb) => {
|
|
1024
|
+
// R13 A4 — per-IP rate limit runs BEFORE auth so an attacker
|
|
1025
|
+
// hammering verifyClient with bad tokens can't burn CPU on
|
|
1026
|
+
// sha256-hashing every attempt.
|
|
1027
|
+
const limit = checkWsIpRateLimit(info.req);
|
|
1028
|
+
if (!limit.ok) {
|
|
1029
|
+
webLog.warn({
|
|
1030
|
+
event: 'ws.rate_limited',
|
|
1031
|
+
ip: peerIp(info.req),
|
|
1032
|
+
reason: limit.reason,
|
|
1033
|
+
}, 'WS upgrade refused (per-IP rate limit)');
|
|
1034
|
+
return cb(false, 429, 'rate limited');
|
|
1035
|
+
}
|
|
799
1036
|
// Auth-off / loopback bypass — mirror checkAuth's two short-circuits
|
|
800
1037
|
// so dev / local CLI sessions still work without a token.
|
|
801
1038
|
if ((process.env.IMHUB_WEB_AUTH || '').toLowerCase() === 'off')
|
|
@@ -846,6 +1083,89 @@ export async function startWebServer(options) {
|
|
|
846
1083
|
}
|
|
847
1084
|
return 100;
|
|
848
1085
|
})();
|
|
1086
|
+
// R13 A4 — per-IP rate limit. The per-token cap above protects
|
|
1087
|
+
// memory but not connection rate; one attacker IP with a valid
|
|
1088
|
+
// token can still spawn N parallel browser tabs / processes and
|
|
1089
|
+
// saturate the file-descriptor pool.
|
|
1090
|
+
//
|
|
1091
|
+
// IMHUB_WS_MAX_PER_IP active connections per IP (default 20)
|
|
1092
|
+
// IMHUB_WS_MAX_NEW_PER_IP_PER_MIN new connections per IP per minute (default 30)
|
|
1093
|
+
//
|
|
1094
|
+
// Loopback bypasses both — local dev / CLI tooling makes many
|
|
1095
|
+
// short connections legitimately.
|
|
1096
|
+
const wsMaxPerIp = (() => {
|
|
1097
|
+
const raw = process.env.IMHUB_WS_MAX_PER_IP;
|
|
1098
|
+
if (raw) {
|
|
1099
|
+
const n = parseInt(raw, 10);
|
|
1100
|
+
if (Number.isFinite(n) && n > 0)
|
|
1101
|
+
return n;
|
|
1102
|
+
}
|
|
1103
|
+
return 20;
|
|
1104
|
+
})();
|
|
1105
|
+
const wsMaxNewPerIpPerMin = (() => {
|
|
1106
|
+
const raw = process.env.IMHUB_WS_MAX_NEW_PER_IP_PER_MIN;
|
|
1107
|
+
if (raw) {
|
|
1108
|
+
const n = parseInt(raw, 10);
|
|
1109
|
+
if (Number.isFinite(n) && n > 0)
|
|
1110
|
+
return n;
|
|
1111
|
+
}
|
|
1112
|
+
return 30;
|
|
1113
|
+
})();
|
|
1114
|
+
/** Per-IP active count + recent connection timestamps for sliding-window
|
|
1115
|
+
* rate limiting. We don't periodically prune unused entries — at
|
|
1116
|
+
* reasonable defaults the map size is bounded by the number of distinct
|
|
1117
|
+
* client IPs in any 60-second window, far below memory concerns. */
|
|
1118
|
+
const wsPerIp = new Map();
|
|
1119
|
+
function peerIp(req) {
|
|
1120
|
+
return (req.socket.remoteAddress || '').replace(/^::ffff:/, '');
|
|
1121
|
+
}
|
|
1122
|
+
/** Returns {ok:true} when the IP may open a new WS, else {ok:false, reason}. */
|
|
1123
|
+
function checkWsIpRateLimit(req) {
|
|
1124
|
+
if (isLoopbackPeer(req))
|
|
1125
|
+
return { ok: true };
|
|
1126
|
+
const ip = peerIp(req);
|
|
1127
|
+
if (!ip)
|
|
1128
|
+
return { ok: false, reason: 'empty peer ip' }; // defensive — matches isLoopbackPeer policy
|
|
1129
|
+
const now = Date.now();
|
|
1130
|
+
const cutoff = now - 60_000;
|
|
1131
|
+
const slot = wsPerIp.get(ip) ?? { active: 0, recent: [] };
|
|
1132
|
+
slot.recent = slot.recent.filter((t) => t > cutoff);
|
|
1133
|
+
if (slot.active >= wsMaxPerIp) {
|
|
1134
|
+
return { ok: false, reason: `per-IP active cap (${slot.active}/${wsMaxPerIp})` };
|
|
1135
|
+
}
|
|
1136
|
+
if (slot.recent.length >= wsMaxNewPerIpPerMin) {
|
|
1137
|
+
return { ok: false, reason: `per-IP new-conn cap (${slot.recent.length}/${wsMaxNewPerIpPerMin}/min)` };
|
|
1138
|
+
}
|
|
1139
|
+
return { ok: true };
|
|
1140
|
+
}
|
|
1141
|
+
function recordWsIpOpen(req) {
|
|
1142
|
+
if (isLoopbackPeer(req))
|
|
1143
|
+
return;
|
|
1144
|
+
const ip = peerIp(req);
|
|
1145
|
+
if (!ip)
|
|
1146
|
+
return;
|
|
1147
|
+
const slot = wsPerIp.get(ip) ?? { active: 0, recent: [] };
|
|
1148
|
+
slot.active++;
|
|
1149
|
+
slot.recent.push(Date.now());
|
|
1150
|
+
wsPerIp.set(ip, slot);
|
|
1151
|
+
}
|
|
1152
|
+
function recordWsIpClose(req) {
|
|
1153
|
+
if (isLoopbackPeer(req))
|
|
1154
|
+
return;
|
|
1155
|
+
const ip = peerIp(req);
|
|
1156
|
+
if (!ip)
|
|
1157
|
+
return;
|
|
1158
|
+
const slot = wsPerIp.get(ip);
|
|
1159
|
+
if (!slot)
|
|
1160
|
+
return;
|
|
1161
|
+
slot.active = Math.max(0, slot.active - 1);
|
|
1162
|
+
// GC empty slots so the map doesn't grow unboundedly across the
|
|
1163
|
+
// lifetime of the process. A slot is "empty" iff no active conn
|
|
1164
|
+
// AND no recent connection within the window.
|
|
1165
|
+
if (slot.active === 0 && slot.recent.filter((t) => t > Date.now() - 60_000).length === 0) {
|
|
1166
|
+
wsPerIp.delete(ip);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
849
1169
|
wss.on('connection', (ws, req) => {
|
|
850
1170
|
if (clients.size >= maxWsClients) {
|
|
851
1171
|
// 1013 = "Try Again Later" per RFC 6455. Slightly nicer than a flat
|
|
@@ -858,6 +1178,9 @@ export async function startWebServer(options) {
|
|
|
858
1178
|
ws.close(1013, 'Server too busy');
|
|
859
1179
|
return;
|
|
860
1180
|
}
|
|
1181
|
+
// R13 A4 — book the per-IP slot now that we know the upgrade
|
|
1182
|
+
// succeeded. The matching close hook decrements below.
|
|
1183
|
+
recordWsIpOpen(req);
|
|
861
1184
|
// R9: token verified at upgrade time by verifyClient; we read the
|
|
862
1185
|
// tokenId here so get-history can enforce ownership when the client
|
|
863
1186
|
// wants to rekey to a persisted threadId. 'anon' is the loopback /
|
|
@@ -978,6 +1301,7 @@ export async function startWebServer(options) {
|
|
|
978
1301
|
const liveId = client.id;
|
|
979
1302
|
webLog.info({ clientId: liveId }, 'Client disconnected');
|
|
980
1303
|
clients.delete(liveId);
|
|
1304
|
+
recordWsIpClose(req); // R13 A4
|
|
981
1305
|
// R9: drop the ownership entry only for server-generated ids that
|
|
982
1306
|
// were never rekeyed to a persistent client-side threadId. Rekeyed
|
|
983
1307
|
// ids stay registered so the operator's next reconnect with the
|
|
@@ -989,6 +1313,7 @@ export async function startWebServer(options) {
|
|
|
989
1313
|
const liveId = client.id;
|
|
990
1314
|
webLog.error({ clientId: liveId, err: err instanceof Error ? err.message : String(err) }, 'Client WebSocket error');
|
|
991
1315
|
clients.delete(liveId);
|
|
1316
|
+
recordWsIpClose(req); // R13 A4
|
|
992
1317
|
if (liveId === clientId)
|
|
993
1318
|
threadOwners.delete(liveId);
|
|
994
1319
|
});
|
|
@@ -1101,6 +1426,19 @@ export async function startWebServer(options) {
|
|
|
1101
1426
|
}
|
|
1102
1427
|
wss.close();
|
|
1103
1428
|
httpServer.close();
|
|
1429
|
+
// R14 — force-drop in-flight HTTP connections so the listening
|
|
1430
|
+
// port releases immediately. httpServer.close() alone only stops
|
|
1431
|
+
// accepting NEW connections; existing keep-alive sockets keep
|
|
1432
|
+
// port 3000 bound until they drain, which races a `systemctl
|
|
1433
|
+
// restart` and produces EADDRINUSE on the new process.
|
|
1434
|
+
// closeAllConnections is Node 18.2+; older runtimes get the
|
|
1435
|
+
// legacy (slower) behavior.
|
|
1436
|
+
try {
|
|
1437
|
+
const fn = httpServer.closeAllConnections;
|
|
1438
|
+
if (typeof fn === 'function')
|
|
1439
|
+
fn.call(httpServer);
|
|
1440
|
+
}
|
|
1441
|
+
catch { /* ignore */ }
|
|
1104
1442
|
},
|
|
1105
1443
|
};
|
|
1106
1444
|
}
|
|
@@ -1111,11 +1449,18 @@ async function handleGetConfig(_req, res) {
|
|
|
1111
1449
|
try {
|
|
1112
1450
|
const config = await loadConfig();
|
|
1113
1451
|
const agentStatus = await getAgentStatuses();
|
|
1452
|
+
// Compliance gates — when the env flag is off, strip the
|
|
1453
|
+
// corresponding keys from the response so the settings UI never
|
|
1454
|
+
// sees them and renders no tabs / cards. On-disk config is left
|
|
1455
|
+
// untouched: flipping the env back on restores visibility.
|
|
1456
|
+
const { isGlobalImEnabled, isRemoteAgentEnabled } = await import('../core/feature-flags.js');
|
|
1457
|
+
const showGlobalIm = isGlobalImEnabled();
|
|
1458
|
+
const showRemoteAgent = isRemoteAgentEnabled();
|
|
1114
1459
|
sendJson(res, 200, {
|
|
1115
1460
|
messengers: config.messengers,
|
|
1116
1461
|
agents: config.agents,
|
|
1117
1462
|
defaultAgent: config.defaultAgent,
|
|
1118
|
-
telegram: config.telegram
|
|
1463
|
+
telegram: showGlobalIm && config.telegram
|
|
1119
1464
|
? { botToken: mask(config.telegram.botToken), channelId: config.telegram.channelId }
|
|
1120
1465
|
: undefined,
|
|
1121
1466
|
feishu: config.feishu
|
|
@@ -1124,7 +1469,7 @@ async function handleGetConfig(_req, res) {
|
|
|
1124
1469
|
dingtalk: config.dingtalk
|
|
1125
1470
|
? { clientId: config.dingtalk.clientId, clientSecret: mask(config.dingtalk.clientSecret), channelId: config.dingtalk.channelId }
|
|
1126
1471
|
: undefined,
|
|
1127
|
-
discord: config.discord
|
|
1472
|
+
discord: showGlobalIm && config.discord
|
|
1128
1473
|
? {
|
|
1129
1474
|
botToken: mask(config.discord.botToken),
|
|
1130
1475
|
channelId: config.discord.channelId,
|
|
@@ -1132,13 +1477,29 @@ async function handleGetConfig(_req, res) {
|
|
|
1132
1477
|
allowedChannels: config.discord.allowedChannels,
|
|
1133
1478
|
}
|
|
1134
1479
|
: undefined,
|
|
1135
|
-
acpAgents:
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1480
|
+
acpAgents: showRemoteAgent
|
|
1481
|
+
? config.acpAgents?.map(a => ({
|
|
1482
|
+
...a,
|
|
1483
|
+
auth: a.auth
|
|
1484
|
+
? { ...a.auth, token: a.auth.token ? mask(a.auth.token) : undefined }
|
|
1485
|
+
: undefined,
|
|
1486
|
+
}))
|
|
1487
|
+
: undefined,
|
|
1488
|
+
// Feature flags surfaced for the SPA so it can hide the
|
|
1489
|
+
// corresponding tabs / nav entries. Field names are
|
|
1490
|
+
// intentionally neutral — they don't reveal which IMs / agent
|
|
1491
|
+
// types the flag controls beyond what the settings page
|
|
1492
|
+
// already exposes.
|
|
1493
|
+
features: {
|
|
1494
|
+
globalIm: showGlobalIm,
|
|
1495
|
+
remoteAgent: showRemoteAgent,
|
|
1496
|
+
},
|
|
1141
1497
|
webPort: config.webPort,
|
|
1498
|
+
// R15 — acpPort travels only when remote-agent feature is on.
|
|
1499
|
+
// Pairs with the SPA hiding the ACP-port input under the same
|
|
1500
|
+
// gate (see web-app/src/routes/settings/service.tsx). When off,
|
|
1501
|
+
// PUT /api/config also drops incoming.acpPort (below).
|
|
1502
|
+
acpPort: showRemoteAgent ? config.acpPort : undefined,
|
|
1142
1503
|
agentStatus,
|
|
1143
1504
|
});
|
|
1144
1505
|
}
|
|
@@ -1151,6 +1512,29 @@ async function handlePutConfig(req, res) {
|
|
|
1151
1512
|
const body = await readBody(req, res);
|
|
1152
1513
|
const incoming = JSON.parse(body);
|
|
1153
1514
|
const existing = await loadConfig();
|
|
1515
|
+
// Compliance gates — silently drop keys the operator's deployment
|
|
1516
|
+
// is not allowed to surface. We don't 4xx here because the SPA may
|
|
1517
|
+
// round-trip a stale snapshot that happens to include these keys;
|
|
1518
|
+
// dropping them keeps existing on-disk values untouched (the
|
|
1519
|
+
// round-trip becomes a no-op for that field).
|
|
1520
|
+
//
|
|
1521
|
+
// We also scrub `messengers[]` so a hidden slot can't be toggled
|
|
1522
|
+
// via direct PUT (the SPA filters in render but a stale draft from
|
|
1523
|
+
// before the env flag was disabled would otherwise re-add the
|
|
1524
|
+
// entry on next save).
|
|
1525
|
+
const { isGlobalImEnabled, isRemoteAgentEnabled } = await import('../core/feature-flags.js');
|
|
1526
|
+
if (!isGlobalImEnabled()) {
|
|
1527
|
+
delete incoming.telegram;
|
|
1528
|
+
delete incoming.discord;
|
|
1529
|
+
if (Array.isArray(incoming.messengers)) {
|
|
1530
|
+
incoming.messengers = incoming.messengers
|
|
1531
|
+
.filter((m) => m !== 'telegram' && m !== 'discord');
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (!isRemoteAgentEnabled()) {
|
|
1535
|
+
delete incoming.acpAgents;
|
|
1536
|
+
delete incoming.acpPort;
|
|
1537
|
+
}
|
|
1154
1538
|
const merged = { ...existing };
|
|
1155
1539
|
for (const key of Object.keys(incoming)) {
|
|
1156
1540
|
const val = incoming[key];
|
|
@@ -1216,6 +1600,20 @@ async function handlePutConfig(req, res) {
|
|
|
1216
1600
|
return;
|
|
1217
1601
|
}
|
|
1218
1602
|
await saveConfig(result.config);
|
|
1603
|
+
// R12 ⑤ — persistent audit. `details.keys` is the list of top-level
|
|
1604
|
+
// config keys touched (e.g. ['feishu', 'discord']) — never the raw
|
|
1605
|
+
// values, which would leak tokens / app secrets through the audit
|
|
1606
|
+
// table.
|
|
1607
|
+
try {
|
|
1608
|
+
const { logAuditEvent } = await import('../core/audit-log.js');
|
|
1609
|
+
logAuditEvent({
|
|
1610
|
+
eventType: 'config.put',
|
|
1611
|
+
actor: getRequestActor(req),
|
|
1612
|
+
outcome: 'ok',
|
|
1613
|
+
details: { keys: Object.keys(incoming) },
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
catch { /* best-effort */ }
|
|
1219
1617
|
sendJson(res, 200, { ok: true });
|
|
1220
1618
|
}
|
|
1221
1619
|
catch (err) {
|
|
@@ -1317,13 +1715,26 @@ async function handleCreateOrUpdateWorkspace(req, res, expectedId) {
|
|
|
1317
1715
|
const { workspaceRegistry } = await import('../core/workspace.js');
|
|
1318
1716
|
workspaceRegistry.add(v.cfg);
|
|
1319
1717
|
await persistWorkspacesToConfig(workspaceRegistry.listFull().filter((w) => w.id !== 'default'));
|
|
1718
|
+
// R12 ⑤ — persistent audit. Only id + agent count go into details;
|
|
1719
|
+
// member list (potentially identifiers) stays out of the audit row.
|
|
1720
|
+
try {
|
|
1721
|
+
const { logAuditEvent } = await import('../core/audit-log.js');
|
|
1722
|
+
logAuditEvent({
|
|
1723
|
+
eventType: 'workspace.add',
|
|
1724
|
+
actor: getRequestActor(req),
|
|
1725
|
+
target: v.cfg.id,
|
|
1726
|
+
outcome: 'ok',
|
|
1727
|
+
details: { agents: v.cfg.agents.length, members: v.cfg.members?.length ?? 0 },
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
catch { /* best-effort */ }
|
|
1320
1731
|
sendJson(res, 200, { ok: true, workspace: v.cfg });
|
|
1321
1732
|
}
|
|
1322
1733
|
catch (err) {
|
|
1323
1734
|
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
1324
1735
|
}
|
|
1325
1736
|
}
|
|
1326
|
-
async function handleDeleteWorkspace(
|
|
1737
|
+
async function handleDeleteWorkspace(req, res, id) {
|
|
1327
1738
|
try {
|
|
1328
1739
|
const { workspaceRegistry } = await import('../core/workspace.js');
|
|
1329
1740
|
if (id === 'default') {
|
|
@@ -1336,6 +1747,16 @@ async function handleDeleteWorkspace(_req, res, id) {
|
|
|
1336
1747
|
return;
|
|
1337
1748
|
}
|
|
1338
1749
|
await persistWorkspacesToConfig(workspaceRegistry.listFull().filter((w) => w.id !== 'default'));
|
|
1750
|
+
try {
|
|
1751
|
+
const { logAuditEvent } = await import('../core/audit-log.js');
|
|
1752
|
+
logAuditEvent({
|
|
1753
|
+
eventType: 'workspace.delete',
|
|
1754
|
+
actor: getRequestActor(req),
|
|
1755
|
+
target: id,
|
|
1756
|
+
outcome: 'ok',
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
catch { /* best-effort */ }
|
|
1339
1760
|
sendJson(res, 200, { ok: true });
|
|
1340
1761
|
}
|
|
1341
1762
|
catch (err) {
|
|
@@ -1456,8 +1877,54 @@ async function handleServiceStatus(res) {
|
|
|
1456
1877
|
}
|
|
1457
1878
|
async function handleServiceStart(res) {
|
|
1458
1879
|
try {
|
|
1459
|
-
const { detectService, spawnBackground } = await import('../cli-ui/service.js');
|
|
1880
|
+
const { detectService, spawnBackground, reapStrayAgimProcesses } = await import('../cli-ui/service.js');
|
|
1460
1881
|
const st = detectService();
|
|
1882
|
+
// R14 §四 — when systemd owns the unit, ALL lifecycle commands
|
|
1883
|
+
// must route through systemctl so systemd stays the single
|
|
1884
|
+
// authority. Previously this handler bypassed systemd entirely
|
|
1885
|
+
// (always spawnBackground), creating a second instance
|
|
1886
|
+
// competing for port 3000. Now:
|
|
1887
|
+
// - systemd active → no-op, report alreadyRunning
|
|
1888
|
+
// - systemd inactive/failed/activating → systemctl start
|
|
1889
|
+
// (preceded by reap so any stray pids from a botched
|
|
1890
|
+
// previous run get cleared first; same cgroup-escape
|
|
1891
|
+
// recovery as restart).
|
|
1892
|
+
if (st.mode === 'systemd') {
|
|
1893
|
+
if (st.active === 'active') {
|
|
1894
|
+
sendJson(res, 200, { ok: true, alreadyRunning: true, mode: 'systemd', pid: st.pid, active: st.active });
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
try {
|
|
1898
|
+
const reap = await reapStrayAgimProcesses({
|
|
1899
|
+
excludePids: st.pid != null ? [st.pid] : [],
|
|
1900
|
+
sigtermTimeoutMs: 5_000,
|
|
1901
|
+
});
|
|
1902
|
+
if (reap.reaped.length > 0 || reap.survived.length > 0) {
|
|
1903
|
+
webLog.warn({
|
|
1904
|
+
event: 'web.service.start.reap',
|
|
1905
|
+
reaped: reap.reaped,
|
|
1906
|
+
survived: reap.survived,
|
|
1907
|
+
}, `R14 reap before systemctl start: killed ${reap.reaped.length} stray pid(s)`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
catch (err) {
|
|
1911
|
+
webLog.warn({ event: 'web.service.start.reap_failed', err: String(err) });
|
|
1912
|
+
}
|
|
1913
|
+
try {
|
|
1914
|
+
const { spawn } = await import('node:child_process');
|
|
1915
|
+
const unitName = existsSync('/etc/systemd/system/agim.service') ? 'agim.service' : 'im-hub.service';
|
|
1916
|
+
// Detached + fire-and-forget so the HTTP response isn't held
|
|
1917
|
+
// for the full systemd activation timeline.
|
|
1918
|
+
const child = spawn('systemctl', ['start', unitName], { detached: true, stdio: 'ignore' });
|
|
1919
|
+
child.unref();
|
|
1920
|
+
sendJson(res, 200, { ok: true, mode: 'systemd', starting: true, previousActive: st.active });
|
|
1921
|
+
}
|
|
1922
|
+
catch (err) {
|
|
1923
|
+
sendJson(res, 500, { error: 'systemctl start spawn failed: ' + (err instanceof Error ? err.message : String(err)) });
|
|
1924
|
+
}
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
// Non-systemd: existing behavior.
|
|
1461
1928
|
if (st.mode !== 'none') {
|
|
1462
1929
|
sendJson(res, 200, { ok: true, alreadyRunning: true, mode: st.mode, pid: st.pid });
|
|
1463
1930
|
return;
|
|
@@ -1473,7 +1940,7 @@ async function handleServiceStart(res) {
|
|
|
1473
1940
|
}
|
|
1474
1941
|
}
|
|
1475
1942
|
async function handleServiceStop(res) {
|
|
1476
|
-
const { detectService } = await import('../cli-ui/service.js');
|
|
1943
|
+
const { detectService, reapStrayAgimProcesses } = await import('../cli-ui/service.js');
|
|
1477
1944
|
const st = detectService();
|
|
1478
1945
|
if (st.mode === 'systemd') {
|
|
1479
1946
|
// systemctl stop handles the kill for us — no self-exit needed.
|
|
@@ -1482,6 +1949,24 @@ async function handleServiceStop(res) {
|
|
|
1482
1949
|
// Match service.ts's detection: prefer agim.service, fall back.
|
|
1483
1950
|
const unitName = existsSync('/etc/systemd/system/agim.service') ? 'agim.service' : 'im-hub.service';
|
|
1484
1951
|
execSync(`systemctl stop ${unitName}`);
|
|
1952
|
+
// R14 §四 — sweep stray pids after systemctl stop, same way
|
|
1953
|
+
// restart does pre-stop. systemctl reports success when its
|
|
1954
|
+
// cgroup-kill returns, even if a child escaped the control
|
|
1955
|
+
// group (the dbus-run-session bug). We finish the job here so
|
|
1956
|
+
// a subsequent web Start doesn't EADDRINUSE-loop. Exclude
|
|
1957
|
+
// process.pid via reap's built-in self-exclusion — we don't
|
|
1958
|
+
// want to kill the very process answering this HTTP request.
|
|
1959
|
+
try {
|
|
1960
|
+
const reap = await reapStrayAgimProcesses({ sigtermTimeoutMs: 5_000 });
|
|
1961
|
+
if (reap.reaped.length > 0 || reap.survived.length > 0) {
|
|
1962
|
+
webLog.warn({
|
|
1963
|
+
event: 'web.service.stop.reap',
|
|
1964
|
+
reaped: reap.reaped,
|
|
1965
|
+
survived: reap.survived,
|
|
1966
|
+
}, `R14 reap after systemctl stop: killed ${reap.reaped.length} stray pid(s)`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
catch { /* best-effort */ }
|
|
1485
1970
|
sendJson(res, 200, { ok: true, mode: 'systemd' });
|
|
1486
1971
|
}
|
|
1487
1972
|
catch (err) {
|
|
@@ -1501,9 +1986,48 @@ async function handleServiceStop(res) {
|
|
|
1501
1986
|
}, 200);
|
|
1502
1987
|
}
|
|
1503
1988
|
async function handleServiceRestart(res) {
|
|
1504
|
-
const { detectService } = await import('../cli-ui/service.js');
|
|
1989
|
+
const { detectService, reapStrayAgimProcesses } = await import('../cli-ui/service.js');
|
|
1505
1990
|
const st = detectService();
|
|
1506
1991
|
if (st.mode === 'systemd') {
|
|
1992
|
+
// R14 — reap stray agim processes BEFORE systemctl restart. The
|
|
1993
|
+
// motivating bug: a dbus-run-session wrapper in keyring.conf
|
|
1994
|
+
// detached its children from systemd's cgroup, so old agim
|
|
1995
|
+
// outlived `systemctl stop` and the new instance hit EADDRINUSE
|
|
1996
|
+
// on port 3000. We can't fix the unit file from here, but we can
|
|
1997
|
+
// make web restart self-healing by sweeping pids that pgrep can
|
|
1998
|
+
// still see. excludePids = the current MainPID (systemd will
|
|
1999
|
+
// handle that one) and our own pid (this very web request is
|
|
2000
|
+
// running inside agim).
|
|
2001
|
+
try {
|
|
2002
|
+
const result = await reapStrayAgimProcesses({
|
|
2003
|
+
excludePids: st.pid != null ? [st.pid] : [],
|
|
2004
|
+
sigtermTimeoutMs: 5_000,
|
|
2005
|
+
});
|
|
2006
|
+
if (result.reaped.length > 0 || result.survived.length > 0) {
|
|
2007
|
+
webLog.warn({
|
|
2008
|
+
event: 'web.service.restart.reap',
|
|
2009
|
+
reaped: result.reaped,
|
|
2010
|
+
survived: result.survived,
|
|
2011
|
+
}, `R14 reap: killed ${result.reaped.length} stray agim pid(s), ${result.survived.length} survived to SIGKILL`);
|
|
2012
|
+
try {
|
|
2013
|
+
const { logAuditEvent } = await import('../core/audit-log.js');
|
|
2014
|
+
logAuditEvent({
|
|
2015
|
+
eventType: 'config.put',
|
|
2016
|
+
actor: 'system',
|
|
2017
|
+
target: 'service.restart.reap',
|
|
2018
|
+
outcome: 'ok',
|
|
2019
|
+
details: { reaped: result.reaped.length, survived: result.survived.length, mainPid: st.pid },
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
catch { /* best-effort */ }
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
catch (err) {
|
|
2026
|
+
// Reap is best-effort — log + continue to systemctl. If reap
|
|
2027
|
+
// failed because pgrep isn't available, systemctl restart will
|
|
2028
|
+
// still try (and may succeed if the cgroup is OK).
|
|
2029
|
+
webLog.warn({ event: 'web.service.restart.reap_failed', err: String(err) });
|
|
2030
|
+
}
|
|
1507
2031
|
try {
|
|
1508
2032
|
const { spawn } = await import('node:child_process');
|
|
1509
2033
|
const unitName = existsSync('/etc/systemd/system/agim.service') ? 'agim.service' : 'im-hub.service';
|
|
@@ -2115,6 +2639,18 @@ async function handlePutEnv(req, res) {
|
|
|
2115
2639
|
else
|
|
2116
2640
|
process.env[k] = v;
|
|
2117
2641
|
}
|
|
2642
|
+
// R12 ⑤ — persistent audit. Track WHICH keys were touched, never the
|
|
2643
|
+
// values themselves (env can hold SMTP_PASS, BAIDU_AK, etc.).
|
|
2644
|
+
try {
|
|
2645
|
+
const { logAuditEvent } = await import('../core/audit-log.js');
|
|
2646
|
+
logAuditEvent({
|
|
2647
|
+
eventType: 'env.put',
|
|
2648
|
+
actor: getRequestActor(req),
|
|
2649
|
+
outcome: 'ok',
|
|
2650
|
+
details: { keys: Object.keys(safe) },
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
catch { /* best-effort */ }
|
|
2118
2654
|
sendJson(res, 200, { ok: true, updated: Object.keys(safe) });
|
|
2119
2655
|
}
|
|
2120
2656
|
catch (err) {
|
|
@@ -2264,6 +2800,36 @@ async function handleAudit(_req, res, url) {
|
|
|
2264
2800
|
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2265
2801
|
}
|
|
2266
2802
|
}
|
|
2803
|
+
/** R12 ⑤ — GET /api/audit/events. Queryable by type, actor, days, limit.
|
|
2804
|
+
* Returns the JSON-encoded `details` field as a string; the client can
|
|
2805
|
+
* JSON.parse on demand (avoids accidental schema coupling). */
|
|
2806
|
+
async function handleAuditEvents(_req, res, url) {
|
|
2807
|
+
try {
|
|
2808
|
+
const { queryAuditEvents } = await import('../core/audit-log.js');
|
|
2809
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') || '100', 10) || 100, 1), 1000);
|
|
2810
|
+
const days = parseInt(url.searchParams.get('days') || '30', 10) || 30;
|
|
2811
|
+
const actor = url.searchParams.get('actor') || undefined;
|
|
2812
|
+
const typesParam = url.searchParams.get('types');
|
|
2813
|
+
// Accept comma-separated event types; reject anything not in the
|
|
2814
|
+
// known union (defensive against open-ended filters that miss the
|
|
2815
|
+
// SQL prepared-statement param check).
|
|
2816
|
+
const KNOWN_TYPES = [
|
|
2817
|
+
'approval.allow', 'approval.deny',
|
|
2818
|
+
'admin.elevate', 'admin.revoke',
|
|
2819
|
+
'config.put', 'env.put',
|
|
2820
|
+
'token.create', 'token.revoke',
|
|
2821
|
+
'workspace.add', 'workspace.delete',
|
|
2822
|
+
];
|
|
2823
|
+
const types = typesParam
|
|
2824
|
+
? typesParam.split(',').map((s) => s.trim()).filter((s) => KNOWN_TYPES.includes(s))
|
|
2825
|
+
: undefined;
|
|
2826
|
+
const events = queryAuditEvents({ limit, days, actor, types });
|
|
2827
|
+
sendJson(res, 200, { events });
|
|
2828
|
+
}
|
|
2829
|
+
catch (err) {
|
|
2830
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2267
2833
|
/**
|
|
2268
2834
|
* Per-agent operational health snapshot. Drives the Health tab in /tasks.
|
|
2269
2835
|
*
|
|
@@ -2794,9 +3360,28 @@ async function handleSkillsList(_req, res) {
|
|
|
2794
3360
|
// skillhub.cn proxy with 5-min cache — shows top-50 popular skills so the
|
|
2795
3361
|
// user can discover new ones without leaving the dashboard. The actual
|
|
2796
3362
|
// install still happens via the skillhub CLI on the host (see docs).
|
|
3363
|
+
//
|
|
3364
|
+
// Enterprise / air-gapped deployments can disable this endpoint via
|
|
3365
|
+
// `IMHUB_SKILLHUB_ENABLED=0`. When disabled, the API returns a stub
|
|
3366
|
+
// shape `{ disabled: true, items: [] }` so the UI can show "disabled
|
|
3367
|
+
// by enterprise policy" without breaking the page. This is the only
|
|
3368
|
+
// product-default outbound call to a non-IM domain, so the toggle
|
|
3369
|
+
// matters for organisations doing tcpdump-level egress audits.
|
|
2797
3370
|
let remoteHotCache = null;
|
|
2798
3371
|
const REMOTE_HOT_TTL_MS = 5 * 60_000;
|
|
3372
|
+
function isSkillhubEnabled() {
|
|
3373
|
+
const v = (process.env.IMHUB_SKILLHUB_ENABLED ?? '1').trim().toLowerCase();
|
|
3374
|
+
return v !== '0' && v !== 'false' && v !== 'no' && v !== 'off';
|
|
3375
|
+
}
|
|
2799
3376
|
async function handleSkillsRemoteHot(_req, res) {
|
|
3377
|
+
if (!isSkillhubEnabled()) {
|
|
3378
|
+
// Disabled by enterprise policy. Return an empty-but-valid shape
|
|
3379
|
+
// so the SPA can render "disabled" copy without a network error
|
|
3380
|
+
// toast. Status 200 because this is a deliberate operator choice,
|
|
3381
|
+
// not a transient failure.
|
|
3382
|
+
sendJson(res, 200, { disabled: true, items: [], reason: 'IMHUB_SKILLHUB_ENABLED=0' });
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
2800
3385
|
try {
|
|
2801
3386
|
if (remoteHotCache && Date.now() - remoteHotCache.fetchedAt < REMOTE_HOT_TTL_MS) {
|
|
2802
3387
|
sendJson(res, 200, { ...remoteHotCache.data, cached: true, fetchedAt: remoteHotCache.fetchedAt });
|
|
@@ -4027,7 +4612,7 @@ threadOwners) {
|
|
|
4027
4612
|
logger,
|
|
4028
4613
|
userId: `web:${clientId}`,
|
|
4029
4614
|
};
|
|
4030
|
-
logger.info({ event: 'message.received', text
|
|
4615
|
+
logger.info({ event: 'message.received', ...sanitizeUserText(text) });
|
|
4031
4616
|
const result = await routeMessage(parsed, routeCtx);
|
|
4032
4617
|
// String response (built-in commands, errors)
|
|
4033
4618
|
if (typeof result === 'string') {
|