create-workframe 0.1.0
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/.dockerignore +22 -0
- package/.gitignore +73 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +111 -0
- package/SECURITY.md +40 -0
- package/bin/create-workframe.js +2814 -0
- package/bin/workframe.js +329 -0
- package/docs/workspace-instructions/WORKFRAME_DISCORD.md +20 -0
- package/docs/workspace-instructions/WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md +20 -0
- package/docs/workspace-instructions/WORKFRAME_KANBAN.md +20 -0
- package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +21 -0
- package/docs/workspace-instructions/WORKFRAME_ROUTING.md +29 -0
- package/docs/workspace-instructions/WORKFRAME_TELEGRAM.md +19 -0
- package/package.json +67 -0
- package/profiles/README.md +15 -0
- package/profiles/architect/AGENTS.md +29 -0
- package/profiles/architect/SOUL.md +44 -0
- package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/designer/AGENTS.md +26 -0
- package/profiles/designer/SOUL.md +31 -0
- package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/dev/AGENTS.md +28 -0
- package/profiles/dev/SOUL.md +31 -0
- package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/docs/AGENTS.md +27 -0
- package/profiles/docs/SOUL.md +30 -0
- package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/research/AGENTS.md +26 -0
- package/profiles/research/SOUL.md +31 -0
- package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/visionary/AGENTS.md +25 -0
- package/profiles/visionary/SOUL.md +31 -0
- package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -0
- package/profiles/workframe-agent/AGENTS.md +37 -0
- package/profiles/workframe-agent/SETUP.md +185 -0
- package/profiles/workframe-agent/SOUL.md +61 -0
- package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -0
- package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -0
- package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -0
- package/prompts/WORKFRAME_PROMPT_TEMPLATES.md +16 -0
- package/rules/.hermes.md +11 -0
- package/rules/AGENTS.md +22 -0
- package/rules/workspace-README.md +5 -0
- package/scripts/apply-update-hermes.sh +17 -0
- package/scripts/apply-update-workframe.sh +77 -0
- package/scripts/bootstrap-workspace-link.sh +8 -0
- package/scripts/bundle-workframe-ui.mjs +77 -0
- package/scripts/compose-docker-host.sh +37 -0
- package/scripts/create_workframe_scaffold.py +648 -0
- package/scripts/ensure-compose-host-paths.mjs +51 -0
- package/scripts/fix-zk-encryption-key.sh +35 -0
- package/scripts/lib/install-identity.mjs +212 -0
- package/scripts/lib/workframe-registry.mjs +290 -0
- package/scripts/new-project.mjs +68 -0
- package/scripts/restart-gateway-hermes.sh +12 -0
- package/scripts/security_audit.py +156 -0
- package/scripts/select_agent_pack.py +31 -0
- package/scripts/set-compose-public-url.mjs +92 -0
- package/scripts/setup-stack-secrets.sh +50 -0
- package/scripts/sync-canonical-to-package.mjs +146 -0
- package/scripts/test-scaffold.mjs +390 -0
- package/scripts/verify-public-deploy.sh +105 -0
- package/shared/WORKFRAME_AGENT_LIBRARY.md +31 -0
- package/shared/WORKFRAME_AGENT_OPERATIONS.md +29 -0
- package/shared/WORKFRAME_AGENT_PACKS.json +64 -0
- package/shared/WORKFRAME_AGENT_PACKS.yaml +20 -0
- package/shared/WORKFRAME_CHAT_PERMISSION_MODEL.md +20 -0
- package/shared/WORKFRAME_HANDOFF_SCHEMA.md +25 -0
- package/shared/WORKFRAME_SKILL_CURATION.md +27 -0
- package/shared/agent-avatars/ada.png +0 -0
- package/shared/agent-avatars/aibert.png +0 -0
- package/shared/agent-avatars/amelia.png +0 -0
- package/shared/agent-avatars/andy.png +0 -0
- package/shared/agent-avatars/arc.png +0 -0
- package/shared/agent-avatars/bob.png +0 -0
- package/shared/agent-avatars/buzz.png +0 -0
- package/shared/agent-avatars/carl.png +0 -0
- package/shared/agent-avatars/catalog.json +171 -0
- package/shared/agent-avatars/corbu.png +0 -0
- package/shared/agent-avatars/diana.png +0 -0
- package/shared/agent-avatars/ella.png +0 -0
- package/shared/agent-avatars/elvis.png +0 -0
- package/shared/agent-avatars/f1.png +0 -0
- package/shared/agent-avatars/f2.png +0 -0
- package/shared/agent-avatars/f3.png +0 -0
- package/shared/agent-avatars/f4.png +0 -0
- package/shared/agent-avatars/f5.png +0 -0
- package/shared/agent-avatars/f6.png +0 -0
- package/shared/agent-avatars/frida.png +0 -0
- package/shared/agent-avatars/george.png +0 -0
- package/shared/agent-avatars/grace.png +0 -0
- package/shared/agent-avatars/hedy.png +0 -0
- package/shared/agent-avatars/hermes.png +0 -0
- package/shared/agent-avatars/isaac.png +0 -0
- package/shared/agent-avatars/jes.png +0 -0
- package/shared/agent-avatars/john.png +0 -0
- package/shared/agent-avatars/joni.png +0 -0
- package/shared/agent-avatars/leo.png +0 -0
- package/shared/agent-avatars/louis.png +0 -0
- package/shared/agent-avatars/ludwig.png +0 -0
- package/shared/agent-avatars/m1.png +0 -0
- package/shared/agent-avatars/m2.png +0 -0
- package/shared/agent-avatars/m3.png +0 -0
- package/shared/agent-avatars/m4.png +0 -0
- package/shared/agent-avatars/m5.png +0 -0
- package/shared/agent-avatars/m6.png +0 -0
- package/shared/agent-avatars/marie.png +0 -0
- package/shared/agent-avatars/marilyn.png +0 -0
- package/shared/agent-avatars/neil.png +0 -0
- package/shared/agent-avatars/nikola.png +0 -0
- package/shared/agent-avatars/nina.png +0 -0
- package/shared/agent-avatars/paul.png +0 -0
- package/shared/agent-avatars/ringo.png +0 -0
- package/shared/agent-avatars/rosie.png +0 -0
- package/shared/agent-avatars/ste.png +0 -0
- package/shared/agent-avatars/steve.png +0 -0
- package/shared/agent-avatars/sun.png +0 -0
- package/shared/agent-avatars/tom.png +0 -0
- package/shared/agent-avatars/warren.png +0 -0
- package/shared/agent-avatars/woz.png +0 -0
- package/shared/agent-avatars/zaha.png +0 -0
- package/workframe-api/Dockerfile +14 -0
- package/workframe-api/README.md +28 -0
- package/workframe-api/action_proxy.py +131 -0
- package/workframe-api/auth_rate_limit.py +49 -0
- package/workframe-api/catalog/avatar-catalog.json +171 -0
- package/workframe-api/catalog/logo-catalog.json +86 -0
- package/workframe-api/catalog/user-avatar-catalog.json +171 -0
- package/workframe-api/credential_vault.py +445 -0
- package/workframe-api/data/.gitkeep +0 -0
- package/workframe-api/data/avatar-catalog.json +41 -0
- package/workframe-api/data/logo-catalog.json +14 -0
- package/workframe-api/data/user-avatar-catalog.json +18 -0
- package/workframe-api/email_sender.py +220 -0
- package/workframe-api/google_auth.py +90 -0
- package/workframe-api/install_api.py +359 -0
- package/workframe-api/internal_proxy_auth.py +150 -0
- package/workframe-api/llm_proxy.py +277 -0
- package/workframe-api/oidc_jwt.py +108 -0
- package/workframe-api/package.json +13 -0
- package/workframe-api/platform_auth.py +194 -0
- package/workframe-api/profile_secret_policy.py +86 -0
- package/workframe-api/public/assets/index-DPXu_lGn.css +1 -0
- package/workframe-api/public/assets/index-DYnLrCZZ.js +9 -0
- package/workframe-api/public/assets/index-DglUqFB_.js +9 -0
- package/workframe-api/public/index.html +12 -0
- package/workframe-api/requirements.txt +2 -0
- package/workframe-api/server.py +19646 -0
- package/workframe-api/site_meta.py +271 -0
- package/workframe-api/stack_config.py +427 -0
- package/workframe-api/tests/__init__.py +0 -0
- package/workframe-api/tests/db_setup.py +13 -0
- package/workframe-api/tests/test_admin_updates_gated.py +30 -0
- package/workframe-api/tests/test_agent_dm_bootstrap.py +196 -0
- package/workframe-api/tests/test_agent_profile_sync.py +76 -0
- package/workframe-api/tests/test_auth_email.py +222 -0
- package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +99 -0
- package/workframe-api/tests/test_auth_rate_limit.py +19 -0
- package/workframe-api/tests/test_avatar_resolve.py +77 -0
- package/workframe-api/tests/test_child_soul_template.py +71 -0
- package/workframe-api/tests/test_credential_canary.py +135 -0
- package/workframe-api/tests/test_credential_isolation.py +448 -0
- package/workframe-api/tests/test_credential_resolution.py +206 -0
- package/workframe-api/tests/test_device_oauth.py +108 -0
- package/workframe-api/tests/test_doctor_repair.py +103 -0
- package/workframe-api/tests/test_ensure_profile_api.py +77 -0
- package/workframe-api/tests/test_gateway_compose_security.py +136 -0
- package/workframe-api/tests/test_install_secure_host.py +39 -0
- package/workframe-api/tests/test_internal_proxy_auth.py +125 -0
- package/workframe-api/tests/test_invite_runtime_bootstrap.py +72 -0
- package/workframe-api/tests/test_kanban_delegation.py +185 -0
- package/workframe-api/tests/test_llm_proxy.py +155 -0
- package/workframe-api/tests/test_login_access_policy.py +183 -0
- package/workframe-api/tests/test_mvp_model_bootstrap.py +75 -0
- package/workframe-api/tests/test_onboarding_bootstrap.py +248 -0
- package/workframe-api/tests/test_platform_auth.py +47 -0
- package/workframe-api/tests/test_profile_config_path.py +56 -0
- package/workframe-api/tests/test_profile_config_yaml_repair.py +63 -0
- package/workframe-api/tests/test_profile_create.py +72 -0
- package/workframe-api/tests/test_profile_identity_overlay.py +61 -0
- package/workframe-api/tests/test_profile_install_health.py +45 -0
- package/workframe-api/tests/test_profile_secret_policy.py +57 -0
- package/workframe-api/tests/test_profile_workspace_cwd.py +34 -0
- package/workframe-api/tests/test_provider_bootstrap.py +75 -0
- package/workframe-api/tests/test_provider_connect.py +54 -0
- package/workframe-api/tests/test_room_crud.py +192 -0
- package/workframe-api/tests/test_room_tenancy.py +701 -0
- package/workframe-api/tests/test_runtime_identity_backfill.py +34 -0
- package/workframe-api/tests/test_site_meta.py +81 -0
- package/workframe-api/tests/test_soul_stub.py +42 -0
- package/workframe-api/tests/test_space_member_sync.py +99 -0
- package/workframe-api/tests/test_stripe_stack_config.py +37 -0
- package/workframe-api/tests/test_supervisor_lifecycle.py +52 -0
- package/workframe-api/tests/test_turn_credential_vault.py +125 -0
- package/workframe-api/tests/test_updates.py +176 -0
- package/workframe-api/tests/test_user_cohort.py +113 -0
- package/workframe-api/tests/test_vault_envelope.py +110 -0
- package/workframe-api/tests/test_workspace_members.py +183 -0
- package/workframe-api/tests/test_workspace_messaging_sync.py +125 -0
- package/workframe-api/tests/test_workspace_provider_list.py +57 -0
- package/workframe-api/time-bind-chat.py +99 -0
- package/workframe-api/turn_credentials.py +226 -0
- package/workframe-api/updates.py +417 -0
- package/workframe-api/vault_kek.py +159 -0
- package/workframe-api/zk_auth.py +633 -0
- package/workframe-supervisor/Dockerfile +11 -0
- package/workframe-supervisor/profile_secret_policy.py +76 -0
- package/workframe-supervisor/server.py +787 -0
- package/workframe-supervisor/tests/test_exec_guard.py +42 -0
- package/workframe-supervisor/tests/test_server_import.py +21 -0
- package/workframe-ui/docker/nginx.conf +85 -0
- package/workframe-ui/public/assets/1-DLJbBkOb.png +0 -0
- package/workframe-ui/public/assets/10-uwRwj5ce.png +0 -0
- package/workframe-ui/public/assets/11-5OuV9F_e.png +0 -0
- package/workframe-ui/public/assets/12-u_axjxW-.png +0 -0
- package/workframe-ui/public/assets/13-ldSvcMsH.png +0 -0
- package/workframe-ui/public/assets/14-xdcALEYD.png +0 -0
- package/workframe-ui/public/assets/15-aZ4snEFB.png +0 -0
- package/workframe-ui/public/assets/16-L_5-DttY.png +0 -0
- package/workframe-ui/public/assets/2-zOPZTppD.png +0 -0
- package/workframe-ui/public/assets/3-Dc3WoVu5.png +0 -0
- package/workframe-ui/public/assets/4-C50hk7_m.png +0 -0
- package/workframe-ui/public/assets/5-Eweetkq4.png +0 -0
- package/workframe-ui/public/assets/6-5sOXgfkw.png +0 -0
- package/workframe-ui/public/assets/7-BqRBCbiC.png +0 -0
- package/workframe-ui/public/assets/8-DEDKS94h.png +0 -0
- package/workframe-ui/public/assets/9-DNj34GW-.png +0 -0
- package/workframe-ui/public/assets/ada-DsvuOc9n.png +0 -0
- package/workframe-ui/public/assets/aibert-BCz8Lo8H.png +0 -0
- package/workframe-ui/public/assets/amelia-DUf3EBGu.png +0 -0
- package/workframe-ui/public/assets/andy-Cpymuhhx.png +0 -0
- package/workframe-ui/public/assets/arc-CBDYvkAF.js +1 -0
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +1 -0
- package/workframe-ui/public/assets/architectureDiagram-3BPJPVTR-XnBRKeW0.js +36 -0
- package/workframe-ui/public/assets/array-BifhSqXX.js +1 -0
- package/workframe-ui/public/assets/avatars/ada.png +0 -0
- package/workframe-ui/public/assets/avatars/aibert.png +0 -0
- package/workframe-ui/public/assets/avatars/amelia.png +0 -0
- package/workframe-ui/public/assets/avatars/andy.png +0 -0
- package/workframe-ui/public/assets/avatars/bob.png +0 -0
- package/workframe-ui/public/assets/avatars/buzz.png +0 -0
- package/workframe-ui/public/assets/avatars/carl.png +0 -0
- package/workframe-ui/public/assets/avatars/catalog.json +171 -0
- package/workframe-ui/public/assets/avatars/corbu.png +0 -0
- package/workframe-ui/public/assets/avatars/diana.png +0 -0
- package/workframe-ui/public/assets/avatars/elvis.png +0 -0
- package/workframe-ui/public/assets/avatars/frida.png +0 -0
- package/workframe-ui/public/assets/avatars/george.png +0 -0
- package/workframe-ui/public/assets/avatars/grace.png +0 -0
- package/workframe-ui/public/assets/avatars/hedy.png +0 -0
- package/workframe-ui/public/assets/avatars/hermes.png +0 -0
- package/workframe-ui/public/assets/avatars/isaac.png +0 -0
- package/workframe-ui/public/assets/avatars/john.png +0 -0
- package/workframe-ui/public/assets/avatars/joni.png +0 -0
- package/workframe-ui/public/assets/avatars/leo.png +0 -0
- package/workframe-ui/public/assets/avatars/louis.png +0 -0
- package/workframe-ui/public/assets/avatars/ludwig.png +0 -0
- package/workframe-ui/public/assets/avatars/marie.png +0 -0
- package/workframe-ui/public/assets/avatars/marilyn.png +0 -0
- package/workframe-ui/public/assets/avatars/nikola.png +0 -0
- package/workframe-ui/public/assets/avatars/nina.png +0 -0
- package/workframe-ui/public/assets/avatars/paul.png +0 -0
- package/workframe-ui/public/assets/avatars/ringo.png +0 -0
- package/workframe-ui/public/assets/avatars/rosie.png +0 -0
- package/workframe-ui/public/assets/avatars/steve.png +0 -0
- package/workframe-ui/public/assets/avatars/sun.png +0 -0
- package/workframe-ui/public/assets/avatars/warren.png +0 -0
- package/workframe-ui/public/assets/avatars/woz.png +0 -0
- package/workframe-ui/public/assets/avatars/zaha.png +0 -0
- package/workframe-ui/public/assets/blockDiagram-GPEHLZMM-VYHUfVhd.js +132 -0
- package/workframe-ui/public/assets/bob-DRz-48Id.png +0 -0
- package/workframe-ui/public/assets/branding/banner.png +0 -0
- package/workframe-ui/public/assets/branding/og-default.png +0 -0
- package/workframe-ui/public/assets/branding/workframe'white.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-1.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-2.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-3.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-4.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-5.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-banner.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-horizontal-mini.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-horizontal-nano.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-horizontal.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-vertical-alt.png +0 -0
- package/workframe-ui/public/assets/branding/workframe-logo-vertical.png +0 -0
- package/workframe-ui/public/assets/branding/workframe.png +0 -0
- package/workframe-ui/public/assets/buzz-mC4PtMvC.png +0 -0
- package/workframe-ui/public/assets/c4Diagram-AAUBKEIU-BTjUcJpm.js +10 -0
- package/workframe-ui/public/assets/carl-CtE74db_.png +0 -0
- package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +1 -0
- package/workframe-ui/public/assets/chunk-2J33WTMH-w7uu7R-b.js +1 -0
- package/workframe-ui/public/assets/chunk-3OPIFGDE-Cb9LtnDX.js +62 -0
- package/workframe-ui/public/assets/chunk-4BX2VUAB-DiQ-qCwH.js +1 -0
- package/workframe-ui/public/assets/chunk-55IACEB6-C-mLFr7z.js +1 -0
- package/workframe-ui/public/assets/chunk-5ZQYHXKU-DOesfiCI.js +2 -0
- package/workframe-ui/public/assets/chunk-727SXJPM-BJ3oBZuz.js +206 -0
- package/workframe-ui/public/assets/chunk-AQP2D5EJ-CCA6xpGs.js +231 -0
- package/workframe-ui/public/assets/chunk-BSJP7CBP-a0cMNFb2.js +1 -0
- package/workframe-ui/public/assets/chunk-CSCIHK7Q-kuqN8EIY.js +122 -0
- package/workframe-ui/public/assets/chunk-FMBD7UC4-DyPgYHCg.js +15 -0
- package/workframe-ui/public/assets/chunk-KSCS5N6A-CdUuvR0V.js +10 -0
- package/workframe-ui/public/assets/chunk-L5ZTLDWV-Dq9NoWmK.js +1 -0
- package/workframe-ui/public/assets/chunk-LZXEDZCA-p74rddlO.js +2 -0
- package/workframe-ui/public/assets/chunk-ND2GUHAM-DBD2u1Gz.js +1 -0
- package/workframe-ui/public/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
- package/workframe-ui/public/assets/chunk-NZK2D7GU-BeIeYFnd.js +1 -0
- package/workframe-ui/public/assets/chunk-O5CBEL6O-ClHc56ib.js +70 -0
- package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +1 -0
- package/workframe-ui/public/assets/chunk-XPW4576I-EFr8R_1p.js +32 -0
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +1 -0
- package/workframe-ui/public/assets/corbu-KiaMXzXQ.png +0 -0
- package/workframe-ui/public/assets/cose-bilkent-S5V4N54A-C7aPBODd.js +1 -0
- package/workframe-ui/public/assets/cytoscape.esm-h6BdjjI9.js +321 -0
- package/workframe-ui/public/assets/dagre-BM42HDAG-BdU1Rv-H.js +4 -0
- package/workframe-ui/public/assets/dagre-Bx709z4p.js +1 -0
- package/workframe-ui/public/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/workframe-ui/public/assets/diagram-2AECGRRQ-DWowSo85.js +43 -0
- package/workframe-ui/public/assets/diagram-5GNKFQAL-MnxBbceO.js +10 -0
- package/workframe-ui/public/assets/diagram-KO2AKTUF-DQaLRXFf.js +3 -0
- package/workframe-ui/public/assets/diagram-LMA3HP47-CQaBud9k.js +24 -0
- package/workframe-ui/public/assets/diagram-OG6HWLK6-D8bAXbY9.js +24 -0
- package/workframe-ui/public/assets/diana-DW0MsL38.png +0 -0
- package/workframe-ui/public/assets/dist-DGpTLHr_.js +1 -0
- package/workframe-ui/public/assets/elvis-LCFaZIcT.png +0 -0
- package/workframe-ui/public/assets/erDiagram-TEJ5UH35-1E-xSvBK.js +85 -0
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +1 -0
- package/workframe-ui/public/assets/flowDiagram-I6XJVG4X-CgOVD5hu.js +162 -0
- package/workframe-ui/public/assets/frida-CXFA0w3F.png +0 -0
- package/workframe-ui/public/assets/ganttDiagram-6RSMTGT7-JFYAIauo.js +292 -0
- package/workframe-ui/public/assets/george-DBSH2Sm2.png +0 -0
- package/workframe-ui/public/assets/gitGraph-WXDBUCRP-B9REenIl.js +1 -0
- package/workframe-ui/public/assets/gitGraphDiagram-PVQCEYII-BQ7NcMSn.js +106 -0
- package/workframe-ui/public/assets/grace-BhV0UPc0.png +0 -0
- package/workframe-ui/public/assets/graphlib-B8gBHxth.js +1 -0
- package/workframe-ui/public/assets/hedy-BR2IHift.png +0 -0
- package/workframe-ui/public/assets/hermes-CqCzcE0y.png +0 -0
- package/workframe-ui/public/assets/index-Dnw6vjqb.js +133 -0
- package/workframe-ui/public/assets/index-DpAGxump.css +1 -0
- package/workframe-ui/public/assets/info-J43DQDTF-CL6-eTjH.js +1 -0
- package/workframe-ui/public/assets/infoDiagram-5YYISTIA-LJTODW4W.js +2 -0
- package/workframe-ui/public/assets/init-D6jRqBbL.js +1 -0
- package/workframe-ui/public/assets/isaac-D1nhJAuv.png +0 -0
- package/workframe-ui/public/assets/ishikawaDiagram-YF4QCWOH-bchrQVuo.js +70 -0
- package/workframe-ui/public/assets/john-zSPWwNi4.png +0 -0
- package/workframe-ui/public/assets/joni-BFLoyfJP.png +0 -0
- package/workframe-ui/public/assets/journeyDiagram-JHISSGLW-DkrvYuxP.js +139 -0
- package/workframe-ui/public/assets/kanban-definition-UN3LZRKU-DFRbj0IG.js +89 -0
- package/workframe-ui/public/assets/katex-Vhh-h91d.js +257 -0
- package/workframe-ui/public/assets/leo-C_3IOL11.png +0 -0
- package/workframe-ui/public/assets/line-Vd48P7-O.js +1 -0
- package/workframe-ui/public/assets/linear-Ckizh2G7.js +1 -0
- package/workframe-ui/public/assets/louis-DEEECFSX.png +0 -0
- package/workframe-ui/public/assets/ludwig-_hoKhhyK.png +0 -0
- package/workframe-ui/public/assets/marie-DET6MsfO.png +0 -0
- package/workframe-ui/public/assets/marilyn-DTqwt8Yh.png +0 -0
- package/workframe-ui/public/assets/mermaid-parser.core-Bkimsnqj.js +4 -0
- package/workframe-ui/public/assets/mermaid.core-x0TvVuPo.js +9 -0
- package/workframe-ui/public/assets/mindmap-definition-RKZ34NQL-6ykAFPEz.js +96 -0
- package/workframe-ui/public/assets/nikola-B4PtHrJv.png +0 -0
- package/workframe-ui/public/assets/nina-BYbrOn0d.png +0 -0
- package/workframe-ui/public/assets/ordinal-hYBb2elL.js +1 -0
- package/workframe-ui/public/assets/packet-YPE3B663-Dw3xgMDt.js +1 -0
- package/workframe-ui/public/assets/path-BWPyau1x.js +1 -0
- package/workframe-ui/public/assets/paul-CGURYQIn.png +0 -0
- package/workframe-ui/public/assets/pie-LRSECV5Y-DATysawG.js +1 -0
- package/workframe-ui/public/assets/pieDiagram-4H26LBE5-SJKD1S0S.js +30 -0
- package/workframe-ui/public/assets/project-logos/1.png +0 -0
- package/workframe-ui/public/assets/project-logos/10.png +0 -0
- package/workframe-ui/public/assets/project-logos/11.png +0 -0
- package/workframe-ui/public/assets/project-logos/12.png +0 -0
- package/workframe-ui/public/assets/project-logos/13.png +0 -0
- package/workframe-ui/public/assets/project-logos/14.png +0 -0
- package/workframe-ui/public/assets/project-logos/15.png +0 -0
- package/workframe-ui/public/assets/project-logos/16.png +0 -0
- package/workframe-ui/public/assets/project-logos/2.png +0 -0
- package/workframe-ui/public/assets/project-logos/3.png +0 -0
- package/workframe-ui/public/assets/project-logos/4.png +0 -0
- package/workframe-ui/public/assets/project-logos/5.png +0 -0
- package/workframe-ui/public/assets/project-logos/6.png +0 -0
- package/workframe-ui/public/assets/project-logos/7.png +0 -0
- package/workframe-ui/public/assets/project-logos/8.png +0 -0
- package/workframe-ui/public/assets/project-logos/9.png +0 -0
- package/workframe-ui/public/assets/project-logos/catalog.json +86 -0
- package/workframe-ui/public/assets/quadrantDiagram-W4KKPZXB-BrYDZX8q.js +7 -0
- package/workframe-ui/public/assets/radar-GUYGQ44K-BmWYPCds.js +1 -0
- package/workframe-ui/public/assets/requirementDiagram-4Y6WPE33-DwL9Mc8e.js +84 -0
- package/workframe-ui/public/assets/ringo-WhfUNOyY.png +0 -0
- package/workframe-ui/public/assets/rosie-CAtcIf87.png +0 -0
- package/workframe-ui/public/assets/rough.esm-CSKSodPl.js +1 -0
- package/workframe-ui/public/assets/sankeyDiagram-5OEKKPKP-DYIFsL8h.js +40 -0
- package/workframe-ui/public/assets/sequenceDiagram-3UESZ5HK-0-FPkFk8.js +162 -0
- package/workframe-ui/public/assets/src-B_od6b6h.js +1 -0
- package/workframe-ui/public/assets/stateDiagram-AJRCARHV-BQCiBk6u.js +1 -0
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +1 -0
- package/workframe-ui/public/assets/steve-CgXXJ9EZ.png +0 -0
- package/workframe-ui/public/assets/sun-BLNAhoZd.png +0 -0
- package/workframe-ui/public/assets/timeline-definition-PNZ67QCA-DS3tFcXj.js +120 -0
- package/workframe-ui/public/assets/treeView-BLDUP644-DSyUCKLY.js +1 -0
- package/workframe-ui/public/assets/treemap-LRROVOQU-CEZaNh5Y.js +1 -0
- package/workframe-ui/public/assets/vennDiagram-CIIHVFJN-CD-Vc9NF.js +34 -0
- package/workframe-ui/public/assets/wardley-L42UT6IY-Drq5w1Mc.js +1 -0
- package/workframe-ui/public/assets/wardleyDiagram-YWT4CUSO-DouXDJoF.js +78 -0
- package/workframe-ui/public/assets/warren-DIH7UKMY.png +0 -0
- package/workframe-ui/public/assets/woz-D2yleG-V.png +0 -0
- package/workframe-ui/public/assets/xychartDiagram-2RQKCTM6-DDf_Lol5.js +7 -0
- package/workframe-ui/public/assets/zaha-wersOEq9.png +0 -0
- package/workframe-ui/public/favicon.ico +0 -0
- package/workframe-ui/public/favicon.svg +7 -0
- package/workframe-ui/public/icons.svg +24 -0
- package/workframe-ui/public/index.html +50 -0
- package/workframe-ui/public/manifest.webmanifest +18 -0
- package/workframe-ui/public/workframe-config.json +4 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""workframe-supervisor — token-gated Docker exec for Hermes profile lifecycle."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import http.client
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import shlex
|
|
11
|
+
import socket
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import urllib.request
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
HERMES_DATA = Path(os.environ.get("HERMES_DATA", "/opt/data"))
|
|
22
|
+
HOST = os.environ.get("HOST", "127.0.0.1")
|
|
23
|
+
PORT = int(os.environ.get("PORT", "8090"))
|
|
24
|
+
VERSION = "0.1.0"
|
|
25
|
+
NATIVE_PROFILE = os.environ.get("WORKFRAME_NATIVE_PROFILE", "").strip()
|
|
26
|
+
DOCKER_SOCK = os.environ.get("DOCKER_SOCK", "/var/run/docker.sock")
|
|
27
|
+
GATEWAY_CONTAINER_NAME = os.environ.get("WORKFRAME_GATEWAY_CONTAINER", "workframe-gateway")
|
|
28
|
+
SUPERVISOR_TOKEN = (
|
|
29
|
+
os.environ.get("WORKFRAME_SUPERVISOR_TOKEN")
|
|
30
|
+
or os.environ.get("SUPERVISOR_TOKEN")
|
|
31
|
+
or ""
|
|
32
|
+
).strip()
|
|
33
|
+
DEPLOYMENT_MODE = (os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "trusted_team").strip().lower()
|
|
34
|
+
ROUTES_JSON = HERMES_DATA / "workframe" / "routes.json"
|
|
35
|
+
SCRIPTS_DIR = Path(os.environ.get("WORKFRAME_SCRIPTS_DIR", "/opt/install/scripts"))
|
|
36
|
+
COMPOSE_DIR = Path(os.environ.get("WORKFRAME_COMPOSE_DIR", "/compose"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _compose_file_args() -> list[str]:
|
|
40
|
+
"""Absolute host bind paths when compose CLI runs inside supervisor (relative paths break)."""
|
|
41
|
+
args = ["-f", str(COMPOSE_DIR / "docker-compose.yml")]
|
|
42
|
+
host_root = os.environ.get("WORKFRAME_HOST_PROJECT_ROOT", "").strip()
|
|
43
|
+
bindings = COMPOSE_DIR / "docker-compose.host-bindings.yml"
|
|
44
|
+
if host_root and bindings.is_file():
|
|
45
|
+
args.extend(["-f", str(bindings)])
|
|
46
|
+
return args
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _compose_run(argv: list[str], *, timeout: float = 120.0) -> subprocess.CompletedProcess[str]:
|
|
50
|
+
cmd = ["docker", "compose", *_compose_file_args(), *argv]
|
|
51
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=str(COMPOSE_DIR))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
from profile_secret_policy import exec_blocked_for_profile
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _exec_targets_runtime_profile_secrets(cmd: list[str], acting_profile: str = "") -> bool:
|
|
58
|
+
if DEPLOYMENT_MODE == "single_user_local":
|
|
59
|
+
return False
|
|
60
|
+
return exec_blocked_for_profile(cmd, acting_profile)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _stack_apply(target: str) -> dict[str, Any]:
|
|
64
|
+
target = str(target or "all").strip().lower()
|
|
65
|
+
if target == "gateway-restart":
|
|
66
|
+
script = SCRIPTS_DIR / "restart-gateway-hermes.sh"
|
|
67
|
+
if not script.is_file():
|
|
68
|
+
raise ValueError("restart_script_missing")
|
|
69
|
+
proc = subprocess.run(
|
|
70
|
+
["bash", str(script)],
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
timeout=300,
|
|
74
|
+
cwd=str(COMPOSE_DIR) if COMPOSE_DIR.is_dir() else None,
|
|
75
|
+
)
|
|
76
|
+
log = f"=== {script} (exit {proc.returncode}) ===\n{proc.stdout}\n{proc.stderr}"
|
|
77
|
+
if proc.returncode != 0:
|
|
78
|
+
raise ValueError("restart_failed:gateway")
|
|
79
|
+
return {"ok": True, "target": "gateway", "log": log[-12000:]}
|
|
80
|
+
if target not in {"hermes", "workframe", "all"}:
|
|
81
|
+
raise ValueError("invalid_update_target")
|
|
82
|
+
scripts: list[Path] = []
|
|
83
|
+
if target in {"hermes", "all"}:
|
|
84
|
+
p = SCRIPTS_DIR / "apply-update-hermes.sh"
|
|
85
|
+
if not p.is_file():
|
|
86
|
+
raise ValueError("update_script_missing:hermes")
|
|
87
|
+
scripts.append(p)
|
|
88
|
+
if target in {"workframe", "all"}:
|
|
89
|
+
p = SCRIPTS_DIR / "apply-update-workframe.sh"
|
|
90
|
+
if not p.is_file():
|
|
91
|
+
raise ValueError("update_script_missing:workframe")
|
|
92
|
+
scripts.append(p)
|
|
93
|
+
logs: list[str] = []
|
|
94
|
+
for script in scripts:
|
|
95
|
+
proc = subprocess.run(
|
|
96
|
+
["bash", str(script)],
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
timeout=900,
|
|
100
|
+
cwd=str(COMPOSE_DIR) if COMPOSE_DIR.is_dir() else None,
|
|
101
|
+
)
|
|
102
|
+
logs.append(f"=== {script} (exit {proc.returncode}) ===\n{proc.stdout}\n{proc.stderr}")
|
|
103
|
+
if proc.returncode != 0:
|
|
104
|
+
raise ValueError(f"update_failed:{script.name}")
|
|
105
|
+
return {"ok": True, "target": target, "log": "\n".join(logs)[-12000:]}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _host_setup_public_https(host: str, port: int) -> dict[str, Any]:
|
|
109
|
+
host = str(host or "").strip().lower()
|
|
110
|
+
if not re.fullmatch(r"[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?", host, re.IGNORECASE):
|
|
111
|
+
raise ValueError("invalid host")
|
|
112
|
+
port = int(port)
|
|
113
|
+
if port < 1 or port > 65535:
|
|
114
|
+
raise ValueError("invalid port")
|
|
115
|
+
root = os.environ.get("WORKFRAME_HOST_PROJECT_ROOT", "").strip() or "/opt/workframe/ProjectX"
|
|
116
|
+
script = f"{root}/scripts/workframe/setup-public-https.sh"
|
|
117
|
+
# ponytail: chroot on host via docker.sock — API container cannot run apt/systemctl on host.
|
|
118
|
+
cmd = [
|
|
119
|
+
"docker",
|
|
120
|
+
"run",
|
|
121
|
+
"--rm",
|
|
122
|
+
"--pull=missing",
|
|
123
|
+
"--privileged",
|
|
124
|
+
"--pid=host",
|
|
125
|
+
"-v",
|
|
126
|
+
"/:/host",
|
|
127
|
+
"debian:bookworm-slim",
|
|
128
|
+
"chroot",
|
|
129
|
+
"/host",
|
|
130
|
+
"bash",
|
|
131
|
+
script,
|
|
132
|
+
host,
|
|
133
|
+
str(port),
|
|
134
|
+
]
|
|
135
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
|
136
|
+
log = f"=== setup-public-https {host}:{port} (exit {proc.returncode}) ===\n{proc.stdout}\n{proc.stderr}"
|
|
137
|
+
if proc.returncode != 0:
|
|
138
|
+
raise ValueError(log[-2000:] or "setup_public_https_failed")
|
|
139
|
+
return {"ok": True, "host": host, "port": port, "log": log[-8000:]}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _host_set_compose_public_url(public_url: str, *, restart: bool = True) -> dict[str, Any]:
|
|
143
|
+
public_url = str(public_url or "").strip()
|
|
144
|
+
if not public_url:
|
|
145
|
+
raise ValueError("url required")
|
|
146
|
+
compose_dir = Path(os.environ.get("WORKFRAME_COMPOSE_DIR", "/compose"))
|
|
147
|
+
env_path = compose_dir / ".env"
|
|
148
|
+
scripts = Path(os.environ.get("WORKFRAME_SCRIPTS_DIR", "/opt/install/scripts"))
|
|
149
|
+
script = scripts / "set-compose-public-url.mjs"
|
|
150
|
+
if not script.is_file():
|
|
151
|
+
raise ValueError("set-compose_public_url_script_missing")
|
|
152
|
+
proc = subprocess.run(
|
|
153
|
+
["node", str(script), public_url, "--env", str(env_path)],
|
|
154
|
+
capture_output=True,
|
|
155
|
+
text=True,
|
|
156
|
+
timeout=60,
|
|
157
|
+
cwd=str(compose_dir),
|
|
158
|
+
)
|
|
159
|
+
log = f"=== set-compose-public-url (exit {proc.returncode}) ===\n{proc.stdout}\n{proc.stderr}"
|
|
160
|
+
if proc.returncode != 0:
|
|
161
|
+
raise ValueError(log[-2000:] or "set_compose_public_url_failed")
|
|
162
|
+
if restart:
|
|
163
|
+
# ponytail: --no-deps — never touch workframe-supervisor (partial up was killing it mid-install).
|
|
164
|
+
restart_proc = _compose_run(["up", "-d", "--no-deps", "workframe-api", "gateway"], timeout=120)
|
|
165
|
+
log += (
|
|
166
|
+
f"\n=== compose up --no-deps workframe-api gateway (exit {restart_proc.returncode}) ===\n"
|
|
167
|
+
f"{restart_proc.stdout}\n{restart_proc.stderr}"
|
|
168
|
+
)
|
|
169
|
+
if restart_proc.returncode != 0:
|
|
170
|
+
raise ValueError(log[-2000:] or "compose_restart_failed")
|
|
171
|
+
try:
|
|
172
|
+
payload = json.loads(proc.stdout.strip())
|
|
173
|
+
except json.JSONDecodeError:
|
|
174
|
+
payload = {"ok": True}
|
|
175
|
+
payload["log"] = log[-8000:]
|
|
176
|
+
payload["restarted"] = restart
|
|
177
|
+
return payload
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _primary_profile() -> str:
|
|
181
|
+
if NATIVE_PROFILE and (HERMES_DATA / "profiles" / NATIVE_PROFILE).is_dir():
|
|
182
|
+
return NATIVE_PROFILE
|
|
183
|
+
root = HERMES_DATA / "profiles"
|
|
184
|
+
if not root.is_dir():
|
|
185
|
+
return ""
|
|
186
|
+
names = sorted(p.name for p in root.iterdir() if p.is_dir())
|
|
187
|
+
return names[0] if names else ""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _profile_dir(profile: str) -> Path:
|
|
191
|
+
return HERMES_DATA / "profiles" / profile
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def safe_profile_slug(value: str) -> str:
|
|
195
|
+
slug = (value or "").strip()
|
|
196
|
+
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", slug):
|
|
197
|
+
raise ValueError("invalid profile")
|
|
198
|
+
return slug
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def profile_exists(profile: str) -> bool:
|
|
202
|
+
return _profile_dir(profile).is_dir()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def load_routes() -> dict[str, Any]:
|
|
206
|
+
default_profile = _primary_profile()
|
|
207
|
+
raw_routes: list[dict[str, Any]] = []
|
|
208
|
+
if ROUTES_JSON.is_file():
|
|
209
|
+
try:
|
|
210
|
+
data = json.loads(ROUTES_JSON.read_text(encoding="utf-8"))
|
|
211
|
+
if isinstance(data, dict) and isinstance(data.get("routes"), list):
|
|
212
|
+
raw_routes = [r for r in data["routes"] if isinstance(r, dict)]
|
|
213
|
+
except (OSError, json.JSONDecodeError):
|
|
214
|
+
pass
|
|
215
|
+
if not raw_routes:
|
|
216
|
+
root = HERMES_DATA / "profiles"
|
|
217
|
+
if root.is_dir():
|
|
218
|
+
for p in sorted(root.iterdir()):
|
|
219
|
+
if p.is_dir():
|
|
220
|
+
raw_routes.append({"profile": p.name})
|
|
221
|
+
routes = []
|
|
222
|
+
for row in raw_routes:
|
|
223
|
+
slug = str(row.get("profile") or "").strip()
|
|
224
|
+
if slug and profile_exists(slug):
|
|
225
|
+
routes.append({"profile": slug})
|
|
226
|
+
return {"default_profile": default_profile, "routes": routes}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def resolve_validated_profile(profile: str) -> str:
|
|
230
|
+
slug = safe_profile_slug(str(profile or _primary_profile()).strip())
|
|
231
|
+
allowed = {r["profile"] for r in load_routes()["routes"]}
|
|
232
|
+
if slug not in allowed:
|
|
233
|
+
raise ValueError(f"unknown profile: {slug}")
|
|
234
|
+
if not profile_exists(slug):
|
|
235
|
+
raise ValueError(f"profile not installed: {slug}")
|
|
236
|
+
return slug
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
_RUNTIME_PROFILE_RE = re.compile(r"^u-[a-z0-9][a-z0-9-]{0,62}$")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _is_runtime_profile_slug(slug: str) -> bool:
|
|
243
|
+
return bool(_RUNTIME_PROFILE_RE.fullmatch(str(slug or "").strip()))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def resolve_hermes_profile(profile: str) -> str:
|
|
247
|
+
"""Workspace routes or per-user runtime u-* dirs on disk."""
|
|
248
|
+
slug = safe_profile_slug(str(profile or _primary_profile()).strip())
|
|
249
|
+
if _is_runtime_profile_slug(slug) and profile_exists(slug):
|
|
250
|
+
return slug
|
|
251
|
+
return resolve_validated_profile(slug)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def gateway_data(profile: str) -> dict[str, Any]:
|
|
255
|
+
path = _profile_dir(profile) / "gateway_state.json"
|
|
256
|
+
base: dict[str, Any] = {
|
|
257
|
+
"ok": False,
|
|
258
|
+
"exists": path.is_file(),
|
|
259
|
+
"state": "unknown",
|
|
260
|
+
"platforms": {},
|
|
261
|
+
"updated_at": None,
|
|
262
|
+
"uptime_seconds": None,
|
|
263
|
+
}
|
|
264
|
+
if not path.is_file():
|
|
265
|
+
return base
|
|
266
|
+
try:
|
|
267
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
268
|
+
except (OSError, json.JSONDecodeError):
|
|
269
|
+
return base
|
|
270
|
+
start = raw.get("start_time")
|
|
271
|
+
uptime = None
|
|
272
|
+
if isinstance(start, (int, float)) and float(start) > 1_000_000_000:
|
|
273
|
+
uptime = max(0.0, time.time() - float(start))
|
|
274
|
+
base.update(
|
|
275
|
+
{
|
|
276
|
+
"ok": True,
|
|
277
|
+
"state": raw.get("gateway_state") or raw.get("state") or "unknown",
|
|
278
|
+
"pid": raw.get("pid"),
|
|
279
|
+
"platforms": raw.get("platforms") or {},
|
|
280
|
+
"updated_at": raw.get("updated_at"),
|
|
281
|
+
"uptime_seconds": uptime,
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
return base
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _profile_api_port(profile: str) -> int:
|
|
288
|
+
if profile == _primary_profile():
|
|
289
|
+
return 8642
|
|
290
|
+
base = 18610
|
|
291
|
+
span = 100
|
|
292
|
+
h = sum(ord(c) for c in profile) % span
|
|
293
|
+
return base + h
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class _UnixHTTPConnection(http.client.HTTPConnection):
|
|
297
|
+
def __init__(self, unix_path: str):
|
|
298
|
+
super().__init__("localhost")
|
|
299
|
+
self.unix_path = unix_path
|
|
300
|
+
|
|
301
|
+
def connect(self) -> None:
|
|
302
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
303
|
+
sock.connect(self.unix_path)
|
|
304
|
+
self.sock = sock
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _docker_request(method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, Any]:
|
|
308
|
+
conn = _UnixHTTPConnection(DOCKER_SOCK)
|
|
309
|
+
payload = json.dumps(body).encode("utf-8") if body is not None else None
|
|
310
|
+
headers = {"Content-Type": "application/json"} if payload is not None else {}
|
|
311
|
+
conn.request(method, f"/v1.41{path}", body=payload, headers=headers)
|
|
312
|
+
resp = conn.getresponse()
|
|
313
|
+
raw = resp.read()
|
|
314
|
+
conn.close()
|
|
315
|
+
if not raw:
|
|
316
|
+
return resp.status, None
|
|
317
|
+
try:
|
|
318
|
+
return resp.status, json.loads(raw.decode("utf-8", errors="replace"))
|
|
319
|
+
except json.JSONDecodeError:
|
|
320
|
+
return resp.status, raw.decode("utf-8", errors="replace")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _docker_exec(container: str, cmd: list[str], acting_profile: str = "") -> tuple[int, str]:
|
|
324
|
+
if _exec_targets_runtime_profile_secrets(cmd, acting_profile):
|
|
325
|
+
return 1, "blocked: runtime profile credential paths are not readable via gateway exec"
|
|
326
|
+
create_status, create_data = _docker_request(
|
|
327
|
+
"POST",
|
|
328
|
+
f"/containers/{urllib.parse.quote(container, safe='')}/exec",
|
|
329
|
+
{
|
|
330
|
+
"AttachStdout": True,
|
|
331
|
+
"AttachStderr": True,
|
|
332
|
+
"Tty": False,
|
|
333
|
+
"Cmd": cmd,
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
if create_status >= 300 or not isinstance(create_data, dict) or not create_data.get("Id"):
|
|
337
|
+
return create_status, f"exec create failed: {create_data}"
|
|
338
|
+
exec_id = str(create_data["Id"])
|
|
339
|
+
start_status, start_data = _docker_request(
|
|
340
|
+
"POST",
|
|
341
|
+
f"/exec/{urllib.parse.quote(exec_id, safe='')}/start",
|
|
342
|
+
{"Detach": False, "Tty": False},
|
|
343
|
+
)
|
|
344
|
+
out = start_data if isinstance(start_data, str) else json.dumps(start_data or {})
|
|
345
|
+
if isinstance(out, str) and out:
|
|
346
|
+
try:
|
|
347
|
+
raw = out.encode("latin-1", errors="ignore")
|
|
348
|
+
buf = bytearray()
|
|
349
|
+
i = 0
|
|
350
|
+
while i + 8 <= len(raw):
|
|
351
|
+
size = int.from_bytes(raw[i + 4 : i + 8], "big")
|
|
352
|
+
i += 8
|
|
353
|
+
if size < 0 or i + size > len(raw):
|
|
354
|
+
break
|
|
355
|
+
buf.extend(raw[i : i + size])
|
|
356
|
+
i += size
|
|
357
|
+
if buf:
|
|
358
|
+
out = buf.decode("utf-8", errors="replace")
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
361
|
+
inspect_status, inspect_data = _docker_request("GET", f"/exec/{urllib.parse.quote(exec_id, safe='')}/json")
|
|
362
|
+
if inspect_status >= 300 or not isinstance(inspect_data, dict):
|
|
363
|
+
return start_status, out
|
|
364
|
+
exit_raw = inspect_data.get("ExitCode")
|
|
365
|
+
exit_code = 0 if exit_raw in (0, "0") else int(exit_raw or 1)
|
|
366
|
+
return exit_code, out
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _docker_exec_detached(container: str, cmd: list[str], acting_profile: str = "") -> tuple[int, str]:
|
|
370
|
+
if _exec_targets_runtime_profile_secrets(cmd, acting_profile):
|
|
371
|
+
return 1, "blocked: runtime profile credential paths are not readable via gateway exec"
|
|
372
|
+
create_status, create_data = _docker_request(
|
|
373
|
+
"POST",
|
|
374
|
+
f"/containers/{urllib.parse.quote(container, safe='')}/exec",
|
|
375
|
+
{
|
|
376
|
+
"AttachStdout": True,
|
|
377
|
+
"AttachStderr": True,
|
|
378
|
+
"Tty": False,
|
|
379
|
+
"Cmd": cmd,
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
if create_status >= 300 or not isinstance(create_data, dict) or not create_data.get("Id"):
|
|
383
|
+
return create_status, f"exec create failed: {create_data}"
|
|
384
|
+
exec_id = str(create_data["Id"])
|
|
385
|
+
start_status, _start_data = _docker_request(
|
|
386
|
+
"POST",
|
|
387
|
+
f"/exec/{urllib.parse.quote(exec_id, safe='')}/start",
|
|
388
|
+
{"Detach": True, "Tty": False},
|
|
389
|
+
)
|
|
390
|
+
if start_status >= 300:
|
|
391
|
+
return start_status, "exec start failed"
|
|
392
|
+
return 0, ""
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _profile_home_container(profile: str) -> str:
|
|
396
|
+
return f"/opt/data/profiles/{safe_profile_slug(profile)}"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _gateway_exec(profile: str, args: list[str]) -> tuple[int, str]:
|
|
400
|
+
prof = resolve_hermes_profile(profile)
|
|
401
|
+
cli = ["/opt/hermes/bin/hermes", "-p", prof, *args]
|
|
402
|
+
if _exec_targets_runtime_profile_secrets(cli, prof):
|
|
403
|
+
return 1, "blocked: runtime profile credential paths are not readable via gateway exec"
|
|
404
|
+
if prof == _primary_profile():
|
|
405
|
+
return _docker_exec(GATEWAY_CONTAINER_NAME, cli, acting_profile=prof)
|
|
406
|
+
home = _profile_home_container(prof)
|
|
407
|
+
inner = " ".join(shlex.quote(part) for part in cli)
|
|
408
|
+
shell = (
|
|
409
|
+
f"export HERMES_HOME={shlex.quote(home)} HOME={shlex.quote(home)}; "
|
|
410
|
+
f"cd {shlex.quote(home)}; {inner}"
|
|
411
|
+
)
|
|
412
|
+
return _docker_exec(GATEWAY_CONTAINER_NAME, ["sh", "-lc", shell], acting_profile=prof)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _configure_profile_api(profile: str) -> tuple[bool, str, int]:
|
|
416
|
+
port = _profile_api_port(profile)
|
|
417
|
+
script = (
|
|
418
|
+
"import yaml\n"
|
|
419
|
+
"from pathlib import Path\n"
|
|
420
|
+
f"d=Path('/opt/data/profiles/{profile}')\n"
|
|
421
|
+
"p=d/'config.yaml'\n"
|
|
422
|
+
"cfg={}\n"
|
|
423
|
+
"if p.exists():\n"
|
|
424
|
+
" cfg=yaml.safe_load(p.read_text(encoding='utf-8')) or {}\n"
|
|
425
|
+
"plats=cfg.setdefault('platforms',{})\n"
|
|
426
|
+
f"native={'True' if profile == _primary_profile() else 'False'}\n"
|
|
427
|
+
"if not native:\n"
|
|
428
|
+
" api_only=plats.get('api_server', {}) if isinstance(plats.get('api_server', {}), dict) else {}\n"
|
|
429
|
+
" plats={'api_server': api_only}\n"
|
|
430
|
+
" for name in ('discord','telegram','slack','whatsapp','webhook','cron'):\n"
|
|
431
|
+
" plats[name]={'enabled': False}\n"
|
|
432
|
+
" cfg['platforms']=plats\n"
|
|
433
|
+
"api=plats.setdefault('api_server',{})\n"
|
|
434
|
+
"api['enabled']=True\n"
|
|
435
|
+
"extra=api.setdefault('extra',{})\n"
|
|
436
|
+
"extra['host']='0.0.0.0'\n"
|
|
437
|
+
f"extra['port']={port}\n"
|
|
438
|
+
"if not extra.get('key'):\n"
|
|
439
|
+
" extra['key']='workframe-local-key'\n"
|
|
440
|
+
"p.write_text(yaml.safe_dump(cfg, sort_keys=False), encoding='utf-8')\n"
|
|
441
|
+
"print('ok')\n"
|
|
442
|
+
)
|
|
443
|
+
code, out = _docker_exec(GATEWAY_CONTAINER_NAME, ["/opt/hermes/.venv/bin/python", "-c", script])
|
|
444
|
+
return code == 0, out, port
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _patch_profile_gateway_run_script(profile: str) -> tuple[bool, str]:
|
|
448
|
+
if profile == _primary_profile():
|
|
449
|
+
return True, "native profile unchanged"
|
|
450
|
+
profile_home = f"/opt/data/profiles/{profile}"
|
|
451
|
+
script = (
|
|
452
|
+
"from pathlib import Path\n"
|
|
453
|
+
f"p=Path('/run/service/gateway-{profile}/run')\n"
|
|
454
|
+
"if not p.exists():\n"
|
|
455
|
+
" print('missing run script')\n"
|
|
456
|
+
" raise SystemExit(1)\n"
|
|
457
|
+
"text=p.read_text(encoding='utf-8')\n"
|
|
458
|
+
"needle='export HERMES_S6_SUPERVISED_CHILD=1\\n'\n"
|
|
459
|
+
f"inject=('export HERMES_S6_SUPERVISED_CHILD=1\\n'"
|
|
460
|
+
f"'export HERMES_HOME={profile_home}\\n'"
|
|
461
|
+
f"'export HOME={profile_home}\\n'"
|
|
462
|
+
f"'cd {profile_home}\\n'"
|
|
463
|
+
"'if [ -z \"$WORKFRAME_PROXY_TOKEN\" ] && [ -f /run/workframe-proxy/token ]; then '\n"
|
|
464
|
+
"'export WORKFRAME_PROXY_TOKEN=\"$(tr -d \\'\\r\\n\\' < /run/workframe-proxy/token)\"; fi\\n'"
|
|
465
|
+
"'unset DISCORD_BOT_TOKEN DISCORD_ALLOWED_USERS DISCORD_HOME_CHANNEL\\n'"
|
|
466
|
+
"'unset TELEGRAM_BOT_TOKEN TELEGRAM_ALLOWED_USERS TELEGRAM_HOME_CHANNEL\\n'"
|
|
467
|
+
"'unset SLACK_BOT_TOKEN SLACK_APP_TOKEN WHATSAPP_MODE\\n'"
|
|
468
|
+
"'unset HERMES_DASHBOARD HERMES_DASHBOARD_HOST HERMES_DASHBOARD_PORT HERMES_DASHBOARD_INSECURE HERMES_DASHBOARD_TUI\\n')\n"
|
|
469
|
+
"if inject not in text:\n"
|
|
470
|
+
" text=text.replace(needle, inject)\n"
|
|
471
|
+
" p.write_text(text, encoding='utf-8')\n"
|
|
472
|
+
"print('ok')\n"
|
|
473
|
+
)
|
|
474
|
+
code, out = _docker_exec(GATEWAY_CONTAINER_NAME, ["/opt/hermes/.venv/bin/python", "-c", script])
|
|
475
|
+
return code == 0, out
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _profile_api_healthy(profile: str, timeout: float = 1.5) -> bool:
|
|
479
|
+
"""Check profile API from inside the gateway container.
|
|
480
|
+
|
|
481
|
+
ponytail: supervisor stays on control-net only; gateway DNS lives on workframe-net.
|
|
482
|
+
"""
|
|
483
|
+
port = _profile_api_port(profile)
|
|
484
|
+
wait = max(1.0, float(timeout))
|
|
485
|
+
script = (
|
|
486
|
+
"import sys,urllib.request\n"
|
|
487
|
+
f"u='http://127.0.0.1:{port}/v1/health'\n"
|
|
488
|
+
"r=urllib.request.Request(u,headers={'Authorization':'Bearer workframe-local-key'},method='GET')\n"
|
|
489
|
+
"try:\n"
|
|
490
|
+
f" urllib.request.urlopen(r,timeout={wait}); print('ok')\n"
|
|
491
|
+
"except Exception as e:\n"
|
|
492
|
+
" print(e,file=sys.stderr); sys.exit(1)\n"
|
|
493
|
+
)
|
|
494
|
+
code, _ = _docker_exec(
|
|
495
|
+
GATEWAY_CONTAINER_NAME,
|
|
496
|
+
["/opt/hermes/.venv/bin/python", "-c", script],
|
|
497
|
+
)
|
|
498
|
+
return code == 0
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def profile_gateway_lifecycle(profile: str, action: str) -> dict[str, Any]:
|
|
502
|
+
prof = resolve_hermes_profile(profile)
|
|
503
|
+
if action not in {"start", "stop", "status", "disable"}:
|
|
504
|
+
raise ValueError("invalid action")
|
|
505
|
+
port = _profile_api_port(prof)
|
|
506
|
+
primary = _primary_profile()
|
|
507
|
+
if prof == primary:
|
|
508
|
+
if action == "disable":
|
|
509
|
+
raise ValueError("cannot disable the native profile")
|
|
510
|
+
if action == "stop":
|
|
511
|
+
raise ValueError("cannot stop the native profile")
|
|
512
|
+
state = gateway_data(prof)
|
|
513
|
+
ok = bool(state.get("ok")) and str(state.get("state") or "").lower() == "running"
|
|
514
|
+
return {
|
|
515
|
+
"ok": ok,
|
|
516
|
+
"profile": prof,
|
|
517
|
+
"action": action,
|
|
518
|
+
"api_port": port,
|
|
519
|
+
"state": state.get("state") or "unknown",
|
|
520
|
+
"details": state,
|
|
521
|
+
}
|
|
522
|
+
if action == "status":
|
|
523
|
+
state = gateway_data(prof)
|
|
524
|
+
ok = bool(state.get("ok")) and str(state.get("state") or "").lower() == "running"
|
|
525
|
+
return {
|
|
526
|
+
"ok": ok,
|
|
527
|
+
"profile": prof,
|
|
528
|
+
"action": action,
|
|
529
|
+
"api_port": port,
|
|
530
|
+
"state": state.get("state") or "unknown",
|
|
531
|
+
"pid": state.get("pid"),
|
|
532
|
+
"uptime_seconds": state.get("uptime_seconds"),
|
|
533
|
+
"updated_at": state.get("updated_at"),
|
|
534
|
+
"platforms": state.get("platforms") or {},
|
|
535
|
+
}
|
|
536
|
+
if action == "disable":
|
|
537
|
+
state = gateway_data(prof)
|
|
538
|
+
if str(state.get("state") or "").lower() in {"running", "starting"}:
|
|
539
|
+
code, out = _gateway_exec(prof, ["gateway", "stop"])
|
|
540
|
+
if code != 0:
|
|
541
|
+
raise ValueError(f"gateway stop failed: {out}")
|
|
542
|
+
script = (
|
|
543
|
+
"import yaml\n"
|
|
544
|
+
"from pathlib import Path\n"
|
|
545
|
+
f"d=Path('/opt/data/profiles/{prof}')\n"
|
|
546
|
+
"p=d/'config.yaml'\n"
|
|
547
|
+
"cfg=yaml.safe_load(p.read_text(encoding='utf-8')) if p.exists() else {}\n"
|
|
548
|
+
"plats=cfg.setdefault('platforms',{})\n"
|
|
549
|
+
"for name, value in list(plats.items()):\n"
|
|
550
|
+
" if isinstance(value, dict):\n"
|
|
551
|
+
" value['enabled']=False\n"
|
|
552
|
+
" else:\n"
|
|
553
|
+
" plats[name]={'enabled': False}\n"
|
|
554
|
+
"cfg['platforms']=plats\n"
|
|
555
|
+
"p.write_text(yaml.safe_dump(cfg, sort_keys=False), encoding='utf-8')\n"
|
|
556
|
+
f"Path('/opt/data/profiles/{prof}/.disabled').write_text('disabled by supervisor\n', encoding='utf-8')\n"
|
|
557
|
+
"print('ok')\n"
|
|
558
|
+
)
|
|
559
|
+
code, out = _docker_exec(GATEWAY_CONTAINER_NAME, ["/opt/hermes/.venv/bin/python", "-c", script])
|
|
560
|
+
if code != 0:
|
|
561
|
+
raise ValueError(f"disable profile config failed: {out}")
|
|
562
|
+
return {"ok": True, "profile": prof, "action": "disable", "state": "disabled", "api_port": port}
|
|
563
|
+
if action == "start":
|
|
564
|
+
if _profile_api_healthy(prof):
|
|
565
|
+
return {
|
|
566
|
+
"ok": True,
|
|
567
|
+
"profile": prof,
|
|
568
|
+
"action": "start",
|
|
569
|
+
"state": "running",
|
|
570
|
+
"api_port": port,
|
|
571
|
+
"detail": "already running",
|
|
572
|
+
}
|
|
573
|
+
ok, out, port = _configure_profile_api(prof)
|
|
574
|
+
if not ok:
|
|
575
|
+
raise ValueError(f"profile api config failed: {out}")
|
|
576
|
+
ok, out = _patch_profile_gateway_run_script(prof)
|
|
577
|
+
if not ok:
|
|
578
|
+
raise ValueError(f"profile api run patch failed: {out}")
|
|
579
|
+
state = gateway_data(prof)
|
|
580
|
+
if str(state.get("state") or "").lower() in {"running", "starting"}:
|
|
581
|
+
code, out = _gateway_exec(prof, ["gateway", "stop"])
|
|
582
|
+
if code != 0:
|
|
583
|
+
raise ValueError(f"gateway stop failed: {out}")
|
|
584
|
+
time.sleep(1.0)
|
|
585
|
+
code, out = _gateway_exec(prof, ["gateway", action])
|
|
586
|
+
if code != 0:
|
|
587
|
+
raise ValueError(f"gateway {action} failed: {out}")
|
|
588
|
+
if action == "start":
|
|
589
|
+
for _ in range(60):
|
|
590
|
+
if _profile_api_healthy(prof):
|
|
591
|
+
break
|
|
592
|
+
time.sleep(0.5)
|
|
593
|
+
else:
|
|
594
|
+
raise ValueError(f"profile api did not become healthy: {prof}")
|
|
595
|
+
state = gateway_data(prof).get("state") or action
|
|
596
|
+
return {"ok": True, "profile": prof, "action": action, "state": state, "api_port": port, "output": out.strip()}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _auth_ok(handler: BaseHTTPRequestHandler) -> bool:
|
|
600
|
+
if not SUPERVISOR_TOKEN:
|
|
601
|
+
return False
|
|
602
|
+
auth = handler.headers.get("Authorization", "")
|
|
603
|
+
if auth.startswith("Bearer "):
|
|
604
|
+
return secrets_compare(auth[7:].strip(), SUPERVISOR_TOKEN)
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def secrets_compare(a: str, b: str) -> bool:
|
|
609
|
+
import hmac
|
|
610
|
+
|
|
611
|
+
return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class Handler(BaseHTTPRequestHandler):
|
|
615
|
+
server_version = "workframe-supervisor/0.1.0"
|
|
616
|
+
|
|
617
|
+
def log_message(self, fmt: str, *args: Any) -> None:
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
def _json(self, status: int, payload: Any) -> None:
|
|
621
|
+
raw = json.dumps(payload).encode("utf-8")
|
|
622
|
+
self.send_response(status)
|
|
623
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
624
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
625
|
+
self.end_headers()
|
|
626
|
+
self.wfile.write(raw)
|
|
627
|
+
|
|
628
|
+
def _read_json(self) -> dict[str, Any]:
|
|
629
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
630
|
+
if length <= 0:
|
|
631
|
+
return {}
|
|
632
|
+
raw = self.rfile.read(length)
|
|
633
|
+
data = json.loads(raw.decode("utf-8"))
|
|
634
|
+
return data if isinstance(data, dict) else {}
|
|
635
|
+
|
|
636
|
+
def do_GET(self) -> None:
|
|
637
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
638
|
+
path = parsed.path
|
|
639
|
+
if path == "/health":
|
|
640
|
+
return self._json(200, {"ok": True, "service": "workframe-supervisor", "version": VERSION})
|
|
641
|
+
if not _auth_ok(self):
|
|
642
|
+
return self._json(401, {"ok": False, "error": "unauthorized"})
|
|
643
|
+
if path == "/v1/profile.status":
|
|
644
|
+
qs = urllib.parse.parse_qs(parsed.query)
|
|
645
|
+
profile = (qs.get("profile") or [""])[0]
|
|
646
|
+
if not profile:
|
|
647
|
+
return self._json(400, {"ok": False, "error": "profile required"})
|
|
648
|
+
try:
|
|
649
|
+
return self._json(200, profile_gateway_lifecycle(profile, "status"))
|
|
650
|
+
except ValueError as exc:
|
|
651
|
+
return self._json(400, {"ok": False, "error": str(exc)})
|
|
652
|
+
if path == "/v1/stack.status":
|
|
653
|
+
profiles = []
|
|
654
|
+
for row in load_routes()["routes"]:
|
|
655
|
+
slug = row["profile"]
|
|
656
|
+
state = gateway_data(slug)
|
|
657
|
+
profiles.append(
|
|
658
|
+
{
|
|
659
|
+
"profile": slug,
|
|
660
|
+
"native": slug == _primary_profile(),
|
|
661
|
+
"state": state.get("state") or "unknown",
|
|
662
|
+
"api_port": _profile_api_port(slug),
|
|
663
|
+
}
|
|
664
|
+
)
|
|
665
|
+
return self._json(
|
|
666
|
+
200,
|
|
667
|
+
{"ok": True, "profiles": profiles, "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())},
|
|
668
|
+
)
|
|
669
|
+
return self._json(404, {"ok": False, "error": "not found"})
|
|
670
|
+
|
|
671
|
+
def do_POST(self) -> None:
|
|
672
|
+
path = urllib.parse.urlparse(self.path).path
|
|
673
|
+
if not _auth_ok(self):
|
|
674
|
+
return self._json(401, {"ok": False, "error": "unauthorized"})
|
|
675
|
+
try:
|
|
676
|
+
body = self._read_json()
|
|
677
|
+
except json.JSONDecodeError:
|
|
678
|
+
return self._json(400, {"ok": False, "error": "invalid json"})
|
|
679
|
+
action_map = {
|
|
680
|
+
"/v1/profile.start": "start",
|
|
681
|
+
"/v1/profile.stop": "stop",
|
|
682
|
+
"/v1/profile.disable": "disable",
|
|
683
|
+
}
|
|
684
|
+
if path not in action_map:
|
|
685
|
+
if path == "/v1/gateway.exec":
|
|
686
|
+
profile = str(body.get("profile") or "").strip()
|
|
687
|
+
args = body.get("args")
|
|
688
|
+
if not profile:
|
|
689
|
+
return self._json(400, {"ok": False, "error": "profile required"})
|
|
690
|
+
if not isinstance(args, list) or not args:
|
|
691
|
+
return self._json(400, {"ok": False, "error": "args required"})
|
|
692
|
+
try:
|
|
693
|
+
prof = resolve_hermes_profile(profile)
|
|
694
|
+
cli_args = [str(part) for part in args]
|
|
695
|
+
code, out = _gateway_exec(prof, cli_args)
|
|
696
|
+
return self._json(
|
|
697
|
+
200,
|
|
698
|
+
{"ok": code == 0, "profile": prof, "exit_code": code, "output": out},
|
|
699
|
+
)
|
|
700
|
+
except ValueError as exc:
|
|
701
|
+
return self._json(400, {"ok": False, "error": str(exc)})
|
|
702
|
+
if path == "/v1/gateway.container_exec":
|
|
703
|
+
if os.environ.get("WORKFRAME_SUPERVISOR_ALLOW_RAW_EXEC", "0") != "1":
|
|
704
|
+
return self._json(403, {"ok": False, "error": "raw_container_exec_disabled"})
|
|
705
|
+
# ponytail: opt-in only (WORKFRAME_SUPERVISOR_ALLOW_RAW_EXEC=1); default off (0022 N1)
|
|
706
|
+
args = body.get("args")
|
|
707
|
+
if not isinstance(args, list) or not args:
|
|
708
|
+
return self._json(400, {"ok": False, "error": "args required"})
|
|
709
|
+
detach = bool(body.get("detach"))
|
|
710
|
+
try:
|
|
711
|
+
cli_args = [str(part) for part in args]
|
|
712
|
+
if detach:
|
|
713
|
+
code, out = _docker_exec_detached(GATEWAY_CONTAINER_NAME, cli_args)
|
|
714
|
+
else:
|
|
715
|
+
code, out = _docker_exec(GATEWAY_CONTAINER_NAME, cli_args)
|
|
716
|
+
return self._json(
|
|
717
|
+
200,
|
|
718
|
+
{"ok": code == 0, "exit_code": code, "output": out, "detached": detach},
|
|
719
|
+
)
|
|
720
|
+
except Exception as exc: # noqa: BLE001
|
|
721
|
+
return self._json(500, {"ok": False, "error": str(exc)})
|
|
722
|
+
if path == "/v1/stack.apply":
|
|
723
|
+
target = str(body.get("target") or "all").strip().lower()
|
|
724
|
+
try:
|
|
725
|
+
return self._json(200, _stack_apply(target))
|
|
726
|
+
except ValueError as exc:
|
|
727
|
+
return self._json(400, {"ok": False, "error": str(exc)})
|
|
728
|
+
except Exception as exc: # noqa: BLE001
|
|
729
|
+
return self._json(500, {"ok": False, "error": str(exc)})
|
|
730
|
+
if path == "/v1/host.setup_public_https":
|
|
731
|
+
host = str(body.get("host") or "").strip()
|
|
732
|
+
try:
|
|
733
|
+
port = int(body.get("port") or os.environ.get("WORKFRAME_UI_PORT", "18644"))
|
|
734
|
+
except (TypeError, ValueError):
|
|
735
|
+
return self._json(400, {"ok": False, "error": "invalid port"})
|
|
736
|
+
try:
|
|
737
|
+
return self._json(200, _host_setup_public_https(host, port))
|
|
738
|
+
except ValueError as exc:
|
|
739
|
+
return self._json(400, {"ok": False, "error": str(exc)})
|
|
740
|
+
except Exception as exc: # noqa: BLE001
|
|
741
|
+
return self._json(500, {"ok": False, "error": str(exc)})
|
|
742
|
+
if path == "/v1/host.set_compose_public_url":
|
|
743
|
+
public_url = str(body.get("url") or body.get("app_base_url") or "").strip()
|
|
744
|
+
restart = body.get("restart", True) is not False
|
|
745
|
+
try:
|
|
746
|
+
return self._json(200, _host_set_compose_public_url(public_url, restart=restart))
|
|
747
|
+
except ValueError as exc:
|
|
748
|
+
return self._json(400, {"ok": False, "error": str(exc)})
|
|
749
|
+
except Exception as exc: # noqa: BLE001
|
|
750
|
+
return self._json(500, {"ok": False, "error": str(exc)})
|
|
751
|
+
if path == "/v1/hermes.user_exec":
|
|
752
|
+
home = str(body.get("home") or "").strip()
|
|
753
|
+
args = body.get("args")
|
|
754
|
+
if not home or not home.startswith("/opt/data/profiles/"):
|
|
755
|
+
return self._json(400, {"ok": False, "error": "home required"})
|
|
756
|
+
slug = safe_profile_slug(home.rsplit("/", 1)[-1])
|
|
757
|
+
home = f"/opt/data/profiles/{slug}"
|
|
758
|
+
if not isinstance(args, list) or not args:
|
|
759
|
+
return self._json(400, {"ok": False, "error": "args required"})
|
|
760
|
+
inner = " ".join(shlex.quote(str(part)) for part in args)
|
|
761
|
+
shell = (
|
|
762
|
+
f"export HERMES_HOME={shlex.quote(home)} HOME={shlex.quote(home)}; "
|
|
763
|
+
f"mkdir -p {shlex.quote(home)}; cd {shlex.quote(home)}; "
|
|
764
|
+
f"/opt/hermes/bin/hermes {inner}"
|
|
765
|
+
)
|
|
766
|
+
code, out = _docker_exec(GATEWAY_CONTAINER_NAME, ["sh", "-lc", shell], acting_profile=slug)
|
|
767
|
+
return self._json(200, {"ok": code == 0, "exit_code": code, "output": out})
|
|
768
|
+
return self._json(404, {"ok": False, "error": "not found"})
|
|
769
|
+
profile = str(body.get("profile") or "").strip()
|
|
770
|
+
if not profile:
|
|
771
|
+
return self._json(400, {"ok": False, "error": "profile required"})
|
|
772
|
+
try:
|
|
773
|
+
return self._json(200, profile_gateway_lifecycle(profile, action_map[path]))
|
|
774
|
+
except ValueError as exc:
|
|
775
|
+
return self._json(400, {"ok": False, "error": str(exc)})
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def main() -> None:
|
|
779
|
+
if not SUPERVISOR_TOKEN:
|
|
780
|
+
raise SystemExit("WORKFRAME_SUPERVISOR_TOKEN is required")
|
|
781
|
+
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
|
782
|
+
print(f"workframe-supervisor listening on {HOST}:{PORT}", flush=True)
|
|
783
|
+
server.serve_forever()
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
if __name__ == "__main__":
|
|
787
|
+
main()
|