fuli-context 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/README.zh-CN.md +9 -4
- package/dist/web/assets/{AboutPage-BAD0iPVX.js → AboutPage-Dized8Uz.js} +1 -1
- package/dist/web/assets/{AgentConversations-1iGTd4sz.js → AgentConversations-Ccy41mEA.js} +1 -1
- package/dist/web/assets/{AgentHand-D_RafKOM.js → AgentHand-UzVhbKUC.js} +1 -1
- package/dist/web/assets/{AgentName-B5LKQ09q.js → AgentName-DJpvkub2.js} +1 -1
- package/dist/web/assets/AgentProfilePage-CFViQ8kj.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-CBFTg_IE.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-Cv-vxw2C.css +1 -0
- package/dist/web/assets/{ConnectionsPage-Tr2rRVOZ.js → ConnectionsPage-uXcabab_.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-D8Hvhc-M.js +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-TBl_HNj6.js +3 -0
- package/dist/web/assets/KnowledgeEditDialog-HmiqLC-Y.js +1 -0
- package/dist/web/assets/KnowledgeInspector-Cj2yK4Qh.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DT3BROZm.js +1 -0
- package/dist/web/assets/KnowledgePage-C4J4VueL.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DA_dqVRG.js +4 -0
- package/dist/web/assets/{NotFoundPage-fetSnp0N.js → NotFoundPage-x_Yd59mm.js} +1 -1
- package/dist/web/assets/{OverviewPage-BD4yY2Xz.js → OverviewPage-3eZjQ26v.js} +1 -1
- package/dist/web/assets/OverviewPage-8LBN8Shi.css +1 -0
- package/dist/web/assets/PersonalProfilePage-CDQk096L.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-CJxp4Wyz.js +1 -0
- package/dist/web/assets/{ProjectAgentFirstTask-BcxqmKXQ.js → ProjectAgentFirstTask-BkRJ0zwO.js} +1 -1
- package/dist/web/assets/{ProjectAgentsPage-BWDJBESb.js → ProjectAgentsPage-BuNNqSEy.js} +8 -8
- package/dist/web/assets/{ProjectScopePicker-yodM7lF_.js → ProjectScopePicker-CiDxM2Yx.js} +1 -1
- package/dist/web/assets/PublicProjectsPage-ByOT559r.js +1 -0
- package/dist/web/assets/{ReviewPage-Cy0Rt4CO.js → ReviewPage-C0Vdr9uq.js} +1 -1
- package/dist/web/assets/RoundtablesPage-Drou-B81.css +1 -0
- package/dist/web/assets/RoundtablesPage-L3ShEWS3.js +1 -0
- package/dist/web/assets/SettingsPage-BE8pWbI6.css +1 -0
- package/dist/web/assets/SettingsPage-McYuinGz.js +1 -0
- package/dist/web/assets/{SharedJefaReportPage-C56kPciB.js → SharedJefaReportPage-Bg6lIk3E.js} +1 -1
- package/dist/web/assets/UiButton-x8hiHNRM.js +1 -0
- package/dist/web/assets/UiDialog-mjletK0y.js +1 -0
- package/dist/web/assets/{UiDisclosure-D0iMTplF.js → UiDisclosure-_IxeiexW.js} +1 -1
- package/dist/web/assets/{UiSegmented-DIJW6wv3.js → UiSegmented-WFfz9Evl.js} +1 -1
- package/dist/web/assets/{UiSelect-CMR0QqF6.js → UiSelect-K29jnV-1.js} +1 -1
- package/dist/web/assets/{WritingTastePage-QgNzssfF.js → WritingTastePage-DCULB-d2.js} +1 -1
- package/dist/web/assets/agent-pins-DS4mNsNt.js +1 -0
- package/dist/web/assets/agent-profile-Bh3MJGvb.css +1 -0
- package/dist/web/assets/{attention-store-B1DI8wmd.js → attention-store-CA5y8j1R.js} +1 -1
- package/dist/web/assets/catalog-D0VEfOAV.js +1 -0
- package/dist/web/assets/{client-CR-W5Wzp.js → client-DnwnUBQV.js} +1 -1
- package/dist/web/assets/{console-D7GHOMEC.js → console-Cw7TzOSC.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-Yc13viwr.js +1 -0
- package/dist/web/assets/{i18n-CVFhT_k7.js → i18n-oJFlt8hW.js} +3 -3
- package/dist/web/assets/{identity-CM6Dmy00.js → identity-DSGS4CZc.js} +1 -1
- package/dist/web/assets/index-BfHCcUL2.css +1 -0
- package/dist/web/assets/index-CxNER7Oo.js +5 -0
- package/dist/web/assets/{judgment-types-CJBEyJs1.js → judgment-types-DR931cZ7.js} +1 -1
- package/dist/web/assets/{model-WFhQZ3v_.js → model-BJ-UF4cs.js} +1 -1
- package/dist/web/assets/{personal-profile-graph-BwCh2M9G.js → personal-profile-graph-SOK0BR0w.js} +1 -1
- package/dist/web/assets/project-team-api-C_0qJk_B.js +1 -0
- package/dist/web/assets/{shared-report-CeIvrL9a.js → shared-report-CiX64ZiD.js} +1 -1
- package/dist/web/assets/useAgentRoster-B84mzZIg.js +1 -0
- package/dist/web/assets/{useApi-CROJJdhE-BYhbjaZB.js → useApi-CROJJdhE-7gt0QyQ5.js} +1 -1
- package/dist/web/assets/{vue-router-DaxrQwX3.js → vue-router-CCvPGaLD.js} +1 -1
- package/dist/web/index.html +15 -14
- package/dist/web/licenses/ant-design-icons.txt +22 -0
- package/docs/agent-conversations-and-collaboration.md +3 -3
- package/docs/agent-roundtable.md +33 -0
- package/docs/client-connections.md +108 -0
- package/docs/employee-agents.md +3 -3
- package/docs/project-agent-memory.md +6 -6
- package/graph-provider/fuli_graph/default_project_lead.py +1 -1
- package/graph-provider/fuli_graph/project_agent_delegations.py +70 -2
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +18 -1
- package/graph-provider/fuli_graph/project_agent_remote_origins.py +154 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +1 -1
- package/graph-provider/fuli_graph/runtime.py +7 -0
- package/graph-provider/fuli_graph/store_project_agent_context.py +3 -2
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +1 -1
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +1 -1
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +5 -5
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +2 -2
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +2 -2
- package/graph-provider/fuli_graph/store_project_agents.py +6 -6
- package/graph-provider/fuli_graph/system_agents.py +12 -2
- package/graph-provider/fuli_graph/system_hr_identity.py +2 -2
- package/graph-provider/pyproject.toml +2 -1
- package/npm-shrinkwrap.json +280 -5
- package/package.json +6 -2
- package/skills/operating-fuli/SKILL.md +3 -3
- package/src/agent-peer/coordinator-http.js +117 -0
- package/src/agent-peer/coordinator-service.js +258 -0
- package/src/agent-peer/coordinator-store.js +95 -0
- package/src/agent-peer/device-identity.js +70 -0
- package/src/agent-peer/http.js +47 -0
- package/src/agent-peer/node-store.js +127 -0
- package/src/agent-peer/origin-proof.js +56 -0
- package/src/agent-peer/peer-port.js +140 -0
- package/src/agent-peer/peer-runtime.js +298 -0
- package/src/agent-peer/peer-transport.js +78 -0
- package/src/agent-peer/protocol.js +161 -0
- package/src/agent-peer/runner.js +251 -0
- package/src/agent-roundtable/http.js +17 -3
- package/src/agent-roundtable/service.js +272 -43
- package/src/agent-roundtable/tools.js +15 -1
- package/src/agent-roundtable/wake.js +12 -3
- package/src/agent-tools/graph-definitions.js +14 -5
- package/src/agent-tools/graph-handlers.js +1 -0
- package/src/agent-tools/interface-catalog.js +10 -1
- package/src/agent-tools/project-agent-definitions.js +1 -1
- package/src/agents/identity-receipt.js +1 -1
- package/src/cli/command-registry.js +3 -0
- package/src/cli/connect-command.js +31 -0
- package/src/cli/mcp-command.js +101 -0
- package/src/cli/peer-command.js +100 -0
- package/src/cli/setup-agent-selection.js +2 -2
- package/src/cli/setup-command.js +12 -5
- package/src/cli.js +16 -0
- package/src/employees/catalog/bole.json +2 -2
- package/src/graphiti/federated-application.js +4 -1
- package/src/graphiti/project-agent-control-plane.js +7 -0
- package/src/graphiti/project-agent-task-entry.js +4 -4
- package/src/graphiti/project-agent-team-context.js +16 -18
- package/src/graphiti/project-team-view.js +82 -0
- package/src/graphiti/provider-client.js +9 -0
- package/src/graphiti/task-entry-preferences.js +1 -1
- package/src/http/api-router.js +10 -1
- package/src/http/graph-api-router.js +7 -0
- package/src/mcp/runtime.js +2 -1
- package/src/mcp/tool-annotations.js +4 -3
- package/src/mcp/tool-profiles.js +36 -0
- package/src/mcp-server.js +2 -100
- package/src/server.js +14 -2
- package/src/setup/agent-installation-status.js +5 -3
- package/src/setup/agents.js +23 -3
- package/src/setup/claude-desktop-config.js +60 -0
- package/src/setup/client-connection.js +25 -0
- package/src/setup/paths.js +3 -1
- package/src/setup/setup.js +1 -1
- package/src/setup/stable-package-root.js +29 -0
- package/src/system/system-service.js +5 -0
- package/dist/web/assets/AgentProfilePage-CioOdkBM.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage--1A9nvNA.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-C7OUQH9v.css +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-1QevD4Qv.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-B0vQIgwA.js +0 -3
- package/dist/web/assets/KnowledgeEditDialog-ClnrfPZi.js +0 -1
- package/dist/web/assets/KnowledgeInspector-Te1hJZbx.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CCCrWl6Y.js +0 -1
- package/dist/web/assets/KnowledgePage-DGOcBwr8.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-cKn1NCRe.js +0 -4
- package/dist/web/assets/OverviewPage-By-fUzSG.css +0 -1
- package/dist/web/assets/PersonalProfilePage-DGJVP_VB.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-D25HYMgD.js +0 -1
- package/dist/web/assets/PublicProjectsPage-CyNxVzW3.js +0 -1
- package/dist/web/assets/RoundtablesPage-BjVhvwMc.js +0 -1
- package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +0 -1
- package/dist/web/assets/SettingsPage-CCZqZtnH.js +0 -1
- package/dist/web/assets/SettingsPage-DYz5bTwa.css +0 -1
- package/dist/web/assets/UiDialog-CvDxg3B1.js +0 -1
- package/dist/web/assets/agent-pins-BcJ_qr8n.js +0 -1
- package/dist/web/assets/agent-profile-BZj3ezWe.css +0 -1
- package/dist/web/assets/catalog-DbXv0_bR.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-Dj2GMzCI.js +0 -1
- package/dist/web/assets/index-CJ6Jmok9.css +0 -1
- package/dist/web/assets/index-CRa3GnYi.js +0 -5
- package/dist/web/assets/useAgentRoster-DCArHlxK.js +0 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Origin proof for asks this installation sends to Agents on other devices.
|
|
2
|
+
|
|
3
|
+
A device signature only proves which installation sent a message. The
|
|
4
|
+
receiving device also needs proof that this authority, not merely the device,
|
|
5
|
+
let the current project lead's live task send exactly that message. This
|
|
6
|
+
authority signs that statement with a key that never leaves it; the receiver
|
|
7
|
+
checks it with the public key it bound when the owners paired the devices.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
from hashlib import sha256
|
|
14
|
+
|
|
15
|
+
from fastapi import HTTPException
|
|
16
|
+
from nacl.signing import SigningKey
|
|
17
|
+
from neo4j.exceptions import ConstraintError
|
|
18
|
+
from pydantic import Field
|
|
19
|
+
|
|
20
|
+
from .models import SourceApplication, StrictModel
|
|
21
|
+
from .personal_project_access import authorize_personal_project
|
|
22
|
+
|
|
23
|
+
OPAQUE = r'^[A-Za-z0-9][A-Za-z0-9._:-]*$'
|
|
24
|
+
DIGEST = r'^[0-9a-f]{64}$'
|
|
25
|
+
CANONICAL_TIME = r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
|
|
26
|
+
PROOF_VERSION = 'fuli-remote-origin/1'
|
|
27
|
+
MAX_LIFETIME = timedelta(minutes=30)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AgentRemoteOriginIssue(StrictModel):
|
|
31
|
+
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
32
|
+
task_context_token: str = Field(min_length=1, max_length=256)
|
|
33
|
+
source_application: SourceApplication
|
|
34
|
+
message_id: str = Field(min_length=1, max_length=128, pattern=OPAQUE)
|
|
35
|
+
digest: str = Field(pattern=DIGEST)
|
|
36
|
+
target_node: str = Field(pattern=DIGEST)
|
|
37
|
+
binding_id: str = Field(min_length=1, max_length=128, pattern=OPAQUE)
|
|
38
|
+
expires_at: str = Field(pattern=CANONICAL_TIME)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _encode(value):
|
|
42
|
+
return urlsafe_b64encode(value).rstrip(b'=').decode()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _decode(value):
|
|
46
|
+
return urlsafe_b64decode(value + '=' * (-len(value) % 4))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def canonical_time(value):
|
|
50
|
+
return value.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.') + f'{value.microsecond // 1000:03d}Z'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class StoreRemoteAgentOrigins:
|
|
54
|
+
async def remote_origin_authority(self, actor, personal_space_id):
|
|
55
|
+
"""Public half of this space's origin key; created once and never rotated silently."""
|
|
56
|
+
self._require_personal()
|
|
57
|
+
await self.authorize(actor, personal_space_id, 'maintainer')
|
|
58
|
+
key = await self._remote_origin_key(personal_space_id)
|
|
59
|
+
return {'algorithm': 'Ed25519', 'key_id': key['key_id'], 'public_key': key['public_key']}
|
|
60
|
+
|
|
61
|
+
async def _remote_origin_key(self, space_id):
|
|
62
|
+
candidate = SigningKey.generate()
|
|
63
|
+
public = bytes(candidate.verify_key)
|
|
64
|
+
try:
|
|
65
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
66
|
+
'''MERGE (key:FuliRemoteOriginKey {space_id: $space_id})
|
|
67
|
+
ON CREATE SET key.private_key = $private_key, key.public_key = $public_key,
|
|
68
|
+
key.key_id = $key_id, key.created_at = datetime()
|
|
69
|
+
RETURN key.private_key AS private_key, key.public_key AS public_key, key.key_id AS key_id''',
|
|
70
|
+
space_id=space_id, private_key=_encode(bytes(candidate)),
|
|
71
|
+
public_key=_encode(public), key_id=sha256(public).hexdigest()[:32])
|
|
72
|
+
except ConstraintError:
|
|
73
|
+
# A concurrent first use created the key; everyone must use that one.
|
|
74
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
75
|
+
'''MATCH (key:FuliRemoteOriginKey {space_id: $space_id})
|
|
76
|
+
RETURN key.private_key AS private_key, key.public_key AS public_key, key.key_id AS key_id''',
|
|
77
|
+
space_id=space_id)
|
|
78
|
+
return rows[0]
|
|
79
|
+
|
|
80
|
+
async def issue_remote_agent_origin(self, actor, request):
|
|
81
|
+
self._require_personal()
|
|
82
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
83
|
+
expires = datetime.fromisoformat(request.expires_at.replace('Z', '+00:00'))
|
|
84
|
+
now = datetime.now(timezone.utc)
|
|
85
|
+
if expires <= now or expires - now > MAX_LIFETIME:
|
|
86
|
+
raise HTTPException(422, 'A remote ask needs a lifetime of at most 30 minutes')
|
|
87
|
+
context = await self.get_task_context(actor, request.personal_space_id,
|
|
88
|
+
request.task_context_token, request.source_application)
|
|
89
|
+
agent_id = context.get('project_agent_id')
|
|
90
|
+
project_id = context.get('personal_project_id')
|
|
91
|
+
if not agent_id or not project_id or (context.get('checkpoint') or {}).get('phase') == 'complete':
|
|
92
|
+
raise HTTPException(409, 'A remote ask requires an active project task of a FULI Agent')
|
|
93
|
+
await authorize_personal_project(self, actor, space, project_id)
|
|
94
|
+
policy = await self.get_project_agent_coordination_policy(actor, request.personal_space_id, project_id)
|
|
95
|
+
# Members reach other devices through their own lead, never directly.
|
|
96
|
+
if not policy.team_lead_agent_id or agent_id != policy.team_lead_agent_id:
|
|
97
|
+
raise HTTPException(403, 'Only the project lead can ask Agents on other devices')
|
|
98
|
+
agent = await self.get_project_agent(actor, request.personal_space_id, project_id, agent_id)
|
|
99
|
+
if agent.profile.status != 'active':
|
|
100
|
+
raise HTTPException(403, 'The sending Agent is not active')
|
|
101
|
+
task_hash = sha256(request.task_context_token.encode()).hexdigest()
|
|
102
|
+
# The checks above can be overtaken by a new task turn or a lead handoff.
|
|
103
|
+
# Those writers lock the session, the role and the policy; this takes the
|
|
104
|
+
# same locks and re-checks under them before recording the origin.
|
|
105
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
106
|
+
'''MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})-[:HAS_TASK_CONTEXT_SESSION]->
|
|
107
|
+
(session:FuliTaskContextSession)-[:HAS_CONTEXT]->(task:FuliTaskContext {token: $token})
|
|
108
|
+
SET session._fuli_write_lock = true REMOVE session._fuli_write_lock
|
|
109
|
+
WITH space, session, task
|
|
110
|
+
MATCH (space)-[:CONTAINS_PROJECT]->(project:FuliPersonalProject {project_id: $project_id})
|
|
111
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(role:FuliProjectAgent {agent_id: $agent_id})
|
|
112
|
+
SET role._task_lifecycle_lock = true REMOVE role._task_lifecycle_lock
|
|
113
|
+
WITH space, session, task, project, role
|
|
114
|
+
MATCH (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy:FuliProjectAgentCoordinationPolicy)
|
|
115
|
+
SET policy._fuli_write_lock = true REMOVE policy._fuli_write_lock
|
|
116
|
+
WITH session.current_token = task.token AND NOT coalesce(task.completed, false)
|
|
117
|
+
AND task.personal_project_id = $project_id AND task.project_agent_id = $agent_id AS live_task,
|
|
118
|
+
policy.team_lead_agent_id = $agent_id AS lead,
|
|
119
|
+
role.status = 'active' AND EXISTS {
|
|
120
|
+
MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
121
|
+
(:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->(role)
|
|
122
|
+
} AS assigned
|
|
123
|
+
WITH live_task, lead, assigned, live_task AND lead AND assigned AS allowed
|
|
124
|
+
FOREACH (ignored IN CASE WHEN allowed THEN [1] ELSE [] END |
|
|
125
|
+
MERGE (origin:FuliRemoteAgentOrigin {space_id: $space_id, message_id: $message_id})
|
|
126
|
+
ON CREATE SET origin.digest = $digest, origin.agent_id = $agent_id, origin.project_id = $project_id,
|
|
127
|
+
origin.target_node = $target_node, origin.binding_id = $binding_id, origin.task_hash = $task_hash,
|
|
128
|
+
origin.issued_at = datetime(), origin.expires_at = datetime($expires_at))
|
|
129
|
+
WITH live_task, lead, assigned, allowed
|
|
130
|
+
OPTIONAL MATCH (origin:FuliRemoteAgentOrigin {space_id: $space_id, message_id: $message_id})
|
|
131
|
+
WITH live_task, lead, assigned,
|
|
132
|
+
allowed AND origin.digest = $digest AND origin.agent_id = $agent_id AND origin.task_hash = $task_hash
|
|
133
|
+
AND origin.target_node = $target_node AND origin.binding_id = $binding_id AS same
|
|
134
|
+
RETURN live_task, lead, assigned, same''',
|
|
135
|
+
space_id=request.personal_space_id, token=request.task_context_token, message_id=request.message_id,
|
|
136
|
+
digest=request.digest, agent_id=agent_id, project_id=project_id, target_node=request.target_node,
|
|
137
|
+
binding_id=request.binding_id, task_hash=task_hash, expires_at=request.expires_at)
|
|
138
|
+
row = rows[0] if rows else None
|
|
139
|
+
if not row or not row['live_task']:
|
|
140
|
+
raise HTTPException(404, 'The sending task is no longer current')
|
|
141
|
+
if not row['lead'] or not row['assigned']:
|
|
142
|
+
raise HTTPException(403, 'Only the project lead can ask Agents on other devices')
|
|
143
|
+
if not row['same']:
|
|
144
|
+
raise HTTPException(409, 'This message ID already belongs to a different remote ask')
|
|
145
|
+
key = await self._remote_origin_key(request.personal_space_id)
|
|
146
|
+
payload = json.dumps({
|
|
147
|
+
'version': PROOF_VERSION, 'key_id': key['key_id'], 'space_id': request.personal_space_id,
|
|
148
|
+
'project_id': project_id, 'agent_id': agent_id, 'task_hash': task_hash,
|
|
149
|
+
'message_id': request.message_id, 'digest': request.digest, 'target_node': request.target_node,
|
|
150
|
+
'binding_id': request.binding_id, 'issued_at': canonical_time(now), 'expires_at': request.expires_at,
|
|
151
|
+
}, sort_keys=True, separators=(',', ':')).encode()
|
|
152
|
+
signature = SigningKey(_decode(key['private_key'])).sign(payload).signature
|
|
153
|
+
return {'agent_id': agent_id, 'agent_name': agent.profile.display_name or agent.profile.name or agent_id,
|
|
154
|
+
'proof': {'payload': _encode(payload), 'signature': _encode(signature)}}
|
|
@@ -21,7 +21,7 @@ ProjectAgentTaskDuration = Literal['ongoing', 'one_off']
|
|
|
21
21
|
ProjectAgentStaffingIntent = Literal[
|
|
22
22
|
'reuse_preferred',
|
|
23
23
|
# Like reuse_preferred, but a project without a team lead hires its first
|
|
24
|
-
# lead through
|
|
24
|
+
# lead through AR without waiting for confirmation.
|
|
25
25
|
'default_lead',
|
|
26
26
|
'new_durable',
|
|
27
27
|
'temporary',
|
|
@@ -331,6 +331,13 @@ class GraphitiRuntime:
|
|
|
331
331
|
'FOR (n:FuliProjectAgentExecutorOutcomeReset) REQUIRE n.id IS UNIQUE',
|
|
332
332
|
'CREATE CONSTRAINT fuli_project_agent_executor_observation_id IF NOT EXISTS '
|
|
333
333
|
'FOR (n:FuliProjectAgentExecutorObservation) REQUIRE n.id IS UNIQUE',
|
|
334
|
+
'CREATE CONSTRAINT fuli_remote_origin_key_space IF NOT EXISTS '
|
|
335
|
+
'FOR (n:FuliRemoteOriginKey) REQUIRE n.space_id IS UNIQUE',
|
|
336
|
+
'CREATE CONSTRAINT fuli_remote_agent_origin_message IF NOT EXISTS '
|
|
337
|
+
'FOR (n:FuliRemoteAgentOrigin) REQUIRE (n.space_id, n.message_id) IS UNIQUE',
|
|
338
|
+
'CREATE CONSTRAINT fuli_remote_delegation_attempt IF NOT EXISTS '
|
|
339
|
+
'FOR (n:FuliRemoteDelegationAttempt) '
|
|
340
|
+
'REQUIRE (n.space_id, n.origin_node, n.origin_message_id, n.origin_attempt_id) IS UNIQUE',
|
|
334
341
|
):
|
|
335
342
|
await self.driver.execute_query(query)
|
|
336
343
|
for query in (
|
|
@@ -5,9 +5,10 @@ from .project_agent_context_models import ProjectAgentContextResolution
|
|
|
5
5
|
from .project_agent_task_models import ProjectAgentTaskSubmit
|
|
6
6
|
from .task_context_temporary_owner import temporary_task_owner
|
|
7
7
|
from .project_agent_delegations import StoreAgentDelegations
|
|
8
|
+
from .project_agent_remote_origins import StoreRemoteAgentOrigins
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class StoreProjectAgentContext(StoreAgentDelegations):
|
|
11
|
+
class StoreProjectAgentContext(StoreAgentDelegations, StoreRemoteAgentOrigins):
|
|
11
12
|
async def resolve_project_agent_context(self, actor, request):
|
|
12
13
|
self._require_personal()
|
|
13
14
|
space = await self.authorize(actor, request.personal_space_id, 'reader')
|
|
@@ -68,7 +69,7 @@ class StoreProjectAgentContext(StoreAgentDelegations):
|
|
|
68
69
|
)
|
|
69
70
|
if selected and not self._implicit_owner_allowed(selected[0], fallback_selection):
|
|
70
71
|
selected, reason, basis = [], 'no_match', [
|
|
71
|
-
'management or
|
|
72
|
+
'management or AR peer cannot own unrelated project context implicitly',
|
|
72
73
|
]
|
|
73
74
|
if selected:
|
|
74
75
|
reason = 'project_context_fallback'
|
|
@@ -113,7 +113,7 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
113
113
|
if (profile.status != 'active' or not active_assignment
|
|
114
114
|
or profile.agent_type != 'durable'
|
|
115
115
|
or any(cap.startswith('fuli.employee:') for cap in profile.capabilities)):
|
|
116
|
-
raise HTTPException(status_code=422, detail='team roles require active durable project assignments;
|
|
116
|
+
raise HTTPException(status_code=422, detail='team roles require active durable project assignments; AR and project managers remain peers')
|
|
117
117
|
updated_at = now_utc()
|
|
118
118
|
written, _, _ = await self.runtime.driver.execute_query(
|
|
119
119
|
'''
|
|
@@ -618,7 +618,7 @@ class StoreProjectAgentTaskReads:
|
|
|
618
618
|
if must_disclose:
|
|
619
619
|
notice = ' '.join(
|
|
620
620
|
(
|
|
621
|
-
f'
|
|
621
|
+
f'AR {item.hr_agent_id} recruited '
|
|
622
622
|
f'{item.proposed_profile.name} as a '
|
|
623
623
|
f'{item.position_kind} {item.participant_role} Agent for '
|
|
624
624
|
f'{item.proposed_profile.responsibility}. '
|
|
@@ -33,7 +33,7 @@ RECRUITMENT_CLAIM_TTL = timedelta(minutes=2)
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class StoreProjectAgentTaskRecruitment:
|
|
36
|
-
"""Own the audited
|
|
36
|
+
"""Own the audited AR lifecycle independently from task routing mechanics."""
|
|
37
37
|
|
|
38
38
|
async def get_project_agent_recruitment_policy(
|
|
39
39
|
self,
|
|
@@ -202,7 +202,7 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
202
202
|
if not raw.get('hr_agent_id'):
|
|
203
203
|
raise HTTPException(
|
|
204
204
|
status_code=409,
|
|
205
|
-
detail='recruitment cannot proceed without an active
|
|
205
|
+
detail='recruitment cannot proceed without an active AR Agent',
|
|
206
206
|
)
|
|
207
207
|
raw['_guarded_task_id'] = raw['task_id']
|
|
208
208
|
selected = await self._provision_recruitment(actor, raw)
|
|
@@ -321,7 +321,7 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
321
321
|
model_strategy_source=model_source,
|
|
322
322
|
outcome=outcome,
|
|
323
323
|
reason=recruitment['reason_code'],
|
|
324
|
-
match_basis=['
|
|
324
|
+
match_basis=['AR recruitment approved and Agent assigned'],
|
|
325
325
|
candidate_agent_ids=[routing_selected['agent_id']],
|
|
326
326
|
created_at=now,
|
|
327
327
|
**self._decision_executor_fields(executor_decision),
|
|
@@ -333,7 +333,7 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
333
333
|
)
|
|
334
334
|
explanation = self._routing_explanation(
|
|
335
335
|
recruitment['reason_code'],
|
|
336
|
-
['
|
|
336
|
+
['AR recruitment approved and Agent assigned'],
|
|
337
337
|
executor_decision,
|
|
338
338
|
)
|
|
339
339
|
event_id = stable_uuid(
|
|
@@ -445,7 +445,7 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
445
445
|
decision_id=decision_id,
|
|
446
446
|
decision_json=decision.model_dump_json(),
|
|
447
447
|
event_id=event_id,
|
|
448
|
-
event_summary=f'
|
|
448
|
+
event_summary=f'AR recruitment approved: {approval_reason}',
|
|
449
449
|
expected_task_revision=expected_task_revision,
|
|
450
450
|
updated_at=now,
|
|
451
451
|
)
|
|
@@ -84,7 +84,7 @@ class StoreProjectAgentTaskStaffing:
|
|
|
84
84
|
return [lead], [lead, *members], 'project_team_lead', [
|
|
85
85
|
'project team lead is the stable conversation owner',
|
|
86
86
|
f'{len(members)} active team member(s) available for explicit workstreams',
|
|
87
|
-
'
|
|
87
|
+
'AR and project manager remain peer roles',
|
|
88
88
|
]
|
|
89
89
|
normalized_work_kind = request.work_kind.casefold()
|
|
90
90
|
required = {item.casefold() for item in request.required_capabilities}
|
|
@@ -886,7 +886,7 @@ class StoreProjectAgentTaskStaffing:
|
|
|
886
886
|
status_code=422,
|
|
887
887
|
detail=(
|
|
888
888
|
'parallel work requires at least two active Agents; '
|
|
889
|
-
'automatic recruitment needs an active
|
|
889
|
+
'automatic recruitment needs an active AR Agent, otherwise '
|
|
890
890
|
'recruit another qualified collaborator or assign enough '
|
|
891
891
|
'existing Agents before enabling parallel work'
|
|
892
892
|
),
|
|
@@ -272,7 +272,7 @@ class StoreProjectAgentTasks(
|
|
|
272
272
|
*match_basis,
|
|
273
273
|
'parallel automatic recruitment: filled '
|
|
274
274
|
f'{recruited_count} missing participant slot(s)',
|
|
275
|
-
'
|
|
275
|
+
'AR recruitment recorded before Agent creation',
|
|
276
276
|
]
|
|
277
277
|
if recruited_lead:
|
|
278
278
|
model_strategy, model_source = self._effective_model_strategy(
|
|
@@ -322,7 +322,7 @@ class StoreProjectAgentTasks(
|
|
|
322
322
|
else 'no_match'
|
|
323
323
|
)
|
|
324
324
|
match_basis = [
|
|
325
|
-
'
|
|
325
|
+
'AR recruitment recorded before Agent creation',
|
|
326
326
|
*(
|
|
327
327
|
[
|
|
328
328
|
'parallel auto-staffing: added '
|
|
@@ -68,11 +68,11 @@ class StoreProjectAgents:
|
|
|
68
68
|
) -> ProjectAgentRecord:
|
|
69
69
|
self._require_personal()
|
|
70
70
|
if request.agent_id == LEGACY_HR_AGENT_ID:
|
|
71
|
-
raise HTTPException(409, 'This
|
|
71
|
+
raise HTTPException(409, 'This AR identity has moved to employee.bole')
|
|
72
72
|
if request.profile.agent_type == 'temporary' and not recruitment_id:
|
|
73
73
|
raise HTTPException(
|
|
74
74
|
status_code=422,
|
|
75
|
-
detail='temporary Agents must be created by an audited
|
|
75
|
+
detail='temporary Agents must be created by an audited AR recruitment',
|
|
76
76
|
)
|
|
77
77
|
if (
|
|
78
78
|
request.profile.agent_type == 'coordinator'
|
|
@@ -88,7 +88,7 @@ class StoreProjectAgents:
|
|
|
88
88
|
):
|
|
89
89
|
raise HTTPException(
|
|
90
90
|
status_code=422,
|
|
91
|
-
detail='only the system-managed Bole identity may use
|
|
91
|
+
detail='only the system-managed Bole identity may use AR type',
|
|
92
92
|
)
|
|
93
93
|
if (
|
|
94
94
|
request.agent_id == SYSTEM_COORDINATOR_AGENT_ID
|
|
@@ -104,7 +104,7 @@ class StoreProjectAgents:
|
|
|
104
104
|
):
|
|
105
105
|
raise HTTPException(
|
|
106
106
|
status_code=422,
|
|
107
|
-
detail='the system
|
|
107
|
+
detail='the system AR identity cannot change Agent type',
|
|
108
108
|
)
|
|
109
109
|
if (
|
|
110
110
|
request.agent_id in (SYSTEM_HR_AGENT_ID, SYSTEM_COORDINATOR_AGENT_ID)
|
|
@@ -112,7 +112,7 @@ class StoreProjectAgents:
|
|
|
112
112
|
):
|
|
113
113
|
raise HTTPException(
|
|
114
114
|
status_code=422,
|
|
115
|
-
detail='the project manager and
|
|
115
|
+
detail='the project manager and AR must remain active',
|
|
116
116
|
)
|
|
117
117
|
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
118
118
|
if request.personal_project_id:
|
|
@@ -402,7 +402,7 @@ class StoreProjectAgents:
|
|
|
402
402
|
if agent_id == SYSTEM_HR_AGENT_ID:
|
|
403
403
|
raise HTTPException(
|
|
404
404
|
status_code=422,
|
|
405
|
-
detail='the system
|
|
405
|
+
detail='the system AR cannot be archived',
|
|
406
406
|
)
|
|
407
407
|
await self.authorize(actor, personal_space_id, 'maintainer')
|
|
408
408
|
if not reason.strip():
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Fixed roles: Jefa (project manager), Bole (
|
|
1
|
+
"""Fixed roles: Jefa (project manager), Bole (AR), and Tonborg (judgment).
|
|
2
2
|
|
|
3
3
|
Jefa also coordinates task routing, replacing the earlier separate coordinator
|
|
4
4
|
identity. That legacy node is retired, not deleted, so old tasks keep their
|
|
@@ -15,6 +15,8 @@ SYSTEM_COORDINATOR_AGENT_ID = 'employee.jefa'
|
|
|
15
15
|
LEGACY_COORDINATOR_AGENT_ID = 'fuli-project-coordinator'
|
|
16
16
|
SYSTEM_HR_AGENT_ID = 'employee.bole'
|
|
17
17
|
SYSTEM_JUDGMENT_AGENT_ID = 'employee.tonborg'
|
|
18
|
+
# Upgrade only the former built-in copy; custom responsibilities are user data.
|
|
19
|
+
LEGACY_HR_RESPONSIBILITY = 'HR:维护 Agent 人员分布与工作状态,按需招募并保留招募记录。'
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def coordinator_profile() -> ProjectAgentProfile:
|
|
@@ -37,7 +39,7 @@ def hr_profile() -> ProjectAgentProfile:
|
|
|
37
39
|
return ProjectAgentProfile(
|
|
38
40
|
name='Bole',
|
|
39
41
|
occupation_emoji='🔎',
|
|
40
|
-
responsibility='
|
|
42
|
+
responsibility='AR:维护 Agent 人员分布与工作状态,按需招募并保留招募记录。',
|
|
41
43
|
agent_type='hr',
|
|
42
44
|
work_kinds=['agent-recruitment', 'staffing-review'],
|
|
43
45
|
capabilities=['Agent 招募', '人员分布', '工作状态', '招募审计', 'fuli.employee:bole'],
|
|
@@ -86,8 +88,13 @@ async def ensure_system_identity(driver, provider_id, space_id, agent_id):
|
|
|
86
88
|
id=node_id,
|
|
87
89
|
)
|
|
88
90
|
profile = default
|
|
91
|
+
upgrade_responsibility = False
|
|
89
92
|
if rows and rows[0]['profile_json']:
|
|
90
93
|
stored = json.loads(rows[0]['profile_json'])
|
|
94
|
+
upgrade_responsibility = (agent_id == SYSTEM_HR_AGENT_ID
|
|
95
|
+
and stored.get('responsibility') == LEGACY_HR_RESPONSIBILITY)
|
|
96
|
+
if upgrade_responsibility:
|
|
97
|
+
stored['responsibility'] = default.responsibility
|
|
91
98
|
profile = ProjectAgentProfile.model_validate(
|
|
92
99
|
{**stored, 'agent_type': default.agent_type, 'status': 'active'}
|
|
93
100
|
)
|
|
@@ -107,6 +114,8 @@ async def ensure_system_identity(driver, provider_id, space_id, agent_id):
|
|
|
107
114
|
agent.created_at = $now,
|
|
108
115
|
agent.updated_at = $now
|
|
109
116
|
SET agent.profile_json = $profile_json,
|
|
117
|
+
agent.responsibility = CASE WHEN $upgrade_responsibility
|
|
118
|
+
THEN $responsibility ELSE agent.responsibility END,
|
|
110
119
|
agent.agent_type = $agent_type,
|
|
111
120
|
agent.status = 'active',
|
|
112
121
|
agent.system_managed = true
|
|
@@ -119,6 +128,7 @@ async def ensure_system_identity(driver, provider_id, space_id, agent_id):
|
|
|
119
128
|
name=profile.name,
|
|
120
129
|
occupation_emoji=profile.occupation_emoji,
|
|
121
130
|
responsibility=profile.responsibility,
|
|
131
|
+
upgrade_responsibility=upgrade_responsibility,
|
|
122
132
|
capabilities=profile.capabilities,
|
|
123
133
|
work_kinds=profile.work_kinds,
|
|
124
134
|
agent_type=profile.agent_type,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Lossless, exact-ID upgrade of the original
|
|
1
|
+
"""Lossless, exact-ID upgrade of the original AR into the built-in Bole.
|
|
2
2
|
|
|
3
3
|
The old node and its original edges remain an audit source, not another active
|
|
4
4
|
Agent. Only this reserved legacy ID is eligible; names never establish identity.
|
|
@@ -103,7 +103,7 @@ async def merge_legacy_hr(store, space_id):
|
|
|
103
103
|
for edge in edges:
|
|
104
104
|
kind = edge['kind']
|
|
105
105
|
if not re.fullmatch(r'[A-Z][A-Z0-9_]*', kind):
|
|
106
|
-
raise HTTPException(409, 'Unsupported
|
|
106
|
+
raise HTTPException(409, 'Unsupported AR relationship; no data was changed')
|
|
107
107
|
start_id = row['canonical_element'] if edge['start_id'] == row['legacy_element'] else edge['start_id']
|
|
108
108
|
end_id = row['canonical_element'] if edge['end_id'] == row['legacy_element'] else edge['end_id']
|
|
109
109
|
copied, _, _ = await driver.execute_query(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fuli-graph-provider"
|
|
3
|
-
version = "0.12.
|
|
3
|
+
version = "0.12.2"
|
|
4
4
|
description = "Graphiti-backed personal and shared project provider for Fuli."
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
@@ -8,6 +8,7 @@ dependencies = [
|
|
|
8
8
|
"graphiti-core==0.29.2",
|
|
9
9
|
"openai>=1.91,<2",
|
|
10
10
|
"pydantic-settings==2.10.1",
|
|
11
|
+
"pynacl==1.6.2",
|
|
11
12
|
"uvicorn[standard]==0.35.0",
|
|
12
13
|
]
|
|
13
14
|
|