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,417 @@
|
|
|
1
|
+
"""Admin stack updates — version checks + safe in-place apply (preserves runtime/DB)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
HERMES_IMAGE = os.environ.get("WORKFRAME_HERMES_IMAGE", "nousresearch/hermes-agent")
|
|
15
|
+
HERMES_TAG = os.environ.get("WORKFRAME_HERMES_TAG", "latest")
|
|
16
|
+
NPM_PACKAGE = os.environ.get("WORKFRAME_NPM_PACKAGE", "create-workframe")
|
|
17
|
+
RELEASES_URL = str(os.environ.get("WORKFRAME_RELEASES_URL", "")).strip()
|
|
18
|
+
DOCKER_SOCK = os.environ.get("DOCKER_SOCK", "/var/run/docker.sock")
|
|
19
|
+
GATEWAY_CONTAINER = os.environ.get("WORKFRAME_GATEWAY_CONTAINER", "workframe-gateway")
|
|
20
|
+
API_VERSION = str(os.environ.get("WORKFRAME_API_VERSION", "")).strip()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _version_tuple(raw: str) -> tuple[int, ...]:
|
|
24
|
+
text = re.sub(r"^workframe-api-", "", str(raw or "").strip())
|
|
25
|
+
nums: list[int] = []
|
|
26
|
+
for part in re.split(r"[.+_-]", text):
|
|
27
|
+
if part.isdigit():
|
|
28
|
+
nums.append(int(part))
|
|
29
|
+
elif nums:
|
|
30
|
+
break
|
|
31
|
+
return tuple(nums)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _version_lt(current: str, latest: str) -> bool:
|
|
35
|
+
cur = str(current or "").strip()
|
|
36
|
+
lat = str(latest or "").strip()
|
|
37
|
+
if not lat:
|
|
38
|
+
return False
|
|
39
|
+
if not cur:
|
|
40
|
+
return True
|
|
41
|
+
return _version_tuple(cur) < _version_tuple(lat)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _http_json(url: str, timeout: float = 12.0) -> dict[str, Any]:
|
|
45
|
+
req = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "workframe-api"})
|
|
46
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
47
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
48
|
+
return data if isinstance(data, dict) else {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _npm_latest_version() -> str:
|
|
52
|
+
data = _http_json(f"https://registry.npmjs.org/{urllib.parse.quote(NPM_PACKAGE)}/latest")
|
|
53
|
+
return str(data.get("version") or "").strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _docker_hub_digest(repo: str, tag: str) -> str:
|
|
57
|
+
url = f"https://hub.docker.com/v2/repositories/{repo}/tags/{urllib.parse.quote(tag)}"
|
|
58
|
+
data = _http_json(url)
|
|
59
|
+
# ponytail: tag digest matches docker pull :tag RepoDigests; images[0] may be arm64 on multi-arch repos
|
|
60
|
+
top = str(data.get("digest") or "").strip()
|
|
61
|
+
if top:
|
|
62
|
+
return top
|
|
63
|
+
for entry in data.get("images") or []:
|
|
64
|
+
if not isinstance(entry, dict) or not entry.get("digest"):
|
|
65
|
+
continue
|
|
66
|
+
if entry.get("architecture") == "amd64" and entry.get("os") == "linux":
|
|
67
|
+
return str(entry["digest"]).strip()
|
|
68
|
+
for entry in data.get("images") or []:
|
|
69
|
+
if isinstance(entry, dict) and entry.get("digest"):
|
|
70
|
+
return str(entry["digest"]).strip()
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _docker_sock_request(method: str, path: str, body: bytes | None = None) -> tuple[int, Any]:
|
|
75
|
+
import http.client
|
|
76
|
+
import socket as pysocket
|
|
77
|
+
|
|
78
|
+
if not Path(DOCKER_SOCK).exists():
|
|
79
|
+
return 0, {"error": "docker_socket_missing"}
|
|
80
|
+
conn = http.client.HTTPConnection("localhost", timeout=120)
|
|
81
|
+
conn.sock = pysocket.socket(pysocket.AF_UNIX, pysocket.SOCK_STREAM)
|
|
82
|
+
conn.sock.connect(DOCKER_SOCK)
|
|
83
|
+
headers = {"Content-Type": "application/json"} if body else {}
|
|
84
|
+
conn.request(method, path, body=body, headers=headers)
|
|
85
|
+
resp = conn.getresponse()
|
|
86
|
+
raw = resp.read()
|
|
87
|
+
conn.close()
|
|
88
|
+
if not raw:
|
|
89
|
+
return resp.status, {}
|
|
90
|
+
try:
|
|
91
|
+
return resp.status, json.loads(raw.decode("utf-8"))
|
|
92
|
+
except json.JSONDecodeError:
|
|
93
|
+
return resp.status, raw.decode("utf-8", errors="replace")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _container_image_digest(name: str) -> tuple[str, str]:
|
|
97
|
+
status, data = _docker_sock_request("GET", f"/containers/{name}/json")
|
|
98
|
+
if status != 200 or not isinstance(data, dict):
|
|
99
|
+
return "", ""
|
|
100
|
+
image_id = str(data.get("Image") or "")
|
|
101
|
+
ist, idata = _docker_sock_request("GET", f"/images/{image_id}/json")
|
|
102
|
+
digest = ""
|
|
103
|
+
ref = HERMES_IMAGE
|
|
104
|
+
if ist == 200 and isinstance(idata, dict):
|
|
105
|
+
digests = idata.get("RepoDigests") or []
|
|
106
|
+
if digests:
|
|
107
|
+
digest = str(digests[0]).split("@")[-1]
|
|
108
|
+
tags = idata.get("RepoTags") or []
|
|
109
|
+
if tags:
|
|
110
|
+
ref = str(tags[0])
|
|
111
|
+
return digest, ref
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _read_installed_workframe_version(project_root: Path) -> dict[str, str]:
|
|
115
|
+
out = {"api": API_VERSION, "package": "", "manifest_generator": ""}
|
|
116
|
+
manifest = project_root / "workframe-manifest.json"
|
|
117
|
+
if manifest.is_file():
|
|
118
|
+
try:
|
|
119
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
120
|
+
out["package"] = str(data.get("package_version") or "")
|
|
121
|
+
out["manifest_generator"] = str(data.get("generator") or "")
|
|
122
|
+
except Exception: # noqa: BLE001
|
|
123
|
+
pass
|
|
124
|
+
if not out["api"]:
|
|
125
|
+
try:
|
|
126
|
+
import server as _server # noqa: WPS433
|
|
127
|
+
|
|
128
|
+
out["api"] = str(getattr(_server, "VERSION", ""))
|
|
129
|
+
except Exception: # noqa: BLE001
|
|
130
|
+
pass
|
|
131
|
+
if not out["package"]:
|
|
132
|
+
out["package"] = out["api"]
|
|
133
|
+
return out
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _compose_dir() -> Path:
|
|
137
|
+
for raw in (
|
|
138
|
+
os.environ.get("WORKFRAME_HOST_COMPOSE_DIR", ""),
|
|
139
|
+
os.environ.get("WORKFRAME_COMPOSE_DIR", ""),
|
|
140
|
+
os.environ.get("WORKFRAME_PROJECT_ROOT", ""),
|
|
141
|
+
"/compose",
|
|
142
|
+
"/project",
|
|
143
|
+
):
|
|
144
|
+
p = Path(str(raw or "").strip())
|
|
145
|
+
if p.is_dir() and (p / "docker-compose.yml").is_file():
|
|
146
|
+
return p
|
|
147
|
+
return Path(".")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _project_root() -> Path:
|
|
151
|
+
for raw in (os.environ.get("WORKFRAME_PROJECT_ROOT", ""), "/project", os.environ.get("WORKFRAME_COMPOSE_DIR", "")):
|
|
152
|
+
p = Path(str(raw or "").strip())
|
|
153
|
+
if p.is_dir() and (p / "workframe-manifest.json").is_file():
|
|
154
|
+
return p
|
|
155
|
+
for raw in (os.environ.get("WORKFRAME_PROJECT_ROOT", ""), "/project", os.environ.get("WORKFRAME_COMPOSE_DIR", "")):
|
|
156
|
+
p = Path(str(raw or "").strip())
|
|
157
|
+
if p.is_dir() and (p / "docker-compose.yml").is_file():
|
|
158
|
+
return p
|
|
159
|
+
return _compose_dir()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _script_path(name: str) -> Path | None:
|
|
163
|
+
roots = [
|
|
164
|
+
Path(f"/opt/install/scripts/{name}"),
|
|
165
|
+
Path(f"/opt/install/scripts/workframe/{name}"),
|
|
166
|
+
]
|
|
167
|
+
mode = str(os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "trusted_team").strip().lower()
|
|
168
|
+
if mode == "single_user_local":
|
|
169
|
+
roots.extend(
|
|
170
|
+
[
|
|
171
|
+
_project_root() / "scripts" / "workframe" / name,
|
|
172
|
+
_project_root() / "scripts" / name,
|
|
173
|
+
],
|
|
174
|
+
)
|
|
175
|
+
for path in roots:
|
|
176
|
+
if path.is_file():
|
|
177
|
+
return path
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _host_compose_ready() -> bool:
|
|
182
|
+
host_raw = str(os.environ.get("WORKFRAME_HOST_COMPOSE_DIR", "")).strip()
|
|
183
|
+
if not host_raw:
|
|
184
|
+
return False
|
|
185
|
+
host = Path(host_raw)
|
|
186
|
+
if host.is_dir() and (host / "docker-compose.yml").is_file():
|
|
187
|
+
return True
|
|
188
|
+
# ponytail: Windows host paths are not visible inside the API container — trust /compose mount
|
|
189
|
+
compose = _compose_dir()
|
|
190
|
+
return compose.joinpath("docker-compose.yml").is_file()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _docker_apply_ready() -> tuple[bool, str | None]:
|
|
194
|
+
if not Path(DOCKER_SOCK).exists():
|
|
195
|
+
return False, "Docker socket is not available to the API container."
|
|
196
|
+
if not _compose_dir().joinpath("docker-compose.yml").is_file():
|
|
197
|
+
return False, "docker-compose.yml was not found for this stack."
|
|
198
|
+
if not _host_compose_ready():
|
|
199
|
+
return False, (
|
|
200
|
+
"Set WORKFRAME_HOST_COMPOSE_DIR to the host compose folder so updates run on the Docker host."
|
|
201
|
+
)
|
|
202
|
+
return True, None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _product_state(*, update_available: bool, can_update: bool) -> str:
|
|
206
|
+
if update_available and can_update:
|
|
207
|
+
return "available"
|
|
208
|
+
if update_available:
|
|
209
|
+
return "blocked"
|
|
210
|
+
return "current"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def parse_hermes_version_output(text: str) -> str:
|
|
214
|
+
"""Extract semver from `hermes --version` stdout."""
|
|
215
|
+
match = re.search(r"Hermes Agent v(\d+\.\d+\.\d+)", str(text or ""))
|
|
216
|
+
return match.group(1) if match else ""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _read_hermes_agent_version() -> str:
|
|
220
|
+
"""Native Hermes semver via gateway exec (lazy import avoids server load cycle)."""
|
|
221
|
+
try:
|
|
222
|
+
import server as _server # noqa: WPS433
|
|
223
|
+
|
|
224
|
+
return _server._hermes_agent_version()
|
|
225
|
+
except Exception: # noqa: BLE001
|
|
226
|
+
return ""
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _releases_manifest() -> dict[str, Any]:
|
|
230
|
+
if not RELEASES_URL:
|
|
231
|
+
return {}
|
|
232
|
+
try:
|
|
233
|
+
return _http_json(RELEASES_URL)
|
|
234
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
|
|
235
|
+
return {}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def updates_available(*, desktop_version: str = "", hermes_agent_version: str = "") -> dict[str, Any]:
|
|
239
|
+
compose_dir = _compose_dir()
|
|
240
|
+
project_root = _project_root()
|
|
241
|
+
docker_ok = Path(DOCKER_SOCK).exists()
|
|
242
|
+
installed = _read_installed_workframe_version(project_root)
|
|
243
|
+
|
|
244
|
+
npm_latest = ""
|
|
245
|
+
try:
|
|
246
|
+
npm_latest = _npm_latest_version()
|
|
247
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError):
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
releases = _releases_manifest()
|
|
251
|
+
workframe_latest = str(releases.get("workframe") or releases.get("create_workframe") or npm_latest or "")
|
|
252
|
+
desktop_latest = str(releases.get("desktop") or os.environ.get("WORKFRAME_DESKTOP_LATEST", "0.1.0"))
|
|
253
|
+
|
|
254
|
+
installed_pkg = installed.get("package") or installed.get("api") or ""
|
|
255
|
+
workframe_update = bool(workframe_latest and _version_lt(installed_pkg, workframe_latest))
|
|
256
|
+
|
|
257
|
+
hermes_digest, hermes_ref = _container_image_digest(GATEWAY_CONTAINER)
|
|
258
|
+
hermes_tag = hermes_ref.rsplit(":", 1)[-1] if hermes_ref and ":" in hermes_ref else HERMES_TAG
|
|
259
|
+
hermes_latest_digest = ""
|
|
260
|
+
try:
|
|
261
|
+
hermes_latest_digest = _docker_hub_digest(HERMES_IMAGE, HERMES_TAG)
|
|
262
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError):
|
|
263
|
+
pass
|
|
264
|
+
hermes_update = bool(
|
|
265
|
+
docker_ok
|
|
266
|
+
and hermes_latest_digest
|
|
267
|
+
and hermes_digest
|
|
268
|
+
and hermes_digest != hermes_latest_digest,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
desktop_installed = str(desktop_version or "").strip()
|
|
272
|
+
desktop_update = bool(desktop_latest and desktop_installed and _version_lt(desktop_installed, desktop_latest))
|
|
273
|
+
|
|
274
|
+
digest_short = hermes_latest_digest
|
|
275
|
+
if len(digest_short) > 28:
|
|
276
|
+
digest_short = digest_short[:28] + "…"
|
|
277
|
+
|
|
278
|
+
docker_apply_ok, docker_apply_reason = _docker_apply_ready()
|
|
279
|
+
hermes_script_ok = _script_path("apply-update-hermes.sh") is not None
|
|
280
|
+
workframe_script_ok = _script_path("apply-update-workframe.sh") is not None
|
|
281
|
+
hermes_can_update = bool(docker_apply_ok and hermes_script_ok)
|
|
282
|
+
workframe_can_update = bool(docker_apply_ok and workframe_script_ok)
|
|
283
|
+
hermes_reason = docker_apply_reason
|
|
284
|
+
if not hermes_reason and hermes_update and not hermes_script_ok:
|
|
285
|
+
hermes_reason = "Hermes update script is missing from this install."
|
|
286
|
+
workframe_reason = docker_apply_reason
|
|
287
|
+
if not workframe_reason and workframe_update and not workframe_script_ok:
|
|
288
|
+
workframe_reason = "Workframe update script is missing from this install."
|
|
289
|
+
if not workframe_reason and workframe_update and not workframe_latest:
|
|
290
|
+
workframe_reason = "No published npm release to update to yet."
|
|
291
|
+
|
|
292
|
+
agent_version = str(hermes_agent_version or "").strip() or _read_hermes_agent_version()
|
|
293
|
+
hermes_current = agent_version or hermes_tag
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
"ok": True,
|
|
297
|
+
"docker_available": docker_ok,
|
|
298
|
+
"compose_dir": str(compose_dir),
|
|
299
|
+
"project_root": str(project_root),
|
|
300
|
+
"workframe": {
|
|
301
|
+
"current": installed_pkg,
|
|
302
|
+
"latest": workframe_latest,
|
|
303
|
+
"update_available": workframe_update,
|
|
304
|
+
"can_update": workframe_can_update,
|
|
305
|
+
"state": _product_state(update_available=workframe_update, can_update=workframe_can_update),
|
|
306
|
+
"reason": workframe_reason,
|
|
307
|
+
"update_mode": "docker-compose-rebuild",
|
|
308
|
+
"install_kind": "docker",
|
|
309
|
+
"components": ["ui", "api", "supervisor"],
|
|
310
|
+
},
|
|
311
|
+
"hermes": {
|
|
312
|
+
"current": hermes_current,
|
|
313
|
+
"agent_version": agent_version,
|
|
314
|
+
"image_tag": hermes_tag,
|
|
315
|
+
"latest": "",
|
|
316
|
+
"current_image": hermes_ref,
|
|
317
|
+
"current_digest": hermes_digest[:28] + "…" if len(hermes_digest) > 28 else hermes_digest,
|
|
318
|
+
"latest_digest": digest_short,
|
|
319
|
+
"image": f"{HERMES_IMAGE}:{HERMES_TAG}",
|
|
320
|
+
"update_available": hermes_update,
|
|
321
|
+
"can_update": hermes_can_update,
|
|
322
|
+
"state": _product_state(update_available=hermes_update, can_update=hermes_can_update),
|
|
323
|
+
"reason": hermes_reason,
|
|
324
|
+
"update_mode": "docker-compose-pull",
|
|
325
|
+
"install_kind": "docker",
|
|
326
|
+
"can_restart_gateway": bool(docker_apply_ok and _script_path("restart-gateway-hermes.sh") is not None),
|
|
327
|
+
},
|
|
328
|
+
"desktop": {
|
|
329
|
+
"current": desktop_installed,
|
|
330
|
+
"latest": desktop_latest,
|
|
331
|
+
"update_available": desktop_update,
|
|
332
|
+
"can_update": False,
|
|
333
|
+
"state": "available" if desktop_update else "current",
|
|
334
|
+
"reason": "Desktop updates are distributed separately from the Docker stack.",
|
|
335
|
+
"update_mode": "manual-download",
|
|
336
|
+
"install_kind": "desktop",
|
|
337
|
+
"download_url": str(releases.get("desktop_download_url") or ""),
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def apply_update(target: str) -> dict[str, Any]:
|
|
343
|
+
if os.environ.get("WORKFRAME_ENABLE_ADMIN_UPDATES") != "1":
|
|
344
|
+
raise ValueError("admin_updates_disabled")
|
|
345
|
+
target = str(target or "all").strip().lower()
|
|
346
|
+
if target not in {"hermes", "workframe", "all"}:
|
|
347
|
+
raise ValueError("invalid_update_target")
|
|
348
|
+
if not Path(DOCKER_SOCK).exists():
|
|
349
|
+
raise ValueError("docker_unavailable")
|
|
350
|
+
|
|
351
|
+
scripts: list[str] = []
|
|
352
|
+
if target in {"hermes", "all"}:
|
|
353
|
+
script = _script_path("apply-update-hermes.sh")
|
|
354
|
+
if not script:
|
|
355
|
+
raise ValueError("update_script_missing:hermes")
|
|
356
|
+
scripts.append(str(script))
|
|
357
|
+
if target in {"workframe", "all"}:
|
|
358
|
+
script = _script_path("apply-update-workframe.sh")
|
|
359
|
+
if not script:
|
|
360
|
+
raise ValueError("update_script_missing:workframe")
|
|
361
|
+
scripts.append(str(script))
|
|
362
|
+
|
|
363
|
+
env = os.environ.copy()
|
|
364
|
+
env.setdefault("WORKFRAME_COMPOSE_DIR", str(_compose_dir()))
|
|
365
|
+
env.setdefault("WORKFRAME_PROJECT_ROOT", str(_project_root()))
|
|
366
|
+
|
|
367
|
+
logs: list[str] = []
|
|
368
|
+
for script in scripts:
|
|
369
|
+
proc = subprocess.run(
|
|
370
|
+
["bash", script],
|
|
371
|
+
capture_output=True,
|
|
372
|
+
text=True,
|
|
373
|
+
timeout=900,
|
|
374
|
+
env=env,
|
|
375
|
+
cwd=env["WORKFRAME_COMPOSE_DIR"],
|
|
376
|
+
)
|
|
377
|
+
logs.append(f"=== {script} (exit {proc.returncode}) ===\n{proc.stdout}\n{proc.stderr}")
|
|
378
|
+
if proc.returncode != 0:
|
|
379
|
+
raise ValueError(f"update_failed:{Path(script).name}")
|
|
380
|
+
|
|
381
|
+
return {"ok": True, "target": target, "log": "\n".join(logs)[-12000:]}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def restart_gateway() -> dict[str, Any]:
|
|
385
|
+
if os.environ.get("WORKFRAME_ENABLE_ADMIN_UPDATES") != "1":
|
|
386
|
+
raise ValueError("admin_updates_disabled")
|
|
387
|
+
if not Path(DOCKER_SOCK).exists():
|
|
388
|
+
raise ValueError("docker_unavailable")
|
|
389
|
+
docker_apply_ok, docker_apply_reason = _docker_apply_ready()
|
|
390
|
+
if not docker_apply_ok:
|
|
391
|
+
raise ValueError(str(docker_apply_reason or "docker_apply_unavailable"))
|
|
392
|
+
script = _script_path("restart-gateway-hermes.sh")
|
|
393
|
+
if not script:
|
|
394
|
+
raise ValueError("restart_script_missing:gateway")
|
|
395
|
+
|
|
396
|
+
env = os.environ.copy()
|
|
397
|
+
env.setdefault("WORKFRAME_COMPOSE_DIR", str(_compose_dir()))
|
|
398
|
+
env.setdefault("WORKFRAME_PROJECT_ROOT", str(_project_root()))
|
|
399
|
+
proc = subprocess.run(
|
|
400
|
+
["bash", str(script)],
|
|
401
|
+
capture_output=True,
|
|
402
|
+
text=True,
|
|
403
|
+
timeout=300,
|
|
404
|
+
env=env,
|
|
405
|
+
cwd=env["WORKFRAME_COMPOSE_DIR"],
|
|
406
|
+
)
|
|
407
|
+
log = f"=== {script} (exit {proc.returncode}) ===\n{proc.stdout}\n{proc.stderr}"
|
|
408
|
+
if proc.returncode != 0:
|
|
409
|
+
raise ValueError("restart_failed:gateway")
|
|
410
|
+
return {"ok": True, "target": "gateway", "log": log[-12000:]}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
assert _version_lt("0.1.0", "0.1.1")
|
|
415
|
+
assert not _version_lt("0.1.0", "0.1.0")
|
|
416
|
+
assert parse_hermes_version_output("Hermes Agent v0.17.0 (2026.6.19)") == "0.17.0"
|
|
417
|
+
print("updates module ok")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Vault master key (KEK) — in-memory only; separate from ZK_AUTH_ENCRYPTION_KEY."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import secrets
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
14
|
+
|
|
15
|
+
DATA_DIR = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data"))
|
|
16
|
+
VAULT_KEK_FILE = DATA_DIR / ".vault_kek"
|
|
17
|
+
META_TABLE = "vault_meta"
|
|
18
|
+
|
|
19
|
+
_KEK: bytes | None = None
|
|
20
|
+
_PBKDF2_ROUNDS = 200_000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _b64e(raw: bytes) -> str:
|
|
24
|
+
return base64.b64encode(raw).decode("ascii")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _b64d(value: str) -> bytes:
|
|
28
|
+
raw = base64.b64decode(str(value or "").strip())
|
|
29
|
+
if len(raw) != 32:
|
|
30
|
+
raise ValueError("vault KEK must be 32 bytes")
|
|
31
|
+
return raw
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _derive_wrap_key(passphrase: str, salt: bytes) -> bytes:
|
|
35
|
+
phrase = str(passphrase or "")
|
|
36
|
+
if len(phrase) < 12:
|
|
37
|
+
raise ValueError("passphrase must be at least 12 characters")
|
|
38
|
+
return hashlib.pbkdf2_hmac("sha256", phrase.encode("utf-8"), salt, _PBKDF2_ROUNDS, dklen=32)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _aes_gcm_encrypt(key: bytes, plaintext: bytes) -> dict[str, str]:
|
|
42
|
+
iv = os.urandom(12)
|
|
43
|
+
ct = AESGCM(key).encrypt(iv, plaintext, None)
|
|
44
|
+
return {
|
|
45
|
+
"v": 1,
|
|
46
|
+
"alg": "AES-256-GCM",
|
|
47
|
+
"iv": _b64e(iv),
|
|
48
|
+
"tag": _b64e(ct[-16:]),
|
|
49
|
+
"ciphertext": _b64e(ct[:-16]),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _aes_gcm_decrypt(key: bytes, payload: dict[str, Any]) -> bytes:
|
|
54
|
+
if int(payload.get("v") or 0) != 1 or str(payload.get("alg") or "") != "AES-256-GCM":
|
|
55
|
+
raise ValueError("unsupported wrap payload")
|
|
56
|
+
iv = base64.b64decode(str(payload["iv"]))
|
|
57
|
+
tag = base64.b64decode(str(payload["tag"]))
|
|
58
|
+
ct = base64.b64decode(str(payload["ciphertext"]))
|
|
59
|
+
return AESGCM(key).decrypt(iv, ct + tag, None)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def kek_in_memory() -> bool:
|
|
63
|
+
return _KEK is not None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_kek() -> bytes:
|
|
67
|
+
if _KEK is None:
|
|
68
|
+
raise RuntimeError("vault_sealed")
|
|
69
|
+
return _KEK
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def set_kek(raw: bytes) -> None:
|
|
73
|
+
global _KEK
|
|
74
|
+
if len(raw) != 32:
|
|
75
|
+
raise ValueError("KEK must be 32 bytes")
|
|
76
|
+
_KEK = bytes(raw)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def clear_kek() -> None:
|
|
80
|
+
global _KEK
|
|
81
|
+
_KEK = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_kek_from_env() -> bool:
|
|
85
|
+
raw = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
|
|
86
|
+
if not raw:
|
|
87
|
+
return False
|
|
88
|
+
set_kek(_b64d(raw))
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_kek_from_file() -> bool:
|
|
93
|
+
if not VAULT_KEK_FILE.is_file():
|
|
94
|
+
return False
|
|
95
|
+
try:
|
|
96
|
+
text = VAULT_KEK_FILE.read_text(encoding="utf-8").strip()
|
|
97
|
+
payload = json.loads(text) if text.startswith("{") else {"key": text}
|
|
98
|
+
key_b64 = str(payload.get("key") or text).strip()
|
|
99
|
+
set_kek(_b64d(key_b64))
|
|
100
|
+
return True
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def persist_kek_file() -> None:
|
|
106
|
+
"""Persist in-memory KEK for single-tenant bootstrap (chmod 600)."""
|
|
107
|
+
if _KEK is None:
|
|
108
|
+
return
|
|
109
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
VAULT_KEK_FILE.write_text(
|
|
111
|
+
json.dumps({"v": 1, "key": _b64e(_KEK)}),
|
|
112
|
+
encoding="utf-8",
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
os.chmod(VAULT_KEK_FILE, 0o600)
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def generate_and_persist_kek() -> bytes:
|
|
121
|
+
set_kek(os.urandom(32))
|
|
122
|
+
persist_kek_file()
|
|
123
|
+
return get_kek()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def wrap_kek_for_passphrase(passphrase: str) -> tuple[str, str]:
|
|
127
|
+
"""Return (salt_b64, wrapped_kek_json) for vault_meta."""
|
|
128
|
+
if _KEK is None:
|
|
129
|
+
raise RuntimeError("vault_sealed")
|
|
130
|
+
salt = os.urandom(16)
|
|
131
|
+
wrap_key = _derive_wrap_key(passphrase, salt)
|
|
132
|
+
wrapped = _aes_gcm_encrypt(wrap_key, _KEK)
|
|
133
|
+
return _b64e(salt), json.dumps(wrapped)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def unwrap_kek_from_passphrase(passphrase: str, salt_b64: str, wrapped_json: str) -> bytes:
|
|
137
|
+
salt = base64.b64decode(str(salt_b64 or "").strip())
|
|
138
|
+
wrap_key = _derive_wrap_key(passphrase, salt)
|
|
139
|
+
payload = json.loads(wrapped_json)
|
|
140
|
+
return _aes_gcm_decrypt(wrap_key, payload)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def unseal_for_tests() -> None:
|
|
144
|
+
"""Tests: deterministic KEK without touching disk."""
|
|
145
|
+
seed = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
|
|
146
|
+
if seed:
|
|
147
|
+
set_kek(_b64d(seed))
|
|
148
|
+
return
|
|
149
|
+
set_kek(hashlib.sha256(b"workframe-test-vault-kek").digest())
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
generate_and_persist_kek()
|
|
154
|
+
assert kek_in_memory()
|
|
155
|
+
salt, wrapped = wrap_kek_for_passphrase("test-passphrase-12")
|
|
156
|
+
clear_kek()
|
|
157
|
+
set_kek(unwrap_kek_from_passphrase("test-passphrase-12", salt, wrapped))
|
|
158
|
+
assert kek_in_memory()
|
|
159
|
+
print("vault_kek ok")
|