fuli-context 0.3.0 → 0.5.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 (95) hide show
  1. package/README.en.md +336 -0
  2. package/README.md +220 -174
  3. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +1 -0
  4. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +1 -0
  6. package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +1 -0
  8. package/dist/web/assets/KnowledgePage-CNQXd15y.js +1 -0
  9. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +4 -0
  10. package/dist/web/assets/OverviewPage-ul7g7HXh.js +1 -0
  11. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-S8ZcG2LU.css +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +1 -0
  14. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +1 -0
  15. package/dist/web/assets/ReviewPage-7MDDUDgA.js +1 -0
  16. package/dist/web/assets/console-B_Ena2DS.js +5 -0
  17. package/dist/web/assets/identity-CjBwYSMX.js +1 -0
  18. package/dist/web/assets/{index-Devf7glZ.css → index-CpLDx3of.css} +1 -1
  19. package/dist/web/assets/index-DAzeFJSc.js +2 -0
  20. package/dist/web/assets/model-C-5RVIJ2.js +1 -0
  21. package/dist/web/assets/{paths-B790iniO.js → paths-BkULqmGI.js} +1 -1
  22. package/dist/web/assets/{vue-router-BYTAoWT3.js → vue-router-DnD3hUm7.js} +1 -1
  23. package/dist/web/index.html +5 -6
  24. package/graph-provider/fuli_graph/app.py +48 -0
  25. package/graph-provider/fuli_graph/common_knowledge.py +443 -0
  26. package/graph-provider/fuli_graph/common_knowledge_models.py +54 -0
  27. package/graph-provider/fuli_graph/graph_models.py +9 -0
  28. package/graph-provider/fuli_graph/graph_query.py +25 -0
  29. package/graph-provider/fuli_graph/knowledge_audit.py +211 -0
  30. package/graph-provider/fuli_graph/knowledge_feedback_models.py +69 -0
  31. package/graph-provider/fuli_graph/knowledge_management.py +37 -0
  32. package/graph-provider/fuli_graph/knowledge_search.py +24 -2
  33. package/graph-provider/fuli_graph/models.py +69 -0
  34. package/graph-provider/fuli_graph/search_projection.py +59 -0
  35. package/graph-provider/fuli_graph/store_knowledge.py +2 -0
  36. package/graph-provider/pyproject.toml +1 -1
  37. package/npm-shrinkwrap.json +97 -2
  38. package/package.json +3 -1
  39. package/skills/capturing-session-knowledge/SKILL.md +83 -13
  40. package/src/agent-tools/graph-definitions.js +181 -13
  41. package/src/agent-tools/graph-handlers.js +19 -0
  42. package/src/cli/command-registry.js +3 -58
  43. package/src/cli/local-runtime-command.js +16 -2
  44. package/src/cli/local-runtime-options.js +6 -2
  45. package/src/cli.js +2 -37
  46. package/src/graphiti/agent-knowledge-workflows.js +543 -0
  47. package/src/graphiti/federated-application.js +85 -104
  48. package/src/graphiti/knowledge-provider-mapping.js +91 -0
  49. package/src/graphiti/provider-client.js +15 -0
  50. package/src/graphiti/source-uri.js +29 -0
  51. package/src/http/request-policy.js +37 -2
  52. package/src/local-runtime/lifecycle.js +11 -1
  53. package/src/mcp/common-knowledge-preview-tokens.js +67 -0
  54. package/src/mcp/create-mcp-server.js +43 -7
  55. package/src/mcp/instructions.js +6 -7
  56. package/src/mcp/lifecycle-audit.js +27 -0
  57. package/src/mcp/task-context-registry.js +93 -0
  58. package/src/mcp/tool-annotations.js +16 -1
  59. package/src/mcp/tool-result.js +24 -0
  60. package/src/runtime-options.js +7 -8
  61. package/src/server/lan-access.js +52 -0
  62. package/src/server.js +67 -8
  63. package/src/setup/agent-installation-status.js +14 -2
  64. package/src/setup/agents.js +12 -1
  65. package/src/setup/claude-code-config.js +194 -0
  66. package/src/setup/config-backup.js +20 -5
  67. package/src/setup/graph-runtime.js +78 -9
  68. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +0 -1
  69. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +0 -1
  70. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +0 -1
  71. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +0 -1
  72. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +0 -1
  73. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +0 -1
  74. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +0 -4
  75. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +0 -1
  76. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +0 -1
  77. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +0 -1
  78. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +0 -1
  79. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +0 -1
  80. package/dist/web/assets/ReviewPage-Dztk2jDG.js +0 -1
  81. package/dist/web/assets/console-tbvTUi6p.js +0 -1
  82. package/dist/web/assets/identity-B3s5CXhv.js +0 -1
  83. package/dist/web/assets/index-DwUDhQKc.js +0 -6
  84. package/dist/web/assets/model-DVT8sjy9.js +0 -1
  85. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +0 -1
  86. package/src/cli/commands/candidates.js +0 -17
  87. package/src/cli/commands/ingestion.js +0 -32
  88. package/src/cli/commands/reads.js +0 -50
  89. package/src/cli/commands/spaces.js +0 -20
  90. package/src/cli/invocation.js +0 -47
  91. package/src/cli/migrate-command.js +0 -71
  92. package/src/cli/migration-destination.js +0 -117
  93. package/src/storage/import-json.js +0 -70
  94. package/src/storage/legacy-record-validation.js +0 -152
  95. package/src/storage/legacy-snapshot-validation.js +0 -32
@@ -23,6 +23,23 @@ import {
23
23
  sourceConsoleUrl
24
24
  } from './source-marker.js';
25
25
  import { resolvePersonalProjectPath } from './project-path-context.js';
26
+ import { TaskContextRegistry } from '../mcp/task-context-registry.js';
27
+ import {
28
+ agentProjectResolution,
29
+ beginTaskContext as beginTaskContextWorkflow,
30
+ checkpointTaskKnowledge as checkpointTaskKnowledgeWorkflow,
31
+ discoverCommonKnowledgeCandidates as discoverCommonKnowledgeCandidatesWorkflow,
32
+ providerCommonKnowledgePromotion,
33
+ recordDecisionTrace as recordDecisionTraceWorkflow,
34
+ recordKnowledgeFeedback as recordKnowledgeFeedbackWorkflow,
35
+ searchCurrentProjectKnowledge as searchCurrentProjectKnowledgeWorkflow
36
+ } from './agent-knowledge-workflows.js';
37
+ import {
38
+ assertPublicKnowledgeEligible,
39
+ providerConfirmationActor,
40
+ providerConfirmationBasis,
41
+ providerEpisode
42
+ } from './knowledge-provider-mapping.js';
26
43
 
27
44
  export function openFederatedGraphApplication({
28
45
  runtimeConfigPath,
@@ -51,7 +68,9 @@ export class FederatedGraphApplication {
51
68
  capturePolicyStore = new CapturePolicyStore(),
52
69
  agentAccessPolicyStore = new AgentAccessPolicyStore(),
53
70
  consoleUrl = sourceConsoleUrl(null),
54
- projectPathResolver = resolvePersonalProjectPath
71
+ projectPathResolver = resolvePersonalProjectPath,
72
+ taskContextRegistry = new TaskContextRegistry(),
73
+ providerRequestTimeoutMs = undefined
55
74
  } = {}) {
56
75
  this.graphiti = true;
57
76
  this.config = config;
@@ -59,10 +78,12 @@ export class FederatedGraphApplication {
59
78
  this.agentAccessPolicyStore = agentAccessPolicyStore;
60
79
  this.consoleUrl = consoleUrl;
61
80
  this.projectPathResolver = projectPathResolver;
81
+ this.taskContextRegistry = taskContextRegistry;
62
82
  this.personal = new GraphitiProviderClient({
63
83
  baseUrl: config.personal.providerUrl,
64
84
  accessToken: config.personal.accessToken,
65
- fetchImpl
85
+ fetchImpl,
86
+ requestTimeoutMs: providerRequestTimeoutMs
66
87
  });
67
88
  this.workspaces = new Map(
68
89
  config.workspaces.map((workspace) => {
@@ -72,13 +93,31 @@ export class FederatedGraphApplication {
72
93
  client: new GraphitiProviderClient({
73
94
  baseUrl: url,
74
95
  accessToken: workspace.accessToken,
75
- fetchImpl
96
+ fetchImpl,
97
+ requestTimeoutMs: providerRequestTimeoutMs
76
98
  })
77
99
  }];
78
100
  })
79
101
  );
80
102
  }
81
103
 
104
+ async beginTaskContext(input) {
105
+ return beginTaskContextWorkflow(this, input);
106
+ }
107
+
108
+ async checkpointTaskKnowledge(input) {
109
+ return checkpointTaskKnowledgeWorkflow(this, input);
110
+ }
111
+
112
+ verifyTaskCheckpoint({ sessionId }) {
113
+ return this.taskContextRegistry.verify(sessionId);
114
+ }
115
+
116
+ async recordDecisionTrace(input) {
117
+ this.#assertActivePersonalSpace(input.personalSpaceId);
118
+ return recordDecisionTraceWorkflow(this, input);
119
+ }
120
+
82
121
  async captureSessionKnowledge(input) {
83
122
  const capturePolicy = this.getCapturePolicy();
84
123
  if (!capturePolicy.enabled) {
@@ -377,6 +416,36 @@ export class FederatedGraphApplication {
377
416
  };
378
417
  }
379
418
 
419
+ async searchCurrentProjectKnowledge(input) {
420
+ const resolution = await this.#resolvePreferenceProject({
421
+ personalProjectId: null,
422
+ projectPath: input.projectPath
423
+ });
424
+ return searchCurrentProjectKnowledgeWorkflow(this, resolution, input);
425
+ }
426
+
427
+ async discoverCommonKnowledgeCandidates(input) {
428
+ this.#assertActivePersonalSpace(input.personalSpaceId);
429
+ return discoverCommonKnowledgeCandidatesWorkflow(this, input);
430
+ }
431
+
432
+ async previewCommonKnowledgePromotion(input) {
433
+ this.#assertActivePersonalSpace(input.personalSpaceId);
434
+ assertSafeKnowledgeMutation(input);
435
+ return this.personal.previewCommonKnowledgePromotion(
436
+ providerCommonKnowledgePromotion(input)
437
+ );
438
+ }
439
+
440
+ async applyCommonKnowledgePromotion(input) {
441
+ this.#assertActivePersonalSpace(input.personalSpaceId);
442
+ assertSafeKnowledgeMutation(input);
443
+ return this.personal.applyCommonKnowledgePromotion({
444
+ ...providerCommonKnowledgePromotion(input),
445
+ operation_actor: input.operationActor ?? 'agent'
446
+ });
447
+ }
448
+
380
449
  async getKnowledgeGraph({
381
450
  spaceId,
382
451
  providerUrl = null,
@@ -425,6 +494,17 @@ export class FederatedGraphApplication {
425
494
  });
426
495
  }
427
496
 
497
+ async recordKnowledgeFeedback(input) {
498
+ this.#assertActivePersonalSpace(input.personalSpaceId);
499
+ assertSafeKnowledgeMutation({
500
+ items: input.items.map((item) => ({
501
+ existenceReason: item.reason,
502
+ quadrantReason: item.evidenceSummary
503
+ }))
504
+ });
505
+ return recordKnowledgeFeedbackWorkflow(this, input);
506
+ }
507
+
428
508
  async reviseKnowledgeItem(input) {
429
509
  this.#assertActivePersonalSpace(input.personalSpaceId);
430
510
  assertSafeKnowledgeMutation(input);
@@ -1039,22 +1119,11 @@ function preferenceConflictPairKey(leftId, rightId) {
1039
1119
  return [leftId, rightId].sort().join('\u0000');
1040
1120
  }
1041
1121
 
1042
- function agentProjectResolution(resolution) {
1043
- const result = {
1044
- status: resolution.status,
1045
- basis: resolution.basis,
1046
- personal_project_id: resolution.personalProjectId
1047
- };
1048
- if (resolution.candidateCount !== undefined) {
1049
- result.candidate_count = resolution.candidateCount;
1050
- }
1051
- return result;
1052
- }
1053
-
1054
1122
  function assertNoCredentials(input) {
1055
1123
  const content = JSON.stringify({
1056
1124
  name: input.name,
1057
1125
  sourceDescription: input.sourceDescription,
1126
+ sourceUri: input.sourceUri,
1058
1127
  sourceExcerpt: input.sourceExcerpt,
1059
1128
  summary: input.summary,
1060
1129
  entities: input.entities,
@@ -1120,6 +1189,7 @@ function assertSafeKnowledgeMutation(input) {
1120
1189
  note: input.note,
1121
1190
  newProjectName: input.newProjectName,
1122
1191
  newProjectPurpose: input.newProjectPurpose,
1192
+ humanConfirmationReason: input.humanConfirmationReason,
1123
1193
  batchItems: input.items?.map((item) => ({
1124
1194
  existenceReason: item.existenceReason,
1125
1195
  quadrantReason: item.quadrantReason
@@ -1133,95 +1203,6 @@ function assertSafeKnowledgeMutation(input) {
1133
1203
  }
1134
1204
  }
1135
1205
 
1136
- function providerEpisode(input) {
1137
- return {
1138
- idempotency_key: input.idempotencyKey,
1139
- session_id: input.sessionId,
1140
- name: input.name,
1141
- source_kind: input.sourceKind,
1142
- source_description: input.sourceDescription,
1143
- source_application: input.sourceApplication ?? null,
1144
- source_turn_id: input.sourceTurnId ?? null,
1145
- source_excerpt: input.sourceExcerpt ?? null,
1146
- reference_time: input.referenceTime,
1147
- summary: input.summary ?? '',
1148
- sensitivity: input.sensitivity ?? 'normal',
1149
- entities: input.entities.map((entity) => ({
1150
- key: entity.key,
1151
- name: entity.name,
1152
- type: entity.type,
1153
- summary: entity.summary ?? '',
1154
- origin_quadrant: entity.originQuadrant ?? 'known_known',
1155
- current_quadrant: entity.currentQuadrant ?? entity.originQuadrant ?? 'known_known',
1156
- epistemic_status: entity.epistemicStatus ?? 'confirmed',
1157
- confirmation_status: entity.confirmationStatus,
1158
- confirmation_basis: providerConfirmationBasis(entity.confirmationBasis),
1159
- reasoning_summary: entity.reasoningSummary ?? null,
1160
- profile_aspect: entity.profileAspect ?? null,
1161
- inheritance_mode: entity.inheritanceMode ?? 'local_only',
1162
- inherited_project_ids: entity.inheritedProjectIds ?? [],
1163
- attributes: entity.attributes ?? {}
1164
- })),
1165
- relationships: input.relationships.map((relationship) => ({
1166
- key: relationship.key,
1167
- source: relationship.source,
1168
- target: relationship.target,
1169
- type: relationship.type,
1170
- fact: relationship.fact,
1171
- valid_at: relationship.validAt ?? null,
1172
- invalid_at: relationship.invalidAt ?? null,
1173
- supersedes: relationship.supersedes ?? [],
1174
- confidence: relationship.confidence ?? 1,
1175
- origin_quadrant: relationship.originQuadrant ?? 'known_known',
1176
- current_quadrant: relationship.currentQuadrant ??
1177
- relationship.originQuadrant ?? 'known_known',
1178
- epistemic_status: relationship.epistemicStatus ?? 'confirmed',
1179
- confirmation_status: relationship.confirmationStatus,
1180
- confirmation_basis: providerConfirmationBasis(relationship.confirmationBasis),
1181
- reasoning_summary: relationship.reasoningSummary ?? null,
1182
- profile_aspect: relationship.profileAspect ?? null,
1183
- inheritance_mode: relationship.inheritanceMode ?? 'local_only',
1184
- inherited_project_ids: relationship.inheritedProjectIds ?? [],
1185
- attributes: relationship.attributes ?? {}
1186
- }))
1187
- };
1188
- }
1189
-
1190
- function assertPublicKnowledgeEligible(episode) {
1191
- const blocked = [...episode.entities, ...episode.relationships].find((item) =>
1192
- item.profile_aspect ||
1193
- item.confirmation_status !== 'confirmed' ||
1194
- !item.confirmation_basis?.confirmed_by ||
1195
- !item.confirmation_basis?.confirmed_at ||
1196
- !['user', 'authoritative_source'].includes(item.confirmation_basis.confirmed_by.kind)
1197
- );
1198
- if (blocked) {
1199
- throw new TypeError(
1200
- 'Only knowledge with an auditable confirmation can enter public review'
1201
- );
1202
- }
1203
- }
1204
-
1205
- function providerConfirmationBasis(basis) {
1206
- if (!basis) return undefined;
1207
- return {
1208
- existence_reason: basis.existenceReason,
1209
- quadrant_reason: basis.quadrantReason,
1210
- proposed_by: providerConfirmationActor(basis.proposedBy),
1211
- confirmed_by: providerConfirmationActor(basis.confirmedBy),
1212
- confirmed_at: basis.confirmedAt ?? null,
1213
- agent_policy_version: basis.agentPolicyVersion ?? null
1214
- };
1215
- }
1216
-
1217
- function providerConfirmationActor(actor) {
1218
- if (!actor) return null;
1219
- return {
1220
- kind: actor.kind,
1221
- label: actor.label ?? null
1222
- };
1223
- }
1224
-
1225
1206
  function providerProjectProfile(profile) {
1226
1207
  if (!profile || typeof profile !== 'object') throw new TypeError('Project profile is required');
1227
1208
  const assessment = profile.assessment ? sanitizeProjectAssessment(profile.assessment) : null;
@@ -0,0 +1,91 @@
1
+ import { onlineSourceUri } from './source-uri.js';
2
+
3
+ export function providerEpisode(input) {
4
+ return {
5
+ idempotency_key: input.idempotencyKey,
6
+ session_id: input.sessionId,
7
+ name: input.name,
8
+ source_kind: input.sourceKind,
9
+ source_description: input.sourceDescription,
10
+ source_uri: onlineSourceUri(input.sourceUri),
11
+ source_application: input.sourceApplication ?? null,
12
+ source_turn_id: input.sourceTurnId ?? null,
13
+ source_excerpt: input.sourceExcerpt ?? null,
14
+ reference_time: input.referenceTime,
15
+ summary: input.summary ?? '',
16
+ sensitivity: input.sensitivity ?? 'normal',
17
+ entities: input.entities.map((entity) => ({
18
+ key: entity.key,
19
+ name: entity.name,
20
+ type: entity.type,
21
+ summary: entity.summary ?? '',
22
+ origin_quadrant: entity.originQuadrant ?? 'known_known',
23
+ current_quadrant: entity.currentQuadrant ?? entity.originQuadrant ?? 'known_known',
24
+ epistemic_status: entity.epistemicStatus ?? 'confirmed',
25
+ confirmation_status: entity.confirmationStatus,
26
+ confirmation_basis: providerConfirmationBasis(entity.confirmationBasis),
27
+ reasoning_summary: entity.reasoningSummary ?? null,
28
+ profile_aspect: entity.profileAspect ?? null,
29
+ inheritance_mode: entity.inheritanceMode ?? 'local_only',
30
+ inherited_project_ids: entity.inheritedProjectIds ?? [],
31
+ attributes: entity.attributes ?? {}
32
+ })),
33
+ relationships: input.relationships.map((relationship) => ({
34
+ key: relationship.key,
35
+ source: relationship.source,
36
+ target: relationship.target,
37
+ type: relationship.type,
38
+ fact: relationship.fact,
39
+ valid_at: relationship.validAt ?? null,
40
+ invalid_at: relationship.invalidAt ?? null,
41
+ supersedes: relationship.supersedes ?? [],
42
+ confidence: relationship.confidence ?? 1,
43
+ origin_quadrant: relationship.originQuadrant ?? 'known_known',
44
+ current_quadrant: relationship.currentQuadrant ??
45
+ relationship.originQuadrant ?? 'known_known',
46
+ epistemic_status: relationship.epistemicStatus ?? 'confirmed',
47
+ confirmation_status: relationship.confirmationStatus,
48
+ confirmation_basis: providerConfirmationBasis(relationship.confirmationBasis),
49
+ reasoning_summary: relationship.reasoningSummary ?? null,
50
+ profile_aspect: relationship.profileAspect ?? null,
51
+ inheritance_mode: relationship.inheritanceMode ?? 'local_only',
52
+ inherited_project_ids: relationship.inheritedProjectIds ?? [],
53
+ attributes: relationship.attributes ?? {}
54
+ }))
55
+ };
56
+ }
57
+
58
+ export function assertPublicKnowledgeEligible(episode) {
59
+ const blocked = [...episode.entities, ...episode.relationships].find((item) =>
60
+ item.profile_aspect ||
61
+ item.confirmation_status !== 'confirmed' ||
62
+ !item.confirmation_basis?.confirmed_by ||
63
+ !item.confirmation_basis?.confirmed_at ||
64
+ !['user', 'authoritative_source'].includes(item.confirmation_basis.confirmed_by.kind)
65
+ );
66
+ if (blocked) {
67
+ throw new TypeError(
68
+ 'Only knowledge with an auditable confirmation can enter public review'
69
+ );
70
+ }
71
+ }
72
+
73
+ export function providerConfirmationBasis(basis) {
74
+ if (!basis) return undefined;
75
+ return {
76
+ existence_reason: basis.existenceReason,
77
+ quadrant_reason: basis.quadrantReason,
78
+ proposed_by: providerConfirmationActor(basis.proposedBy),
79
+ confirmed_by: providerConfirmationActor(basis.confirmedBy),
80
+ confirmed_at: basis.confirmedAt ?? null,
81
+ agent_policy_version: basis.agentPolicyVersion ?? null
82
+ };
83
+ }
84
+
85
+ export function providerConfirmationActor(actor) {
86
+ if (!actor) return null;
87
+ return {
88
+ kind: actor.kind,
89
+ label: actor.label ?? null
90
+ };
91
+ }
@@ -133,6 +133,11 @@ export class GraphitiProviderClient {
133
133
  method: 'POST', body: input
134
134
  });
135
135
  }
136
+ recordKnowledgeFeedback(input) {
137
+ return this.#request('/v1/knowledge/feedback', {
138
+ method: 'POST', body: input
139
+ });
140
+ }
136
141
  reviewHumanChange(itemId, input) {
137
142
  return this.#request(
138
143
  `/v1/knowledge/items/${encodeURIComponent(itemId)}/agent-review`,
@@ -161,6 +166,16 @@ export class GraphitiProviderClient {
161
166
  { method: 'POST', body: input }
162
167
  );
163
168
  }
169
+ previewCommonKnowledgePromotion(input) {
170
+ return this.#request('/v1/knowledge/common-promotions/preview', {
171
+ method: 'POST', body: input
172
+ });
173
+ }
174
+ applyCommonKnowledgePromotion(input) {
175
+ return this.#request('/v1/knowledge/common-promotions', {
176
+ method: 'POST', body: input
177
+ });
178
+ }
164
179
  deferPreferenceConflict(input) {
165
180
  return this.#request('/v1/preference-conflicts/defer', {
166
181
  method: 'POST', body: input
@@ -0,0 +1,29 @@
1
+ const SOURCE_URI_ERROR =
2
+ 'Source URI must be an absolute HTTP(S) URI without credentials';
3
+
4
+ export function onlineSourceUri(value) {
5
+ if (value === undefined || value === null) return null;
6
+ if (
7
+ typeof value !== 'string'
8
+ || value.length < 1
9
+ || value.length > 2048
10
+ || /(?:\s|\p{C})/u.test(value)
11
+ ) {
12
+ throw new TypeError(SOURCE_URI_ERROR);
13
+ }
14
+ let parsed;
15
+ try {
16
+ parsed = new URL(value);
17
+ } catch {
18
+ throw new TypeError(SOURCE_URI_ERROR);
19
+ }
20
+ if (
21
+ !['http:', 'https:'].includes(parsed.protocol)
22
+ || !parsed.hostname
23
+ || parsed.username
24
+ || parsed.password
25
+ ) {
26
+ throw new TypeError(SOURCE_URI_ERROR);
27
+ }
28
+ return value;
29
+ }
@@ -1,3 +1,5 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+
1
3
  import { sendJson } from './response.js';
2
4
 
3
5
  export function localServerAuthority(address) {
@@ -9,18 +11,38 @@ export function localServerAuthority(address) {
9
11
  }
10
12
 
11
13
  export function rejectDisallowedRequest({ request, response, authority }) {
14
+ return rejectRequestOutsidePolicy({ request, response, authority });
15
+ }
16
+
17
+ export function rejectRequestOutsidePolicy({
18
+ request,
19
+ response,
20
+ authority,
21
+ lanAuthorities = [],
22
+ lanAccessToken = null
23
+ }) {
12
24
  const origin = request.headers.origin;
13
25
  const fetchSite = request.headers['sec-fetch-site'];
26
+ const requestAuthority = request.headers.host;
27
+ const lanRequest = lanAuthorities.includes(requestAuthority);
28
+ const loopbackRequest = requestAuthority === authority;
14
29
  if (
15
30
  !authority ||
16
- request.headers.host !== authority ||
17
- (origin !== undefined && origin !== `http://${authority}`) ||
31
+ (requestAuthority !== authority && !lanRequest) ||
32
+ (lanAuthorities.length > 0 && loopbackRequest && !isLoopbackAddress(request.socket?.remoteAddress)) ||
33
+ (origin !== undefined && origin !== `http://${requestAuthority}`) ||
18
34
  (typeof fetchSite === 'string' && fetchSite.toLowerCase() === 'cross-site')
19
35
  ) {
20
36
  sendJson(response, 403, { error: 'Forbidden' });
21
37
  return true;
22
38
  }
23
39
 
40
+ if (lanRequest && !hasLanAccess(request.headers.authorization, lanAccessToken)) {
41
+ response.setHeader('www-authenticate', 'Basic realm="FULI LAN", charset="UTF-8"');
42
+ sendJson(response, 401, { error: 'LAN access code required' });
43
+ return true;
44
+ }
45
+
24
46
  if (requiresJson(request) && !isJson(request.headers['content-type'])) {
25
47
  sendJson(response, 415, { error: 'Unsupported media type' });
26
48
  return true;
@@ -29,6 +51,19 @@ export function rejectDisallowedRequest({ request, response, authority }) {
29
51
  return false;
30
52
  }
31
53
 
54
+ function hasLanAccess(authorization, accessToken) {
55
+ if (typeof accessToken !== 'string' || !accessToken) return false;
56
+ const expected = `Basic ${Buffer.from(`fuli:${accessToken}`).toString('base64')}`;
57
+ if (typeof authorization !== 'string' || authorization.length !== expected.length) {
58
+ return false;
59
+ }
60
+ return timingSafeEqual(Buffer.from(authorization), Buffer.from(expected));
61
+ }
62
+
63
+ function isLoopbackAddress(address) {
64
+ return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
65
+ }
66
+
32
67
  function requiresJson(request) {
33
68
  if (request.method !== 'POST') return false;
34
69
  const pathname = new URL(request.url, 'http://127.0.0.1').pathname;
@@ -20,6 +20,7 @@ export async function startLocalRuntime(input, dependencies = {}) {
20
20
  paths: input.paths,
21
21
  personalSpaceName: input.personalSpaceName,
22
22
  port: input.port,
23
+ lan: input.lan === true,
23
24
  personalOnly: !managesDevelopmentWorkspace,
24
25
  buildProviders,
25
26
  noStart: false,
@@ -99,7 +100,10 @@ export async function inspectLocalRuntime(input, dependencies = {}) {
99
100
  console: {
100
101
  status: consoleReady ? 'ready' : processAlive ? 'unverified' : 'stopped',
101
102
  url: stateLooksOwned ? state.url : `http://127.0.0.1:${input.port}`,
102
- pid: processAlive ? state.pid : null
103
+ pid: processAlive ? state.pid : null,
104
+ ...(stateLooksOwned && state.lan === true
105
+ ? { lan: true, lanUrls: safeLanUrls(state.lanUrls) }
106
+ : {})
103
107
  },
104
108
  personal,
105
109
  public: {
@@ -237,6 +241,12 @@ function isRuntimeState(state) {
237
241
  Number.isInteger(state.pid) && typeof state.url === 'string';
238
242
  }
239
243
 
244
+ function safeLanUrls(value) {
245
+ return Array.isArray(value)
246
+ ? value.filter((url) => typeof url === 'string' && url.startsWith('http://'))
247
+ : [];
248
+ }
249
+
240
250
  function isProcessAlive(pid) {
241
251
  try { process.kill(pid, 0); return true; } catch { return false; }
242
252
  }
@@ -0,0 +1,67 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { ApplicationError } from '../app/application-error.js';
4
+
5
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
6
+
7
+ export function createCommonKnowledgePreviewTokens({
8
+ now = Date.now,
9
+ createToken = randomUUID,
10
+ ttlMs = DEFAULT_TTL_MS
11
+ } = {}) {
12
+ const previews = new Map();
13
+
14
+ return Object.freeze({
15
+ issue(input) {
16
+ pruneExpired(previews, now());
17
+ const token = createToken();
18
+ previews.set(token, {
19
+ expiresAt: now() + ttlMs,
20
+ intent: promotionIntent(input)
21
+ });
22
+ return token;
23
+ },
24
+
25
+ consume(token, input) {
26
+ if (!token) {
27
+ throw new ApplicationError(
28
+ 'preview_required',
29
+ 'Preview this exact common-knowledge promotion before applying it'
30
+ );
31
+ }
32
+ const preview = previews.get(token);
33
+ if (!preview || preview.expiresAt <= now()) {
34
+ previews.delete(token);
35
+ throw new ApplicationError(
36
+ 'preview_expired',
37
+ 'Common-knowledge preview authorization is missing or expired'
38
+ );
39
+ }
40
+ if (preview.intent !== promotionIntent(input)) {
41
+ throw new ApplicationError(
42
+ 'preview_mismatch',
43
+ 'Common-knowledge promotion no longer matches its preview'
44
+ );
45
+ }
46
+ previews.delete(token);
47
+ }
48
+ });
49
+ }
50
+
51
+ function promotionIntent(input) {
52
+ return JSON.stringify({
53
+ personalSpaceId: input.personalSpaceId,
54
+ parentProjectId: input.parentProjectId,
55
+ itemKind: input.itemKind,
56
+ canonicalItemId: input.canonicalItemId,
57
+ duplicateItemIds: [...(input.duplicateItemIds ?? [])].sort(),
58
+ reason: input.reason,
59
+ humanConfirmationReason: input.humanConfirmationReason
60
+ });
61
+ }
62
+
63
+ function pruneExpired(previews, currentTime) {
64
+ for (const [token, preview] of previews) {
65
+ if (preview.expiresAt <= currentTime) previews.delete(token);
66
+ }
67
+ }
@@ -5,13 +5,23 @@ import { callAgentTool, listAgentTools } from '../agent-tools.js';
5
5
  import { FULI_VERSION } from '../package-metadata.js';
6
6
  import { MCP_INSTRUCTIONS } from './instructions.js';
7
7
  import { createProjectActionPreviewTokens } from './project-action-preview-tokens.js';
8
+ import { createCommonKnowledgePreviewTokens } from './common-knowledge-preview-tokens.js';
9
+ import { auditLifecycleTool } from './lifecycle-audit.js';
8
10
  import { annotationsFor } from './tool-annotations.js';
9
- import { errorToolResult, protocolErrorResult, successToolResult } from './tool-result.js';
11
+ import {
12
+ errorToolResult,
13
+ hookAdditionalContextToolResult,
14
+ protocolErrorResult,
15
+ successToolResult
16
+ } from './tool-result.js';
10
17
  import { jsonSchemaToZod, openObjectSchema } from './tool-schema.js';
11
18
 
12
19
  const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
20
+ begin_task_context: 16 * 1024,
13
21
  get_collaboration_preferences: 16 * 1024,
14
- search_knowledge_graph: 32 * 1024
22
+ search_knowledge_graph: 32 * 1024,
23
+ search_current_project_knowledge: 64 * 1024,
24
+ discover_common_knowledge_candidates: 32 * 1024
15
25
  });
16
26
 
17
27
  export function createMcpServer(app) {
@@ -27,6 +37,7 @@ export function createMcpServer(app) {
27
37
  }
28
38
 
29
39
  function createToolMap(app, projectActionPreviews) {
40
+ const commonKnowledgePreviews = createCommonKnowledgePreviewTokens();
30
41
  return new Map(listAgentTools().map((definition) => [definition.name, {
31
42
  definition,
32
43
  schema: jsonSchemaToZod(definition.inputSchema),
@@ -34,7 +45,8 @@ function createToolMap(app, projectActionPreviews) {
34
45
  app,
35
46
  definition.name,
36
47
  input,
37
- projectActionPreviews
48
+ projectActionPreviews,
49
+ commonKnowledgePreviews
38
50
  )
39
51
  }]));
40
52
  }
@@ -62,15 +74,29 @@ function registerCallHandler(server, tools) {
62
74
 
63
75
  async function invokeTool(tool, input) {
64
76
  try {
65
- return successToolResult(await tool.invoke(input), {
66
- limitBytes: TOOL_RESULT_LIMIT_BYTES[tool.definition.name]
67
- });
77
+ const value = await tool.invoke(input);
78
+ auditLifecycleTool(tool.definition.name);
79
+ const limitBytes = TOOL_RESULT_LIMIT_BYTES[tool.definition.name];
80
+ if (tool.definition.name === 'begin_task_context') {
81
+ return hookAdditionalContextToolResult(value, {
82
+ hookEventName: 'UserPromptSubmit',
83
+ label: 'Fuli task context. Apply effective_preferences and use taskContextToken for the final checkpoint.',
84
+ limitBytes
85
+ });
86
+ }
87
+ return successToolResult(value, { limitBytes });
68
88
  } catch (error) {
69
89
  return errorToolResult(error);
70
90
  }
71
91
  }
72
92
 
73
- async function invokeAgentTool(app, name, input, projectActionPreviews) {
93
+ async function invokeAgentTool(
94
+ app,
95
+ name,
96
+ input,
97
+ projectActionPreviews,
98
+ commonKnowledgePreviews
99
+ ) {
74
100
  if (name === 'preview_personal_project_action') {
75
101
  const result = await callAgentTool(app, name, input);
76
102
  return {
@@ -81,5 +107,15 @@ async function invokeAgentTool(app, name, input, projectActionPreviews) {
81
107
  if (name === 'apply_personal_project_action') {
82
108
  projectActionPreviews.consume(input.previewToken, input);
83
109
  }
110
+ if (name === 'preview_common_knowledge_promotion') {
111
+ const result = await callAgentTool(app, name, input);
112
+ return {
113
+ ...result,
114
+ previewToken: commonKnowledgePreviews.issue(input)
115
+ };
116
+ }
117
+ if (name === 'apply_common_knowledge_promotion') {
118
+ commonKnowledgePreviews.consume(input.previewToken, input);
119
+ }
84
120
  return callAgentTool(app, name, input);
85
121
  }