fuli-context 0.10.0 → 0.11.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.
Files changed (212) hide show
  1. package/README.md +27 -30
  2. package/README.zh-CN.md +22 -26
  3. package/dist/web/assets/AboutPage-cW9hDghN.js +1 -0
  4. package/dist/web/assets/AgentConversations-CGznTWuV.js +2 -0
  5. package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
  6. package/dist/web/assets/AgentHand-DkyF4vgk.js +1 -0
  7. package/dist/web/assets/AgentName-C1mAVr-v.js +1 -0
  8. package/dist/web/assets/AgentProfilePage-pKUR0rfj.js +1 -0
  9. package/dist/web/assets/AgentsDirectoryPage-CUlbT2hD.js +1 -0
  10. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
  11. package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
  12. package/dist/web/assets/ConnectionsPage-CuDHIJXu.js +1 -0
  13. package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
  14. package/dist/web/assets/EmployeeRecruitDialog-D9QfGipd.js +1 -0
  15. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
  16. package/dist/web/assets/EmployeeWorkbenchPage-DAtvGney.js +3 -0
  17. package/dist/web/assets/KnowledgeEditDialog-a-5rasG0.js +1 -0
  18. package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
  19. package/dist/web/assets/KnowledgeInspector-BGsqjNY2.js +1 -0
  20. package/dist/web/assets/KnowledgeOrganizerPage-BndgUsHS.js +1 -0
  21. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
  22. package/dist/web/assets/KnowledgePage-CQweO0YJ.js +1 -0
  23. package/dist/web/assets/KnowledgeWorkspace-BtUjKTLb.js +4 -0
  24. package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
  25. package/dist/web/assets/NotFoundPage-BmQTTqQn.js +1 -0
  26. package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
  27. package/dist/web/assets/OverviewPage-C73HyTsM.js +1 -0
  28. package/dist/web/assets/PersonalProfilePage-BMuOrppx.js +1 -0
  29. package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
  30. package/dist/web/assets/PersonalProjectsPage-Cq9_kFvs.js +1 -0
  31. package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
  32. package/dist/web/assets/ProjectAgentFirstTask-CaHvOjkp.js +1 -0
  33. package/dist/web/assets/ProjectAgentsPage-DWGkgIsr.css +1 -0
  34. package/dist/web/assets/ProjectAgentsPage-WVcNdzVi.js +12 -0
  35. package/dist/web/assets/ProjectScopePicker-zAgiaZKL.js +1 -0
  36. package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
  37. package/dist/web/assets/PublicProjectsPage-DL093f65.js +1 -0
  38. package/dist/web/assets/ReviewPage-D_NIhPx8.js +1 -0
  39. package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
  40. package/dist/web/assets/RoundtablesPage-CdVVyZE-.js +1 -0
  41. package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
  42. package/dist/web/assets/SettingsPage-CbLaau8Q.js +1 -0
  43. package/dist/web/assets/SharedJefaReportPage-aygd6wvb.js +1 -0
  44. package/dist/web/assets/UiDialog-Q7fsJUoO.js +1 -0
  45. package/dist/web/assets/UiDisclosure-BS-muK5j.js +1 -0
  46. package/dist/web/assets/UiSegmented-CT28bxqw.js +1 -0
  47. package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
  48. package/dist/web/assets/UiSelect-DcGpPQQo.js +1 -0
  49. package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
  50. package/dist/web/assets/WritingTastePage-CYlwAFqV.js +1 -0
  51. package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
  52. package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-D-qIFRIb.js} +1 -1
  53. package/dist/web/assets/{catalog-DN72452f.js → catalog-UDv854JN.js} +1 -1
  54. package/dist/web/assets/client-DLS5FZBI.js +1 -0
  55. package/dist/web/assets/{console-BtPqo79j.js → console-CqzwSo7a.js} +1 -1
  56. package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-DRoIz42V.js} +1 -1
  57. package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-b14l2pQA.js} +3 -3
  58. package/dist/web/assets/{identity-B3zEw-1u.js → identity-BWbPmZZf.js} +1 -1
  59. package/dist/web/assets/index-D1OHIz4c.js +5 -0
  60. package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
  61. package/dist/web/assets/{model-S7ehAjky.js → model-DupZy8Qb.js} +1 -1
  62. package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BPTtMJTZ.js} +1 -1
  63. package/dist/web/assets/shared-report-BYnLhmCZ.js +1 -0
  64. package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-ep7SpKv1.js} +1 -1
  65. package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-CEQAEuug.js} +1 -1
  66. package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-B6ZjK-Br.js} +1 -1
  67. package/dist/web/index.html +14 -16
  68. package/docs/agent-conversations-and-collaboration.md +9 -9
  69. package/docs/agent-roundtable.md +77 -0
  70. package/docs/employee-agents.md +2 -2
  71. package/docs/project-agent-memory.md +1 -1
  72. package/graph-provider/fuli_graph/agent_conversation_models.py +11 -2
  73. package/graph-provider/fuli_graph/conversation_context.py +40 -9
  74. package/graph-provider/fuli_graph/default_project_lead.py +88 -0
  75. package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
  76. package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
  77. package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
  78. package/graph-provider/fuli_graph/project_agent_memory_routes.py +20 -0
  79. package/graph-provider/fuli_graph/project_agent_models.py +1 -0
  80. package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
  81. package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
  82. package/graph-provider/fuli_graph/runtime.py +10 -76
  83. package/graph-provider/fuli_graph/store.py +2 -0
  84. package/graph-provider/fuli_graph/store_agent_conversations.py +60 -8
  85. package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
  86. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
  87. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +3 -1
  88. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +3 -2
  89. package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
  90. package/graph-provider/fuli_graph/store_project_agents.py +56 -129
  91. package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
  92. package/graph-provider/fuli_graph/system_agents.py +130 -0
  93. package/graph-provider/fuli_graph/system_hr_identity.py +1 -1
  94. package/graph-provider/fuli_graph/task_context_routes.py +10 -0
  95. package/graph-provider/pyproject.toml +1 -1
  96. package/npm-shrinkwrap.json +2 -2
  97. package/package.json +2 -2
  98. package/src/agent-roundtable/http.js +14 -0
  99. package/src/agent-roundtable/service.js +247 -0
  100. package/src/agent-roundtable/store.js +86 -0
  101. package/src/agent-roundtable/tools.js +61 -0
  102. package/src/agent-roundtable/wake.js +100 -0
  103. package/src/agent-tools/agent-conversation-definitions.js +3 -3
  104. package/src/agent-tools/graph-definitions.js +3 -2
  105. package/src/agent-tools/interface-catalog.js +0 -5
  106. package/src/agent-tools.js +2 -2
  107. package/src/agents/claude-code/wake.js +37 -0
  108. package/src/agents/codex/wake.js +47 -0
  109. package/src/agents/identity-receipt.js +35 -7
  110. package/src/agents/session-log.js +34 -0
  111. package/src/agents/wake-registry.js +14 -0
  112. package/src/cli/command-registry.js +0 -2
  113. package/src/cli/setup-command.js +2 -5
  114. package/src/cli/update-command.js +13 -2
  115. package/src/cli.js +0 -5
  116. package/src/employees/management-policy.js +4 -2
  117. package/src/employees/manifest.js +3 -0
  118. package/src/employees/recruitment.js +3 -2
  119. package/src/graphiti/agent-conversations.js +11 -9
  120. package/src/graphiti/agent-knowledge-workflows.js +34 -7
  121. package/src/graphiti/federated-application.js +17 -5
  122. package/src/graphiti/project-agent-mapping.js +2 -0
  123. package/src/graphiti/project-agent-task-entry.js +38 -8
  124. package/src/graphiti/project-agent-team-context.js +4 -1
  125. package/src/graphiti/project-path-context.js +7 -2
  126. package/src/graphiti/provider-client.js +17 -0
  127. package/src/graphiti/repository-project-registration.js +17 -0
  128. package/src/graphiti/task-entry-preferences.js +17 -4
  129. package/src/graphiti/task-entry-project-scope.js +67 -0
  130. package/src/http/api-router.js +5 -3
  131. package/src/http/error-mapping.js +0 -2
  132. package/src/mcp/instructions.js +5 -5
  133. package/src/mcp/session-id.js +1 -1
  134. package/src/mcp/tool-annotations.js +3 -4
  135. package/src/setup/agents.js +1 -41
  136. package/src/setup/graph-runtime.js +15 -2
  137. package/src/setup/session-skill.js +22 -2
  138. package/dist/web/assets/AboutPage-D-GyJNv1.js +0 -1
  139. package/dist/web/assets/AgentConversations-DOuZgUeA.js +0 -2
  140. package/dist/web/assets/AgentConversations-DS8h3yKn.css +0 -1
  141. package/dist/web/assets/AgentHand-eGsXoGSq.js +0 -1
  142. package/dist/web/assets/AgentName-Dtmu9Ff5.js +0 -1
  143. package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +0 -1
  144. package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +0 -1
  145. package/dist/web/assets/ConnectionsPage-C1w99yN3.css +0 -1
  146. package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +0 -1
  147. package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +0 -1
  148. package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +0 -1
  149. package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +0 -1
  150. package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +0 -3
  151. package/dist/web/assets/GrowthLoading-KTjE2e50.js +0 -1
  152. package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +0 -1
  153. package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +0 -1
  154. package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +0 -1
  155. package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +0 -1
  156. package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +0 -1
  157. package/dist/web/assets/KnowledgePage-POkCMW8f.js +0 -1
  158. package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +0 -4
  159. package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +0 -1
  160. package/dist/web/assets/NotFoundPage-BCMZbeB8.js +0 -1
  161. package/dist/web/assets/OverviewPage-0CFWCG9-.css +0 -1
  162. package/dist/web/assets/OverviewPage-0mYifpjp.js +0 -1
  163. package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +0 -1
  164. package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +0 -1
  165. package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +0 -1
  166. package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +0 -1
  167. package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +0 -1
  168. package/dist/web/assets/ProjectAgentsPage-CPgKVLgg.js +0 -12
  169. package/dist/web/assets/ProjectAgentsPage-DeuRkMp1.css +0 -1
  170. package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +0 -1
  171. package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +0 -1
  172. package/dist/web/assets/ReviewPage-CelTZU4C.js +0 -1
  173. package/dist/web/assets/RoundtablesPage-C9dXfWCl.js +0 -2
  174. package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +0 -1
  175. package/dist/web/assets/SearchableSelect-f7EHbyDL.js +0 -1
  176. package/dist/web/assets/SettingsPage-5g7mRN2F.css +0 -1
  177. package/dist/web/assets/SettingsPage-nAijmm5f.js +0 -1
  178. package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +0 -1
  179. package/dist/web/assets/UiDisclosure-CWlC91Zt.js +0 -1
  180. package/dist/web/assets/WritingTastePage-DaHWY61P.js +0 -1
  181. package/dist/web/assets/WritingTastePage-SMGF39mf.css +0 -1
  182. package/dist/web/assets/agent-profile-CBvJHGjt.css +0 -1
  183. package/dist/web/assets/client-B2cRl-7c.js +0 -1
  184. package/dist/web/assets/copy-D2LqwtPZ.js +0 -1
  185. package/dist/web/assets/index-CNp0tRNp.css +0 -1
  186. package/dist/web/assets/index-ulISYH02.js +0 -5
  187. package/dist/web/assets/shared-report-DEK9wvhl.js +0 -1
  188. package/dist/web/assets/useModalDialog-D8tiJfbc.js +0 -1
  189. package/docs/roundtable.md +0 -46
  190. package/src/agents/a2a/roundtable-participant.js +0 -374
  191. package/src/agents/claude-code/roundtable-participant.js +0 -24
  192. package/src/agents/codex/roundtable-config.js +0 -32
  193. package/src/agents/codex/roundtable-participant.js +0 -34
  194. package/src/agents/grok/roundtable-participant.js +0 -185
  195. package/src/agents/pi/roundtable-participant.js +0 -193
  196. package/src/agents/pi/roundtable-result-format.js +0 -183
  197. package/src/cli/roundtable-command.js +0 -79
  198. package/src/roundtables/application.js +0 -69
  199. package/src/roundtables/domain.js +0 -181
  200. package/src/roundtables/http-router.js +0 -58
  201. package/src/roundtables/mcp-server.js +0 -55
  202. package/src/roundtables/participant-registry.js +0 -12
  203. package/src/roundtables/process.js +0 -70
  204. package/src/roundtables/profiles.js +0 -84
  205. package/src/roundtables/result-contract.js +0 -47
  206. package/src/roundtables/server.js +0 -52
  207. package/src/roundtables/service.js +0 -480
  208. package/src/roundtables/store.js +0 -63
  209. package/src/roundtables/tool-contract.js +0 -58
  210. package/src/roundtables/turn-prompt.js +0 -24
  211. package/src/roundtables/worker.js +0 -85
  212. /package/dist/web/assets/{GrowthLoading-CIuQ1icy.css → client-CIuQ1icy.css} +0 -0
@@ -1,69 +0,0 @@
1
- import { join } from 'node:path';
2
- import { mkdirSync } from 'node:fs';
3
- import { createRoundtableService } from './service.js';
4
-
5
- // Application boundary: validate identity/scope against Fuli, not the local log.
6
- export function createFuliRoundtableService({ app, dataDir }) {
7
- let core;
8
- const getCore = () => {
9
- if (core) return core;
10
- mkdirSync(dataDir, { recursive: true });
11
- return core = createRoundtableService({ databasePath: join(dataDir, 'roundtables.sqlite'),
12
- contextProvider: async ({ binding, seat, actor }) => {
13
- if (!binding || !seat.agentId || !seat.shareAgentContext) return null;
14
- const agent = await app.getProjectAgent({ ...binding, agentId: seat.agentId });
15
- if (!(agent.profile.allowedClients ?? []).includes(actor.sourceApplication)) {
16
- return { status: 'client_not_allowed', guidance: 'Private Agent context requires an explicitly allowed authenticated client; shared room messages remain available.' };
17
- }
18
- return app.getProjectAgentContext({ personalProjectId: binding.personalProjectId,
19
- agentId: seat.agentId, queries: [], sourceApplication: actor.sourceApplication });
20
- },
21
- verifyCompletion: async ({ binding }) => {
22
- if (!binding?.taskId || !binding.artifactRevision) return { verified: false, taskStatus: 'unverified', evidence: [] };
23
- const task = await app.viewProjectAgentTask({ ...binding, includeEvents: false });
24
- const gate = await app.agentVerification('query', { ...binding });
25
- return { verified: gate.status === 'passed' && task.status === 'completed',
26
- taskStatus: task.status, evidence: [{ source: 'fuli_quality_gate', artifactRevision: binding.artifactRevision, status: gate.status }] };
27
- }
28
- });
29
- };
30
- return new Proxy({}, {
31
- get(_target, property) {
32
- if (property === 'bindingAuthority') return 'fuli_application';
33
- if (property === 'then') return undefined;
34
- if (property === 'close') return () => core?.close();
35
- if (property !== 'create') return (...args) => getCore()[property](...args);
36
- return async (input, actor) => {
37
- let binding = input.binding ?? null;
38
- let projectId = input.personalProjectId ?? binding?.personalProjectId;
39
- if (input.projectPath && !projectId) {
40
- const resolution = await app.listCurrentProjectAgents({ projectPath: input.projectPath });
41
- projectId = resolution.personal_project_id;
42
- if (!projectId) throw new TypeError('Register or select the exact Fuli project; no ambiguous project is inferred');
43
- }
44
- if (projectId) {
45
- const spaceId = app.config.personal.spaceId;
46
- if (binding?.personalSpaceId && binding.personalSpaceId !== spaceId) throw new TypeError('Roundtable must use the active personal space');
47
- const projects = await app.listPersonalProjects({ personalSpaceId: spaceId });
48
- if (!projects.some((project) => project.project_id === projectId)) throw new TypeError('Fuli project not found');
49
- binding = { personalSpaceId: spaceId, personalProjectId: projectId,
50
- taskId: binding?.taskId ?? null, artifactRevision: binding?.artifactRevision ?? null };
51
- if (binding.taskId) {
52
- if (!binding.artifactRevision) throw new TypeError('Fuli task binding requires its current artifact revision');
53
- await app.viewProjectAgentTask({ ...binding, includeEvents: false });
54
- }
55
- for (const seat of input.seats ?? []) {
56
- if (seat.agentId) {
57
- const agent = await app.getProjectAgent({ ...binding, agentId: seat.agentId });
58
- if (agent.profile.status !== 'active') throw new TypeError('Roundtable requires an active Fuli Agent');
59
- }
60
- }
61
- } else {
62
- if ((input.seats ?? []).some((seat) => seat.agentId || seat.shareAgentContext)) throw new TypeError('Fuli Agent binding requires an exact project');
63
- binding = null;
64
- }
65
- return getCore().create({ ...input, binding }, actor);
66
- };
67
- }
68
- });
69
- }
@@ -1,181 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { posix, win32 } from 'node:path';
3
-
4
- export const DEFAULT_LIMITS = Object.freeze({ maxRounds: 3, maxMessages: 30, maxPeerMessages: 100, maxDurationMs: 1_800_000, turnTimeoutMs: 300_000 });
5
- export const TERMINAL_STATUSES = new Set(['concluded', 'failed', 'cancelled']);
6
- const ROLES = new Set(['moderator', 'specialist', 'implementer', 'reviewer']);
7
- const RUNTIMES = new Set(['mcp', 'codex', 'claude-code', 'pi', 'grok', 'a2a']);
8
- const PHASES = ['discussion', 'planning', 'implementation', 'review', 'synthesis'];
9
-
10
- export class RoundtableError extends Error {
11
- constructor(code, message, statusCode = 400) {
12
- super(message);
13
- this.name = 'RoundtableError';
14
- this.code = code;
15
- this.statusCode = statusCode;
16
- this.status = statusCode;
17
- }
18
- }
19
-
20
- export function fail(code, message, status = 400) { throw new RoundtableError(code, message, status); }
21
-
22
- export function text(value, name, max = 16_384) {
23
- if (typeof value !== 'string' || !value.trim() || Buffer.byteLength(value, 'utf8') > max) fail('invalid_input', `${name} must be nonempty and at most ${max} UTF-8 bytes`);
24
- return value.trim();
25
- }
26
-
27
- export function normalizeLimits(input = {}) {
28
- const limits = { ...DEFAULT_LIMITS };
29
- for (const [key, maximum] of Object.entries(DEFAULT_LIMITS)) {
30
- const value = input[key] ?? maximum;
31
- if (!Number.isSafeInteger(value) || value < 1 || value > maximum) fail('invalid_limits', `${key} must be between 1 and ${maximum}`);
32
- limits[key] = value;
33
- }
34
- limits.turnTimeoutMs = Math.min(limits.turnTimeoutMs, limits.maxDurationMs);
35
- return limits;
36
- }
37
-
38
- export function newRoom(input, now) {
39
- if (!['discussion', 'collaboration'].includes(input.mode ?? 'discussion')) fail('invalid_mode', 'Unknown roundtable mode');
40
- if (!Array.isArray(input.seats) || input.seats.length < 2 || input.seats.length > 6) fail('invalid_seats', 'Choose between two and six seats');
41
- const seats = input.seats.map(raw => {
42
- const id = text(raw.id ?? randomUUID(), 'seat.id', 128);
43
- if (!input.binding && (raw.agentId || raw.shareAgentContext === true)) fail('invalid_binding', 'Standalone seats cannot claim a Fuli Agent identity or private context');
44
- if (!ROLES.has(raw.role)) fail('invalid_role', 'Unknown seat role');
45
- if (!RUNTIMES.has(raw.runtime ?? 'mcp')) fail('invalid_runtime', 'Unknown participant runtime');
46
- const execution = { permission: 'read-only', workspace: null };
47
- if (raw.execution?.permission === 'workspace-write') {
48
- if (raw.role !== 'implementer') fail('invalid_permission', 'Only implementer seats can receive workspace write permission');
49
- execution.permission = 'workspace-write';
50
- execution.workspace = text(raw.execution.workspace, 'execution.workspace', 4096);
51
- if (!posix.isAbsolute(execution.workspace) && !win32.isAbsolute(execution.workspace)) fail('invalid_workspace', 'Write permission requires an explicit absolute workspace');
52
- } else if (raw.execution?.permission && raw.execution.permission !== 'read-only') fail('invalid_permission', 'Unknown execution permission');
53
- return { id, name: text(raw.name, 'seat.name', 256), role: raw.role, runtime: raw.runtime ?? 'mcp', agentId: raw.agentId ? text(raw.agentId, 'seat.agentId', 256) : null, shareAgentContext: raw.shareAgentContext === true, identityKind: input.binding && raw.agentId ? 'fuli' : 'standalone', execution, selfProfile: null, joinedAt: null, joinedInvitationId: null, sourceApplication: null, sourceSessionId: null };
54
- });
55
- if (new Set(seats.map(s => s.id)).size !== seats.length) fail('duplicate_seat', 'Seat IDs must be distinct');
56
- if (seats.filter(s => s.role === 'moderator').length !== 1) fail('invalid_moderator', 'Exactly one moderator is required');
57
- if (input.mode === 'collaboration' && (!seats.some(seat => seat.role === 'implementer') || !seats.some(seat => seat.role === 'reviewer'))) {
58
- fail('missing_collaboration_role', 'Collaboration requires an implementer and an independent reviewer');
59
- }
60
- const id = randomUUID();
61
- let binding = null;
62
- if (input.binding) {
63
- binding = { personalSpaceId: text(input.binding.personalSpaceId, 'binding.personalSpaceId', 256) };
64
- const projectKey = input.binding.personalProjectId ? 'personalProjectId' : 'projectId';
65
- binding[projectKey] = text(input.binding[projectKey], `binding.${projectKey}`, 256);
66
- for (const key of ['taskId', 'coordinatedTaskId', 'artifactRevision']) if (input.binding[key] !== undefined && input.binding[key] !== null) binding[key] = text(input.binding[key], `binding.${key}`, 256);
67
- }
68
- const implementers = seats.filter(s => s.role === 'implementer');
69
- const tasks = input.mode === 'collaboration' ? implementationTasks(input.tasks, implementers, id, input.goal) : [];
70
- if (input.mode === 'collaboration') {
71
- for (const seat of seats.filter(s => s.role === 'reviewer')) tasks.push({ id: `${id}:review:${seat.id}`, title: 'Review implementation / 审查实施', instructions: input.goal, seatId: seat.id, phase: 'review', status: 'pending', dependencies: tasks.filter(t => t.phase === 'implementation').map(t => t.id), permission: 'read-only', workspace: null, artifacts: [], verification: null });
72
- }
73
- return { id, goal: text(input.goal, 'goal'), mode: input.mode ?? 'discussion', binding, scope: binding ? { kind: 'fuli', ...binding } : { kind: 'temporary', id: `temporary:${id}`, label: 'Temporary collaboration / 临时协作', identityAuthority: 'standalone' }, seats, limits: normalizeLimits(input.limits), status: 'draft', phase: 'discussion', revision: 0, round: 1, phaseIndex: 0, agentMessageCount: 0, peerMessageCount: 0, nextSeq: 1, nextTurnNumber: 1, createdAt: now, updatedAt: now, startedAt: null, stopReason: null, currentTurn: null, turns: [], tasks, outcome: null, events: [] };
74
- }
75
-
76
- function implementationTasks(input, implementers, roomId, goal) {
77
- if (input !== undefined && (!Array.isArray(input) || !input.length || input.length > 24)) fail('invalid_tasks', 'Provide between one and 24 tasks');
78
- const supplied = input ?? implementers.map((seat, index) => ({ id: `${roomId}:implementation:${seat.id}`, title: `Implement: ${seat.name}`, seatId: seat.id, instructions: goal, dependsOn: index ? [`${roomId}:implementation:${implementers[index - 1].id}`] : [] }));
79
- const tasks = supplied.map(raw => {
80
- const seat = implementers.find(s => s.id === raw.seatId);
81
- if (!seat) fail('invalid_task_seat', 'Tasks can only be assigned to this room\'s implementer seats');
82
- const dependencies = raw.dependsOn ?? [];
83
- if (!Array.isArray(dependencies) || dependencies.length > 24) fail('invalid_dependencies', 'Task dependencies must be a bounded list');
84
- return { id: text(raw.id ?? randomUUID(), 'task.id', 256), title: text(raw.title, 'task.title', 512), instructions: text(raw.instructions ?? goal, 'task.instructions', 8192), seatId: seat.id, phase: 'implementation', status: 'pending', dependencies: dependencies.map(dep => text(dep, 'task dependency', 256)), permission: seat.execution.permission, workspace: seat.execution.workspace, artifacts: [], verification: null };
85
- });
86
- if (new Set(tasks.map(task => task.id)).size !== tasks.length) fail('duplicate_task', 'Task IDs must be distinct');
87
- const byId = new Map(tasks.map(task => [task.id, task])), visiting = new Set(), visited = new Set();
88
- const visit = id => {
89
- if (visiting.has(id)) fail('task_cycle', 'Task dependencies must be acyclic');
90
- if (visited.has(id)) return;
91
- const task = byId.get(id); if (!task) fail('invalid_dependencies', 'Dependencies must refer to tasks in this room');
92
- visiting.add(id); for (const dependency of task.dependencies) visit(dependency); visiting.delete(id); visited.add(id);
93
- };
94
- for (const task of tasks) visit(task.id);
95
- if (input) for (const seat of implementers) if (!tasks.some(t => t.seatId === seat.id)) fail('unassigned_implementer', 'Every implementer needs an assigned task');
96
- // A seat reports one bundle per turn; bundle dependencies must also be acyclic.
97
- orderedImplementers(implementers, tasks);
98
- return tasks;
99
- }
100
-
101
- function orderedImplementers(seats, tasks) {
102
- const pending = [...seats], result = [], byId = new Map(tasks.map(t => [t.id, t]));
103
- while (pending.length) {
104
- const index = pending.findIndex(seat => tasks.filter(t => t.seatId === seat.id).every(task => task.dependencies.every(id => byId.get(id).seatId === seat.id || result.some(done => done.id === byId.get(id).seatId))));
105
- if (index < 0) fail('task_bundle_cycle', 'Dependencies between seat task bundles must be acyclic');
106
- result.push(pending.splice(index, 1)[0]);
107
- }
108
- return result;
109
- }
110
-
111
- export function assertMutable(room) {
112
- if (TERMINAL_STATUSES.has(room.status)) fail('terminal_room', 'Terminal roundtables are immutable', 409);
113
- }
114
-
115
- export function event(room, kind, now, details = {}) {
116
- room.events.push({ id: randomUUID(), kind, createdAt: now, ...details });
117
- // Events are operational metadata; the immutable message and attempt history is retained.
118
- if (room.events.length > 500) room.events.shift();
119
- }
120
-
121
- export function phaseSeats(room) {
122
- if (room.phase === 'discussion') return room.seats;
123
- if (room.phase === 'planning' || room.phase === 'synthesis') return room.seats.filter(s => s.role === 'moderator');
124
- const seats = room.seats.filter(s => s.role === (room.phase === 'implementation' ? 'implementer' : 'reviewer'));
125
- return room.phase === 'implementation' ? orderedImplementers(seats, room.tasks.filter(t => t.phase === 'implementation')) : seats;
126
- }
127
-
128
- export function scheduleTurn(room, now) {
129
- if (room.status !== 'active' || room.currentTurn) return;
130
- const seat = phaseSeats(room)[room.phaseIndex];
131
- if (!seat) {
132
- room.status = 'waiting_input';
133
- room.stopReason = `missing_${room.phase}_seat`;
134
- event(room, 'phase_blocked', now, { reason: room.stopReason });
135
- return;
136
- }
137
- const tasks = room.tasks.filter(t => t.phase === room.phase && t.seatId === seat.id), task = tasks[0];
138
- if (tasks.some(assigned => assigned.dependencies.some(id => { const dependency = room.tasks.find(t => t.id === id); return dependency?.seatId !== seat.id && dependency?.status !== 'reported_complete'; }))) {
139
- room.status = 'waiting_input'; room.stopReason = 'task_dependencies_incomplete'; return;
140
- }
141
- room.currentTurn = { id: `${room.id}:turn:${room.nextTurnNumber++}`, seatId: seat.id, phase: room.phase, round: room.round, status: 'pending', fence: 0, attemptId: null, deadline: null, taskId: task?.id ?? null, taskIds: tasks.map(t => t.id), attempts: [] };
142
- }
143
-
144
- export function finishTurn(room, now) {
145
- const turn = room.currentTurn;
146
- room.turns.push(turn);
147
- room.currentTurn = null;
148
- room.phaseIndex += 1;
149
- if (room.phaseIndex < phaseSeats(room).length) { scheduleTurn(room, now); return; }
150
- room.phaseIndex = 0;
151
- if (room.phase === 'discussion' && room.round < room.limits.maxRounds) { room.round += 1; scheduleTurn(room, now); return; }
152
- if (room.phase === 'synthesis') {
153
- room.status = room.mode === 'discussion' ? 'concluded' : 'waiting_input';
154
- room.stopReason = room.mode === 'discussion' ? null : 'owner_completion_required';
155
- if (room.outcome) room.outcome.acceptance = 'pending';
156
- event(room, 'synthesis_complete', now, { status: room.status });
157
- return;
158
- }
159
- room.phase = room.mode === 'discussion' ? 'synthesis' : PHASES[PHASES.indexOf(room.phase) + 1];
160
- event(room, 'phase_changed', now, { phase: room.phase });
161
- scheduleTurn(room, now);
162
- }
163
-
164
- export function budgetReason(room, now) {
165
- if (room.startedAt !== null && now - room.startedAt >= room.limits.maxDurationMs) return 'duration_limit';
166
- if (room.agentMessageCount >= room.limits.maxMessages) return 'message_limit';
167
- return null;
168
- }
169
-
170
- export function publicRoom(room) {
171
- const { nextSeq, nextTurnNumber, phaseIndex, currentTurn, turns, tasks, outcome, events, ...result } = room;
172
- return structuredClone({ ...result, phaseIndex, seats: room.seats.map(publicSeat) });
173
- }
174
-
175
- export function publicSeat(seat) { const { joinedInvitationId, ...result } = seat; return structuredClone(result); }
176
-
177
- export function publicTurn(turn) {
178
- if (!turn) return null;
179
- const { attempts, ...result } = turn;
180
- return structuredClone(result);
181
- }
@@ -1,58 +0,0 @@
1
- import { readJson, sendJson } from '../http/response.js';
2
-
3
- const owner = Object.freeze({ kind: 'owner' });
4
-
5
- // The parent local-console request policy must authorize the owner before entry.
6
- export async function handleRoundtableApiRequest({ request, response, url, service }) {
7
- if (!service || !url.pathname.startsWith('/api/roundtables')) return false;
8
- if (url.pathname === '/api/roundtables') {
9
- if (request.method === 'GET') sendJson(response, 200, await service.list({}, owner));
10
- else if (request.method === 'POST') {
11
- const input = await readJson(request);
12
- if (service.bindingAuthority !== 'fuli_application' &&
13
- (input.binding || input.projectPath || input.personalProjectId || (input.seats ?? []).some((seat) => seat.agentId || seat.shareAgentContext))) {
14
- throw new TypeError('Standalone rooms cannot claim a Fuli project or Agent; connect the validated Fuli application');
15
- }
16
- sendJson(response, 201, await service.create(input, owner));
17
- }
18
- else sendJson(response, 405, { error: 'Method not allowed' });
19
- return true;
20
- }
21
- const route = url.pathname.match(/^\/api\/roundtables\/([^/]+)(?:\/(control|messages|invites|revoke))?$/);
22
- if (!route) { sendJson(response, 404, { error: 'Roundtable route not found' }); return true; }
23
- const roomId = decodeURIComponent(route[1]);
24
- if (!route[2] && request.method === 'GET') {
25
- const after = Number(url.searchParams.get('after') ?? 0);
26
- const limit = Number(url.searchParams.get('limit') ?? 100);
27
- sendJson(response, 200, await service.read({ roomId, after, limit }, owner));
28
- } else if (route[2] && request.method === 'POST') {
29
- const methods = { control: 'control', messages: 'addHumanMessage', invites: 'invite', revoke: 'revoke' };
30
- const body = await readJson(request);
31
- const { actor: _actor, taskVerification: _fakeVerification, ...safe } = body;
32
- sendJson(response, 200, await service[methods[route[2]]]({ ...safe, roomId }, owner));
33
- } else sendJson(response, 405, { error: 'Method not allowed' });
34
- return true;
35
- }
36
-
37
- export async function handleRoundtablePeerRequest({ request, response, url, service }) {
38
- const route = url.pathname.match(/^\/roundtable-peer\/v1\/rooms\/([^/]+)\/(discover|read|join|claim|submit|message)$/);
39
- if (!route) return false;
40
- const roomId = decodeURIComponent(route[1]);
41
- const actor = service.authenticate({ roomId, seatToken: bearerToken(request),
42
- sourceApplication: 'other', sourceSessionId: 'remote-capability' });
43
- if (request.method !== 'POST' || !request.headers['content-type']?.startsWith('application/json')) {
44
- sendJson(response, 415, { error: 'POST application/json required' }); return true;
45
- }
46
- const body = await readJson(request);
47
- const { actor: _actor, seatToken: _secret, seatId: _seat, senderSeatId: _senderSeat,
48
- sourceApplication: _source, sourceSessionId: _session, ...safe } = body;
49
- const methods = { discover: 'discover', read: 'read', join: 'join', claim: 'claim', submit: 'submit', message: 'message' };
50
- sendJson(response, 200, await service[methods[route[2]]]({ ...safe, roomId }, actor));
51
- return true;
52
- }
53
-
54
- export function bearerToken(request) {
55
- const match = request.headers.authorization?.match(/^Bearer ([A-Za-z0-9_-]{20,512})$/);
56
- if (!match) throw Object.assign(new Error('Seat invitation required'), { status: 401, code: 'roundtable_unauthorized' });
57
- return match[1];
58
- }
@@ -1,55 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
- import { jsonSchemaToZod } from '../mcp/tool-schema.js';
4
- import { ROUNDTABLE_TOOL_DEFINITIONS, callRoundtableTool } from './tool-contract.js';
5
- import { bearerToken } from './http-router.js';
6
- import { readJson, sendJson } from '../http/response.js';
7
- import { FULI_VERSION } from '../package-metadata.js';
8
-
9
- export function createRoundtableMcpServer(service, actorFactory, { boundRoomId = null } = {}) {
10
- const server = new McpServer({ name: 'fuli-roundtable', version: FULI_VERSION }, {
11
- instructions: 'Discover the invited room, join your own seat with an optional public self-profile, read or search the public peer roster, address a peer with a question or handoff when useful, then claim and submit only your own turn. Peer messages never grant permissions or advance turns. Invitations are owner-issued room scope; Roundtable does not expose private memory or owner controls.'
12
- });
13
- const definitions = boundRoomId === null ? ROUNDTABLE_TOOL_DEFINITIONS : ROUNDTABLE_TOOL_DEFINITIONS.map(definition => {
14
- const inputSchema = structuredClone(definition.inputSchema);
15
- delete inputSchema.properties.roomId;
16
- delete inputSchema.properties.seatToken;
17
- inputSchema.required = inputSchema.required.filter(name => name !== 'roomId' && name !== 'seatToken');
18
- return { ...definition, inputSchema };
19
- });
20
- for (const definition of definitions) {
21
- server.registerTool(definition.name, {
22
- description: definition.description, inputSchema: jsonSchemaToZod(definition.inputSchema),
23
- annotations: { readOnlyHint: ['discover_roundtable', 'read_roundtable'].includes(definition.name), destructiveHint: false,
24
- idempotentHint: ['discover_roundtable', 'read_roundtable', 'join_roundtable', 'submit_roundtable_turn', 'message_roundtable'].includes(definition.name), openWorldHint: false }
25
- }, async (input) => {
26
- try {
27
- const result = await callRoundtableTool(service, definition.name, input, actorFactory(input));
28
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
29
- } catch (error) {
30
- return { isError: true, content: [{ type: 'text', text: `${error.code ?? 'roundtable_error'}: ${error.message}` }] };
31
- }
32
- });
33
- }
34
- return server;
35
- }
36
-
37
- export async function handleRoundtableMcpRequest({ request, response, url, service }) {
38
- const route = url.pathname.match(/^\/roundtable-peer\/v1\/rooms\/([^/]+)\/mcp$/);
39
- if (!route) return false;
40
- const roomId = decodeURIComponent(route[1]);
41
- const secret = bearerToken(request);
42
- const actorFactory = (input = {}) => {
43
- if (input.roomId !== undefined && input.roomId !== roomId) throw Object.assign(new Error('Invitation is bound to another room'), { code: 'roundtable_forbidden' });
44
- return service.authenticate({ roomId, seatToken: secret, sourceApplication: 'other', sourceSessionId: 'remote-mcp-capability' });
45
- };
46
- actorFactory({ roomId }); // Authenticate even initialize/list requests.
47
- if (request.method !== 'POST') { sendJson(response, 405, { error: 'This MCP endpoint uses stateless POST responses' }); return true; }
48
- const server = createRoundtableMcpServer(service, actorFactory, { boundRoomId: roomId });
49
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
50
- try {
51
- await server.connect(transport);
52
- await transport.handleRequest(request, response, await readJson(request));
53
- } finally { await server.close(); }
54
- return true;
55
- }
@@ -1,12 +0,0 @@
1
- import { createCodexParticipant } from '../agents/codex/roundtable-participant.js';
2
- import { createClaudeCodeParticipant } from '../agents/claude-code/roundtable-participant.js';
3
- import { createGrokParticipant } from '../agents/grok/roundtable-participant.js';
4
- import { createA2AParticipant } from '../agents/a2a/roundtable-participant.js';
5
- import { createPiParticipant } from '../agents/pi/roundtable-participant.js';
6
-
7
- export function createRoundtableParticipant(runtime, options) {
8
- const factories = { codex: createCodexParticipant, 'claude-code': createClaudeCodeParticipant,
9
- grok: createGrokParticipant, a2a: createA2AParticipant, pi: createPiParticipant };
10
- if (!factories[runtime]) throw new TypeError('Select codex, claude-code, pi, grok or a2a; MCP seats join through their client');
11
- return factories[runtime](options);
12
- }
@@ -1,70 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
-
3
- // Own exactly one child/process group; never search for or kill unrelated clients.
4
- export function runParticipantProcess(command, args, { cwd, env = process.env, signal,
5
- input, timeoutMs = 300_000, maxOutputBytes = 2_000_000 } = {}) {
6
- signal?.throwIfAborted();
7
- return new Promise((resolve, reject) => {
8
- const child = spawn(command, args, { cwd, env, shell: false, windowsHide: true,
9
- detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
10
- let output = '', outputBytes = 0, failure, settled = false, hardTimer;
11
- const kill = () => {
12
- if (!child.pid) return;
13
- if (process.platform === 'win32') {
14
- const killer = spawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore', shell: false });
15
- killer.on('error', () => child.kill()); killer.unref();
16
- } else { try { process.kill(-child.pid, 'SIGKILL'); } catch {} }
17
- };
18
- const finish = (error, code) => {
19
- if (settled) return;
20
- settled = true;
21
- clearTimeout(timer); clearTimeout(hardTimer); signal?.removeEventListener('abort', abort);
22
- kill(); child.stdin.destroy(); child.stdout.destroy(); child.stderr.destroy(); child.unref();
23
- if (error || failure) reject(error ?? failure); else resolve({ code, stdout: output });
24
- };
25
- const stop = (code) => {
26
- if (failure || settled) return;
27
- failure = Object.assign(new Error(code), { code }); kill();
28
- hardTimer = setTimeout(() => finish(failure), 2000);
29
- };
30
- const abort = () => stop('cancelled');
31
- const timer = setTimeout(() => stop('turn_timeout'), timeoutMs);
32
- signal?.addEventListener('abort', abort, { once: true });
33
- child.stdout.on('data', (chunk) => {
34
- outputBytes += chunk.length;
35
- if (outputBytes > maxOutputBytes) { stop('output_too_large'); return; }
36
- output += chunk.toString('utf8');
37
- });
38
- // Drain upstream stderr without echoing keys, prompts, settings or credentials.
39
- child.stderr.on('data', () => {});
40
- child.on('error', () => finish(Object.assign(new Error('Participant process unavailable'), { code: 'runtime_unavailable' })));
41
- child.on('close', (code) => finish(null, code));
42
- child.stdin.on('error', () => {});
43
- child.stdin.end(input);
44
- });
45
- }
46
-
47
- export function parseParticipantEvents(application, result) {
48
- const events = result.stdout.split(/\r?\n/).flatMap((line) => { try { return [JSON.parse(line)]; } catch { return []; } });
49
- let body = '', sessionId = null, usage = null, failed = result.code !== 0;
50
- let codexCompleted = false, pendingCodexError = false;
51
- for (const event of events) {
52
- sessionId ??= event.thread_id ?? event.session_id ?? null;
53
- if (application === 'codex') {
54
- if (event.type === 'item.completed' && event.item?.type === 'agent_message') body = event.item.text ?? '';
55
- if (event.type === 'turn.completed') {
56
- usage = event.usage ?? null; codexCompleted = true; pendingCodexError = false;
57
- }
58
- if (event.type === 'error') pendingCodexError = true;
59
- if (event.type === 'turn.failed') failed = true;
60
- } else if (event.type === 'result') {
61
- body = event.structured_output ? JSON.stringify(event.structured_output) : event.result ?? ''; usage = event.usage ?? null; failed ||= Boolean(event.is_error);
62
- }
63
- }
64
- const actual = { sourceApplication: application, sessionId, model: null,
65
- usage: usage ? { source: 'executor', ...usage } : null, evidenceLevel: 'real_cli' };
66
- if (application === 'codex' && (!codexCompleted || pendingCodexError)) failed = true;
67
- if (failed) throw Object.assign(new Error('Participant execution failed; inspect client authorization locally'), { code: 'runtime_failed', actual });
68
- if (!body || Buffer.byteLength(body) > 16_384) throw Object.assign(new Error('Participant final response missing or too large'), { code: 'response_invalid', actual });
69
- return { body, actual };
70
- }
@@ -1,84 +0,0 @@
1
- import { fail, text } from './domain.js';
2
-
3
- export const ROUNDTABLE_PROTOCOL = Object.freeze({
4
- name: 'fuli-roundtable',
5
- version: '1'
6
- });
7
-
8
- const MAX_PROFILE_CAPABILITIES = 32;
9
- const MAX_CAPABILITY_BYTES = 128;
10
- const MAX_QUERY_BYTES = 128;
11
-
12
- /**
13
- * Self profiles are public declarations. They never change the coordinator's
14
- * seat role, runtime, execution permission or private Agent context.
15
- */
16
- export function normalizeSelfProfile(value) {
17
- if (value === undefined || value === null) return null;
18
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
19
- fail('invalid_self_profile', 'selfProfile must be an object');
20
- }
21
- const result = {};
22
- if (value.responsibility !== undefined) result.responsibility = text(value.responsibility, 'selfProfile.responsibility', 2048);
23
- if (value.introduction !== undefined) result.introduction = text(value.introduction, 'selfProfile.introduction', 2048);
24
- if (value.capabilities !== undefined) {
25
- if (!Array.isArray(value.capabilities) || value.capabilities.length > MAX_PROFILE_CAPABILITIES) {
26
- fail('invalid_self_profile', `selfProfile.capabilities must contain at most ${MAX_PROFILE_CAPABILITIES} entries`);
27
- }
28
- const capabilities = value.capabilities.map((capability) => text(capability, 'selfProfile.capability', MAX_CAPABILITY_BYTES));
29
- if (new Set(capabilities.map(capability => capability.toLocaleLowerCase())).size !== capabilities.length) {
30
- fail('invalid_self_profile', 'selfProfile.capabilities must be unique');
31
- }
32
- result.capabilities = capabilities;
33
- }
34
- if (!Object.keys(result).length) fail('invalid_self_profile', 'selfProfile must declare responsibility, capabilities or introduction');
35
- return result;
36
- }
37
-
38
- export function normalizeCapabilityQuery(value) {
39
- if (value === undefined || value === null || value === '') return null;
40
- return text(value, 'capabilityQuery', MAX_QUERY_BYTES).toLocaleLowerCase();
41
- }
42
-
43
- export function matchesCapabilityQuery(seat, query) {
44
- if (!query) return true;
45
- const profile = seat.selfProfile;
46
- return Array.isArray(profile?.capabilities) && profile.capabilities.some(capability => capability.toLocaleLowerCase().includes(query));
47
- }
48
-
49
- export function publicRoster(seats, query = null) {
50
- return seats.filter(seat => matchesCapabilityQuery(seat, query));
51
- }
52
-
53
- export function nextRoundtableAction(room, seat) {
54
- if (!seat.joinedAt) return { action: 'join', required: true, seatId: seat.id, reason: 'seat_not_joined' };
55
- if (room.status === 'concluded' || room.status === 'cancelled' || room.status === 'failed') {
56
- return { action: 'done', required: false, reason: room.status };
57
- }
58
- const turn = room.currentTurn;
59
- if (turn) {
60
- const ownTurn = turn.seatId === seat.id;
61
- if (room.status !== 'active') {
62
- return { action: 'wait', required: ownTurn, reason: room.stopReason ?? room.status, turnId: turn.id, seatId: turn.seatId, phase: turn.phase };
63
- }
64
- if (ownTurn && turn.status === 'pending') {
65
- return { action: 'claim', required: true, turnId: turn.id, phase: turn.phase };
66
- }
67
- if (ownTurn && turn.status === 'claimed') {
68
- return { action: 'submit', required: true, turnId: turn.id, attemptId: turn.attemptId, fence: turn.fence, deadline: turn.deadline, phase: turn.phase };
69
- }
70
- if (ownTurn) {
71
- return { action: 'wait', required: true, reason: room.stopReason ?? turn.status, turnId: turn.id, seatId: turn.seatId, phase: turn.phase };
72
- }
73
- return { action: 'wait', required: false, reason: 'another_seat_turn', turnId: turn.id, seatId: turn.seatId, phase: turn.phase };
74
- }
75
- if (room.status === 'draft') return { action: 'wait', required: false, reason: 'room_not_started' };
76
- return { action: 'wait', required: false, reason: room.stopReason ?? room.status };
77
- }
78
-
79
- export const DISCOVERY_INSTRUCTIONS = Object.freeze([
80
- 'Join your invited seat with an optional public selfProfile describing your responsibility and capabilities.',
81
- 'Use capabilityQuery in discover_roundtable to find invited peers with matching declared capabilities.',
82
- 'Use message_roundtable for an addressed question or handoff; peer messages never claim or advance a turn.',
83
- 'When nextAction.action is claim, claim your own turn, follow its scoped context, and submit one final response.'
84
- ]);
@@ -1,47 +0,0 @@
1
- export const PARTICIPANT_RESULT_SCHEMA = {
2
- type: 'object', additionalProperties: false,
3
- required: ['body', 'status', 'artifacts', 'verification', 'dissent'],
4
- properties: {
5
- body: { type: 'string', minLength: 1, maxLength: 12000 },
6
- status: { type: 'string', enum: ['completed', 'failed', 'blocked'] },
7
- artifacts: { type: 'array', maxItems: 20, items: { type: 'object', additionalProperties: false,
8
- required: ['id', 'uri'], properties: { id: { type: 'string', minLength: 1, maxLength: 1024 }, uri: { type: 'string', minLength: 1, maxLength: 2048 } } } },
9
- verification: { type: 'object', additionalProperties: false, required: ['passed', 'summary'],
10
- properties: { passed: { type: ['boolean', 'null'] }, summary: { type: 'string', maxLength: 2048 } } },
11
- dissent: { type: 'array', maxItems: 16, items: { type: 'string', minLength: 1, maxLength: 1024 } }
12
- }
13
- };
14
-
15
- export function parseRoundtableParticipantResult(result, phase) {
16
- let envelope;
17
- try { envelope = JSON.parse(result.body); } catch {
18
- throw Object.assign(new Error('Participant must return the requested JSON result envelope'), { code: 'result_contract_invalid', actual: result.actual });
19
- }
20
- if (!envelope || typeof envelope.body !== 'string' || !envelope.body.trim() || Buffer.byteLength(envelope.body) > 16_384 ||
21
- !['completed', 'failed', 'blocked'].includes(envelope.status) || !Array.isArray(envelope.artifacts) || envelope.artifacts.length > 20 ||
22
- envelope.artifacts.some(artifact => !artifact || !boundedText(artifact.id, 1024) || !boundedText(artifact.uri, 2048)) ||
23
- !Array.isArray(envelope.dissent) || envelope.dissent.length > 16 || envelope.dissent.some(item => !boundedText(item, 2048)) ||
24
- Buffer.byteLength(JSON.stringify(envelope.dissent), 'utf8') > 8192 ||
25
- !envelope.verification || ![true, false, null].includes(envelope.verification.passed) ||
26
- typeof envelope.verification.summary !== 'string' || Buffer.byteLength(envelope.verification.summary, 'utf8') > 2048) {
27
- throw Object.assign(new Error('Invalid participant result structure'), { code: 'result_contract_invalid', actual: result.actual });
28
- }
29
- if (envelope.status === 'completed' && phase === 'review' && typeof envelope.verification.passed !== 'boolean') {
30
- throw Object.assign(new Error('Review requires an explicit verification verdict'), { code: 'review_verdict_required', actual: result.actual });
31
- }
32
- if (envelope.status === 'completed' && phase === 'implementation' && envelope.artifacts.length === 0) {
33
- throw Object.assign(new Error('Implementation requires actual artifact references'), { code: 'artifact_required', actual: result.actual });
34
- }
35
- const artifacts = [...envelope.artifacts, ...(Array.isArray(result.artifacts) ? result.artifacts : [])];
36
- if (artifacts.length > 20 || artifacts.some(artifact => typeof artifact === 'string' ? !boundedText(artifact, 1024) :
37
- !artifact || !boundedText(artifact.id ?? artifact.artifactId, 1024) ||
38
- ['name', 'uri', 'mimeType', 'kind'].some(key => artifact[key] !== undefined && !boundedText(artifact[key], 2048)))) {
39
- throw Object.assign(new Error('Invalid or excessive adapter artifact references'), { code: 'result_contract_invalid', actual: result.actual });
40
- }
41
- return { ...result, body: envelope.body, status: envelope.status,
42
- artifacts, verification: envelope.verification, dissent: envelope.dissent };
43
- }
44
-
45
- function boundedText(value, bytes) {
46
- return typeof value === 'string' && Boolean(value.trim()) && Buffer.byteLength(value, 'utf8') <= bytes;
47
- }
@@ -1,52 +0,0 @@
1
- import { createServer } from 'node:http';
2
- import { mkdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { createRoundtableService } from './service.js';
5
- import { handleRoundtableApiRequest, handleRoundtablePeerRequest } from './http-router.js';
6
- import { handleRoundtableMcpRequest } from './mcp-server.js';
7
- import { serveStatic } from '../http/static-handler.js';
8
- import { sendJson } from '../http/response.js';
9
- import { localServerAuthority, rejectRequestOutsidePolicy } from '../http/request-policy.js';
10
- import { mapHttpError } from '../http/error-mapping.js';
11
-
12
- export async function createRoundtableServer({ dataDir, service: configuredService,
13
- port = 3738, host = '127.0.0.1', allowedHosts = [] } = {}) {
14
- if (!configuredService && !dataDir) throw new TypeError('Roundtable data directory required');
15
- if (dataDir) mkdirSync(dataDir, { recursive: true });
16
- const service = configuredService ?? createRoundtableService({ databasePath: join(dataDir, 'roundtables.sqlite') });
17
- let authority;
18
- const server = createServer((request, response) => {
19
- void (async () => {
20
- const url = new URL(request.url, 'http://127.0.0.1');
21
- if (url.pathname.startsWith('/roundtable-peer/')) {
22
- // A public reverse proxy must preserve an explicitly allowed Host.
23
- if (![authority, ...allowedHosts].includes(request.headers.host) || request.headers.origin) {
24
- sendJson(response, 403, { error: 'Peer authority forbidden' }); return;
25
- }
26
- if (await handleRoundtableMcpRequest({ request, response, url, service }) ||
27
- await handleRoundtablePeerRequest({ request, response, url, service })) return;
28
- sendJson(response, 404, { error: 'Peer route not found' }); return;
29
- }
30
- const local = ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(request.socket.remoteAddress);
31
- if (!local || rejectRequestOutsidePolicy({ request, response, authority })) {
32
- if (!response.writableEnded) sendJson(response, 403, { error: 'Owner console is local only' }); return;
33
- }
34
- if (url.pathname === '/api/health') { sendJson(response, 200, { status: 'ready', service: 'fuli-roundtable' }); return; }
35
- if (await handleRoundtableApiRequest({ request, response, url, service })) return;
36
- if (url.pathname.startsWith('/api/')) { sendJson(response, 404, { error: 'Only Roundtable APIs are available on this coordinator' }); return; }
37
- serveStatic(url.pathname, response);
38
- })().catch((error) => {
39
- if (response.headersSent || response.destroyed) return;
40
- const mapped = error.status ? { status: error.status, body: { error: error.message, code: error.code } } : mapHttpError(error);
41
- sendJson(response, mapped.status, mapped.body);
42
- });
43
- });
44
- try {
45
- await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, host, resolve); });
46
- authority = localServerAuthority(server.address());
47
- } catch (error) { if (!configuredService) service.close(); throw error; }
48
- let closing;
49
- return { server, service, url: `http://${authority}`,
50
- close: () => closing ??= new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
51
- .finally(() => { if (!configuredService) service.close(); }) };
52
- }