create-workframe 0.1.0 → 0.1.2
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/LICENSE +201 -201
- package/NOTICE +12 -12
- package/README.md +8 -92
- package/SECURITY.md +38 -40
- package/bin/workframe.js +329 -329
- package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +1 -1
- package/docs/workspace-instructions/WORKFRAME_ROUTING.md +8 -8
- package/package.json +3 -6
- package/profiles/architect/AGENTS.md +29 -29
- package/profiles/architect/SOUL.md +2 -2
- package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/designer/AGENTS.md +26 -26
- package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/dev/AGENTS.md +28 -28
- package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/docs/AGENTS.md +27 -27
- package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/research/AGENTS.md +26 -26
- package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/visionary/AGENTS.md +25 -25
- package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/workframe-agent/AGENTS.md +37 -37
- package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
- package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
- package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
- package/rules/workspace-README.md +5 -5
- package/scripts/bundle-workframe-ui.mjs +3 -3
- package/scripts/ensure-compose-host-paths.mjs +51 -51
- package/scripts/lib/install-identity.mjs +212 -212
- package/scripts/set-compose-public-url.mjs +92 -92
- package/scripts/sync-canonical-to-package.mjs +27 -9
- package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
- package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
- package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
- package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
- package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
- package/workframe-api/README.md +26 -28
- package/workframe-api/action_proxy.py +131 -131
- package/workframe-api/auth_rate_limit.py +49 -49
- package/workframe-api/credential_vault.py +445 -445
- package/workframe-api/data/avatar-catalog.json +41 -41
- package/workframe-api/email_sender.py +220 -220
- package/workframe-api/google_auth.py +90 -90
- package/workframe-api/install_api.py +359 -359
- package/workframe-api/internal_proxy_auth.py +150 -150
- package/workframe-api/llm_proxy.py +277 -277
- package/workframe-api/oidc_jwt.py +108 -108
- package/workframe-api/package.json +12 -13
- package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
- package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
- package/workframe-api/requirements.txt +2 -2
- package/workframe-api/site_meta.py +271 -271
- package/workframe-api/stack_config.py +427 -427
- package/workframe-api/time-bind-chat.py +99 -99
- package/workframe-api/turn_credentials.py +226 -226
- package/workframe-api/updates.py +417 -417
- package/workframe-api/vault_kek.py +159 -159
- package/workframe-api/zk_auth.py +633 -633
- package/workframe-supervisor/Dockerfile +11 -11
- package/workframe-supervisor/server.py +787 -787
- package/workframe-ui/docker/nginx.conf +85 -85
- package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
- package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
- package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
- package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
- package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
- package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
- package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
- package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
- package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
- package/workframe-ui/public/favicon.svg +7 -7
- package/workframe-ui/public/index.html +50 -50
- package/workframe-ui/public/workframe-config.json +3 -3
- package/scripts/security_audit.py +0 -156
- package/scripts/test-scaffold.mjs +0 -390
- package/workframe-api/tests/__init__.py +0 -0
- package/workframe-api/tests/db_setup.py +0 -13
- package/workframe-api/tests/test_admin_updates_gated.py +0 -30
- package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
- package/workframe-api/tests/test_agent_profile_sync.py +0 -76
- package/workframe-api/tests/test_auth_email.py +0 -222
- package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
- package/workframe-api/tests/test_auth_rate_limit.py +0 -19
- package/workframe-api/tests/test_avatar_resolve.py +0 -77
- package/workframe-api/tests/test_child_soul_template.py +0 -71
- package/workframe-api/tests/test_credential_canary.py +0 -135
- package/workframe-api/tests/test_credential_isolation.py +0 -448
- package/workframe-api/tests/test_credential_resolution.py +0 -206
- package/workframe-api/tests/test_device_oauth.py +0 -108
- package/workframe-api/tests/test_doctor_repair.py +0 -103
- package/workframe-api/tests/test_ensure_profile_api.py +0 -77
- package/workframe-api/tests/test_gateway_compose_security.py +0 -136
- package/workframe-api/tests/test_install_secure_host.py +0 -39
- package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
- package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
- package/workframe-api/tests/test_kanban_delegation.py +0 -185
- package/workframe-api/tests/test_llm_proxy.py +0 -155
- package/workframe-api/tests/test_login_access_policy.py +0 -183
- package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
- package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
- package/workframe-api/tests/test_platform_auth.py +0 -47
- package/workframe-api/tests/test_profile_config_path.py +0 -56
- package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
- package/workframe-api/tests/test_profile_create.py +0 -72
- package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
- package/workframe-api/tests/test_profile_install_health.py +0 -45
- package/workframe-api/tests/test_profile_secret_policy.py +0 -57
- package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
- package/workframe-api/tests/test_provider_bootstrap.py +0 -75
- package/workframe-api/tests/test_provider_connect.py +0 -54
- package/workframe-api/tests/test_room_crud.py +0 -192
- package/workframe-api/tests/test_room_tenancy.py +0 -701
- package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
- package/workframe-api/tests/test_site_meta.py +0 -81
- package/workframe-api/tests/test_soul_stub.py +0 -42
- package/workframe-api/tests/test_space_member_sync.py +0 -99
- package/workframe-api/tests/test_stripe_stack_config.py +0 -37
- package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
- package/workframe-api/tests/test_turn_credential_vault.py +0 -125
- package/workframe-api/tests/test_updates.py +0 -176
- package/workframe-api/tests/test_user_cohort.py +0 -113
- package/workframe-api/tests/test_vault_envelope.py +0 -110
- package/workframe-api/tests/test_workspace_members.py +0 -183
- package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
- package/workframe-api/tests/test_workspace_provider_list.py +0 -57
- package/workframe-supervisor/tests/test_exec_guard.py +0 -42
- package/workframe-supervisor/tests/test_server_import.py +0 -21
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
- package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
- package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +0 -1
|
@@ -1,150 +1,150 @@
|
|
|
1
|
-
"""Shared auth for /internal/llm and /internal/action — network + optional proxy token."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import hmac
|
|
6
|
-
import ipaddress
|
|
7
|
-
import os
|
|
8
|
-
import secrets
|
|
9
|
-
from http.server import BaseHTTPRequestHandler
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
PROXY_TOKEN_ENV = "WORKFRAME_PROXY_TOKEN"
|
|
13
|
-
PROXY_TOKEN_HEADER = "X-Workframe-Proxy-Token"
|
|
14
|
-
PROFILE_HEADER = "X-Workframe-Profile"
|
|
15
|
-
PROXY_TOKEN_FILE = ".proxy_token"
|
|
16
|
-
SHARED_PROXY_TOKEN_PATH = Path("/run/workframe-proxy/token")
|
|
17
|
-
|
|
18
|
-
_TOKEN: str | None = None
|
|
19
|
-
_TOKEN_PATH: Path | None = None
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _data_dir() -> Path:
|
|
23
|
-
return Path(
|
|
24
|
-
os.environ.get("WORKFRAME_API_DATA_DIR")
|
|
25
|
-
or os.environ.get("MISSION_DATA_DIR")
|
|
26
|
-
or Path(__file__).resolve().parent / "data"
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _deployment_mode() -> str:
|
|
31
|
-
return (os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "trusted_team").strip().lower()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def proxy_token_configured() -> bool:
|
|
35
|
-
return bool(_loaded_proxy_token())
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _loaded_proxy_token() -> str:
|
|
39
|
-
global _TOKEN, _TOKEN_PATH
|
|
40
|
-
if _TOKEN is not None:
|
|
41
|
-
return _TOKEN
|
|
42
|
-
env = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
|
|
43
|
-
if env:
|
|
44
|
-
_TOKEN = env
|
|
45
|
-
return _TOKEN
|
|
46
|
-
path = _data_dir() / PROXY_TOKEN_FILE
|
|
47
|
-
_TOKEN_PATH = path
|
|
48
|
-
if path.is_file():
|
|
49
|
-
_TOKEN = path.read_text(encoding="utf-8").strip()
|
|
50
|
-
return _TOKEN
|
|
51
|
-
_TOKEN = ""
|
|
52
|
-
return ""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _sync_shared_proxy_token(token: str) -> None:
|
|
56
|
-
value = str(token or "").strip()
|
|
57
|
-
if not value:
|
|
58
|
-
return
|
|
59
|
-
try:
|
|
60
|
-
SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
-
SHARED_PROXY_TOKEN_PATH.write_text(value + "\n", encoding="utf-8")
|
|
62
|
-
except OSError:
|
|
63
|
-
pass
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def bootstrap_proxy_token(*, allow_generate_file: bool = True) -> str:
|
|
67
|
-
"""Load proxy token from env or data dir; optionally create file for local dogfood."""
|
|
68
|
-
global _TOKEN
|
|
69
|
-
existing = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
|
|
70
|
-
if existing:
|
|
71
|
-
_TOKEN = existing
|
|
72
|
-
_sync_shared_proxy_token(existing)
|
|
73
|
-
return existing
|
|
74
|
-
path = _data_dir() / PROXY_TOKEN_FILE
|
|
75
|
-
if path.is_file():
|
|
76
|
-
_TOKEN = path.read_text(encoding="utf-8").strip()
|
|
77
|
-
_sync_shared_proxy_token(_TOKEN)
|
|
78
|
-
return _TOKEN
|
|
79
|
-
if not allow_generate_file or _deployment_mode() == "public_multi_user":
|
|
80
|
-
_TOKEN = ""
|
|
81
|
-
return ""
|
|
82
|
-
token = secrets.token_urlsafe(32)
|
|
83
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
-
path.write_text(token + "\n", encoding="utf-8")
|
|
85
|
-
# ponytail: gateway reads same token from shared compose volume when env unset
|
|
86
|
-
try:
|
|
87
|
-
SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
-
SHARED_PROXY_TOKEN_PATH.write_text(token + "\n", encoding="utf-8")
|
|
89
|
-
except OSError:
|
|
90
|
-
pass
|
|
91
|
-
_TOKEN = token
|
|
92
|
-
return token
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def reset_proxy_token_for_tests() -> None:
|
|
96
|
-
global _TOKEN, _TOKEN_PATH
|
|
97
|
-
_TOKEN = None
|
|
98
|
-
_TOKEN_PATH = None
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _client_ip(handler: BaseHTTPRequestHandler) -> str:
|
|
102
|
-
return str(handler.client_address[0] if handler.client_address else "").strip()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def is_internal_client(host: str) -> bool:
|
|
106
|
-
"""Private/docker callers only — not routable public addresses."""
|
|
107
|
-
addr = str(host or "").strip()
|
|
108
|
-
if not addr:
|
|
109
|
-
return False
|
|
110
|
-
if addr in {"127.0.0.1", "::1", "localhost"}:
|
|
111
|
-
return True
|
|
112
|
-
try:
|
|
113
|
-
ip = ipaddress.ip_address(addr.split("%", 1)[0])
|
|
114
|
-
except ValueError:
|
|
115
|
-
return False
|
|
116
|
-
return ip.is_private or ip.is_loopback
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _header_token(handler: BaseHTTPRequestHandler) -> str:
|
|
120
|
-
return str(handler.headers.get(PROXY_TOKEN_HEADER) or "").strip()
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def authorize_internal_proxy(handler: BaseHTTPRequestHandler) -> tuple[bool, str]:
|
|
124
|
-
"""Return (ok, error_code)."""
|
|
125
|
-
if not is_internal_client(_client_ip(handler)):
|
|
126
|
-
return False, "internal only"
|
|
127
|
-
expected = _loaded_proxy_token()
|
|
128
|
-
if not expected:
|
|
129
|
-
return True, ""
|
|
130
|
-
got = _header_token(handler)
|
|
131
|
-
if not got or not hmac.compare_digest(got, expected):
|
|
132
|
-
return False, "proxy token required"
|
|
133
|
-
return True, ""
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if __name__ == "__main__":
|
|
137
|
-
reset_proxy_token_for_tests()
|
|
138
|
-
os.environ[PROXY_TOKEN_ENV] = "test-proxy-token"
|
|
139
|
-
assert proxy_token_configured()
|
|
140
|
-
|
|
141
|
-
class _H:
|
|
142
|
-
client_address = ("172.19.0.2", 1234)
|
|
143
|
-
headers = {PROXY_TOKEN_HEADER: "test-proxy-token"}
|
|
144
|
-
|
|
145
|
-
ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
|
|
146
|
-
assert ok and not err
|
|
147
|
-
_H.headers = {}
|
|
148
|
-
ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
|
|
149
|
-
assert not ok and err == "proxy token required"
|
|
150
|
-
print("internal_proxy_auth ok")
|
|
1
|
+
"""Shared auth for /internal/llm and /internal/action — network + optional proxy token."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hmac
|
|
6
|
+
import ipaddress
|
|
7
|
+
import os
|
|
8
|
+
import secrets
|
|
9
|
+
from http.server import BaseHTTPRequestHandler
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
PROXY_TOKEN_ENV = "WORKFRAME_PROXY_TOKEN"
|
|
13
|
+
PROXY_TOKEN_HEADER = "X-Workframe-Proxy-Token"
|
|
14
|
+
PROFILE_HEADER = "X-Workframe-Profile"
|
|
15
|
+
PROXY_TOKEN_FILE = ".proxy_token"
|
|
16
|
+
SHARED_PROXY_TOKEN_PATH = Path("/run/workframe-proxy/token")
|
|
17
|
+
|
|
18
|
+
_TOKEN: str | None = None
|
|
19
|
+
_TOKEN_PATH: Path | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _data_dir() -> Path:
|
|
23
|
+
return Path(
|
|
24
|
+
os.environ.get("WORKFRAME_API_DATA_DIR")
|
|
25
|
+
or os.environ.get("MISSION_DATA_DIR")
|
|
26
|
+
or Path(__file__).resolve().parent / "data"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _deployment_mode() -> str:
|
|
31
|
+
return (os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "trusted_team").strip().lower()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def proxy_token_configured() -> bool:
|
|
35
|
+
return bool(_loaded_proxy_token())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _loaded_proxy_token() -> str:
|
|
39
|
+
global _TOKEN, _TOKEN_PATH
|
|
40
|
+
if _TOKEN is not None:
|
|
41
|
+
return _TOKEN
|
|
42
|
+
env = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
|
|
43
|
+
if env:
|
|
44
|
+
_TOKEN = env
|
|
45
|
+
return _TOKEN
|
|
46
|
+
path = _data_dir() / PROXY_TOKEN_FILE
|
|
47
|
+
_TOKEN_PATH = path
|
|
48
|
+
if path.is_file():
|
|
49
|
+
_TOKEN = path.read_text(encoding="utf-8").strip()
|
|
50
|
+
return _TOKEN
|
|
51
|
+
_TOKEN = ""
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sync_shared_proxy_token(token: str) -> None:
|
|
56
|
+
value = str(token or "").strip()
|
|
57
|
+
if not value:
|
|
58
|
+
return
|
|
59
|
+
try:
|
|
60
|
+
SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
SHARED_PROXY_TOKEN_PATH.write_text(value + "\n", encoding="utf-8")
|
|
62
|
+
except OSError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def bootstrap_proxy_token(*, allow_generate_file: bool = True) -> str:
|
|
67
|
+
"""Load proxy token from env or data dir; optionally create file for local dogfood."""
|
|
68
|
+
global _TOKEN
|
|
69
|
+
existing = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
|
|
70
|
+
if existing:
|
|
71
|
+
_TOKEN = existing
|
|
72
|
+
_sync_shared_proxy_token(existing)
|
|
73
|
+
return existing
|
|
74
|
+
path = _data_dir() / PROXY_TOKEN_FILE
|
|
75
|
+
if path.is_file():
|
|
76
|
+
_TOKEN = path.read_text(encoding="utf-8").strip()
|
|
77
|
+
_sync_shared_proxy_token(_TOKEN)
|
|
78
|
+
return _TOKEN
|
|
79
|
+
if not allow_generate_file or _deployment_mode() == "public_multi_user":
|
|
80
|
+
_TOKEN = ""
|
|
81
|
+
return ""
|
|
82
|
+
token = secrets.token_urlsafe(32)
|
|
83
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
path.write_text(token + "\n", encoding="utf-8")
|
|
85
|
+
# ponytail: gateway reads same token from shared compose volume when env unset
|
|
86
|
+
try:
|
|
87
|
+
SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
SHARED_PROXY_TOKEN_PATH.write_text(token + "\n", encoding="utf-8")
|
|
89
|
+
except OSError:
|
|
90
|
+
pass
|
|
91
|
+
_TOKEN = token
|
|
92
|
+
return token
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def reset_proxy_token_for_tests() -> None:
|
|
96
|
+
global _TOKEN, _TOKEN_PATH
|
|
97
|
+
_TOKEN = None
|
|
98
|
+
_TOKEN_PATH = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _client_ip(handler: BaseHTTPRequestHandler) -> str:
|
|
102
|
+
return str(handler.client_address[0] if handler.client_address else "").strip()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_internal_client(host: str) -> bool:
|
|
106
|
+
"""Private/docker callers only — not routable public addresses."""
|
|
107
|
+
addr = str(host or "").strip()
|
|
108
|
+
if not addr:
|
|
109
|
+
return False
|
|
110
|
+
if addr in {"127.0.0.1", "::1", "localhost"}:
|
|
111
|
+
return True
|
|
112
|
+
try:
|
|
113
|
+
ip = ipaddress.ip_address(addr.split("%", 1)[0])
|
|
114
|
+
except ValueError:
|
|
115
|
+
return False
|
|
116
|
+
return ip.is_private or ip.is_loopback
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _header_token(handler: BaseHTTPRequestHandler) -> str:
|
|
120
|
+
return str(handler.headers.get(PROXY_TOKEN_HEADER) or "").strip()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def authorize_internal_proxy(handler: BaseHTTPRequestHandler) -> tuple[bool, str]:
|
|
124
|
+
"""Return (ok, error_code)."""
|
|
125
|
+
if not is_internal_client(_client_ip(handler)):
|
|
126
|
+
return False, "internal only"
|
|
127
|
+
expected = _loaded_proxy_token()
|
|
128
|
+
if not expected:
|
|
129
|
+
return True, ""
|
|
130
|
+
got = _header_token(handler)
|
|
131
|
+
if not got or not hmac.compare_digest(got, expected):
|
|
132
|
+
return False, "proxy token required"
|
|
133
|
+
return True, ""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
reset_proxy_token_for_tests()
|
|
138
|
+
os.environ[PROXY_TOKEN_ENV] = "test-proxy-token"
|
|
139
|
+
assert proxy_token_configured()
|
|
140
|
+
|
|
141
|
+
class _H:
|
|
142
|
+
client_address = ("172.19.0.2", 1234)
|
|
143
|
+
headers = {PROXY_TOKEN_HEADER: "test-proxy-token"}
|
|
144
|
+
|
|
145
|
+
ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
|
|
146
|
+
assert ok and not err
|
|
147
|
+
_H.headers = {}
|
|
148
|
+
ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
|
|
149
|
+
assert not ok and err == "proxy token required"
|
|
150
|
+
print("internal_proxy_auth ok")
|