fuli-context 0.7.2 → 0.7.4-beta.1
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 +173 -21
- package/README.zh-CN.md +146 -12
- package/compose.graphiti.yml +2 -0
- package/dist/web/assets/{AboutPage-DfnZo7Lo.js → AboutPage-CftRexSY.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-BloaSKmt.js → ConnectionsPage-D5J91x6d.js} +1 -1
- package/dist/web/assets/{useMinimumLoadingDisplay-BpCyBeko.css → KnowledgeEditDialog-DWUV_NPp.css} +1 -1
- package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +1 -0
- package/dist/web/assets/{KnowledgeInspector-Bjhdu2mf.css → KnowledgeInspector--1NujtXW.css} +1 -1
- package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +1 -0
- package/dist/web/assets/KnowledgePage-OUT07aKH.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +4 -0
- package/dist/web/assets/OverviewPage-CHE6XhdS.js +1 -0
- package/dist/web/assets/PersonalProfilePage-B7T09bow.css +1 -0
- package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +1 -0
- package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +1 -0
- package/dist/web/assets/{ReviewPage-B7EN817i.js → ReviewPage-DUeyu7E8.js} +1 -1
- package/dist/web/assets/{SearchableSelect-ClLN0rIb.js → SearchableSelect-C5RiB5F8.js} +1 -1
- package/dist/web/assets/SettingsPage-BMuASSzQ.js +1 -0
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +1 -0
- package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +1 -0
- package/dist/web/assets/WritingTastePage-DSgvTH6x.css +1 -0
- package/dist/web/assets/{console-CbDsKrsO.js → console-DMvHxKf9.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
- package/dist/web/assets/{i18n-BQcns1PH.js → i18n-CWfd8Dtt.js} +1 -1
- package/dist/web/assets/{identity-B8K4VYg6.js → identity-C9mLdR8L.js} +1 -1
- package/dist/web/assets/index-F5e2sQ7p.js +2 -0
- package/dist/web/assets/index-X2j29j8i.css +1 -0
- package/dist/web/assets/{model-eF52GgPF.js → model-CsYAR8O3.js} +1 -1
- package/dist/web/assets/paths-CDREouhi.js +1 -0
- package/dist/web/assets/useApi-CROJJdhE-BLQiPVw0.js +1 -0
- package/dist/web/assets/vue-router-DN8x6iY1.js +1 -0
- package/dist/web/index.html +7 -6
- package/graph-provider/fuli_graph/app.py +238 -2
- package/graph-provider/fuli_graph/auth.py +18 -0
- package/graph-provider/fuli_graph/collaboration_context.py +253 -17
- package/graph-provider/fuli_graph/common_knowledge.py +6 -2
- package/graph-provider/fuli_graph/config.py +5 -0
- package/graph-provider/fuli_graph/graph_models.py +6 -0
- package/graph-provider/fuli_graph/graph_projection.py +14 -0
- package/graph-provider/fuli_graph/graph_query.py +27 -3
- package/graph-provider/fuli_graph/knowledge_management.py +19 -1
- package/graph-provider/fuli_graph/knowledge_review.py +49 -4
- package/graph-provider/fuli_graph/knowledge_review_models.py +11 -5
- package/graph-provider/fuli_graph/knowledge_search.py +11 -0
- package/graph-provider/fuli_graph/models.py +15 -2
- package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
- package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
- package/graph-provider/fuli_graph/project_action_models.py +22 -0
- package/graph-provider/fuli_graph/project_knowledge.py +121 -2
- package/graph-provider/fuli_graph/provider_values.py +24 -0
- package/graph-provider/fuli_graph/runtime.py +78 -0
- package/graph-provider/fuli_graph/search_projection.py +14 -1
- package/graph-provider/fuli_graph/store_knowledge.py +116 -2
- package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
- package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
- package/graph-provider/fuli_graph/workflow_observations.py +84 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +4 -4
- package/skills/capturing-session-knowledge/SKILL.md +32 -5
- package/skills/flreview/SKILL.md +68 -40
- package/skills/flreview/agents/openai.yaml +2 -2
- package/src/agent-tools/graph-definitions.js +140 -24
- package/src/agent-tools/graph-handlers.js +10 -4
- package/src/cli/command-registry.js +1 -1
- package/src/cli/local-runtime-command.js +69 -2
- package/src/graphiti/agent-knowledge-workflows.js +497 -16
- package/src/graphiti/collaboration-preference-projection.js +162 -0
- package/src/graphiti/federated-application.js +157 -162
- package/src/graphiti/federated-search-support.js +102 -0
- package/src/graphiti/knowledge-provider-mapping.js +15 -0
- package/src/graphiti/provider-client.js +162 -10
- package/src/graphiti/related-project-suggestions.js +109 -0
- package/src/graphiti/runtime-config.js +14 -0
- package/src/graphiti/source-marker.js +2 -2
- package/src/graphiti/user-taste-skill.js +241 -0
- package/src/graphiti/workflow-candidate-mapping.js +87 -0
- package/src/graphiti/workflow-observation.js +82 -0
- package/src/graphiti/writing-taste-profile-workflow.js +25 -0
- package/src/graphiti/writing-taste-profile.js +490 -0
- package/src/http/graph-api-router.js +15 -2
- package/src/mcp/context-resources.js +158 -0
- package/src/mcp/create-mcp-server.js +41 -5
- package/src/mcp/instructions.js +5 -7
- package/src/mcp/session-id.js +60 -0
- package/src/mcp/task-context-registry.js +15 -3
- package/src/mcp/tool-annotations.js +8 -1
- package/src/mcp/tool-result.js +28 -4
- package/src/server.js +0 -1
- package/src/setup/claude-code-config.js +17 -6
- package/src/setup/graph-runtime.js +20 -2
- package/src/system/runtime-settings.js +121 -2
- package/src/system/system-service.js +5 -1
- package/README.en.md +0 -5
- package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +0 -1
- package/dist/web/assets/KnowledgePage-BWL0jT01.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +0 -4
- package/dist/web/assets/OverviewPage-5sJoO1Gh.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
- package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +0 -1
- package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +0 -1
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
- package/dist/web/assets/SettingsPage-Chd_B3eH.js +0 -1
- package/dist/web/assets/index-CB8Xmsc2.js +0 -2
- package/dist/web/assets/index-CZeuPh-U.css +0 -1
- package/dist/web/assets/paths-hwz-Ac1g.js +0 -1
- package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +0 -1
- package/dist/web/assets/vue-router-BsEp_XwI.js +0 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager
|
|
2
2
|
from typing import Annotated
|
|
3
3
|
|
|
4
|
-
from fastapi import Depends, FastAPI, Header, Query
|
|
4
|
+
from fastapi import Depends, FastAPI, Header, HTTPException, Query
|
|
5
5
|
|
|
6
|
-
from .auth import
|
|
6
|
+
from .auth import (
|
|
7
|
+
matches_bootstrap_token,
|
|
8
|
+
matches_human_review_token,
|
|
9
|
+
matches_workflow_observation_token,
|
|
10
|
+
)
|
|
7
11
|
from .config import Settings, get_settings
|
|
8
12
|
from .graph_models import GraphResult
|
|
9
13
|
from .knowledge_usage_models import KnowledgeUsageCreate, KnowledgeUsageResult
|
|
@@ -30,7 +34,30 @@ from .project_action_models import (
|
|
|
30
34
|
KnowledgeProjectActionResult,
|
|
31
35
|
KnowledgeProjectPreviewRecord,
|
|
32
36
|
KnowledgeProjectPreviewRequest,
|
|
37
|
+
PersonalProjectRelationReviewRecord,
|
|
38
|
+
PersonalProjectRelationReviewRequest,
|
|
39
|
+
)
|
|
40
|
+
from .personal_global_preference_models import (
|
|
41
|
+
PersonalGlobalPreferenceDecisionApply,
|
|
42
|
+
PersonalGlobalPreferenceDecisionInspection,
|
|
43
|
+
PersonalGlobalPreferenceDecisionIntent,
|
|
44
|
+
PersonalGlobalPreferenceDecisionPreview,
|
|
45
|
+
PersonalGlobalPreferenceDecisionRecord,
|
|
46
|
+
PersonalGlobalPreferenceDecisionStatusRequest,
|
|
47
|
+
PersonalGlobalPreferenceDecisionStatusResult,
|
|
48
|
+
PersonalGlobalPreferenceScopeOptions,
|
|
49
|
+
PersonalGlobalPreferenceScopeOptionsRequest,
|
|
50
|
+
)
|
|
51
|
+
from .workflow_candidate_models import (
|
|
52
|
+
WorkflowCandidate,
|
|
53
|
+
WorkflowCandidatePage,
|
|
54
|
+
WorkflowCandidateReview,
|
|
55
|
+
WorkflowCandidateReviewIntent,
|
|
56
|
+
WorkflowCandidateReviewPreview,
|
|
57
|
+
WorkflowCandidateSearch,
|
|
58
|
+
WorkflowRecommendationPage,
|
|
33
59
|
)
|
|
60
|
+
from .workflow_observation_models import WorkflowTransitionObservation
|
|
34
61
|
from .models import (
|
|
35
62
|
BootstrapRequest,
|
|
36
63
|
BootstrapResult,
|
|
@@ -105,6 +132,7 @@ from .knowledge_management import (
|
|
|
105
132
|
from .project_knowledge import (
|
|
106
133
|
apply_knowledge_project_action,
|
|
107
134
|
preview_knowledge_project_action,
|
|
135
|
+
review_personal_project_relation,
|
|
108
136
|
)
|
|
109
137
|
from .preference_conflicts import (
|
|
110
138
|
complete_preference_conflict,
|
|
@@ -112,6 +140,20 @@ from .preference_conflicts import (
|
|
|
112
140
|
list_preference_conflicts,
|
|
113
141
|
resolve_preference_conflict,
|
|
114
142
|
)
|
|
143
|
+
from .personal_global_preferences import (
|
|
144
|
+
apply_personal_global_preference_decision,
|
|
145
|
+
inspect_personal_global_preference_decision,
|
|
146
|
+
personal_global_preference_decision_status,
|
|
147
|
+
personal_global_preference_scope_options,
|
|
148
|
+
preview_personal_global_preference_decision,
|
|
149
|
+
)
|
|
150
|
+
from .workflow_candidates import (
|
|
151
|
+
preview_workflow_candidate_review,
|
|
152
|
+
recommend_workflow_candidates,
|
|
153
|
+
review_workflow_candidate,
|
|
154
|
+
search_workflow_candidates,
|
|
155
|
+
)
|
|
156
|
+
from .workflow_observations import record_workflow_transition_observation
|
|
115
157
|
from .runtime import GraphitiRuntime
|
|
116
158
|
from .store import GraphStore
|
|
117
159
|
|
|
@@ -147,6 +189,51 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
147
189
|
|
|
148
190
|
Actor = Annotated[dict, Depends(current_actor)]
|
|
149
191
|
|
|
192
|
+
async def current_human_reviewer(
|
|
193
|
+
actor: Actor,
|
|
194
|
+
x_fuli_human_review_token: Annotated[str | None, Header()] = None,
|
|
195
|
+
) -> dict:
|
|
196
|
+
if not resolved_settings.human_review_token:
|
|
197
|
+
raise HTTPException(
|
|
198
|
+
status_code=503,
|
|
199
|
+
detail='human workflow review channel is not configured',
|
|
200
|
+
)
|
|
201
|
+
if not matches_human_review_token(
|
|
202
|
+
x_fuli_human_review_token,
|
|
203
|
+
resolved_settings.human_review_token,
|
|
204
|
+
):
|
|
205
|
+
raise HTTPException(
|
|
206
|
+
status_code=403,
|
|
207
|
+
detail='independent human review proof required',
|
|
208
|
+
)
|
|
209
|
+
return {**actor, '_human_review_verified': True}
|
|
210
|
+
|
|
211
|
+
HumanReviewer = Annotated[dict, Depends(current_human_reviewer)]
|
|
212
|
+
|
|
213
|
+
async def current_workflow_observer(
|
|
214
|
+
actor: Actor,
|
|
215
|
+
x_fuli_workflow_observation_token: Annotated[
|
|
216
|
+
str | None,
|
|
217
|
+
Header(),
|
|
218
|
+
] = None,
|
|
219
|
+
) -> dict:
|
|
220
|
+
if not resolved_settings.workflow_observation_token:
|
|
221
|
+
raise HTTPException(
|
|
222
|
+
status_code=503,
|
|
223
|
+
detail='MCP host workflow observation channel is not configured',
|
|
224
|
+
)
|
|
225
|
+
if not matches_workflow_observation_token(
|
|
226
|
+
x_fuli_workflow_observation_token,
|
|
227
|
+
resolved_settings.workflow_observation_token,
|
|
228
|
+
):
|
|
229
|
+
raise HTTPException(
|
|
230
|
+
status_code=403,
|
|
231
|
+
detail='independent MCP host observation proof required',
|
|
232
|
+
)
|
|
233
|
+
return actor
|
|
234
|
+
|
|
235
|
+
WorkflowObserver = Annotated[dict, Depends(current_workflow_observer)]
|
|
236
|
+
|
|
150
237
|
@application.get('/health')
|
|
151
238
|
async def health() -> dict:
|
|
152
239
|
return await store.health()
|
|
@@ -337,6 +424,137 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
337
424
|
async def commit_personal(request: KnowledgeCommit, actor: Actor) -> CommitResult:
|
|
338
425
|
return await store.commit_personal(actor, request)
|
|
339
426
|
|
|
427
|
+
@application.post('/v1/workflow-observations', response_model=CommitResult)
|
|
428
|
+
async def record_workflow_observation(
|
|
429
|
+
request: WorkflowTransitionObservation,
|
|
430
|
+
actor: WorkflowObserver,
|
|
431
|
+
) -> CommitResult:
|
|
432
|
+
return await record_workflow_transition_observation(store, actor, request)
|
|
433
|
+
|
|
434
|
+
@application.post(
|
|
435
|
+
'/v1/personal-global-preference-candidates/decision-status',
|
|
436
|
+
response_model=PersonalGlobalPreferenceDecisionStatusResult,
|
|
437
|
+
)
|
|
438
|
+
async def personal_global_preference_status(
|
|
439
|
+
request: PersonalGlobalPreferenceDecisionStatusRequest,
|
|
440
|
+
actor: Actor,
|
|
441
|
+
) -> PersonalGlobalPreferenceDecisionStatusResult:
|
|
442
|
+
return await personal_global_preference_decision_status(
|
|
443
|
+
store,
|
|
444
|
+
actor,
|
|
445
|
+
request,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
@application.post(
|
|
449
|
+
'/v1/personal-global-preference-candidates/{candidate_id}/scope-options',
|
|
450
|
+
response_model=PersonalGlobalPreferenceScopeOptions,
|
|
451
|
+
)
|
|
452
|
+
async def personal_global_preference_scopes(
|
|
453
|
+
candidate_id: str,
|
|
454
|
+
request: PersonalGlobalPreferenceScopeOptionsRequest,
|
|
455
|
+
actor: Actor,
|
|
456
|
+
) -> PersonalGlobalPreferenceScopeOptions:
|
|
457
|
+
return await personal_global_preference_scope_options(
|
|
458
|
+
store,
|
|
459
|
+
actor,
|
|
460
|
+
candidate_id,
|
|
461
|
+
request,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
@application.post(
|
|
465
|
+
'/v1/personal-global-preference-candidates/{candidate_id}/decision-inspection',
|
|
466
|
+
response_model=PersonalGlobalPreferenceDecisionInspection,
|
|
467
|
+
)
|
|
468
|
+
async def inspect_personal_global_preference(
|
|
469
|
+
candidate_id: str,
|
|
470
|
+
request: PersonalGlobalPreferenceDecisionIntent,
|
|
471
|
+
actor: Actor,
|
|
472
|
+
) -> PersonalGlobalPreferenceDecisionInspection:
|
|
473
|
+
return await inspect_personal_global_preference_decision(
|
|
474
|
+
store,
|
|
475
|
+
actor,
|
|
476
|
+
candidate_id,
|
|
477
|
+
request,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
@application.post(
|
|
481
|
+
'/v1/personal-global-preference-candidates/{candidate_id}/decision-preview',
|
|
482
|
+
response_model=PersonalGlobalPreferenceDecisionPreview,
|
|
483
|
+
)
|
|
484
|
+
async def preview_personal_global_preference(
|
|
485
|
+
candidate_id: str,
|
|
486
|
+
request: PersonalGlobalPreferenceDecisionIntent,
|
|
487
|
+
actor: HumanReviewer,
|
|
488
|
+
) -> PersonalGlobalPreferenceDecisionPreview:
|
|
489
|
+
return await preview_personal_global_preference_decision(
|
|
490
|
+
store,
|
|
491
|
+
actor,
|
|
492
|
+
candidate_id,
|
|
493
|
+
request,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
@application.post(
|
|
497
|
+
'/v1/personal-global-preference-candidates/{candidate_id}/decision',
|
|
498
|
+
response_model=PersonalGlobalPreferenceDecisionRecord,
|
|
499
|
+
)
|
|
500
|
+
async def apply_personal_global_preference(
|
|
501
|
+
candidate_id: str,
|
|
502
|
+
request: PersonalGlobalPreferenceDecisionApply,
|
|
503
|
+
actor: HumanReviewer,
|
|
504
|
+
) -> PersonalGlobalPreferenceDecisionRecord:
|
|
505
|
+
return await apply_personal_global_preference_decision(
|
|
506
|
+
store,
|
|
507
|
+
actor,
|
|
508
|
+
candidate_id,
|
|
509
|
+
request,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
@application.post(
|
|
513
|
+
'/v1/workflow-candidates/recommendations',
|
|
514
|
+
response_model=WorkflowRecommendationPage,
|
|
515
|
+
)
|
|
516
|
+
async def workflow_candidate_recommendations(
|
|
517
|
+
request: WorkflowCandidateSearch,
|
|
518
|
+
actor: Actor,
|
|
519
|
+
) -> WorkflowRecommendationPage:
|
|
520
|
+
return await recommend_workflow_candidates(store, actor, request)
|
|
521
|
+
|
|
522
|
+
@application.post(
|
|
523
|
+
'/v1/workflow-candidates/search',
|
|
524
|
+
response_model=WorkflowCandidatePage,
|
|
525
|
+
)
|
|
526
|
+
async def workflow_candidate_search(
|
|
527
|
+
request: WorkflowCandidateSearch,
|
|
528
|
+
actor: Actor,
|
|
529
|
+
) -> WorkflowCandidatePage:
|
|
530
|
+
return await search_workflow_candidates(store, actor, request)
|
|
531
|
+
|
|
532
|
+
@application.post(
|
|
533
|
+
'/v1/workflow-candidates/{candidate_id}/review-preview',
|
|
534
|
+
response_model=WorkflowCandidateReviewPreview,
|
|
535
|
+
)
|
|
536
|
+
async def preview_workflow_review(
|
|
537
|
+
candidate_id: str,
|
|
538
|
+
request: WorkflowCandidateReviewIntent,
|
|
539
|
+
actor: HumanReviewer,
|
|
540
|
+
) -> WorkflowCandidateReviewPreview:
|
|
541
|
+
return await preview_workflow_candidate_review(
|
|
542
|
+
store, actor, candidate_id, request
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
@application.post(
|
|
546
|
+
'/v1/workflow-candidates/{candidate_id}/review',
|
|
547
|
+
response_model=WorkflowCandidate,
|
|
548
|
+
)
|
|
549
|
+
async def review_workflow(
|
|
550
|
+
candidate_id: str,
|
|
551
|
+
request: WorkflowCandidateReview,
|
|
552
|
+
actor: HumanReviewer,
|
|
553
|
+
) -> WorkflowCandidate:
|
|
554
|
+
return await review_workflow_candidate(
|
|
555
|
+
store, actor, candidate_id, request
|
|
556
|
+
)
|
|
557
|
+
|
|
340
558
|
@application.post(
|
|
341
559
|
'/v1/knowledge/reviews/start',
|
|
342
560
|
response_model=KnowledgeReviewRun,
|
|
@@ -577,6 +795,24 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
577
795
|
) -> KnowledgeProjectActionResult:
|
|
578
796
|
return await apply_knowledge_project_action(store, actor, item_id, request)
|
|
579
797
|
|
|
798
|
+
@application.post(
|
|
799
|
+
'/v1/personal-spaces/{space_id}/project-relations/{relation_id}/review',
|
|
800
|
+
response_model=PersonalProjectRelationReviewRecord,
|
|
801
|
+
)
|
|
802
|
+
async def review_local_project_relation(
|
|
803
|
+
space_id: str,
|
|
804
|
+
relation_id: str,
|
|
805
|
+
request: PersonalProjectRelationReviewRequest,
|
|
806
|
+
actor: HumanReviewer,
|
|
807
|
+
) -> PersonalProjectRelationReviewRecord:
|
|
808
|
+
return await review_personal_project_relation(
|
|
809
|
+
store,
|
|
810
|
+
actor,
|
|
811
|
+
space_id,
|
|
812
|
+
relation_id,
|
|
813
|
+
request,
|
|
814
|
+
)
|
|
815
|
+
|
|
580
816
|
@application.post(
|
|
581
817
|
'/v1/projects/{project_id}/proposals',
|
|
582
818
|
response_model=ProposalRecord,
|
|
@@ -15,3 +15,21 @@ def matches_bootstrap_token(provided: str | None, expected: str) -> bool:
|
|
|
15
15
|
if not provided:
|
|
16
16
|
return False
|
|
17
17
|
return hmac.compare_digest(provided, expected)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def matches_human_review_token(
|
|
21
|
+
provided: str | None,
|
|
22
|
+
expected: str | None,
|
|
23
|
+
) -> bool:
|
|
24
|
+
if not provided or not expected:
|
|
25
|
+
return False
|
|
26
|
+
return hmac.compare_digest(provided, expected)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def matches_workflow_observation_token(
|
|
30
|
+
provided: str | None,
|
|
31
|
+
expected: str | None,
|
|
32
|
+
) -> bool:
|
|
33
|
+
if not provided or not expected:
|
|
34
|
+
return False
|
|
35
|
+
return hmac.compare_digest(provided, expected)
|