create-workframe 0.1.12 → 0.1.14
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/README.md +1 -1
- package/bin/create-workframe.js +2 -2
- package/bin/workframe.js +130 -2
- package/docs/security.md +2 -2
- package/package.json +1 -1
- package/scripts/bundle-workframe-ui.mjs +9 -0
- package/scripts/sync-canonical-to-package.mjs +1 -0
- package/scripts/verify-public-deploy.sh +65 -3
- package/workframe-api/action_proxy.py +23 -17
- package/workframe-api/activity_feed.py +798 -0
- package/workframe-api/api_meta.py +157 -0
- package/workframe-api/auth_gate.py +566 -0
- package/workframe-api/avatar_registry.py +343 -0
- package/workframe-api/broker_audit.py +164 -0
- package/workframe-api/cell_authority.py +231 -0
- package/workframe-api/chat_bind.py +319 -0
- package/workframe-api/chat_sessions.py +509 -0
- package/workframe-api/chat_stream.py +694 -0
- package/workframe-api/cleanup_dogfood_smoke.py +252 -0
- package/workframe-api/credential_broker.py +162 -0
- package/workframe-api/credential_resolve.py +202 -0
- package/workframe-api/credential_store.py +245 -0
- package/workframe-api/crew_registry.py +250 -0
- package/workframe-api/db_schema.py +755 -0
- package/workframe-api/docker_gateway.py +166 -0
- package/workframe-api/doctor_runtime.py +109 -0
- package/workframe-api/domain/__init__.py +47 -0
- package/workframe-api/domain/entities.py +252 -0
- package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
- package/workframe-api/egress_policy.py +42 -0
- package/workframe-api/handler_modules/__init__.py +17 -0
- package/workframe-api/handler_modules/handler_admin.py +542 -0
- package/workframe-api/handler_modules/handler_auth.py +512 -0
- package/workframe-api/handler_modules/handler_chat.py +641 -0
- package/workframe-api/handler_modules/handler_install.py +178 -0
- package/workframe-api/handler_modules/handler_provider.py +325 -0
- package/workframe-api/handler_modules/handler_workspace.py +1420 -0
- package/workframe-api/health_monitor.py +80 -0
- package/workframe-api/hermes_admin.py +756 -0
- package/workframe-api/hermes_profiles.py +1328 -0
- package/workframe-api/install_api.py +123 -0
- package/workframe-api/kanban_cron.py +473 -0
- package/workframe-api/lane_bindings.py +423 -0
- package/workframe-api/llm_proxy.py +17 -43
- package/workframe-api/mention_helpers.py +180 -0
- package/workframe-api/mention_invoke.py +431 -0
- package/workframe-api/model_surface.py +1139 -0
- package/workframe-api/oauth_pending.py +85 -0
- package/workframe-api/oauth_redirect.py +381 -0
- package/workframe-api/package.json +2 -2
- package/workframe-api/profile_api_lifecycle.py +66 -0
- package/workframe-api/profile_gateway.py +436 -0
- package/workframe-api/provider_bindings.py +673 -0
- package/workframe-api/provider_bootstrap.py +347 -0
- package/workframe-api/provider_catalog.py +180 -0
- package/workframe-api/rooms.py +1613 -0
- package/workframe-api/route_registry.py +331 -191
- package/workframe-api/run-typecheck.mjs +2 -1
- package/workframe-api/run_authority.py +287 -0
- package/workframe-api/run_ledger.py +470 -0
- package/workframe-api/run_surface_wiring.py +344 -0
- package/workframe-api/runtime_cohort.py +752 -0
- package/workframe-api/runtime_tokens.py +127 -0
- package/workframe-api/server.py +1453 -19513
- package/workframe-api/snapshot_feed.py +49 -0
- package/workframe-api/stack_config.py +33 -1
- package/workframe-api/supervisor_client.py +154 -0
- package/workframe-api/test_api_meta_build_stamp.py +34 -0
- package/workframe-api/test_cell_authority.py +79 -0
- package/workframe-api/test_chat_bind.py +48 -0
- package/workframe-api/test_credential_lease_matrix.py +171 -0
- package/workframe-api/test_credential_resolve.py +51 -0
- package/workframe-api/test_credential_store.py +27 -0
- package/workframe-api/test_dogfood_flows.py +346 -0
- package/workframe-api/test_domain_entities.py +152 -0
- package/workframe-api/test_exception_hygiene.py +12 -0
- package/workframe-api/test_hermes_admin.py +72 -0
- package/workframe-api/test_install_wizard_resume.py +78 -0
- package/workframe-api/test_local_bootstrap.py +67 -0
- package/workframe-api/test_mention_helpers.py +35 -0
- package/workframe-api/test_mention_invoke.py +26 -0
- package/workframe-api/test_model_surface_consistency.py +1 -0
- package/workframe-api/test_oauth_pending.py +41 -0
- package/workframe-api/test_provider_bindings.py +52 -0
- package/workframe-api/test_provider_catalog.py +28 -0
- package/workframe-api/test_route_registry.py +171 -35
- package/workframe-api/test_run_authority.py +112 -0
- package/workframe-api/test_run_ledger.py +157 -0
- package/workframe-api/test_run_surface_wiring.py +130 -0
- package/workframe-api/test_runtime_cohort.py +85 -0
- package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
- package/workframe-api/test_server_reexports.py +99 -0
- package/workframe-api/test_stack_config_install_smtp.py +7 -0
- package/workframe-api/turn_credentials.py +28 -13
- package/workframe-api/turn_overlay.py +715 -0
- package/workframe-api/updates.py +16 -1
- package/workframe-api/user_prefs.py +387 -0
- package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
- package/workframe-api/workspace_bootstrap.py +536 -0
- package/workframe-api/workspace_files.py +344 -0
- package/workframe-api/workspace_messaging.py +206 -0
- package/workframe-api/zk_auth.py +3 -2
- package/workframe-supervisor/server.py +10 -1
- package/workframe-supervisor/test_supervisor_negative.py +75 -0
- package/workframe-ui/public/assets/{arc-B0OFRGmJ.js → arc-BT9iZUSd.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-m1aPUQ_r.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-CaEDd3np.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-DHJhMrIS.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-Cdyz9hIV.js} +1 -1
- package/workframe-ui/public/assets/channel-3M69ekE5.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-BbZx76LX.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DcSxjTIP.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-B7F-kTY1.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-BwwEiRTK.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-DAi_NksX.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-BIaF4XCN.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-DjYSk1dG.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-_twnyfgV.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CkNmdy5s.js} +2 -2
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CjLqd-gl.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-_6oNqFjj.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-CeO9Am_D.js} +1 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-sRM4YeIe.js +2 -0
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-CH8tMb21.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-DAI1bsII.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-B2e-5a2G.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-BMl9ed8P.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-C08HH6BU.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-DM2-0q37.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BFcOE4Aq.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BFcOE4Aq.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-BnbbewLv.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-C4zmB1Vt.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-n-BJmsiC.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-BzQGQnZi.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-Cx_De-wS.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-CIOI3Lok.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-DkRuPLWY.js} +1 -1
- package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-XhCT1Q-V.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-C3GwhDU1.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-DIVBL6CF.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-CLWH8z2e.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D4h66Vy3.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-IW5F-Q6c.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-BjNIbmRM.js} +1 -1
- package/workframe-ui/public/assets/index-DL2vJP4L.css +1 -0
- package/workframe-ui/public/assets/index-ElS_D59P.js +130 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CcJspM79.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-DP4xR9jt.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-D4cZb21V.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-DsLuHkIJ.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Wu65nOsR.js} +1 -1
- package/workframe-ui/public/assets/{line-CgQsmU35.js → line-ByBDzRpy.js} +1 -1
- package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-DzXi2vnq.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-CaB-1Ckn.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-D7HTHC17.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BfKsC9aj.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-ClpU98TO.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-KeOyY_-u.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-CUpjnzbs.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-CJdPTszF.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-C9BpandZ.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-CHDZPwry.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-CMJJh91s.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-BsV4GppP.js} +1 -1
- package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-WHks82Up.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-eSX9P8z0.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-FzMK4PkM.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Dg1UkhGo.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-B7yGsBuj.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-j_G2oyQ_.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-BRrT0jqC.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-DEdUqez2.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-C-bBK9Bb.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-DhH5xuR2.js} +1 -1
- package/workframe-ui/public/index.html +11 -7
- package/workframe-ui/public/workframe-build.json +5 -0
- package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +0 -1
- package/workframe-ui/public/assets/channel-fNHbDpV4.js +0 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-DMfdMUwz.js +0 -2
- package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +0 -1
- package/workframe-ui/public/assets/index-8CuZDEIG.css +0 -1
- package/workframe-ui/public/assets/index-xS9lFekI.js +0 -129
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +0 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Local bootstrap must require a real owner email — no synthetic placeholders."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
API_DIR = Path(__file__).resolve().parent
|
|
11
|
+
if str(API_DIR) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(API_DIR))
|
|
13
|
+
|
|
14
|
+
os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
|
|
15
|
+
os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
|
|
16
|
+
os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
|
|
17
|
+
os.environ["DEPLOYMENT_MODE"] = "single_user_local"
|
|
18
|
+
|
|
19
|
+
import server # noqa: E402
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _CaptureHandler(server.Handler):
|
|
23
|
+
captured: tuple[int, dict] | None = None
|
|
24
|
+
|
|
25
|
+
def _json(self, status: int, payload: dict, **kwargs) -> None: # type: ignore[override]
|
|
26
|
+
type(self).captured = (status, payload)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_local_bootstrap_rejects_missing_email() -> None:
|
|
30
|
+
handler = _CaptureHandler.__new__(_CaptureHandler)
|
|
31
|
+
with patch.object(server, "DEPLOYMENT_MODE", "single_user_local"), patch.object(
|
|
32
|
+
server, "_install_window_open", return_value=True,
|
|
33
|
+
):
|
|
34
|
+
handler._route_post_auth_local_bootstrap({})
|
|
35
|
+
assert _CaptureHandler.captured is not None
|
|
36
|
+
status, body = _CaptureHandler.captured
|
|
37
|
+
assert status == 400
|
|
38
|
+
assert body.get("error") == "email_required"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_local_bootstrap_uses_submitted_email() -> None:
|
|
42
|
+
handler = _CaptureHandler.__new__(_CaptureHandler)
|
|
43
|
+
with patch.object(server, "DEPLOYMENT_MODE", "single_user_local"), patch.object(
|
|
44
|
+
server, "_install_window_open", return_value=True,
|
|
45
|
+
), patch.object(
|
|
46
|
+
server._zk,
|
|
47
|
+
"create_session_for_email",
|
|
48
|
+
return_value={"user_id": "u-test", "session_id": "s-test", "refresh_token": "r-test"},
|
|
49
|
+
) as create_session, patch.object(handler, "_ensure_user"), patch.object(
|
|
50
|
+
handler, "_first_owner_bootstrap",
|
|
51
|
+
), patch.object(server, "_session_profile_payload", return_value={"ok": True}), patch.object(
|
|
52
|
+
server, "_session_cookie_secure", return_value=False,
|
|
53
|
+
), patch.object(
|
|
54
|
+
server._zk, "session_cookie_value", return_value="cookie=test",
|
|
55
|
+
):
|
|
56
|
+
handler._route_post_auth_local_bootstrap(
|
|
57
|
+
{"email": "Owner@Example.com", "display_name": "Owner"},
|
|
58
|
+
)
|
|
59
|
+
create_session.assert_called_once_with("owner@example.com")
|
|
60
|
+
assert _CaptureHandler.captured is not None
|
|
61
|
+
assert _CaptureHandler.captured[0] == 200
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
test_local_bootstrap_rejects_missing_email()
|
|
66
|
+
test_local_bootstrap_uses_submitted_email()
|
|
67
|
+
print("ok")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""WF-032 mention_helpers self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
9
|
+
|
|
10
|
+
import mention_helpers # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_mention_handle() -> None:
|
|
14
|
+
assert mention_helpers.mention_handle("Alice Smith") == "alicesmith"
|
|
15
|
+
assert mention_helpers.mention_handle("", "alice@example.com") == "alice"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_parse_room_mentions() -> None:
|
|
19
|
+
agents = [{"id": "a1", "slug": "dev", "display_name": "Dev Agent"}]
|
|
20
|
+
result = mention_helpers.parse_room_mentions("@dev please review", agents, [])
|
|
21
|
+
assert len(result["agents"]) == 1
|
|
22
|
+
assert result["agents"][0]["slug"] == "dev"
|
|
23
|
+
assert result["users"] == []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_parse_mentions() -> None:
|
|
27
|
+
rows = mention_helpers.parse_mentions("hello @architect and @dev")
|
|
28
|
+
assert [r["slug"] for r in rows] == ["architect", "dev"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
test_mention_handle()
|
|
33
|
+
test_parse_room_mentions()
|
|
34
|
+
test_parse_mentions()
|
|
35
|
+
print("test_mention_helpers: ok")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""WF-032 mention_invoke self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
9
|
+
|
|
10
|
+
import mention_invoke # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_invoke_mention_missing_agent() -> None:
|
|
14
|
+
"""No-op when agent row lacks slug/id."""
|
|
15
|
+
mention_invoke._invoke_room_agent_mention(
|
|
16
|
+
"room-1",
|
|
17
|
+
"ws-1",
|
|
18
|
+
{"slug": "", "id": ""},
|
|
19
|
+
"",
|
|
20
|
+
"msg-1",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
test_invoke_mention_missing_agent()
|
|
26
|
+
print("test_mention_invoke: ok")
|
|
@@ -19,6 +19,7 @@ spec = importlib.util.spec_from_file_location("wf_server_model_surface", ROOT /
|
|
|
19
19
|
server = importlib.util.module_from_spec(spec)
|
|
20
20
|
sys.modules["wf_server_model_surface"] = server
|
|
21
21
|
spec.loader.exec_module(server)
|
|
22
|
+
sys.modules["server"] = server # ponytail: extracted modules resolve _srv() to this instance
|
|
22
23
|
|
|
23
24
|
PROF = "surface-test-agent"
|
|
24
25
|
prof_dir = server._profile_dir(PROF)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""WF-032 oauth_pending store/take self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
11
|
+
|
|
12
|
+
import oauth_pending # noqa: E402
|
|
13
|
+
import server # noqa: E402
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_store_take_round_trip() -> None:
|
|
17
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
18
|
+
os.environ["WORKFRAME_API_DATA_DIR"] = tmp
|
|
19
|
+
server.DATA_DIR = Path(tmp)
|
|
20
|
+
server.AUTH_DB_PATH = Path(tmp) / "auth.db"
|
|
21
|
+
server._ensure_workframe_db_schema()
|
|
22
|
+
oauth_pending.store_pending("state-1", "user-1", "github", "verifier-abc", "ws-1")
|
|
23
|
+
row = oauth_pending.take_pending("state-1")
|
|
24
|
+
assert row is not None
|
|
25
|
+
assert row["user_id"] == "user-1"
|
|
26
|
+
assert row["provider"] == "github"
|
|
27
|
+
assert row["code_verifier"] == "verifier-abc"
|
|
28
|
+
assert oauth_pending.take_pending("state-1") is None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_pkce_pair() -> None:
|
|
32
|
+
verifier, challenge = oauth_pending.pkce_pair()
|
|
33
|
+
assert len(verifier) > 20
|
|
34
|
+
assert len(challenge) > 20
|
|
35
|
+
assert verifier != challenge
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
test_pkce_pair()
|
|
40
|
+
test_store_take_round_trip()
|
|
41
|
+
print("test_oauth_pending: ok")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""WF-032 provider_bindings pure helpers self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
9
|
+
|
|
10
|
+
import provider_bindings # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_hermes_oauth_auth_keys() -> None:
|
|
14
|
+
keys = provider_bindings._hermes_oauth_auth_keys("openai-codex")
|
|
15
|
+
assert "openai-codex" in keys
|
|
16
|
+
assert "codex" in keys
|
|
17
|
+
assert "openai_codex" in keys
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_auth_json_has_oauth_material() -> None:
|
|
21
|
+
pool_only = {
|
|
22
|
+
"version": 1,
|
|
23
|
+
"providers": {},
|
|
24
|
+
"credential_pool": {"openai-codex": [{"id": "x", "auth_type": "oauth"}]},
|
|
25
|
+
}
|
|
26
|
+
assert provider_bindings._auth_json_has_oauth_material(pool_only)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_merge_oauth_auth_into_profile() -> None:
|
|
30
|
+
pool_only = {
|
|
31
|
+
"version": 1,
|
|
32
|
+
"providers": {},
|
|
33
|
+
"credential_pool": {"openai-codex": [{"id": "x", "auth_type": "oauth"}]},
|
|
34
|
+
}
|
|
35
|
+
merged: dict = {"version": 1, "providers": {}, "credential_pool": {}}
|
|
36
|
+
assert provider_bindings._merge_oauth_auth_into_profile(merged, pool_only, "openai-codex")
|
|
37
|
+
assert merged["credential_pool"].get("openai-codex")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_parse_device_oauth_log() -> None:
|
|
41
|
+
text = "Open this URL\n https://auth.openai.com/device\nEnter this code\n ABCD-1234"
|
|
42
|
+
parsed = provider_bindings._parse_device_oauth_log(text)
|
|
43
|
+
assert parsed["verification_uri"] and "openai.com" in parsed["verification_uri"]
|
|
44
|
+
assert parsed["user_code"] == "ABCD-1234"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
test_hermes_oauth_auth_keys()
|
|
49
|
+
test_auth_json_has_oauth_material()
|
|
50
|
+
test_merge_oauth_auth_into_profile()
|
|
51
|
+
test_parse_device_oauth_log()
|
|
52
|
+
print("test_provider_bindings: ok")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""WF-032 provider_catalog self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
9
|
+
|
|
10
|
+
import provider_catalog # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_catalog_lookup() -> None:
|
|
14
|
+
gh = provider_catalog.catalog_provider("github")
|
|
15
|
+
assert gh is not None
|
|
16
|
+
assert gh.get("connect_mode") == "oauth"
|
|
17
|
+
assert provider_catalog.catalog_provider_for_llm("openai-codex") is not None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_user_only() -> None:
|
|
21
|
+
assert provider_catalog.provider_user_only("github")
|
|
22
|
+
assert not provider_catalog.provider_user_only("openrouter")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
test_catalog_lookup()
|
|
27
|
+
test_user_only()
|
|
28
|
+
print("test_provider_catalog: ok")
|
|
@@ -12,17 +12,46 @@ sys.path.insert(0, str(ROOT))
|
|
|
12
12
|
|
|
13
13
|
import route_registry # noqa: E402
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
def _handler_source_text() -> str:
|
|
17
|
+
"""server.Handler bodies plus extracted handler_modules mixins (WF-032)."""
|
|
18
|
+
parts = [(ROOT / "server.py").read_text(encoding="utf-8")]
|
|
19
|
+
mods = ROOT / "handler_modules"
|
|
20
|
+
if mods.is_dir():
|
|
21
|
+
for path in sorted(mods.glob("*.py")):
|
|
22
|
+
if path.name == "__init__.py":
|
|
23
|
+
continue
|
|
24
|
+
parts.append(path.read_text(encoding="utf-8"))
|
|
25
|
+
return "\n".join(parts)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _sample_path_from_label(label: str, pattern: re.Pattern[str] | None = None) -> str:
|
|
29
|
+
sample = (
|
|
30
|
+
label.replace("{id}", "sample-id")
|
|
31
|
+
.replace("{slug}", "sample-slug")
|
|
32
|
+
.replace("{token}", "sample-token")
|
|
33
|
+
.replace("{provider}", "github")
|
|
34
|
+
.replace("{cid}", "cred-1")
|
|
35
|
+
.replace("{path}", "docs/readme.md")
|
|
36
|
+
.replace("{kind}", "og")
|
|
37
|
+
)
|
|
38
|
+
if pattern is not None and not pattern.fullmatch(sample):
|
|
39
|
+
raise AssertionError(f"sample path does not match pattern: {sample} {label}")
|
|
40
|
+
return sample
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Auth level coverage (4-value enum)
|
|
16
44
|
assert route_registry.resolve_auth_level("GET", "/api/health") == route_registry.AuthLevel.PUBLIC
|
|
17
45
|
assert route_registry.resolve_auth_level("GET", "/api/meta") == route_registry.AuthLevel.PUBLIC
|
|
18
|
-
assert route_registry.resolve_auth_level("GET", "/api/snapshot") == route_registry.AuthLevel.
|
|
19
|
-
assert route_registry.resolve_auth_level("POST", "/api/auth/start") == route_registry.AuthLevel.
|
|
46
|
+
assert route_registry.resolve_auth_level("GET", "/api/snapshot") == route_registry.AuthLevel.SESSION
|
|
47
|
+
assert route_registry.resolve_auth_level("POST", "/api/auth/start") == route_registry.AuthLevel.PUBLIC
|
|
20
48
|
assert route_registry.resolve_auth_level("GET", "/api/me") == route_registry.AuthLevel.SESSION
|
|
21
|
-
assert route_registry.resolve_auth_level("GET", "/api/
|
|
49
|
+
assert route_registry.resolve_auth_level("GET", "/api/admin/vault/status") == route_registry.AuthLevel.ROLE_OWNER_ADMIN
|
|
50
|
+
assert route_registry.resolve_auth_level("GET", "/api/workspace/default/rooms") == route_registry.AuthLevel.SESSION
|
|
22
51
|
|
|
23
|
-
# Unknown /api paths
|
|
24
|
-
assert route_registry.resolve_auth_level("GET", "/api/not-a-real-handler")
|
|
25
|
-
assert
|
|
52
|
+
# Unknown /api paths are unregistered → None (404 at dispatch, not auth-gated)
|
|
53
|
+
assert route_registry.resolve_auth_level("GET", "/api/not-a-real-handler") is None
|
|
54
|
+
assert route_registry.authorize_request(
|
|
26
55
|
"GET",
|
|
27
56
|
"/api/not-a-real-handler",
|
|
28
57
|
"",
|
|
@@ -33,39 +62,80 @@ assert not route_registry.authorize_request(
|
|
|
33
62
|
attach_user=lambda _u: None,
|
|
34
63
|
)
|
|
35
64
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
# Registered session route without session → 401
|
|
66
|
+
assert not route_registry.authorize_request(
|
|
67
|
+
"GET",
|
|
68
|
+
"/api/me",
|
|
69
|
+
"",
|
|
70
|
+
deployment_mode="trusted_team",
|
|
71
|
+
dev_local_unsafe=False,
|
|
72
|
+
install_window_open=False,
|
|
73
|
+
validate_session=lambda _s: None,
|
|
74
|
+
attach_user=lambda _u: None,
|
|
75
|
+
)
|
|
43
76
|
|
|
44
|
-
# ROUTES handlers
|
|
45
|
-
|
|
77
|
+
# ROUTES table: dispatched routes have handlers on server.Handler (+ handler_modules)
|
|
78
|
+
handler_src = _handler_source_text()
|
|
46
79
|
missing_handlers: list[str] = []
|
|
47
80
|
for route in route_registry.ROUTES:
|
|
48
|
-
if
|
|
81
|
+
if not route.handler:
|
|
82
|
+
continue
|
|
83
|
+
if f"def {route.handler}(" not in handler_src:
|
|
49
84
|
missing_handlers.append(f"{route.method} {route.path} -> {route.handler}")
|
|
50
85
|
assert not missing_handlers, f"missing handler methods: {missing_handlers}"
|
|
51
86
|
|
|
52
|
-
#
|
|
87
|
+
# Every pattern-backed route has a handler and auth via label-derived sample path
|
|
88
|
+
missing_pattern_handlers: list[str] = []
|
|
89
|
+
missing_pattern_auth: list[str] = []
|
|
90
|
+
for rp in route_registry.ROUTE_PATTERNS:
|
|
91
|
+
if not rp.handler:
|
|
92
|
+
missing_pattern_handlers.append(f"{rp.method} {rp.label}")
|
|
93
|
+
continue
|
|
94
|
+
if f"def {rp.handler}(" not in handler_src:
|
|
95
|
+
missing_pattern_handlers.append(f"{rp.method} {rp.label} -> {rp.handler}")
|
|
96
|
+
sample = _sample_path_from_label(rp.label, rp.pattern)
|
|
97
|
+
if route_registry.lookup_auth(rp.method, sample) is None:
|
|
98
|
+
missing_pattern_auth.append(f"{rp.method} {sample}")
|
|
99
|
+
assert not missing_pattern_handlers, f"missing pattern handlers: {missing_pattern_handlers}"
|
|
100
|
+
assert not missing_pattern_auth, f"missing pattern auth: {missing_pattern_auth}"
|
|
101
|
+
|
|
102
|
+
# Pattern handlers must not re-dispatch with legacy startswith guards
|
|
103
|
+
pattern_method_blocks = re.findall(
|
|
104
|
+
r"(def (_route_pattern_\w+)\([^)]*\)[^:]*:.*?)(?=\n def |\Z)",
|
|
105
|
+
handler_src,
|
|
106
|
+
flags=re.DOTALL,
|
|
107
|
+
)
|
|
108
|
+
legacy_guards: list[str] = []
|
|
109
|
+
for block, name in pattern_method_blocks:
|
|
110
|
+
if 'path.startswith("/api/' in block or "path.startswith('/api/" in block:
|
|
111
|
+
legacy_guards.append(name)
|
|
112
|
+
assert not legacy_guards, f"legacy startswith guards in pattern handlers: {legacy_guards}"
|
|
113
|
+
|
|
114
|
+
# Every if-chain literal /api path must be registered with explicit auth
|
|
115
|
+
server_src = (ROOT / "server.py").read_text(encoding="utf-8")
|
|
53
116
|
handler_paths = set(re.findall(r'if path == "(/api/[^"]+)"', server_src))
|
|
54
117
|
handler_paths |= set(re.findall(r"if path == \'(/api/[^\']+)\'", server_src))
|
|
55
|
-
|
|
118
|
+
unregistered: list[str] = []
|
|
56
119
|
for p in sorted(handler_paths):
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
120
|
+
if route_registry.lookup_auth("GET", p) is None and route_registry.lookup_auth("POST", p) is None:
|
|
121
|
+
unregistered.append(p)
|
|
122
|
+
assert not unregistered, f"unregistered auth for if-chain paths: {unregistered}"
|
|
123
|
+
|
|
124
|
+
# Pattern-backed routes from startswith / fullmatch in do_GET/do_POST
|
|
125
|
+
_pattern_probes = [
|
|
126
|
+
("GET", "/api/public/branding/og"),
|
|
127
|
+
("GET", "/api/workspace/ws-1/rooms"),
|
|
128
|
+
("GET", "/api/rooms/room-1/messages"),
|
|
129
|
+
("POST", "/api/rooms/room-1/messages/send"),
|
|
130
|
+
("PATCH", "/api/workspace/ws-1"),
|
|
131
|
+
("DELETE", "/api/memory/mem-1"),
|
|
132
|
+
]
|
|
133
|
+
for method, path in _pattern_probes:
|
|
134
|
+
assert route_registry.lookup_auth(method, path) is not None, f"missing pattern auth: {method} {path}"
|
|
135
|
+
|
|
136
|
+
# sessionless_ok data GETs
|
|
137
|
+
snap = route_registry.lookup_auth("GET", "/api/snapshot")
|
|
138
|
+
assert snap is not None and snap.sessionless_ok
|
|
69
139
|
assert not route_registry.authorize_request(
|
|
70
140
|
"GET",
|
|
71
141
|
"/api/agents",
|
|
@@ -87,10 +157,39 @@ assert route_registry.authorize_request(
|
|
|
87
157
|
attach_user=lambda _u: None,
|
|
88
158
|
)
|
|
89
159
|
|
|
90
|
-
|
|
91
|
-
assert
|
|
92
|
-
|
|
93
|
-
|
|
160
|
+
# owner/admin gate
|
|
161
|
+
assert not route_registry.authorize_request(
|
|
162
|
+
"GET",
|
|
163
|
+
"/api/admin/vault/status",
|
|
164
|
+
"sid-ok",
|
|
165
|
+
deployment_mode="trusted_team",
|
|
166
|
+
dev_local_unsafe=False,
|
|
167
|
+
install_window_open=False,
|
|
168
|
+
validate_session=lambda s: {"user_id": "u1"} if s == "sid-ok" else None,
|
|
169
|
+
attach_user=lambda _u: None,
|
|
170
|
+
get_workspace_role=lambda: "member",
|
|
171
|
+
)
|
|
172
|
+
assert route_registry.authorize_request(
|
|
173
|
+
"GET",
|
|
174
|
+
"/api/admin/vault/status",
|
|
175
|
+
"sid-ok",
|
|
176
|
+
deployment_mode="trusted_team",
|
|
177
|
+
dev_local_unsafe=False,
|
|
178
|
+
install_window_open=False,
|
|
179
|
+
validate_session=lambda s: {"user_id": "u1"} if s == "sid-ok" else None,
|
|
180
|
+
attach_user=lambda _u: None,
|
|
181
|
+
get_workspace_role=lambda: "owner",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
dispatched_get = [r for r in route_registry.ROUTES if r.method == "GET" and r.handler]
|
|
185
|
+
dispatched_post = [r for r in route_registry.ROUTES if r.method == "POST" and r.handler]
|
|
186
|
+
dispatched_patch = [r for r in route_registry.ROUTES if r.method == "PATCH" and r.handler]
|
|
187
|
+
pattern_dispatched = [rp for rp in route_registry.ROUTE_PATTERNS if rp.handler]
|
|
188
|
+
assert len(dispatched_get) >= 50, f"unexpected GET dispatch count: {len(dispatched_get)}"
|
|
189
|
+
assert len(dispatched_post) >= 40, f"unexpected POST dispatch count: {len(dispatched_post)}"
|
|
190
|
+
assert len(dispatched_patch) >= 4, f"unexpected PATCH dispatch count: {len(dispatched_patch)}"
|
|
191
|
+
assert len(pattern_dispatched) >= 60, f"unexpected pattern dispatch count: {len(pattern_dispatched)}"
|
|
192
|
+
assert len(dispatched_get) + len(dispatched_post) + len(dispatched_patch) + len(pattern_dispatched) >= 160
|
|
94
193
|
|
|
95
194
|
class _PostStub:
|
|
96
195
|
def __init__(self) -> None:
|
|
@@ -106,4 +205,41 @@ assert route_registry.dispatch_post(_stub, "/api/auth/start", {"email": "a@b.c"}
|
|
|
106
205
|
assert _stub.called and _stub.body == {"email": "a@b.c"}
|
|
107
206
|
assert not route_registry.dispatch_post(_stub, "/api/not-registered", {})
|
|
108
207
|
|
|
208
|
+
class _GetStub:
|
|
209
|
+
def __init__(self) -> None:
|
|
210
|
+
self.called = False
|
|
211
|
+
self.pattern_called = False
|
|
212
|
+
|
|
213
|
+
def _route_get_admin_vault_status(self, qs: dict) -> None:
|
|
214
|
+
self.called = True
|
|
215
|
+
|
|
216
|
+
def _route_pattern_get_workspace_events(self, path: str, qs: dict) -> None:
|
|
217
|
+
self.pattern_called = True
|
|
218
|
+
|
|
219
|
+
_get_stub = _GetStub()
|
|
220
|
+
assert route_registry.dispatch_get(_get_stub, "/api/admin/vault/status", {})
|
|
221
|
+
assert _get_stub.called
|
|
222
|
+
assert route_registry.dispatch_get(_get_stub, "/api/admin/vault/status", {}) # idempotent call ok
|
|
223
|
+
|
|
224
|
+
assert route_registry.dispatch_pattern(
|
|
225
|
+
"GET", _get_stub, "/api/workspace/ws-1/events", qs={},
|
|
226
|
+
) is True
|
|
227
|
+
assert _get_stub.pattern_called
|
|
228
|
+
|
|
229
|
+
class _PatchStub:
|
|
230
|
+
def __init__(self) -> None:
|
|
231
|
+
self.called = False
|
|
232
|
+
|
|
233
|
+
def _route_patch_me(self, body: dict) -> None:
|
|
234
|
+
self.called = True
|
|
235
|
+
|
|
236
|
+
_patch_stub = _PatchStub()
|
|
237
|
+
assert route_registry.dispatch_patch(_patch_stub, "/api/me", {"display_name": "x"})
|
|
238
|
+
assert _patch_stub.called
|
|
239
|
+
|
|
240
|
+
# No legacy frozenset allowlists
|
|
241
|
+
assert not hasattr(route_registry, "GET_ALWAYS_PUBLIC_ROUTES")
|
|
242
|
+
assert not hasattr(route_registry, "GET_SINGLE_USER_PUBLIC_ROUTES")
|
|
243
|
+
assert not hasattr(route_registry, "GET_PUBLIC_ROUTES")
|
|
244
|
+
|
|
109
245
|
print("route registry self-check ok")
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""WF-009 RunAuthorityGate self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
9
|
+
|
|
10
|
+
from domain.entities import ActorType, FundingSource, RunSurface # noqa: E402
|
|
11
|
+
from run_authority import ( # noqa: E402
|
|
12
|
+
DENY_DELEGATION,
|
|
13
|
+
DENY_NO_CREDENTIAL_BYOK,
|
|
14
|
+
DENY_NO_CREDENTIAL_COMPANY,
|
|
15
|
+
DENY_PROVIDER_USER_ONLY,
|
|
16
|
+
RunAuthorityContext,
|
|
17
|
+
RunAuthorityRequest,
|
|
18
|
+
evaluate_run_authority,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _req() -> RunAuthorityRequest:
|
|
23
|
+
return RunAuthorityRequest(
|
|
24
|
+
surface=RunSurface.CHAT,
|
|
25
|
+
actor_type=ActorType.USER,
|
|
26
|
+
actor_id="user-1",
|
|
27
|
+
triggering_user_id="user-1",
|
|
28
|
+
profile_slug="u-user-1-native",
|
|
29
|
+
workspace_id="ws-1",
|
|
30
|
+
provider="openrouter",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_byok_user_cred() -> None:
|
|
35
|
+
decision = evaluate_run_authority(
|
|
36
|
+
_req(),
|
|
37
|
+
RunAuthorityContext(user_has_credential=True),
|
|
38
|
+
run_id="run-1",
|
|
39
|
+
)
|
|
40
|
+
assert decision.allowed
|
|
41
|
+
assert decision.payer_user_id == "user-1"
|
|
42
|
+
assert decision.funding_source == FundingSource.BYOK
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_byok_no_cred_denied() -> None:
|
|
46
|
+
decision = evaluate_run_authority(_req(), RunAuthorityContext())
|
|
47
|
+
assert not decision.allowed
|
|
48
|
+
assert decision.deny_reason == DENY_NO_CREDENTIAL_BYOK
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_company_workspace_cred() -> None:
|
|
52
|
+
decision = evaluate_run_authority(
|
|
53
|
+
_req(),
|
|
54
|
+
RunAuthorityContext(workspace_credential_mode="workspace", workspace_has_credential=True),
|
|
55
|
+
)
|
|
56
|
+
assert decision.allowed
|
|
57
|
+
assert decision.funding_source == FundingSource.COMPANY
|
|
58
|
+
assert decision.credential_scope == "workspace"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_company_user_cred_wins() -> None:
|
|
62
|
+
decision = evaluate_run_authority(
|
|
63
|
+
_req(),
|
|
64
|
+
RunAuthorityContext(
|
|
65
|
+
workspace_credential_mode="workspace",
|
|
66
|
+
user_has_credential=True,
|
|
67
|
+
workspace_has_credential=True,
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
assert decision.allowed
|
|
71
|
+
assert decision.funding_source == FundingSource.BYOK
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_user_only_no_fallback() -> None:
|
|
75
|
+
decision = evaluate_run_authority(
|
|
76
|
+
_req(),
|
|
77
|
+
RunAuthorityContext(provider_user_only=True, workspace_has_credential=True),
|
|
78
|
+
)
|
|
79
|
+
assert not decision.allowed
|
|
80
|
+
assert decision.deny_reason == DENY_PROVIDER_USER_ONLY
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_delegation_grantor() -> None:
|
|
84
|
+
decision = evaluate_run_authority(
|
|
85
|
+
_req(),
|
|
86
|
+
RunAuthorityContext(grantor_has_credential={"owner-1": True}),
|
|
87
|
+
)
|
|
88
|
+
assert decision.allowed
|
|
89
|
+
assert decision.payer_user_id == "owner-1"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_delegation_no_grantor_cred() -> None:
|
|
93
|
+
decision = evaluate_run_authority(
|
|
94
|
+
_req(),
|
|
95
|
+
RunAuthorityContext(
|
|
96
|
+
workspace_credential_mode="workspace",
|
|
97
|
+
grantor_has_credential={"owner-1": False},
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
assert not decision.allowed
|
|
101
|
+
assert decision.deny_reason == DENY_NO_CREDENTIAL_COMPANY
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
test_byok_user_cred()
|
|
106
|
+
test_byok_no_cred_denied()
|
|
107
|
+
test_company_workspace_cred()
|
|
108
|
+
test_company_user_cred_wins()
|
|
109
|
+
test_user_only_no_fallback()
|
|
110
|
+
test_delegation_grantor()
|
|
111
|
+
test_delegation_no_grantor_cred()
|
|
112
|
+
print("test_run_authority: ok")
|