create-workframe 0.1.12 → 0.1.13
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-aUUffclm.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
- package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
- package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
- package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-mQWmB3z6.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
- package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
- package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
- package/workframe-ui/public/assets/{line-CgQsmU35.js → line-BTCEHb7l.js} +1 -1
- package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-CkbydpnK.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-DvrtArpk.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-yj_1x_qj.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
- package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-D6mvlP96.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
- package/workframe-ui/public/index.html +7 -5
- 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,157 @@
|
|
|
1
|
+
"""WF-NS-P2 run ledger self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sqlite3
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
13
|
+
|
|
14
|
+
import run_ledger # noqa: E402
|
|
15
|
+
from domain.entities import ( # noqa: E402
|
|
16
|
+
ActorType,
|
|
17
|
+
FundingSource,
|
|
18
|
+
RunStatus,
|
|
19
|
+
RunSurface,
|
|
20
|
+
)
|
|
21
|
+
from run_authority import RunAuthorityDecision, chat_run_request # noqa: E402
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_schema_and_round_trip() -> None:
|
|
25
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
26
|
+
db_path = Path(tmp) / "workframe.db"
|
|
27
|
+
os.environ["WORKFRAME_API_DATA_DIR"] = tmp
|
|
28
|
+
run_ledger.WORKFRAME_DB = db_path
|
|
29
|
+
run_ledger._SCHEMA_READY.clear()
|
|
30
|
+
run_ledger.ensure_schema()
|
|
31
|
+
|
|
32
|
+
conn = sqlite3.connect(str(db_path))
|
|
33
|
+
conn.row_factory = sqlite3.Row
|
|
34
|
+
try:
|
|
35
|
+
decision = RunAuthorityDecision(
|
|
36
|
+
allowed=True,
|
|
37
|
+
deny_reason=None,
|
|
38
|
+
payer_user_id="user-1",
|
|
39
|
+
funding_source=FundingSource.BYOK,
|
|
40
|
+
credential_ref_id=None,
|
|
41
|
+
credential_scope="user",
|
|
42
|
+
grants=(),
|
|
43
|
+
)
|
|
44
|
+
run_ledger.record_authority_decision(
|
|
45
|
+
conn,
|
|
46
|
+
run_id="run-test-1",
|
|
47
|
+
request_surface=RunSurface.CHAT,
|
|
48
|
+
actor_type=ActorType.USER,
|
|
49
|
+
actor_id="user-1",
|
|
50
|
+
triggering_user_id="user-1",
|
|
51
|
+
workspace_id="ws-1",
|
|
52
|
+
agent_id="agent-1",
|
|
53
|
+
runtime_binding_id="bind-1",
|
|
54
|
+
profile_slug="u-user-1-native",
|
|
55
|
+
provider="openrouter",
|
|
56
|
+
room_id="room-1",
|
|
57
|
+
session_id="sess-1",
|
|
58
|
+
decision=decision,
|
|
59
|
+
)
|
|
60
|
+
conn.commit()
|
|
61
|
+
|
|
62
|
+
run = run_ledger.get_run(conn, "run-test-1")
|
|
63
|
+
assert run is not None
|
|
64
|
+
assert run.status == RunStatus.RUNNING
|
|
65
|
+
assert run.funding_source == FundingSource.BYOK
|
|
66
|
+
|
|
67
|
+
run_ledger.complete_run(
|
|
68
|
+
conn,
|
|
69
|
+
"run-test-1",
|
|
70
|
+
model="test/model",
|
|
71
|
+
provider="openrouter",
|
|
72
|
+
funding_source=FundingSource.BYOK,
|
|
73
|
+
payer_user_id="user-1",
|
|
74
|
+
)
|
|
75
|
+
conn.commit()
|
|
76
|
+
|
|
77
|
+
items = conn.execute(
|
|
78
|
+
"SELECT kind, funding_source, amount_usd, receipt_json FROM run_line_items WHERE run_id = ?",
|
|
79
|
+
("run-test-1",),
|
|
80
|
+
).fetchall()
|
|
81
|
+
assert len(items) == 1
|
|
82
|
+
assert str(items[0]["kind"]) == "llm_turn"
|
|
83
|
+
assert items[0]["amount_usd"] is None
|
|
84
|
+
receipt = json.loads(str(items[0]["receipt_json"]))
|
|
85
|
+
assert receipt["run_id"] == "run-test-1"
|
|
86
|
+
assert receipt["payer_user_id"] == "user-1"
|
|
87
|
+
finally:
|
|
88
|
+
conn.close()
|
|
89
|
+
|
|
90
|
+
events = run_ledger.list_run_events_for_room("room-1")
|
|
91
|
+
assert len(events) >= 2
|
|
92
|
+
assert events[0]["source"] == "run_ledger"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_dm_room_chat_run_recorded() -> None:
|
|
96
|
+
"""WF-NS-P2: DM rooms (room_type=direct) share chat_stream run path."""
|
|
97
|
+
dm_room_id = "550e8400-e29b-41d4-a716-446655440099"
|
|
98
|
+
req = chat_run_request(
|
|
99
|
+
triggering_user_id="user-a",
|
|
100
|
+
profile_slug="u-user-a-native",
|
|
101
|
+
workspace_id="ws-1",
|
|
102
|
+
provider="openrouter",
|
|
103
|
+
room_id=dm_room_id,
|
|
104
|
+
)
|
|
105
|
+
assert req.surface == RunSurface.CHAT
|
|
106
|
+
|
|
107
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
108
|
+
db_path = Path(tmp) / "workframe.db"
|
|
109
|
+
os.environ["WORKFRAME_API_DATA_DIR"] = tmp
|
|
110
|
+
run_ledger.WORKFRAME_DB = db_path
|
|
111
|
+
run_ledger._SCHEMA_READY.clear()
|
|
112
|
+
run_ledger.ensure_schema()
|
|
113
|
+
|
|
114
|
+
conn = sqlite3.connect(str(db_path))
|
|
115
|
+
conn.row_factory = sqlite3.Row
|
|
116
|
+
try:
|
|
117
|
+
decision = RunAuthorityDecision(
|
|
118
|
+
allowed=True,
|
|
119
|
+
deny_reason=None,
|
|
120
|
+
payer_user_id="user-a",
|
|
121
|
+
funding_source=FundingSource.BYOK,
|
|
122
|
+
credential_ref_id=None,
|
|
123
|
+
credential_scope="user",
|
|
124
|
+
grants=(),
|
|
125
|
+
)
|
|
126
|
+
run_ledger.record_authority_decision(
|
|
127
|
+
conn,
|
|
128
|
+
run_id="run-dm-1",
|
|
129
|
+
request_surface=req.surface,
|
|
130
|
+
actor_type=req.actor_type,
|
|
131
|
+
actor_id=req.actor_id,
|
|
132
|
+
triggering_user_id="user-a",
|
|
133
|
+
workspace_id="ws-1",
|
|
134
|
+
agent_id=req.agent_id,
|
|
135
|
+
runtime_binding_id=req.runtime_binding_id,
|
|
136
|
+
profile_slug=req.profile_slug,
|
|
137
|
+
provider="openrouter",
|
|
138
|
+
room_id=dm_room_id,
|
|
139
|
+
session_id="sess-dm-1",
|
|
140
|
+
decision=decision,
|
|
141
|
+
)
|
|
142
|
+
conn.commit()
|
|
143
|
+
row = conn.execute(
|
|
144
|
+
"SELECT surface, room_id FROM runs WHERE run_id = ?",
|
|
145
|
+
("run-dm-1",),
|
|
146
|
+
).fetchone()
|
|
147
|
+
assert row is not None
|
|
148
|
+
assert row["surface"] == RunSurface.CHAT.value
|
|
149
|
+
assert row["room_id"] == dm_room_id
|
|
150
|
+
finally:
|
|
151
|
+
conn.close()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
test_schema_and_round_trip()
|
|
156
|
+
test_dm_room_chat_run_recorded()
|
|
157
|
+
print("test_run_ledger: ok")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""WF-NS-P2 run surface wiring self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
12
|
+
|
|
13
|
+
import run_ledger # noqa: E402
|
|
14
|
+
import run_surface_wiring # noqa: E402
|
|
15
|
+
import server # noqa: E402
|
|
16
|
+
from domain.entities import ActorType, RunSurface # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _use_tmp_db(tmp: str) -> Path:
|
|
20
|
+
db_path = Path(tmp) / "workframe.db"
|
|
21
|
+
data = Path(tmp)
|
|
22
|
+
os.environ["WORKFRAME_API_DATA_DIR"] = tmp
|
|
23
|
+
server.DATA_DIR = data
|
|
24
|
+
server.AUTH_DB_PATH = data / "auth.db"
|
|
25
|
+
run_ledger.DATA_DIR = data
|
|
26
|
+
run_ledger.WORKFRAME_DB = db_path
|
|
27
|
+
run_ledger._SCHEMA_READY.clear()
|
|
28
|
+
return db_path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _line_items_for_run(db_path: Path, run_id: str) -> list[sqlite3.Row]:
|
|
32
|
+
conn = sqlite3.connect(str(db_path))
|
|
33
|
+
conn.row_factory = sqlite3.Row
|
|
34
|
+
try:
|
|
35
|
+
return conn.execute(
|
|
36
|
+
"SELECT kind, amount_usd, receipt_json FROM run_line_items WHERE run_id = ?",
|
|
37
|
+
(run_id,),
|
|
38
|
+
).fetchall()
|
|
39
|
+
finally:
|
|
40
|
+
conn.close()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_slash_audit_run_no_gate() -> None:
|
|
44
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
45
|
+
_use_tmp_db(tmp)
|
|
46
|
+
assert run_surface_wiring.slash_requires_authority("help") is False
|
|
47
|
+
assert run_surface_wiring.slash_requires_authority("chat") is True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_record_automated_cron() -> None:
|
|
51
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
52
|
+
db_path = _use_tmp_db(tmp)
|
|
53
|
+
run_ledger.ensure_schema()
|
|
54
|
+
result = run_surface_wiring.record_automated_surface_run(
|
|
55
|
+
{
|
|
56
|
+
"surface": "cron",
|
|
57
|
+
"profile_slug": "u-test-dev",
|
|
58
|
+
"triggering_user_id": "user-1",
|
|
59
|
+
"workspace_id": "ws-1",
|
|
60
|
+
"actor_id": "cron:daily",
|
|
61
|
+
"provider": "openrouter",
|
|
62
|
+
"event_type": "cron.triggered",
|
|
63
|
+
"payload": {"job": "daily"},
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
assert result["ok"] is True
|
|
67
|
+
rid = str(result["run_id"])
|
|
68
|
+
conn = sqlite3.connect(str(db_path))
|
|
69
|
+
conn.row_factory = sqlite3.Row
|
|
70
|
+
row = conn.execute("SELECT surface, status FROM runs WHERE run_id = ?", (rid,)).fetchone()
|
|
71
|
+
conn.close()
|
|
72
|
+
assert row is not None
|
|
73
|
+
assert row["surface"] == RunSurface.CRON.value
|
|
74
|
+
assert row["status"] == "completed"
|
|
75
|
+
items = _line_items_for_run(db_path, rid)
|
|
76
|
+
assert len(items) == 1
|
|
77
|
+
assert items[0]["amount_usd"] is None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_finish_surface_run_emits_receipt() -> None:
|
|
81
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
82
|
+
db_path = _use_tmp_db(tmp)
|
|
83
|
+
run_ledger.ensure_schema()
|
|
84
|
+
rid = run_surface_wiring.record_audit_surface_run(
|
|
85
|
+
surface=RunSurface.SLASH,
|
|
86
|
+
actor_type=ActorType.USER,
|
|
87
|
+
actor_id="user-1",
|
|
88
|
+
triggering_user_id="user-1",
|
|
89
|
+
profile_slug="u-test-dev",
|
|
90
|
+
workspace_id="ws-1",
|
|
91
|
+
event_type="slash.command",
|
|
92
|
+
payload={"command": "help"},
|
|
93
|
+
provider="openrouter",
|
|
94
|
+
)
|
|
95
|
+
run_surface_wiring.finish_surface_run(rid, ok=True, detail="ok")
|
|
96
|
+
items = _line_items_for_run(db_path, rid)
|
|
97
|
+
assert len(items) == 1
|
|
98
|
+
assert items[0]["amount_usd"] is None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_record_automated_webhook() -> None:
|
|
102
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
103
|
+
_use_tmp_db(tmp)
|
|
104
|
+
run_ledger.ensure_schema()
|
|
105
|
+
result = run_surface_wiring.record_automated_surface_run(
|
|
106
|
+
{
|
|
107
|
+
"surface": "webhook",
|
|
108
|
+
"profile_slug": "u-test-dev",
|
|
109
|
+
"triggering_user_id": "user-1",
|
|
110
|
+
"workspace_id": "ws-1",
|
|
111
|
+
"actor_id": "webhook:github",
|
|
112
|
+
"event_type": "webhook.received",
|
|
113
|
+
"provider": "openrouter",
|
|
114
|
+
"payload": {"source": "github"},
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
assert result["ok"] is True
|
|
118
|
+
assert result["surface"] == RunSurface.WEBHOOK.value
|
|
119
|
+
rid = str(result["run_id"])
|
|
120
|
+
items = _line_items_for_run(Path(tmp) / "workframe.db", rid)
|
|
121
|
+
assert len(items) == 1
|
|
122
|
+
assert items[0]["amount_usd"] is None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
test_slash_audit_run_no_gate()
|
|
127
|
+
test_record_automated_cron()
|
|
128
|
+
test_record_automated_webhook()
|
|
129
|
+
test_finish_surface_run_emits_receipt()
|
|
130
|
+
print("test_run_surface_wiring ok")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""WF-032 runtime_cohort self-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
API_DIR = Path(__file__).resolve().parent
|
|
12
|
+
if str(API_DIR) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(API_DIR))
|
|
14
|
+
|
|
15
|
+
os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
|
|
16
|
+
os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
|
|
17
|
+
os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
|
|
18
|
+
|
|
19
|
+
import runtime_cohort # noqa: E402
|
|
20
|
+
import server # noqa: E402
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_runtime_profile_slug_format() -> None:
|
|
24
|
+
slug = runtime_cohort._runtime_profile_slug("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "architect")
|
|
25
|
+
assert slug.startswith("u-")
|
|
26
|
+
assert slug.endswith("-architect")
|
|
27
|
+
assert server._runtime_profile_slug("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "architect") == slug
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_self_grant_forbidden() -> None:
|
|
31
|
+
try:
|
|
32
|
+
runtime_cohort.create_delegation_grant("ws-1", "user-1", "user-1")
|
|
33
|
+
raise AssertionError("expected ValueError")
|
|
34
|
+
except ValueError as exc:
|
|
35
|
+
assert str(exc) == "self_grant_forbidden"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_delegation_grant_round_trip() -> None:
|
|
39
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
40
|
+
os.environ["WORKFRAME_API_DATA_DIR"] = tmp
|
|
41
|
+
server.DATA_DIR = Path(tmp)
|
|
42
|
+
server.AUTH_DB_PATH = Path(tmp) / "auth.db"
|
|
43
|
+
server._ensure_workframe_db_schema()
|
|
44
|
+
ws_id = str(uuid.uuid4())
|
|
45
|
+
grantor = str(uuid.uuid4())
|
|
46
|
+
grantee = str(uuid.uuid4())
|
|
47
|
+
now = server._utc_now()
|
|
48
|
+
conn = server._workframe_db()
|
|
49
|
+
try:
|
|
50
|
+
conn.execute(
|
|
51
|
+
"INSERT INTO workspaces (id, slug, display_name, owner_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
52
|
+
(ws_id, "test-ws", "Test", grantor, now, now),
|
|
53
|
+
)
|
|
54
|
+
for uid, email in ((grantor, "a@test.local"), (grantee, "b@test.local")):
|
|
55
|
+
conn.execute(
|
|
56
|
+
"INSERT INTO users (id, email, display_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
57
|
+
(uid, email, email.split("@")[0], now, now),
|
|
58
|
+
)
|
|
59
|
+
conn.execute(
|
|
60
|
+
"""
|
|
61
|
+
INSERT INTO workspace_memberships (
|
|
62
|
+
id, workspace_id, user_id, role, status, created_at, updated_at
|
|
63
|
+
) VALUES (?, ?, ?, 'member', 'active', ?, ?)
|
|
64
|
+
""",
|
|
65
|
+
(str(uuid.uuid4()), ws_id, uid, now, now),
|
|
66
|
+
)
|
|
67
|
+
conn.commit()
|
|
68
|
+
finally:
|
|
69
|
+
conn.close()
|
|
70
|
+
created = runtime_cohort.create_delegation_grant(ws_id, grantor, grantee)
|
|
71
|
+
assert created["ok"] is True
|
|
72
|
+
grant_id = created["grant"]["id"]
|
|
73
|
+
listed = runtime_cohort.list_delegation_grants(ws_id, grantor)
|
|
74
|
+
assert any(row["id"] == grant_id for row in listed["grants"])
|
|
75
|
+
revoked = runtime_cohort.revoke_delegation_grant(ws_id, grant_id, grantor)
|
|
76
|
+
assert revoked["ok"] is True
|
|
77
|
+
grantors = runtime_cohort._delegation_grantor_ids_for_grantee(grantee, ws_id)
|
|
78
|
+
assert grantor not in grantors
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
test_runtime_profile_slug_format()
|
|
83
|
+
test_self_grant_forbidden()
|
|
84
|
+
test_delegation_grant_round_trip()
|
|
85
|
+
print("test_runtime_cohort: ok")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""WF-NS-P1 secure-mode docker socket boundary 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 server # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_docker_request_blocked_in_secure_mode() -> None:
|
|
14
|
+
prev = server.SECURE_MODE
|
|
15
|
+
try:
|
|
16
|
+
server.SECURE_MODE = True
|
|
17
|
+
try:
|
|
18
|
+
server._docker_request("GET", "/containers/json")
|
|
19
|
+
raise AssertionError("expected RuntimeError in SECURE_MODE")
|
|
20
|
+
except RuntimeError as exc:
|
|
21
|
+
assert "SECURE_MODE" in str(exc)
|
|
22
|
+
finally:
|
|
23
|
+
server.SECURE_MODE = prev
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_public_compose_api_has_no_docker_sock() -> None:
|
|
27
|
+
"""Compose config assertion — complements WF-011 supervisor negatives."""
|
|
28
|
+
repo = Path(__file__).resolve().parents[2]
|
|
29
|
+
public = (repo / "infra" / "compose" / "workframe" / "docker-compose.public.yml").read_text(
|
|
30
|
+
encoding="utf-8"
|
|
31
|
+
)
|
|
32
|
+
api_block = public.split("workframe-api:")[1].split("workframe-supervisor:")[0]
|
|
33
|
+
assert "/var/run/docker.sock" not in api_block
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
test_docker_request_blocked_in_secure_mode()
|
|
38
|
+
test_public_compose_api_has_no_docker_sock()
|
|
39
|
+
print("test_secure_mode_docker_boundary: ok")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""WF-032 guard: server.py must re-export symbols still called from extracted modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
API_DIR = Path(__file__).resolve().parent
|
|
10
|
+
if str(API_DIR) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(API_DIR))
|
|
12
|
+
|
|
13
|
+
os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
|
|
14
|
+
os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
|
|
15
|
+
os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
|
|
16
|
+
|
|
17
|
+
import server # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_REQUIRED = (
|
|
21
|
+
"_parse_workspace_settings",
|
|
22
|
+
"_strip_profile_action_env",
|
|
23
|
+
"_strip_profile_llm_env",
|
|
24
|
+
"_user_action_env_specs",
|
|
25
|
+
"_overlay_turn_user_env",
|
|
26
|
+
"_overlay_turn_provider_env",
|
|
27
|
+
"_llm_proxy_base_url",
|
|
28
|
+
"_require_user_provider",
|
|
29
|
+
"_ensure_profile_llm_proxy",
|
|
30
|
+
"_revoke_turn_credential_lease",
|
|
31
|
+
"_sync_profile_provider_env",
|
|
32
|
+
"_session_info",
|
|
33
|
+
"chat_messages",
|
|
34
|
+
"chat_bootstrap",
|
|
35
|
+
"_gateway_container_exec",
|
|
36
|
+
"_gateway_container_exec_detached",
|
|
37
|
+
"_docker_exec",
|
|
38
|
+
"_gateway_exec",
|
|
39
|
+
"_hermes_agent_version",
|
|
40
|
+
"ensure_profile_api",
|
|
41
|
+
"hermes_commands_catalog",
|
|
42
|
+
"hermes_commands_exec",
|
|
43
|
+
"hermes_usage",
|
|
44
|
+
"_runtime_profile_slug",
|
|
45
|
+
"ensure_runtime_profile",
|
|
46
|
+
"ensure_user_agent_cohort",
|
|
47
|
+
"resolve_runtime_assignee",
|
|
48
|
+
"cohort_runtime_slugs",
|
|
49
|
+
"_prepare_runtime_profile_credentials",
|
|
50
|
+
"purge_stale_runtime_profiles",
|
|
51
|
+
"list_delegation_grants",
|
|
52
|
+
"create_delegation_grant",
|
|
53
|
+
"revoke_delegation_grant",
|
|
54
|
+
"_delegation_grantor_ids_for_grantee",
|
|
55
|
+
"_runtime_profile_on_disk",
|
|
56
|
+
"_invalidate_gateway_registered_cache",
|
|
57
|
+
"_normalize_user_avatar_url",
|
|
58
|
+
"_normalize_logo_url",
|
|
59
|
+
"_normalize_agent_avatar_patch",
|
|
60
|
+
"_resolve_avatar_fields",
|
|
61
|
+
"_upsert_agent_registry_row",
|
|
62
|
+
"_assign_agent_avatar",
|
|
63
|
+
"_avatar_id_for_display_name",
|
|
64
|
+
"_pick_logo_url",
|
|
65
|
+
"_supervisor_ready",
|
|
66
|
+
"_supervisor_request",
|
|
67
|
+
"_maybe_sync_compose_public_url",
|
|
68
|
+
"_supervisor_gateway_exec",
|
|
69
|
+
"_supervisor_container_exec",
|
|
70
|
+
"_supervisor_profile_lifecycle",
|
|
71
|
+
"_parse_messaging_settings_patch",
|
|
72
|
+
"_workspace_messaging_integrations_payload",
|
|
73
|
+
"_sync_workspace_messaging_gateway",
|
|
74
|
+
"room_chat_bind",
|
|
75
|
+
"profile_chat_bind",
|
|
76
|
+
"list_room_sessions",
|
|
77
|
+
"profile_chat_activate_room_session",
|
|
78
|
+
"profile_chat_message",
|
|
79
|
+
"_enrich_room_chat_payload",
|
|
80
|
+
"_room_session_rows",
|
|
81
|
+
"_extract_title",
|
|
82
|
+
"bootstrap_agent_dm_lane",
|
|
83
|
+
"_provision_agent_dm_runtimes",
|
|
84
|
+
"_ensure_default_workspace",
|
|
85
|
+
"_bootstrap_after_setup",
|
|
86
|
+
"_promote_workspace_owner_if_unclaimed",
|
|
87
|
+
"_sync_workspace_home_room",
|
|
88
|
+
"_onboard_workspace_member_rooms",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_server_wf032_reexports_present() -> None:
|
|
93
|
+
missing = [name for name in _REQUIRED if not hasattr(server, name)]
|
|
94
|
+
assert not missing, f"missing server re-exports: {missing}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
test_server_wf032_reexports_present()
|
|
99
|
+
print("ok")
|
|
@@ -53,4 +53,11 @@ os.environ["WORKFRAME_DEPLOYMENT_MODE"] = "trusted_team"
|
|
|
53
53
|
stack_config.patch_stack_config({"deployment_mode": "single_user_local"})
|
|
54
54
|
assert stack_config.resolve_deployment_mode() == "single_user_local"
|
|
55
55
|
|
|
56
|
+
# Admin verify survives reload
|
|
57
|
+
stack_config.patch_stack_config({"install_complete": False})
|
|
58
|
+
stack_config.mark_install_admin_verified("owner@example.com")
|
|
59
|
+
assert stack_config.install_admin_verified()
|
|
60
|
+
cfg = stack_config.get_stack_config()
|
|
61
|
+
assert cfg["smtp"]["admin_email"] == "owner@example.com"
|
|
62
|
+
|
|
56
63
|
print("stack_config install-window smtp self-check ok")
|
|
@@ -157,11 +157,22 @@ def parse_lease_token(value: str) -> str:
|
|
|
157
157
|
return ""
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
def
|
|
161
|
-
|
|
160
|
+
def _lease_row_to_meta(row: sqlite3.Row) -> dict[str, Any]:
|
|
161
|
+
return {
|
|
162
|
+
"run_id": str(row["run_id"]),
|
|
163
|
+
"payer_user_id": str(row["payer_user_id"]),
|
|
164
|
+
"workspace_id": str(row["workspace_id"]),
|
|
165
|
+
"provider": str(row["provider"]),
|
|
166
|
+
"credential_binding_id": str(row["credential_binding_id"] or ""),
|
|
167
|
+
"profile_slug": str(row["profile_slug"]),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def inspect_lease(token: str) -> tuple[str | None, dict[str, Any] | None]:
|
|
172
|
+
"""Return (deny_reason, lease_meta). deny_reason is None when the lease is active."""
|
|
162
173
|
token = parse_lease_token(token)
|
|
163
174
|
if not token:
|
|
164
|
-
return None
|
|
175
|
+
return "missing_token", None
|
|
165
176
|
ensure_schema()
|
|
166
177
|
conn = _connect()
|
|
167
178
|
try:
|
|
@@ -176,16 +187,20 @@ def validate_lease(token: str) -> dict[str, Any] | None:
|
|
|
176
187
|
).fetchone()
|
|
177
188
|
finally:
|
|
178
189
|
conn.close()
|
|
179
|
-
if not row
|
|
180
|
-
return None
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
if not row:
|
|
191
|
+
return "not_found", None
|
|
192
|
+
meta = _lease_row_to_meta(row)
|
|
193
|
+
if row["revoked_at"]:
|
|
194
|
+
return "revoked", meta
|
|
195
|
+
if _expired(str(row["expires_at"] or "")):
|
|
196
|
+
return "expired", meta
|
|
197
|
+
return None, meta
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def validate_lease(token: str) -> dict[str, Any] | None:
|
|
201
|
+
"""Return lease metadata if token is active (not revoked/expired)."""
|
|
202
|
+
deny_reason, lease = inspect_lease(token)
|
|
203
|
+
return lease if deny_reason is None else None
|
|
189
204
|
|
|
190
205
|
|
|
191
206
|
def revoke_lease(run_id: str) -> None:
|