fuli-context 0.2.0

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.
Files changed (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,156 @@
1
+ from graphiti_core.edges import EntityEdge
2
+
3
+ from .models import (
4
+ ConfirmationBasis,
5
+ FactResult,
6
+ _validate_confirmation_state,
7
+ )
8
+ from .provider_values import json_object, native_datetime as _native_datetime
9
+
10
+
11
+ def is_default_retrievable(metadata: dict) -> bool:
12
+ return effective_confirmation_status(metadata) in {
13
+ 'confirmed',
14
+ 'agent_confirmed',
15
+ }
16
+
17
+
18
+ def effective_confirmation_status(metadata: dict) -> str:
19
+ status = metadata.get('confirmation_status', 'pending')
20
+ basis_data = json_object(metadata.get('confirmation_basis_json'))
21
+ if not basis_data:
22
+ return 'pending'
23
+ try:
24
+ basis = ConfirmationBasis.model_validate(basis_data)
25
+ _validate_confirmation_state(status, basis)
26
+ except ValueError:
27
+ return 'pending'
28
+ return status
29
+
30
+
31
+ async def read_edge_epistemic_metadata(store, edge_ids: list[str]) -> dict[str, dict]:
32
+ if not edge_ids:
33
+ return {}
34
+ records, _, _ = await store.runtime.driver.execute_query(
35
+ '''
36
+ MATCH ()-[edge:RELATES_TO]->()
37
+ WHERE edge.uuid IN $edge_ids
38
+ RETURN edge.uuid AS id,
39
+ coalesce(edge.fuli_origin_quadrant, 'known_known') AS origin_quadrant,
40
+ coalesce(edge.fuli_current_quadrant, 'known_known') AS current_quadrant,
41
+ coalesce(edge.fuli_epistemic_status, 'confirmed') AS epistemic_status,
42
+ coalesce(edge.fuli_confirmation_status, 'pending')
43
+ AS confirmation_status,
44
+ edge.fuli_confirmation_basis_json AS confirmation_basis_json,
45
+ edge.fuli_reasoning_summary AS reasoning_summary,
46
+ edge.fuli_profile_aspect AS profile_aspect,
47
+ CASE WHEN edge.fuli_profile_aspect IS NULL THEN NULL
48
+ ELSE coalesce(edge.fuli_preference_scope, 'global') END
49
+ AS preference_scope,
50
+ edge.fuli_preference_project_id AS preference_project_id,
51
+ edge.fuli_key AS key,
52
+ coalesce(edge.fuli_inheritance_mode, 'local_only')
53
+ AS inheritance_mode,
54
+ coalesce(edge.fuli_inherited_project_ids, [])
55
+ AS inherited_project_ids,
56
+ coalesce(edge.fuli_human_edited, false) AS human_edited,
57
+ coalesce(edge.fuli_human_change_status, 'none')
58
+ AS human_change_status,
59
+ coalesce(edge.fuli_human_change_version, 0)
60
+ AS human_change_version,
61
+ edge.fuli_last_human_changed_at AS last_human_changed_at,
62
+ edge.fuli_last_agent_viewed_at AS last_agent_viewed_at,
63
+ edge.fuli_last_agent_reviewed_at AS last_agent_reviewed_at
64
+ ,coalesce(edge.fuli_utility_score, 0.0) AS utility_score
65
+ ,coalesce(edge.fuli_confidence_score, 0.5) AS confidence_score
66
+ ,coalesce(edge.fuli_qualified_use_count, 0)
67
+ AS qualified_use_count
68
+ ,coalesce(edge.fuli_distinct_task_count, 0)
69
+ AS distinct_task_count
70
+ ,edge.fuli_last_used_at AS last_used_at
71
+ ''',
72
+ edge_ids=edge_ids,
73
+ routing_='r',
74
+ )
75
+ result = {}
76
+ for record in records:
77
+ value = dict(record)
78
+ effective_status = effective_confirmation_status(value)
79
+ if effective_status != value.get('confirmation_status'):
80
+ value['confidence_score'] = min(
81
+ float(value.get('confidence_score') or 0.5),
82
+ 0.5,
83
+ )
84
+ value['confirmation_status'] = effective_status
85
+ result[record['id']] = value
86
+ return result
87
+
88
+
89
+ def fact_result(
90
+ edge: EntityEdge,
91
+ names: dict[str, str],
92
+ space_id: str,
93
+ epistemic: dict | None = None,
94
+ ) -> FactResult:
95
+ metadata = epistemic or {}
96
+ return FactResult(
97
+ id=edge.uuid,
98
+ space_id=space_id,
99
+ group_id=edge.group_id,
100
+ source_entity=names.get(edge.source_node_uuid, edge.source_node_uuid),
101
+ target_entity=names.get(edge.target_node_uuid, edge.target_node_uuid),
102
+ relationship=edge.name,
103
+ fact=edge.fact,
104
+ valid_at=edge.valid_at,
105
+ invalid_at=edge.invalid_at,
106
+ created_at=_native_datetime(edge.created_at),
107
+ episodes=edge.episodes,
108
+ origin_quadrant=metadata.get('origin_quadrant', 'known_known'),
109
+ current_quadrant=metadata.get('current_quadrant', 'known_known'),
110
+ epistemic_status=metadata.get('epistemic_status', 'confirmed'),
111
+ confirmation_status=metadata.get('confirmation_status', 'pending'),
112
+ confirmation_basis=_confirmation_basis(
113
+ metadata.get('confirmation_basis_json')
114
+ ),
115
+ reasoning_summary=metadata.get('reasoning_summary'),
116
+ profile_aspect=metadata.get('profile_aspect'),
117
+ preference_scope=metadata.get('preference_scope'),
118
+ preference_project_id=metadata.get('preference_project_id'),
119
+ key=metadata.get('key'),
120
+ defined_project_id=metadata.get('defined_project_id'),
121
+ inheritance_mode=metadata.get('inheritance_mode') or 'local_only',
122
+ inherited_project_ids=metadata.get('inherited_project_ids') or [],
123
+ human_edited=metadata.get('human_edited') is True,
124
+ human_change_status=metadata.get('human_change_status') or 'none',
125
+ human_change_version=int(metadata.get('human_change_version') or 0),
126
+ last_human_changed_at=_native_datetime(
127
+ metadata.get('last_human_changed_at')
128
+ ),
129
+ last_agent_viewed_at=_native_datetime(
130
+ metadata.get('last_agent_viewed_at')
131
+ ),
132
+ last_agent_reviewed_at=_native_datetime(
133
+ metadata.get('last_agent_reviewed_at')
134
+ ),
135
+ utility_score=float(metadata.get('utility_score') or 0),
136
+ confidence_score=float(
137
+ metadata['confidence_score']
138
+ if metadata.get('confidence_score') is not None else 0.5
139
+ ),
140
+ qualified_use_count=int(metadata.get('qualified_use_count') or 0),
141
+ distinct_task_count=int(metadata.get('distinct_task_count') or 0),
142
+ last_used_at=_native_datetime(metadata.get('last_used_at')),
143
+ scope_distance=int(metadata.get('scope_distance') or 0),
144
+ inherited_from_project_id=metadata.get('inherited_from_project_id'),
145
+ scope_path=metadata.get('scope_path') or [],
146
+ )
147
+
148
+
149
+ def _confirmation_basis(value) -> ConfirmationBasis | None:
150
+ data = json_object(value)
151
+ if not data:
152
+ return None
153
+ try:
154
+ return ConfirmationBasis.model_validate(data)
155
+ except ValueError:
156
+ return None
@@ -0,0 +1,24 @@
1
+ from .config import Settings
2
+ from .provider_values import graphiti_group_id, native_datetime
3
+ from .runtime import GraphitiRuntime
4
+ from .store_identity import StoreIdentity
5
+ from .store_knowledge import StoreKnowledge
6
+ from .store_projects import StoreProjects
7
+ from .store_publication import StorePublication
8
+ from .store_records import StoreRecords
9
+
10
+
11
+ class GraphStore(
12
+ StoreIdentity,
13
+ StoreProjects,
14
+ StorePublication,
15
+ StoreKnowledge,
16
+ StoreRecords,
17
+ ):
18
+ def __init__(self, runtime: GraphitiRuntime, settings: Settings):
19
+ self.runtime = runtime
20
+ self.settings = settings
21
+ self._group_locks = {}
22
+
23
+
24
+ __all__ = ['GraphStore', 'graphiti_group_id', 'native_datetime']
@@ -0,0 +1,237 @@
1
+ from uuid import uuid4
2
+
3
+ from fastapi import HTTPException, status
4
+
5
+ from .auth import issue_access_token, token_hash
6
+ from .models import PrincipalResult, SpaceCreate, SpaceRecord
7
+ from .project_management import publish_project
8
+ from .provider_values import graphiti_group_id, now_utc
9
+
10
+
11
+ ROLE_RANK = {'reader': 1, 'contributor': 2, 'maintainer': 3}
12
+
13
+
14
+ class StoreIdentity:
15
+ async def health(self) -> dict:
16
+ records, _, _ = await self.runtime.driver.execute_query(
17
+ 'RETURN 1 AS ready', routing_='r'
18
+ )
19
+ return {
20
+ 'status': 'ready' if records and records[0]['ready'] == 1 else 'degraded',
21
+ 'providerId': self.settings.provider_id,
22
+ 'mode': self.settings.provider_mode,
23
+ 'storage': 'graphiti-neo4j',
24
+ 'llmExtraction': 'agent-structured',
25
+ 'embedding': 'local-hash-v1',
26
+ 'telemetry': False,
27
+ }
28
+
29
+ async def bootstrap(self, principal_name: str) -> tuple[str, str]:
30
+ existing, _, _ = await self.runtime.driver.execute_query(
31
+ '''
32
+ MATCH (p:FuliPrincipal {provider_admin: true})
33
+ RETURN p.id AS id ORDER BY p.created_at LIMIT 1
34
+ ''',
35
+ routing_='r',
36
+ )
37
+ access_token = issue_access_token()
38
+ if existing:
39
+ principal_id = existing[0]['id']
40
+ await self.runtime.driver.execute_query(
41
+ '''
42
+ MATCH (p:FuliPrincipal {id: $id, provider_admin: true})
43
+ SET p.token_hash = $token_hash, p.active = true
44
+ ''',
45
+ id=principal_id,
46
+ token_hash=token_hash(access_token),
47
+ )
48
+ return principal_id, access_token
49
+ principal_id = str(uuid4())
50
+ await self.runtime.driver.execute_query(
51
+ '''
52
+ CREATE (p:FuliPrincipal {
53
+ id: $id,
54
+ name: $name,
55
+ token_hash: $token_hash,
56
+ active: true,
57
+ provider_admin: true,
58
+ created_at: $created_at
59
+ })
60
+ ''',
61
+ id=principal_id,
62
+ name=principal_name,
63
+ token_hash=token_hash(access_token),
64
+ created_at=now_utc(),
65
+ )
66
+ return principal_id, access_token
67
+
68
+ async def authenticate(self, access_token: str) -> dict:
69
+ records, _, _ = await self.runtime.driver.execute_query(
70
+ '''
71
+ MATCH (p:FuliPrincipal {token_hash: $token_hash, active: true})
72
+ RETURN p.id AS id, p.name AS name, p.provider_admin AS provider_admin
73
+ ''',
74
+ token_hash=token_hash(access_token),
75
+ routing_='r',
76
+ )
77
+ if not records:
78
+ raise HTTPException(
79
+ status_code=status.HTTP_401_UNAUTHORIZED,
80
+ detail='invalid access token',
81
+ )
82
+ return dict(records[0])
83
+
84
+ async def create_principal(self, actor: dict, name: str) -> PrincipalResult:
85
+ if not actor.get('provider_admin'):
86
+ raise HTTPException(status_code=403, detail='provider administrator required')
87
+ principal_id = str(uuid4())
88
+ access_token = issue_access_token()
89
+ await self.runtime.driver.execute_query(
90
+ '''
91
+ CREATE (p:FuliPrincipal {
92
+ id: $id,
93
+ name: $name,
94
+ token_hash: $token_hash,
95
+ active: true,
96
+ provider_admin: false,
97
+ created_at: $created_at
98
+ })
99
+ ''',
100
+ id=principal_id,
101
+ name=name,
102
+ token_hash=token_hash(access_token),
103
+ created_at=now_utc(),
104
+ )
105
+ return PrincipalResult(principal_id=principal_id, access_token=access_token)
106
+
107
+ async def create_space(self, actor: dict, request: SpaceCreate) -> SpaceRecord:
108
+ expected_kind = 'personal' if self.settings.provider_mode == 'personal' else 'project'
109
+ if request.kind != expected_kind:
110
+ raise HTTPException(
111
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
112
+ detail=f'{self.settings.provider_mode} provider only accepts {expected_kind} spaces',
113
+ )
114
+ if request.kind == 'project' and request.profile:
115
+ if any(source.sensitivity != 'normal' for source in request.profile.sources):
116
+ raise HTTPException(
117
+ status_code=422,
118
+ detail='private or restricted project sources cannot be published',
119
+ )
120
+ if request.kind == 'project':
121
+ return await publish_project(self, actor, request)
122
+ space_id = str(uuid4())
123
+ group_id = graphiti_group_id(self.settings.provider_id, request.kind, space_id)
124
+ created_at = now_utc()
125
+ visibility = 'private' if request.kind == 'personal' else 'public'
126
+ query = '''
127
+ MATCH (p:FuliPrincipal {id: $principal_id})
128
+ CREATE (s:FuliSpace {
129
+ id: $id,
130
+ name: $name,
131
+ kind: $kind,
132
+ group_id: $group_id,
133
+ description: $description,
134
+ visibility: $visibility,
135
+ publication_key: $publication_key,
136
+ profile_json: $profile_json,
137
+ created_at: $created_at
138
+ })
139
+ CREATE (p)-[:OWNS {created_at: $created_at}]->(s)
140
+ RETURN s
141
+ '''
142
+ records, _, _ = await self.runtime.driver.execute_query(
143
+ query,
144
+ principal_id=actor['id'],
145
+ id=space_id,
146
+ name=request.name,
147
+ kind=request.kind,
148
+ group_id=group_id,
149
+ description=request.description,
150
+ visibility=visibility,
151
+ publication_key=request.publication_key,
152
+ profile_json=request.profile.model_dump_json() if request.profile else None,
153
+ created_at=created_at,
154
+ )
155
+ return self._space(
156
+ records[0]['s'],
157
+ owner_id=actor['id'],
158
+ role='maintainer',
159
+ )
160
+
161
+ async def list_spaces(self, actor: dict) -> list[SpaceRecord]:
162
+ if self.settings.provider_mode == 'personal':
163
+ query = '''
164
+ MATCH (p:FuliPrincipal {id: $principal_id})-[:OWNS]->
165
+ (s:FuliSpace {kind: 'personal'})
166
+ RETURN s, p.id AS owner_id, 'maintainer' AS role
167
+ ORDER BY s.created_at
168
+ '''
169
+ else:
170
+ query = '''
171
+ MATCH (s:FuliSpace {kind: 'project', visibility: 'public'})
172
+ MATCH (owner:FuliPrincipal)-[:OWNS]->(s)
173
+ OPTIONAL MATCH (actor:FuliPrincipal {id: $principal_id})-
174
+ [membership:MEMBER_OF]->(s)
175
+ RETURN s, owner.id AS owner_id,
176
+ coalesce(membership.role, 'reader') AS role
177
+ ORDER BY s.created_at
178
+ '''
179
+ records, _, _ = await self.runtime.driver.execute_query(
180
+ query,
181
+ principal_id=actor['id'],
182
+ routing_='r',
183
+ )
184
+ return [
185
+ self._space(
186
+ record['s'],
187
+ owner_id=record['owner_id'],
188
+ role=record['role'],
189
+ can_manage=(
190
+ bool(actor.get('provider_admin')) or record['owner_id'] == actor['id']
191
+ ),
192
+ )
193
+ for record in records
194
+ ]
195
+
196
+ async def authorize(self, actor: dict, space_id: str, required_role: str) -> dict:
197
+ if self.settings.provider_mode == 'personal':
198
+ records, _, _ = await self.runtime.driver.execute_query(
199
+ '''
200
+ MATCH (principal:FuliPrincipal {id: $principal_id})-[:OWNS]->
201
+ (space:FuliSpace {id: $space_id, kind: 'personal'})
202
+ RETURN space, 'maintainer' AS role
203
+ ''',
204
+ principal_id=actor['id'],
205
+ space_id=space_id,
206
+ routing_='r',
207
+ )
208
+ else:
209
+ records, _, _ = await self.runtime.driver.execute_query(
210
+ '''
211
+ MATCH (space:FuliSpace {
212
+ id: $space_id,
213
+ kind: 'project',
214
+ visibility: 'public'
215
+ })
216
+ OPTIONAL MATCH (principal:FuliPrincipal {id: $principal_id})-
217
+ [membership:MEMBER_OF]->(space)
218
+ RETURN space, coalesce(membership.role, 'reader') AS role
219
+ ''',
220
+ principal_id=actor['id'],
221
+ space_id=space_id,
222
+ routing_='r',
223
+ )
224
+ if not records:
225
+ raise HTTPException(status_code=403, detail='space access denied')
226
+ role = records[0]['role']
227
+ if ROLE_RANK.get(role, 0) < ROLE_RANK[required_role]:
228
+ raise HTTPException(status_code=403, detail=f'{required_role} role required')
229
+ return dict(records[0]['space'])
230
+
231
+ def _require_personal(self) -> None:
232
+ if self.settings.provider_mode != 'personal':
233
+ raise HTTPException(status_code=404, detail='personal provider endpoint')
234
+
235
+ def _require_workspace(self) -> None:
236
+ if self.settings.provider_mode != 'workspace':
237
+ raise HTTPException(status_code=404, detail='workspace provider endpoint')