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,108 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
SERVER_PATH = Path(__file__).resolve().parents[1] / "server.py"
|
|
10
|
-
spec = importlib.util.spec_from_file_location("workframe_server", SERVER_PATH)
|
|
11
|
-
server = importlib.util.module_from_spec(spec)
|
|
12
|
-
assert spec.loader is not None
|
|
13
|
-
spec.loader.exec_module(server)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@pytest.fixture()
|
|
17
|
-
def user_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> str:
|
|
18
|
-
monkeypatch.setattr(server, "HERMES_DATA", tmp_path)
|
|
19
|
-
user_id = "user-oauth-1"
|
|
20
|
-
return user_id
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_parse_device_oauth_log_extracts_uri_and_code() -> None:
|
|
24
|
-
sample = (
|
|
25
|
-
"To continue, follow these steps:\n\n"
|
|
26
|
-
" 1. Open this URL in your browser:\n"
|
|
27
|
-
" \x1b[94mhttps://auth.openai.com/codex/device\x1b[0m\n\n"
|
|
28
|
-
" 2. Enter this code:\n"
|
|
29
|
-
" \x1b[94mABCD-1234\x1b[0m\n\n"
|
|
30
|
-
"Waiting for sign-in...\n"
|
|
31
|
-
)
|
|
32
|
-
parsed = server._parse_device_oauth_log(sample)
|
|
33
|
-
assert parsed["verification_uri"] == "https://auth.openai.com/codex/device"
|
|
34
|
-
assert parsed["user_code"] == "ABCD-1234"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_hermes_oauth_tokens_present_reads_providers_block(user_env: str) -> None:
|
|
38
|
-
auth_path = server._user_hermes_auth_path(user_env)
|
|
39
|
-
auth_path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
-
auth_path.write_text(
|
|
41
|
-
json.dumps(
|
|
42
|
-
{
|
|
43
|
-
"providers": {
|
|
44
|
-
"openai-codex": {
|
|
45
|
-
"tokens": {"access_token": "tok", "refresh_token": "ref"},
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
),
|
|
50
|
-
encoding="utf-8",
|
|
51
|
-
)
|
|
52
|
-
assert server._hermes_oauth_tokens_present(user_env, "openai-codex") is True
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_list_user_providers_marks_codex_connected_from_auth_json(user_env: str) -> None:
|
|
56
|
-
auth_path = server._user_hermes_auth_path(user_env)
|
|
57
|
-
auth_path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
-
auth_path.write_text(
|
|
59
|
-
json.dumps(
|
|
60
|
-
{
|
|
61
|
-
"providers": {
|
|
62
|
-
"openai-codex": {
|
|
63
|
-
"tokens": {"access_token": "tok"},
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
),
|
|
68
|
-
encoding="utf-8",
|
|
69
|
-
)
|
|
70
|
-
payload = server.list_user_providers(user_env)
|
|
71
|
-
codex = next(row for row in payload["providers"] if row["id"] == "codex")
|
|
72
|
-
assert codex["connected"] is True
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def test_load_user_hermes_auth_reads_gateway_fallback(user_env: str, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
76
|
-
monkeypatch.setattr(server, "_read_gateway_data_file", lambda rel: json.dumps({
|
|
77
|
-
"providers": {"openai-codex": {"tokens": {"access_token": "tok"}}},
|
|
78
|
-
}))
|
|
79
|
-
loaded = server._load_user_hermes_auth(user_env)
|
|
80
|
-
assert isinstance(loaded, dict)
|
|
81
|
-
assert server._hermes_oauth_tokens_present(user_env, "openai-codex") is True
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def test_spawn_hermes_device_oauth_uses_detached_exec(user_env: str, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
85
|
-
calls: list[list[str]] = []
|
|
86
|
-
|
|
87
|
-
def fake_detached(cmd: list[str]) -> tuple[int, str]:
|
|
88
|
-
calls.append(cmd)
|
|
89
|
-
return 0, ""
|
|
90
|
-
|
|
91
|
-
monkeypatch.setattr(server, "_gateway_container_exec_detached", fake_detached)
|
|
92
|
-
rc, _ = server._spawn_hermes_device_oauth(user_env, "openai-codex", "/opt/data/profiles/x/.oauth.log")
|
|
93
|
-
assert rc == 0
|
|
94
|
-
assert calls
|
|
95
|
-
joined = " ".join(calls[0])
|
|
96
|
-
assert "auth add openai-codex" in joined
|
|
97
|
-
assert "su -s /bin/sh hermes" in joined
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def test_list_user_providers_marks_deepseek_connected_from_env(user_env: str) -> None:
|
|
101
|
-
env_path = server._user_hermes_env_path(user_env)
|
|
102
|
-
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
-
env_path.write_text("DEEPSEEK_API_KEY=sk-deep\n", encoding="utf-8")
|
|
104
|
-
|
|
105
|
-
payload = server.list_user_providers(user_env)
|
|
106
|
-
deepseek = next(row for row in payload["providers"] if row["id"] == "deepseek")
|
|
107
|
-
|
|
108
|
-
assert deepseek["connected"] is True
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""Explicit doctor repair — opt-in runtime provisioning for agent DM rooms."""
|
|
2
|
-
import os
|
|
3
|
-
import tempfile
|
|
4
|
-
import unittest
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from unittest import mock
|
|
7
|
-
|
|
8
|
-
import server
|
|
9
|
-
from db_setup import ensure_workframe_schemas
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DoctorRepairTests(unittest.TestCase):
|
|
13
|
-
def setUp(self) -> None:
|
|
14
|
-
self.tmp = tempfile.TemporaryDirectory()
|
|
15
|
-
self.addCleanup(self.tmp.cleanup)
|
|
16
|
-
data = Path(self.tmp.name) / "data"
|
|
17
|
-
data.mkdir()
|
|
18
|
-
self.patches = [
|
|
19
|
-
mock.patch.object(server, "DATA_DIR", data),
|
|
20
|
-
mock.patch.object(server, "AUTH_DB_PATH", data / "auth.db"),
|
|
21
|
-
mock.patch.object(server, "_workframe_db_path", return_value=data / "workframe.db"),
|
|
22
|
-
mock.patch.dict(os.environ, {"WORKFRAME_PROJECT": "Workframe"}, clear=False),
|
|
23
|
-
]
|
|
24
|
-
for patch in self.patches:
|
|
25
|
-
patch.start()
|
|
26
|
-
self.addCleanup(patch.stop)
|
|
27
|
-
ensure_workframe_schemas()
|
|
28
|
-
self.workspace_id = "ws-1"
|
|
29
|
-
self.user_id = "user-1"
|
|
30
|
-
self.agent_id = "a0000000-0000-4000-8000-000000000001"
|
|
31
|
-
self.agent_slug = "workframe-agent"
|
|
32
|
-
self.room_id = "room-1"
|
|
33
|
-
conn = server._workframe_db()
|
|
34
|
-
try:
|
|
35
|
-
now = "1"
|
|
36
|
-
conn.execute(
|
|
37
|
-
"INSERT INTO workspaces (id, slug, display_name, owner_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
|
|
38
|
-
(self.workspace_id, "default", "Workframe", self.user_id, "active", now, now),
|
|
39
|
-
)
|
|
40
|
-
conn.execute(
|
|
41
|
-
"""
|
|
42
|
-
INSERT INTO agent_profiles (id, workspace_id, slug, display_name, status, created_at, updated_at)
|
|
43
|
-
VALUES (?, ?, ?, ?, 'available', ?, ?)
|
|
44
|
-
""",
|
|
45
|
-
(self.agent_id, self.workspace_id, self.agent_slug, "Agent", now, now),
|
|
46
|
-
)
|
|
47
|
-
conn.execute(
|
|
48
|
-
"""
|
|
49
|
-
INSERT INTO rooms (
|
|
50
|
-
id, workspace_id, agent_profile_id, name, slug, room_type, status, created_at, updated_at
|
|
51
|
-
) VALUES (?, ?, ?, 'Agent', 'dm-agent', 'direct', 'active', ?, ?)
|
|
52
|
-
""",
|
|
53
|
-
(self.room_id, self.workspace_id, self.agent_id, now, now),
|
|
54
|
-
)
|
|
55
|
-
conn.execute(
|
|
56
|
-
"""
|
|
57
|
-
INSERT INTO room_memberships (id, room_id, user_id, role, status, joined_at, updated_at)
|
|
58
|
-
VALUES ('rm-1', ?, ?, 'member', 'active', ?, ?)
|
|
59
|
-
""",
|
|
60
|
-
(self.room_id, self.user_id, now, now),
|
|
61
|
-
)
|
|
62
|
-
conn.commit()
|
|
63
|
-
finally:
|
|
64
|
-
conn.close()
|
|
65
|
-
|
|
66
|
-
@mock.patch.object(server, "_runtime_profile_on_disk", return_value=False)
|
|
67
|
-
def test_audit_reports_missing(self, _on_disk) -> None:
|
|
68
|
-
out = server.doctor_audit_agent_dm_runtimes()
|
|
69
|
-
self.assertTrue(out["ok"])
|
|
70
|
-
self.assertEqual(out["total"], 1)
|
|
71
|
-
self.assertEqual(out["present"], 0)
|
|
72
|
-
self.assertEqual(len(out["missing"]), 1)
|
|
73
|
-
self.assertEqual(out["missing"][0]["runtime"], "u-user-1-workframe-agent")
|
|
74
|
-
|
|
75
|
-
def test_resolved_session_title_falls_back_to_room_sessions(self) -> None:
|
|
76
|
-
with mock.patch.object(server, "_session_info", return_value={"title": ""}):
|
|
77
|
-
title = server._resolved_session_title(
|
|
78
|
-
"u-user-1-workframe-agent",
|
|
79
|
-
"sid-1",
|
|
80
|
-
"Session with Workframe Agent (9)",
|
|
81
|
-
)
|
|
82
|
-
self.assertEqual(title, "Session with Workframe Agent (9)")
|
|
83
|
-
|
|
84
|
-
@mock.patch.object(server, "_runtime_profile_on_disk")
|
|
85
|
-
@mock.patch.object(server, "ensure_runtime_profile")
|
|
86
|
-
def test_repair_provisions_missing(self, ensure_runtime, on_disk) -> None:
|
|
87
|
-
on_disk.side_effect = [False, True, True]
|
|
88
|
-
|
|
89
|
-
out = server.doctor_repair_agent_dm_runtimes(repair=True)
|
|
90
|
-
|
|
91
|
-
self.assertTrue(out["ok"])
|
|
92
|
-
self.assertEqual(out["missing_before"], 1)
|
|
93
|
-
self.assertEqual(len(out["repaired"]), 1)
|
|
94
|
-
ensure_runtime.assert_called_once_with(
|
|
95
|
-
"u-user-1-workframe-agent",
|
|
96
|
-
self.agent_slug,
|
|
97
|
-
self.user_id,
|
|
98
|
-
self.workspace_id,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if __name__ == "__main__":
|
|
103
|
-
unittest.main()
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import importlib.util
|
|
2
|
-
import unittest
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from unittest.mock import patch
|
|
5
|
-
|
|
6
|
-
ROOT = Path(__file__).resolve().parents[2]
|
|
7
|
-
API = ROOT / "workframe-api" / "server.py"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _load_api():
|
|
11
|
-
spec = importlib.util.spec_from_file_location("workframe_api", API)
|
|
12
|
-
mod = importlib.util.module_from_spec(spec)
|
|
13
|
-
assert spec and spec.loader
|
|
14
|
-
spec.loader.exec_module(mod)
|
|
15
|
-
return mod
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class EnsureProfileApiTest(unittest.TestCase):
|
|
19
|
-
def test_healthy_profile_skips_gateway_reload(self) -> None:
|
|
20
|
-
api = _load_api()
|
|
21
|
-
with patch.object(api, "resolve_validated_profile", return_value="architect"), patch.object(
|
|
22
|
-
api, "_primary_profile", return_value="workframe-agent"
|
|
23
|
-
), patch.object(api, "_bootstrap_profile_providers") as bootstrap, patch.object(
|
|
24
|
-
api, "_profile_api_healthy", return_value=True
|
|
25
|
-
), patch.object(api, "_gateway_exec") as gateway_exec:
|
|
26
|
-
out = api.ensure_profile_api("architect", "user-1", "ws-1")
|
|
27
|
-
bootstrap.assert_not_called()
|
|
28
|
-
gateway_exec.assert_not_called()
|
|
29
|
-
self.assertFalse(out.get("started"))
|
|
30
|
-
|
|
31
|
-
def test_cold_start_waits_for_health(self) -> None:
|
|
32
|
-
api = _load_api()
|
|
33
|
-
with patch.object(api, "resolve_validated_profile", return_value="architect"), patch.object(
|
|
34
|
-
api, "_primary_profile", return_value="workframe-agent"
|
|
35
|
-
), patch.object(api, "_bootstrap_profile_providers", return_value=False), patch.object(
|
|
36
|
-
api, "_profile_api_healthy", side_effect=[False, False, True]
|
|
37
|
-
), patch.object(
|
|
38
|
-
api, "profile_gateway_lifecycle", return_value={"ok": True, "action": "start"}
|
|
39
|
-
) as start, patch.object(api, "_wait_profile_api_healthy", return_value=True) as wait:
|
|
40
|
-
out = api.ensure_profile_api("architect", "user-1", "ws-1")
|
|
41
|
-
start.assert_called_once_with("architect", "start", bootstrap_providers=True)
|
|
42
|
-
wait.assert_called_once()
|
|
43
|
-
self.assertTrue(out.get("started"))
|
|
44
|
-
|
|
45
|
-
def test_cold_start_skips_bootstrap_when_already_seeded(self) -> None:
|
|
46
|
-
api = _load_api()
|
|
47
|
-
with patch.object(api, "resolve_validated_profile", return_value="architect"), patch.object(
|
|
48
|
-
api, "_primary_profile", return_value="workframe-agent"
|
|
49
|
-
), patch.object(api, "_bootstrap_profile_providers") as bootstrap, patch.object(
|
|
50
|
-
api, "_profile_api_healthy", side_effect=[False, False, True]
|
|
51
|
-
), patch.object(
|
|
52
|
-
api, "profile_gateway_lifecycle", return_value={"ok": True, "action": "start"}
|
|
53
|
-
) as start, patch.object(api, "_wait_profile_api_healthy", return_value=True):
|
|
54
|
-
api.ensure_profile_api("architect", "user-1", "ws-1", bootstrap_providers=False)
|
|
55
|
-
bootstrap.assert_not_called()
|
|
56
|
-
start.assert_called_once_with("architect", "start", bootstrap_providers=False)
|
|
57
|
-
|
|
58
|
-
def test_primary_cold_start_configures_and_restarts_gateway(self) -> None:
|
|
59
|
-
api = _load_api()
|
|
60
|
-
with patch.object(api, "resolve_hermes_profile", return_value="workframe-agent"), patch.object(
|
|
61
|
-
api, "_primary_profile", return_value="workframe-agent"
|
|
62
|
-
), patch.object(api, "_profile_api_port", return_value=8642), patch.object(
|
|
63
|
-
api, "_profile_api_healthy", return_value=False
|
|
64
|
-
), patch.object(
|
|
65
|
-
api, "_configure_profile_api", return_value=(True, "ok", 8642)
|
|
66
|
-
) as configure, patch.object(api, "_restart_stack_gateway", return_value={"ok": True}) as restart, patch.object(
|
|
67
|
-
api, "_wait_profile_api_healthy", return_value=True
|
|
68
|
-
) as wait:
|
|
69
|
-
out = api.ensure_profile_api("workframe-agent", "user-1", "ws-1")
|
|
70
|
-
configure.assert_called_once_with("workframe-agent")
|
|
71
|
-
restart.assert_called_once()
|
|
72
|
-
wait.assert_called_once_with("workframe-agent")
|
|
73
|
-
self.assertTrue(out.get("started"))
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if __name__ == "__main__":
|
|
77
|
-
unittest.main()
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
"""Compose and deployment-mode guards for gateway control-plane isolation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
import unittest
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
COMPOSE_PATH = (
|
|
11
|
-
Path(__file__).resolve().parents[3] / "infra" / "compose" / "workframe" / "docker-compose.yml"
|
|
12
|
-
)
|
|
13
|
-
PUBLIC_COMPOSE_PATH = COMPOSE_PATH.parent / "docker-compose.public.yml"
|
|
14
|
-
|
|
15
|
-
# ponytail: substring match — catches ZK_AUTH_HMAC_KEY, WORKFRAME_SUPERVISOR_TOKEN, etc.
|
|
16
|
-
FORBIDDEN_GATEWAY_ENV_MARKERS = (
|
|
17
|
-
"WORKFRAME_SUPERVISOR_TOKEN",
|
|
18
|
-
"WORKFRAME_API_TOKEN",
|
|
19
|
-
"ZK_AUTH_",
|
|
20
|
-
"SMTP_PASS",
|
|
21
|
-
"WORKFRAME_GITHUB_OAUTH_CLIENT_SECRET",
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _service_block(compose_text: str, service: str) -> str:
|
|
26
|
-
match = re.search(rf"^ {re.escape(service)}:\n", compose_text, re.MULTILINE)
|
|
27
|
-
if not match:
|
|
28
|
-
raise AssertionError(f"service {service!r} not found in compose")
|
|
29
|
-
start = match.start()
|
|
30
|
-
tail = compose_text[start + 1 :]
|
|
31
|
-
next_svc = re.search(r"^ [a-z0-9-]+:\n", tail, re.MULTILINE)
|
|
32
|
-
end = start + 1 + (next_svc.start() if next_svc else len(tail))
|
|
33
|
-
return compose_text[start:end]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class GatewayComposeSecurityTests(unittest.TestCase):
|
|
37
|
-
@classmethod
|
|
38
|
-
def setUpClass(cls) -> None:
|
|
39
|
-
cls.compose_text = COMPOSE_PATH.read_text(encoding="utf-8")
|
|
40
|
-
cls.gateway_block = _service_block(cls.compose_text, "gateway")
|
|
41
|
-
cls.api_block = _service_block(cls.compose_text, "workframe-api")
|
|
42
|
-
cls.supervisor_block = _service_block(cls.compose_text, "workframe-supervisor")
|
|
43
|
-
|
|
44
|
-
def test_gateway_has_no_env_file(self) -> None:
|
|
45
|
-
self.assertNotIn("env_file:", self.gateway_block)
|
|
46
|
-
|
|
47
|
-
def test_gateway_not_on_control_net(self) -> None:
|
|
48
|
-
self.assertIn("workframe-net", self.gateway_block)
|
|
49
|
-
self.assertNotIn("control-net", self.gateway_block)
|
|
50
|
-
|
|
51
|
-
def test_supervisor_only_on_control_net(self) -> None:
|
|
52
|
-
self.assertIn("control-net", self.supervisor_block)
|
|
53
|
-
self.assertNotIn("workframe-net", self.supervisor_block)
|
|
54
|
-
|
|
55
|
-
def test_api_on_both_networks(self) -> None:
|
|
56
|
-
self.assertIn("workframe-net", self.api_block)
|
|
57
|
-
self.assertIn("control-net", self.api_block)
|
|
58
|
-
|
|
59
|
-
def test_gateway_environment_has_no_control_secrets(self) -> None:
|
|
60
|
-
env_section = re.search(r"environment:\n((?: - .+\n)+)", self.gateway_block)
|
|
61
|
-
self.assertIsNotNone(env_section, "gateway environment block missing")
|
|
62
|
-
env_lines = env_section.group(1) if env_section else ""
|
|
63
|
-
for marker in FORBIDDEN_GATEWAY_ENV_MARKERS:
|
|
64
|
-
self.assertNotIn(marker, env_lines, f"gateway env must not reference {marker}")
|
|
65
|
-
|
|
66
|
-
def test_gateway_mounts_proxy_token_volume_not_api_data(self) -> None:
|
|
67
|
-
self.assertIn("workframe-proxy-token:/run/workframe-proxy", self.gateway_block)
|
|
68
|
-
self.assertNotIn("workframe-api-data", self.gateway_block)
|
|
69
|
-
|
|
70
|
-
def test_public_overlay_api_has_no_docker_sock(self) -> None:
|
|
71
|
-
public_text = PUBLIC_COMPOSE_PATH.read_text(encoding="utf-8")
|
|
72
|
-
api_block = _service_block(public_text, "workframe-api")
|
|
73
|
-
self.assertNotIn("/var/run/docker.sock", api_block)
|
|
74
|
-
self.assertNotIn(":/project", api_block)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class HermesDashboardGateTests(unittest.TestCase):
|
|
78
|
-
def test_public_mode_denies_member(self) -> None:
|
|
79
|
-
import server
|
|
80
|
-
from unittest.mock import patch
|
|
81
|
-
|
|
82
|
-
handler = type("H", (), {"auth_user": "u1", "auth_role": "member"})()
|
|
83
|
-
with patch.object(server, "DEPLOYMENT_MODE", "public_multi_user"), patch.object(
|
|
84
|
-
server, "DEV_LOCAL_UNSAFE", False
|
|
85
|
-
):
|
|
86
|
-
self.assertEqual(server._hermes_dashboard_gate_status(handler), 403)
|
|
87
|
-
|
|
88
|
-
def test_public_mode_allows_admin(self) -> None:
|
|
89
|
-
import server
|
|
90
|
-
from unittest.mock import patch
|
|
91
|
-
|
|
92
|
-
handler = type("H", (), {"auth_user": "u1", "auth_role": "admin"})()
|
|
93
|
-
with patch.object(server, "DEPLOYMENT_MODE", "public_multi_user"), patch.object(
|
|
94
|
-
server, "DEV_LOCAL_UNSAFE", False
|
|
95
|
-
):
|
|
96
|
-
self.assertEqual(server._hermes_dashboard_gate_status(handler), 204)
|
|
97
|
-
|
|
98
|
-
def test_single_user_local_allows_anonymous(self) -> None:
|
|
99
|
-
import server
|
|
100
|
-
from unittest.mock import patch
|
|
101
|
-
|
|
102
|
-
handler = type("H", (), {"auth_user": "", "auth_role": ""})()
|
|
103
|
-
with patch.object(server, "DEPLOYMENT_MODE", "single_user_local"):
|
|
104
|
-
self.assertEqual(server._hermes_dashboard_gate_status(handler), 204)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class PublicDeploymentValidationTests(unittest.TestCase):
|
|
108
|
-
def test_public_mode_requires_https_and_smtp(self) -> None:
|
|
109
|
-
import server
|
|
110
|
-
from unittest.mock import patch
|
|
111
|
-
|
|
112
|
-
with patch.object(server, "DEPLOYMENT_MODE", "public_multi_user"), patch.object(
|
|
113
|
-
server, "DEV_LOCAL_UNSAFE", False
|
|
114
|
-
), patch.object(server, "SECURE_MODE", True), patch.object(
|
|
115
|
-
server, "_supervisor_ready", return_value=True
|
|
116
|
-
), patch.object(server, "_install_window_open", return_value=False), patch.object(
|
|
117
|
-
server, "APP_BASE_URL", "http://insecure.example"
|
|
118
|
-
), patch.dict(
|
|
119
|
-
"os.environ",
|
|
120
|
-
{
|
|
121
|
-
"ZK_AUTH_HMAC_KEY": "a",
|
|
122
|
-
"ZK_AUTH_ENCRYPTION_KEY": "b",
|
|
123
|
-
"ZK_AUTH_SESSION_SECRET": "c",
|
|
124
|
-
"WORKFRAME_API_TOKEN": "d",
|
|
125
|
-
"SMTP_HOST": "",
|
|
126
|
-
},
|
|
127
|
-
clear=False,
|
|
128
|
-
):
|
|
129
|
-
errors = server._deployment_security_errors()
|
|
130
|
-
self.assertTrue(any("APP_BASE_URL" in e for e in errors))
|
|
131
|
-
self.assertTrue(any("SMTP_HOST" in e for e in errors))
|
|
132
|
-
self.assertTrue(any("WORKFRAME_PROXY_TOKEN" in e for e in errors))
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if __name__ == "__main__":
|
|
136
|
-
unittest.main()
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Install-window host/origin gate — setup must not require ALLOWED_HOSTS to match yet."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import unittest
|
|
6
|
-
from unittest import mock
|
|
7
|
-
|
|
8
|
-
import server
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class InstallSecureHostTests(unittest.TestCase):
|
|
12
|
-
def setUp(self) -> None:
|
|
13
|
-
self._old_hosts = server.ALLOWED_HOSTS
|
|
14
|
-
self._old_dev = server.DEV_LOCAL_UNSAFE
|
|
15
|
-
server.DEV_LOCAL_UNSAFE = False
|
|
16
|
-
server.ALLOWED_HOSTS = ["dev.example.com"]
|
|
17
|
-
|
|
18
|
-
def tearDown(self) -> None:
|
|
19
|
-
server.ALLOWED_HOSTS = self._old_hosts
|
|
20
|
-
server.DEV_LOCAL_UNSAFE = self._old_dev
|
|
21
|
-
|
|
22
|
-
def test_allowed_hosts_unions_loopback_when_configured(self) -> None:
|
|
23
|
-
hosts = server._allowed_hosts()
|
|
24
|
-
self.assertIn("dev.example.com", hosts)
|
|
25
|
-
self.assertIn("127.0.0.1", hosts)
|
|
26
|
-
|
|
27
|
-
def test_install_stack_patch_ok_during_install_window_via_tunnel(self) -> None:
|
|
28
|
-
headers = {"Host": "127.0.0.1:28644", "Origin": "http://127.0.0.1:28644"}
|
|
29
|
-
with mock.patch.object(server, "_install_window_open", return_value=True):
|
|
30
|
-
self.assertTrue(server._secure_host_origin_ok("PATCH", "/api/install/stack", headers))
|
|
31
|
-
|
|
32
|
-
def test_install_stack_patch_denied_after_install_window(self) -> None:
|
|
33
|
-
headers = {"Host": "evil.test", "Origin": "http://evil.test"}
|
|
34
|
-
with mock.patch.object(server, "_install_window_open", return_value=False):
|
|
35
|
-
self.assertFalse(server._secure_host_origin_ok("PATCH", "/api/install/stack", headers))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if __name__ == "__main__":
|
|
39
|
-
unittest.main()
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
"""Internal proxy auth + Google OIDC verification."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import tempfile
|
|
8
|
-
import unittest
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from unittest.mock import patch
|
|
11
|
-
|
|
12
|
-
import google_auth
|
|
13
|
-
import internal_proxy_auth
|
|
14
|
-
import llm_proxy
|
|
15
|
-
import oidc_jwt
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class InternalProxyAuthTests(unittest.TestCase):
|
|
19
|
-
def setUp(self) -> None:
|
|
20
|
-
internal_proxy_auth.reset_proxy_token_for_tests()
|
|
21
|
-
os.environ.pop("WORKFRAME_PROXY_TOKEN", None)
|
|
22
|
-
|
|
23
|
-
def tearDown(self) -> None:
|
|
24
|
-
internal_proxy_auth.reset_proxy_token_for_tests()
|
|
25
|
-
os.environ.pop("WORKFRAME_PROXY_TOKEN", None)
|
|
26
|
-
|
|
27
|
-
def test_private_ip_allows_without_token_when_unconfigured(self) -> None:
|
|
28
|
-
self.assertTrue(internal_proxy_auth.is_internal_client("172.19.0.2"))
|
|
29
|
-
self.assertFalse(internal_proxy_auth.is_internal_client("8.8.8.8"))
|
|
30
|
-
|
|
31
|
-
class _H:
|
|
32
|
-
client_address = ("172.19.0.2", 1)
|
|
33
|
-
headers = {}
|
|
34
|
-
|
|
35
|
-
ok, err = internal_proxy_auth.authorize_internal_proxy(_H()) # type: ignore[arg-type]
|
|
36
|
-
self.assertTrue(ok)
|
|
37
|
-
self.assertEqual(err, "")
|
|
38
|
-
|
|
39
|
-
def test_configured_token_requires_header(self) -> None:
|
|
40
|
-
os.environ["WORKFRAME_PROXY_TOKEN"] = "secret-proxy"
|
|
41
|
-
|
|
42
|
-
class _H:
|
|
43
|
-
client_address = ("172.19.0.2", 1)
|
|
44
|
-
headers = {"X-Workframe-Proxy-Token": "secret-proxy"}
|
|
45
|
-
|
|
46
|
-
ok, err = internal_proxy_auth.authorize_internal_proxy(_H()) # type: ignore[arg-type]
|
|
47
|
-
self.assertTrue(ok)
|
|
48
|
-
|
|
49
|
-
_H.headers = {}
|
|
50
|
-
ok, err = internal_proxy_auth.authorize_internal_proxy(_H()) # type: ignore[arg-type]
|
|
51
|
-
self.assertFalse(ok)
|
|
52
|
-
self.assertEqual(err, "proxy token required")
|
|
53
|
-
|
|
54
|
-
def test_llm_proxy_rejects_missing_token(self) -> None:
|
|
55
|
-
os.environ["WORKFRAME_PROXY_TOKEN"] = "secret-proxy"
|
|
56
|
-
|
|
57
|
-
class FakeWriter:
|
|
58
|
-
def __init__(self) -> None:
|
|
59
|
-
self.data = b""
|
|
60
|
-
|
|
61
|
-
def write(self, chunk: bytes) -> None:
|
|
62
|
-
self.data += chunk
|
|
63
|
-
|
|
64
|
-
def flush(self) -> None:
|
|
65
|
-
pass
|
|
66
|
-
|
|
67
|
-
class FakeHandler:
|
|
68
|
-
client_address = ("172.19.0.2", 1)
|
|
69
|
-
headers = {"Authorization": "Bearer wf_rt_test"}
|
|
70
|
-
|
|
71
|
-
def __init__(self) -> None:
|
|
72
|
-
self.wfile = FakeWriter()
|
|
73
|
-
self.status = 0
|
|
74
|
-
|
|
75
|
-
def send_response(self, status: int) -> None:
|
|
76
|
-
self.status = status
|
|
77
|
-
|
|
78
|
-
def send_header(self, _key: str, _value: str) -> None:
|
|
79
|
-
pass
|
|
80
|
-
|
|
81
|
-
def end_headers(self) -> None:
|
|
82
|
-
pass
|
|
83
|
-
|
|
84
|
-
handler = FakeHandler()
|
|
85
|
-
handled = llm_proxy.handle_proxy_request(
|
|
86
|
-
handler,
|
|
87
|
-
"/internal/llm/openrouter/v1/chat/completions",
|
|
88
|
-
"POST",
|
|
89
|
-
b"{}",
|
|
90
|
-
resolve_secret=lambda *_args: ("OPENROUTER_API_KEY", "sk-test"),
|
|
91
|
-
)
|
|
92
|
-
self.assertTrue(handled)
|
|
93
|
-
self.assertEqual(handler.status, 403)
|
|
94
|
-
self.assertIn(b"proxy token required", handler.wfile.data)
|
|
95
|
-
|
|
96
|
-
def test_bootstrap_writes_shared_and_data_files(self) -> None:
|
|
97
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
98
|
-
os.environ["WORKFRAME_API_DATA_DIR"] = tmp
|
|
99
|
-
internal_proxy_auth.reset_proxy_token_for_tests()
|
|
100
|
-
token = internal_proxy_auth.bootstrap_proxy_token(allow_generate_file=True)
|
|
101
|
-
self.assertTrue(token)
|
|
102
|
-
self.assertTrue((Path(tmp) / ".proxy_token").is_file())
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class GoogleOidcTests(unittest.TestCase):
|
|
106
|
-
def test_verify_google_id_token_checks_signature(self) -> None:
|
|
107
|
-
with patch.object(oidc_jwt, "verify_rs256_id_token") as verify:
|
|
108
|
-
verify.return_value = {
|
|
109
|
-
"email": "user@example.com",
|
|
110
|
-
"name": "User",
|
|
111
|
-
"email_verified": True,
|
|
112
|
-
}
|
|
113
|
-
with patch.object(google_auth, "_google_creds", return_value=("client-id", "secret")):
|
|
114
|
-
google_auth._PENDING["state-1"] = {"invite_email": "", "invite_token": ""}
|
|
115
|
-
with patch.object(google_auth.urllib.request, "urlopen") as urlopen:
|
|
116
|
-
urlopen.return_value.__enter__.return_value.read.return_value = json.dumps(
|
|
117
|
-
{"id_token": "header.payload.sig"}
|
|
118
|
-
).encode()
|
|
119
|
-
profile = google_auth.exchange_google_code("code", "state-1")
|
|
120
|
-
verify.assert_called_once()
|
|
121
|
-
self.assertEqual(profile["email"], "user@example.com")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if __name__ == "__main__":
|
|
125
|
-
unittest.main()
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import tempfile
|
|
2
|
-
import unittest
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from unittest.mock import patch
|
|
5
|
-
|
|
6
|
-
import server
|
|
7
|
-
from db_setup import ensure_workframe_schemas
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class InviteRuntimeBootstrapTests(unittest.TestCase):
|
|
11
|
-
def setUp(self) -> None:
|
|
12
|
-
self._tmp = tempfile.TemporaryDirectory()
|
|
13
|
-
self.addCleanup(self._tmp.cleanup)
|
|
14
|
-
self._old_data_dir = server.DATA_DIR
|
|
15
|
-
self._old_auth_db_path = server.AUTH_DB_PATH
|
|
16
|
-
self._old_hermes_data = server.HERMES_DATA
|
|
17
|
-
server.DATA_DIR = Path(self._tmp.name)
|
|
18
|
-
server.AUTH_DB_PATH = Path(self._tmp.name) / "auth.db"
|
|
19
|
-
server.HERMES_DATA = Path(self._tmp.name) / "hermes"
|
|
20
|
-
(server.HERMES_DATA / "profiles").mkdir(parents=True)
|
|
21
|
-
ensure_workframe_schemas()
|
|
22
|
-
self.workspace_id = "ws-invite"
|
|
23
|
-
self.user_id = "user-invited"
|
|
24
|
-
self.agent_one_id = "a1111111-1111-4111-8111-111111111111"
|
|
25
|
-
self.agent_two_id = "b2222222-2222-4222-8222-222222222222"
|
|
26
|
-
conn = server._workframe_db()
|
|
27
|
-
try:
|
|
28
|
-
now = "1"
|
|
29
|
-
conn.execute(
|
|
30
|
-
"INSERT INTO workspaces (id, slug, display_name, owner_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
|
|
31
|
-
(self.workspace_id, "invite", "Invite", "owner-1", "active", now, now),
|
|
32
|
-
)
|
|
33
|
-
conn.execute(
|
|
34
|
-
"INSERT INTO users (id, display_name, role, status, created_at, updated_at) VALUES (?,?,?,?,?,?)",
|
|
35
|
-
(self.user_id, "Invited", "member", "active", now, now),
|
|
36
|
-
)
|
|
37
|
-
for ap_id, slug in (
|
|
38
|
-
(self.agent_one_id, "agent-one"),
|
|
39
|
-
(self.agent_two_id, "agent-two"),
|
|
40
|
-
):
|
|
41
|
-
conn.execute(
|
|
42
|
-
"""
|
|
43
|
-
INSERT INTO agent_profiles (
|
|
44
|
-
id, workspace_id, slug, display_name, is_native, status, created_at, updated_at
|
|
45
|
-
) VALUES (?, ?, ?, ?, 0, 'available', ?, ?)
|
|
46
|
-
""",
|
|
47
|
-
(ap_id, self.workspace_id, slug, slug, now, now),
|
|
48
|
-
)
|
|
49
|
-
conn.commit()
|
|
50
|
-
finally:
|
|
51
|
-
conn.close()
|
|
52
|
-
|
|
53
|
-
def tearDown(self) -> None:
|
|
54
|
-
server.DATA_DIR = self._old_data_dir
|
|
55
|
-
server.AUTH_DB_PATH = self._old_auth_db_path
|
|
56
|
-
server.HERMES_DATA = self._old_hermes_data
|
|
57
|
-
|
|
58
|
-
@patch.object(server, "resolve_validated_profile", side_effect=lambda slug: slug)
|
|
59
|
-
@patch.object(server, "_runtime_profile_on_disk", return_value=False)
|
|
60
|
-
@patch.object(server, "ensure_runtime_profile")
|
|
61
|
-
def test_provision_invited_member_agent_runtimes(
|
|
62
|
-
self,
|
|
63
|
-
mock_ensure,
|
|
64
|
-
_on_disk,
|
|
65
|
-
_validate,
|
|
66
|
-
) -> None:
|
|
67
|
-
server._provision_invited_member_agent_runtimes(self.workspace_id, self.user_id)
|
|
68
|
-
self.assertEqual(mock_ensure.call_count, 2)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if __name__ == "__main__":
|
|
72
|
-
unittest.main()
|