agim-cli 1.2.22 → 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 +151 -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 +600 -16
- 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,
|
|
@@ -473,15 +630,21 @@ export async function startWebServer(options) {
|
|
|
473
630
|
return handleGetConfig(req, res);
|
|
474
631
|
}
|
|
475
632
|
if (url.pathname === '/api/config' && req.method === 'PUT') {
|
|
633
|
+
if (!requireAdmin(req, res))
|
|
634
|
+
return;
|
|
476
635
|
return handlePutConfig(req, res);
|
|
477
636
|
}
|
|
478
637
|
if (url.pathname === '/api/agents/status' && req.method === 'GET') {
|
|
479
638
|
return handleAgentsStatus(req, res);
|
|
480
639
|
}
|
|
481
640
|
if (url.pathname === '/api/agents/acp/test' && req.method === 'POST') {
|
|
641
|
+
if (!requireAdmin(req, res))
|
|
642
|
+
return;
|
|
482
643
|
return handleAcpTest(req, res);
|
|
483
644
|
}
|
|
484
645
|
if (url.pathname === '/api/agents/acp/discover' && req.method === 'POST') {
|
|
646
|
+
if (!requireAdmin(req, res))
|
|
647
|
+
return;
|
|
485
648
|
return handleAcpDiscover(req, res);
|
|
486
649
|
}
|
|
487
650
|
// Jobs
|
|
@@ -551,30 +714,51 @@ export async function startWebServer(options) {
|
|
|
551
714
|
return handleGetEnv(req, res, url);
|
|
552
715
|
}
|
|
553
716
|
if (url.pathname === '/api/env' && req.method === 'PUT') {
|
|
717
|
+
if (!requireAdmin(req, res))
|
|
718
|
+
return;
|
|
554
719
|
return handlePutEnv(req, res);
|
|
555
720
|
}
|
|
556
721
|
if (url.pathname === '/api/messengers/email/test' && req.method === 'POST') {
|
|
722
|
+
if (!requireAdmin(req, res))
|
|
723
|
+
return;
|
|
557
724
|
return handleEmailTest(req, res);
|
|
558
725
|
}
|
|
559
726
|
if (url.pathname === '/api/workspaces' && req.method === 'GET') {
|
|
560
727
|
return handleListWorkspaces(req, res, url);
|
|
561
728
|
}
|
|
562
729
|
if (url.pathname === '/api/workspaces' && req.method === 'POST') {
|
|
730
|
+
if (!requireAdmin(req, res))
|
|
731
|
+
return;
|
|
563
732
|
return handleCreateOrUpdateWorkspace(req, res);
|
|
564
733
|
}
|
|
565
734
|
const workspaceIdMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)$/);
|
|
566
735
|
if (workspaceIdMatch && req.method === 'PATCH') {
|
|
736
|
+
if (!requireAdmin(req, res))
|
|
737
|
+
return;
|
|
567
738
|
return handleCreateOrUpdateWorkspace(req, res, workspaceIdMatch[1]);
|
|
568
739
|
}
|
|
569
740
|
if (workspaceIdMatch && req.method === 'DELETE') {
|
|
741
|
+
if (!requireAdmin(req, res))
|
|
742
|
+
return;
|
|
570
743
|
return handleDeleteWorkspace(req, res, workspaceIdMatch[1]);
|
|
571
744
|
}
|
|
572
745
|
if (url.pathname === '/api/metrics' && req.method === 'GET') {
|
|
573
746
|
return handleMetrics(req, res, url);
|
|
574
747
|
}
|
|
575
748
|
if (url.pathname === '/api/audit' && req.method === 'GET') {
|
|
749
|
+
if (!requireAdmin(req, res))
|
|
750
|
+
return;
|
|
576
751
|
return handleAudit(req, res, url);
|
|
577
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
|
+
}
|
|
578
762
|
// v1.1.2 — Outbox tab. List rows by status, plus aggregate stats and
|
|
579
763
|
// a retry endpoint for the giving_up row state.
|
|
580
764
|
if (url.pathname === '/api/outbox' && req.method === 'GET') {
|
|
@@ -585,6 +769,8 @@ export async function startWebServer(options) {
|
|
|
585
769
|
}
|
|
586
770
|
const outboxRetryMatch = url.pathname.match(/^\/api\/outbox\/(\d+)\/retry$/);
|
|
587
771
|
if (outboxRetryMatch && req.method === 'POST') {
|
|
772
|
+
if (!requireAdmin(req, res))
|
|
773
|
+
return;
|
|
588
774
|
return handleOutboxRetry(req, res, parseInt(outboxRetryMatch[1], 10));
|
|
589
775
|
}
|
|
590
776
|
// v1.1.3 — A2A tab. Stats over inline rows with parent_id; recent
|
|
@@ -650,6 +836,8 @@ export async function startWebServer(options) {
|
|
|
650
836
|
return handleMemoryFacts(req, res, url);
|
|
651
837
|
}
|
|
652
838
|
if (url.pathname === '/api/memory/facts' && req.method === 'DELETE') {
|
|
839
|
+
if (!requireAdmin(req, res))
|
|
840
|
+
return;
|
|
653
841
|
return handleMemoryBulkDelete(req, res, url);
|
|
654
842
|
}
|
|
655
843
|
const memFactIdMatch = url.pathname.match(/^\/api\/memory\/facts\/(\d+)$/);
|
|
@@ -660,9 +848,13 @@ export async function startWebServer(options) {
|
|
|
660
848
|
return handleMemoryPersona(req, res, url);
|
|
661
849
|
}
|
|
662
850
|
if (url.pathname === '/api/memory/persona' && req.method === 'PUT') {
|
|
851
|
+
if (!requireAdmin(req, res))
|
|
852
|
+
return;
|
|
663
853
|
return handleMemoryPersonaPut(req, res, url);
|
|
664
854
|
}
|
|
665
855
|
if (url.pathname === '/api/memory/persona' && req.method === 'DELETE') {
|
|
856
|
+
if (!requireAdmin(req, res))
|
|
857
|
+
return;
|
|
666
858
|
return handleMemoryPersonaDelete(req, res, url);
|
|
667
859
|
}
|
|
668
860
|
if (url.pathname === '/api/memory/export' && req.method === 'GET') {
|
|
@@ -673,20 +865,30 @@ export async function startWebServer(options) {
|
|
|
673
865
|
return handleVectorStatus(req, res, url);
|
|
674
866
|
}
|
|
675
867
|
if (url.pathname === '/api/memory/vector/test' && req.method === 'POST') {
|
|
868
|
+
if (!requireAdmin(req, res))
|
|
869
|
+
return;
|
|
676
870
|
return handleVectorTest(req, res);
|
|
677
871
|
}
|
|
678
872
|
if (url.pathname === '/api/memory/vector/download' && req.method === 'POST') {
|
|
873
|
+
if (!requireAdmin(req, res))
|
|
874
|
+
return;
|
|
679
875
|
return handleVectorDownload(req, res);
|
|
680
876
|
}
|
|
681
877
|
if (url.pathname === '/api/memory/vector/backfill' && req.method === 'POST') {
|
|
878
|
+
if (!requireAdmin(req, res))
|
|
879
|
+
return;
|
|
682
880
|
return handleVectorBackfill(req, res, url);
|
|
683
881
|
}
|
|
684
882
|
if (url.pathname === '/api/memory/vector/clear' && req.method === 'POST') {
|
|
883
|
+
if (!requireAdmin(req, res))
|
|
884
|
+
return;
|
|
685
885
|
return handleVectorClear(req, res, url);
|
|
686
886
|
}
|
|
687
887
|
// v1.2.2 — manual trigger for the daily consolidation. Useful when the
|
|
688
888
|
// user just wants a persona summary now without waiting 24h.
|
|
689
889
|
if (url.pathname === '/api/memory/consolidate' && req.method === 'POST') {
|
|
890
|
+
if (!requireAdmin(req, res))
|
|
891
|
+
return;
|
|
690
892
|
return handleMemoryConsolidate(req, res);
|
|
691
893
|
}
|
|
692
894
|
if (url.pathname === '/api/memory/consolidate/status' && req.method === 'GET') {
|
|
@@ -720,15 +922,21 @@ export async function startWebServer(options) {
|
|
|
720
922
|
return handleWorkspaceFiles(req, res, url);
|
|
721
923
|
}
|
|
722
924
|
if (url.pathname === '/api/workspace-files' && req.method === 'PUT') {
|
|
925
|
+
if (!requireAdmin(req, res))
|
|
926
|
+
return;
|
|
723
927
|
return handleWorkspaceFileWrite(req, res, url);
|
|
724
928
|
}
|
|
725
929
|
// PR-D: Job batch operations. Same semantics as /api/jobs/:id/cancel
|
|
726
930
|
// and /run but accepts an array of ids in one request — saves N
|
|
727
931
|
// round-trips when the user multi-selects a long list.
|
|
728
932
|
if (url.pathname === '/api/jobs/batch-cancel' && req.method === 'POST') {
|
|
933
|
+
if (!requireAdmin(req, res))
|
|
934
|
+
return;
|
|
729
935
|
return handleBatchJob(req, res, 'cancel');
|
|
730
936
|
}
|
|
731
937
|
if (url.pathname === '/api/jobs/batch-run' && req.method === 'POST') {
|
|
938
|
+
if (!requireAdmin(req, res))
|
|
939
|
+
return;
|
|
732
940
|
return handleBatchJob(req, res, 'run', options.defaultAgent);
|
|
733
941
|
}
|
|
734
942
|
// PR-C: SSE event stream — audit / approval / job / metrics events
|
|
@@ -738,9 +946,13 @@ export async function startWebServer(options) {
|
|
|
738
946
|
return handleEventsSSE(req, res);
|
|
739
947
|
}
|
|
740
948
|
if (url.pathname === '/api/notify' && req.method === 'POST') {
|
|
949
|
+
if (!requireAdmin(req, res))
|
|
950
|
+
return;
|
|
741
951
|
return handleNotify(req, res);
|
|
742
952
|
}
|
|
743
953
|
if (url.pathname === '/api/invoke' && req.method === 'POST') {
|
|
954
|
+
if (!requireAdmin(req, res))
|
|
955
|
+
return;
|
|
744
956
|
return handleInvoke(req, res, options.defaultAgent);
|
|
745
957
|
}
|
|
746
958
|
// WeChat QR login — drives the "扫码登录" button in the web settings
|
|
@@ -749,6 +961,8 @@ export async function startWebServer(options) {
|
|
|
749
961
|
// credentials to disk AND adds 'wechat-ilink' into config.messengers
|
|
750
962
|
// so the next service restart picks the channel up.
|
|
751
963
|
if (url.pathname === '/api/messengers/wechat/qr-start' && req.method === 'POST') {
|
|
964
|
+
if (!requireAdmin(req, res))
|
|
965
|
+
return;
|
|
752
966
|
return handleWechatQrStart(res);
|
|
753
967
|
}
|
|
754
968
|
if (url.pathname === '/api/messengers/wechat/qr-status' && req.method === 'GET') {
|
|
@@ -764,12 +978,18 @@ export async function startWebServer(options) {
|
|
|
764
978
|
return handleServiceStatus(res);
|
|
765
979
|
}
|
|
766
980
|
if (url.pathname === '/api/service/start' && req.method === 'POST') {
|
|
981
|
+
if (!requireAdmin(req, res))
|
|
982
|
+
return;
|
|
767
983
|
return handleServiceStart(res);
|
|
768
984
|
}
|
|
769
985
|
if (url.pathname === '/api/service/stop' && req.method === 'POST') {
|
|
986
|
+
if (!requireAdmin(req, res))
|
|
987
|
+
return;
|
|
770
988
|
return handleServiceStop(res);
|
|
771
989
|
}
|
|
772
990
|
if (url.pathname === '/api/service/restart' && req.method === 'POST') {
|
|
991
|
+
if (!requireAdmin(req, res))
|
|
992
|
+
return;
|
|
773
993
|
return handleServiceRestart(res);
|
|
774
994
|
}
|
|
775
995
|
// Admin allowlist — list + add/remove via the Safety card. Locally
|
|
@@ -780,9 +1000,13 @@ export async function startWebServer(options) {
|
|
|
780
1000
|
return handleAdminAllowlistGet(res);
|
|
781
1001
|
}
|
|
782
1002
|
if (url.pathname === '/api/admin-allowlist' && req.method === 'POST') {
|
|
1003
|
+
if (!requireAdmin(req, res))
|
|
1004
|
+
return;
|
|
783
1005
|
return handleAdminAllowlistAdd(req, res, bindHost);
|
|
784
1006
|
}
|
|
785
1007
|
if (url.pathname === '/api/admin-allowlist' && req.method === 'DELETE') {
|
|
1008
|
+
if (!requireAdmin(req, res))
|
|
1009
|
+
return;
|
|
786
1010
|
return handleAdminAllowlistRemove(req, res, bindHost);
|
|
787
1011
|
}
|
|
788
1012
|
res.writeHead(404);
|
|
@@ -797,6 +1021,18 @@ export async function startWebServer(options) {
|
|
|
797
1021
|
const wss = new WebSocketServer({
|
|
798
1022
|
server: httpServer,
|
|
799
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
|
+
}
|
|
800
1036
|
// Auth-off / loopback bypass — mirror checkAuth's two short-circuits
|
|
801
1037
|
// so dev / local CLI sessions still work without a token.
|
|
802
1038
|
if ((process.env.IMHUB_WEB_AUTH || '').toLowerCase() === 'off')
|
|
@@ -847,6 +1083,89 @@ export async function startWebServer(options) {
|
|
|
847
1083
|
}
|
|
848
1084
|
return 100;
|
|
849
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
|
+
}
|
|
850
1169
|
wss.on('connection', (ws, req) => {
|
|
851
1170
|
if (clients.size >= maxWsClients) {
|
|
852
1171
|
// 1013 = "Try Again Later" per RFC 6455. Slightly nicer than a flat
|
|
@@ -859,6 +1178,9 @@ export async function startWebServer(options) {
|
|
|
859
1178
|
ws.close(1013, 'Server too busy');
|
|
860
1179
|
return;
|
|
861
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);
|
|
862
1184
|
// R9: token verified at upgrade time by verifyClient; we read the
|
|
863
1185
|
// tokenId here so get-history can enforce ownership when the client
|
|
864
1186
|
// wants to rekey to a persisted threadId. 'anon' is the loopback /
|
|
@@ -979,6 +1301,7 @@ export async function startWebServer(options) {
|
|
|
979
1301
|
const liveId = client.id;
|
|
980
1302
|
webLog.info({ clientId: liveId }, 'Client disconnected');
|
|
981
1303
|
clients.delete(liveId);
|
|
1304
|
+
recordWsIpClose(req); // R13 A4
|
|
982
1305
|
// R9: drop the ownership entry only for server-generated ids that
|
|
983
1306
|
// were never rekeyed to a persistent client-side threadId. Rekeyed
|
|
984
1307
|
// ids stay registered so the operator's next reconnect with the
|
|
@@ -990,6 +1313,7 @@ export async function startWebServer(options) {
|
|
|
990
1313
|
const liveId = client.id;
|
|
991
1314
|
webLog.error({ clientId: liveId, err: err instanceof Error ? err.message : String(err) }, 'Client WebSocket error');
|
|
992
1315
|
clients.delete(liveId);
|
|
1316
|
+
recordWsIpClose(req); // R13 A4
|
|
993
1317
|
if (liveId === clientId)
|
|
994
1318
|
threadOwners.delete(liveId);
|
|
995
1319
|
});
|
|
@@ -1102,6 +1426,19 @@ export async function startWebServer(options) {
|
|
|
1102
1426
|
}
|
|
1103
1427
|
wss.close();
|
|
1104
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 */ }
|
|
1105
1442
|
},
|
|
1106
1443
|
};
|
|
1107
1444
|
}
|
|
@@ -1112,11 +1449,18 @@ async function handleGetConfig(_req, res) {
|
|
|
1112
1449
|
try {
|
|
1113
1450
|
const config = await loadConfig();
|
|
1114
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();
|
|
1115
1459
|
sendJson(res, 200, {
|
|
1116
1460
|
messengers: config.messengers,
|
|
1117
1461
|
agents: config.agents,
|
|
1118
1462
|
defaultAgent: config.defaultAgent,
|
|
1119
|
-
telegram: config.telegram
|
|
1463
|
+
telegram: showGlobalIm && config.telegram
|
|
1120
1464
|
? { botToken: mask(config.telegram.botToken), channelId: config.telegram.channelId }
|
|
1121
1465
|
: undefined,
|
|
1122
1466
|
feishu: config.feishu
|
|
@@ -1125,7 +1469,7 @@ async function handleGetConfig(_req, res) {
|
|
|
1125
1469
|
dingtalk: config.dingtalk
|
|
1126
1470
|
? { clientId: config.dingtalk.clientId, clientSecret: mask(config.dingtalk.clientSecret), channelId: config.dingtalk.channelId }
|
|
1127
1471
|
: undefined,
|
|
1128
|
-
discord: config.discord
|
|
1472
|
+
discord: showGlobalIm && config.discord
|
|
1129
1473
|
? {
|
|
1130
1474
|
botToken: mask(config.discord.botToken),
|
|
1131
1475
|
channelId: config.discord.channelId,
|
|
@@ -1133,13 +1477,29 @@ async function handleGetConfig(_req, res) {
|
|
|
1133
1477
|
allowedChannels: config.discord.allowedChannels,
|
|
1134
1478
|
}
|
|
1135
1479
|
: undefined,
|
|
1136
|
-
acpAgents:
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
+
},
|
|
1142
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,
|
|
1143
1503
|
agentStatus,
|
|
1144
1504
|
});
|
|
1145
1505
|
}
|
|
@@ -1152,6 +1512,29 @@ async function handlePutConfig(req, res) {
|
|
|
1152
1512
|
const body = await readBody(req, res);
|
|
1153
1513
|
const incoming = JSON.parse(body);
|
|
1154
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
|
+
}
|
|
1155
1538
|
const merged = { ...existing };
|
|
1156
1539
|
for (const key of Object.keys(incoming)) {
|
|
1157
1540
|
const val = incoming[key];
|
|
@@ -1217,6 +1600,20 @@ async function handlePutConfig(req, res) {
|
|
|
1217
1600
|
return;
|
|
1218
1601
|
}
|
|
1219
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 */ }
|
|
1220
1617
|
sendJson(res, 200, { ok: true });
|
|
1221
1618
|
}
|
|
1222
1619
|
catch (err) {
|
|
@@ -1318,13 +1715,26 @@ async function handleCreateOrUpdateWorkspace(req, res, expectedId) {
|
|
|
1318
1715
|
const { workspaceRegistry } = await import('../core/workspace.js');
|
|
1319
1716
|
workspaceRegistry.add(v.cfg);
|
|
1320
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 */ }
|
|
1321
1731
|
sendJson(res, 200, { ok: true, workspace: v.cfg });
|
|
1322
1732
|
}
|
|
1323
1733
|
catch (err) {
|
|
1324
1734
|
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
1325
1735
|
}
|
|
1326
1736
|
}
|
|
1327
|
-
async function handleDeleteWorkspace(
|
|
1737
|
+
async function handleDeleteWorkspace(req, res, id) {
|
|
1328
1738
|
try {
|
|
1329
1739
|
const { workspaceRegistry } = await import('../core/workspace.js');
|
|
1330
1740
|
if (id === 'default') {
|
|
@@ -1337,6 +1747,16 @@ async function handleDeleteWorkspace(_req, res, id) {
|
|
|
1337
1747
|
return;
|
|
1338
1748
|
}
|
|
1339
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 */ }
|
|
1340
1760
|
sendJson(res, 200, { ok: true });
|
|
1341
1761
|
}
|
|
1342
1762
|
catch (err) {
|
|
@@ -1457,8 +1877,54 @@ async function handleServiceStatus(res) {
|
|
|
1457
1877
|
}
|
|
1458
1878
|
async function handleServiceStart(res) {
|
|
1459
1879
|
try {
|
|
1460
|
-
const { detectService, spawnBackground } = await import('../cli-ui/service.js');
|
|
1880
|
+
const { detectService, spawnBackground, reapStrayAgimProcesses } = await import('../cli-ui/service.js');
|
|
1461
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.
|
|
1462
1928
|
if (st.mode !== 'none') {
|
|
1463
1929
|
sendJson(res, 200, { ok: true, alreadyRunning: true, mode: st.mode, pid: st.pid });
|
|
1464
1930
|
return;
|
|
@@ -1474,7 +1940,7 @@ async function handleServiceStart(res) {
|
|
|
1474
1940
|
}
|
|
1475
1941
|
}
|
|
1476
1942
|
async function handleServiceStop(res) {
|
|
1477
|
-
const { detectService } = await import('../cli-ui/service.js');
|
|
1943
|
+
const { detectService, reapStrayAgimProcesses } = await import('../cli-ui/service.js');
|
|
1478
1944
|
const st = detectService();
|
|
1479
1945
|
if (st.mode === 'systemd') {
|
|
1480
1946
|
// systemctl stop handles the kill for us — no self-exit needed.
|
|
@@ -1483,6 +1949,24 @@ async function handleServiceStop(res) {
|
|
|
1483
1949
|
// Match service.ts's detection: prefer agim.service, fall back.
|
|
1484
1950
|
const unitName = existsSync('/etc/systemd/system/agim.service') ? 'agim.service' : 'im-hub.service';
|
|
1485
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 */ }
|
|
1486
1970
|
sendJson(res, 200, { ok: true, mode: 'systemd' });
|
|
1487
1971
|
}
|
|
1488
1972
|
catch (err) {
|
|
@@ -1502,9 +1986,48 @@ async function handleServiceStop(res) {
|
|
|
1502
1986
|
}, 200);
|
|
1503
1987
|
}
|
|
1504
1988
|
async function handleServiceRestart(res) {
|
|
1505
|
-
const { detectService } = await import('../cli-ui/service.js');
|
|
1989
|
+
const { detectService, reapStrayAgimProcesses } = await import('../cli-ui/service.js');
|
|
1506
1990
|
const st = detectService();
|
|
1507
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
|
+
}
|
|
1508
2031
|
try {
|
|
1509
2032
|
const { spawn } = await import('node:child_process');
|
|
1510
2033
|
const unitName = existsSync('/etc/systemd/system/agim.service') ? 'agim.service' : 'im-hub.service';
|
|
@@ -2116,6 +2639,18 @@ async function handlePutEnv(req, res) {
|
|
|
2116
2639
|
else
|
|
2117
2640
|
process.env[k] = v;
|
|
2118
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 */ }
|
|
2119
2654
|
sendJson(res, 200, { ok: true, updated: Object.keys(safe) });
|
|
2120
2655
|
}
|
|
2121
2656
|
catch (err) {
|
|
@@ -2265,6 +2800,36 @@ async function handleAudit(_req, res, url) {
|
|
|
2265
2800
|
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2266
2801
|
}
|
|
2267
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
|
+
}
|
|
2268
2833
|
/**
|
|
2269
2834
|
* Per-agent operational health snapshot. Drives the Health tab in /tasks.
|
|
2270
2835
|
*
|
|
@@ -2795,9 +3360,28 @@ async function handleSkillsList(_req, res) {
|
|
|
2795
3360
|
// skillhub.cn proxy with 5-min cache — shows top-50 popular skills so the
|
|
2796
3361
|
// user can discover new ones without leaving the dashboard. The actual
|
|
2797
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.
|
|
2798
3370
|
let remoteHotCache = null;
|
|
2799
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
|
+
}
|
|
2800
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
|
+
}
|
|
2801
3385
|
try {
|
|
2802
3386
|
if (remoteHotCache && Date.now() - remoteHotCache.fetchedAt < REMOTE_HOT_TTL_MS) {
|
|
2803
3387
|
sendJson(res, 200, { ...remoteHotCache.data, cached: true, fetchedAt: remoteHotCache.fetchedAt });
|
|
@@ -4028,7 +4612,7 @@ threadOwners) {
|
|
|
4028
4612
|
logger,
|
|
4029
4613
|
userId: `web:${clientId}`,
|
|
4030
4614
|
};
|
|
4031
|
-
logger.info({ event: 'message.received', text
|
|
4615
|
+
logger.info({ event: 'message.received', ...sanitizeUserText(text) });
|
|
4032
4616
|
const result = await routeMessage(parsed, routeCtx);
|
|
4033
4617
|
// String response (built-in commands, errors)
|
|
4034
4618
|
if (typeof result === 'string') {
|