@gmickel/gno 1.27.1 → 1.29.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 (195) hide show
  1. package/README.md +46 -20
  2. package/assets/skill/SKILL.md +59 -1
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/browser-extension/dist/preview.html +1 -1
  8. package/browser-extension/dist/service-worker.js +6 -6
  9. package/package.json +2 -2
  10. package/spec/cli.md +82 -5
  11. package/spec/db/schema.sql +31 -0
  12. package/spec/mcp.md +71 -0
  13. package/spec/output-schemas/ask.schema.json +156 -1
  14. package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
  15. package/spec/output-schemas/collection-egress-check.schema.json +91 -0
  16. package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
  17. package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +183 -3
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
  20. package/spec/output-schemas/egress-audit-management.schema.json +88 -0
  21. package/spec/output-schemas/get.schema.json +94 -0
  22. package/spec/output-schemas/mcp-http-error.schema.json +113 -2
  23. package/spec/output-schemas/mcp-job-status.schema.json +28 -0
  24. package/spec/output-schemas/multi-get.schema.json +128 -0
  25. package/spec/output-schemas/publish-artifact.schema.json +32 -0
  26. package/spec/output-schemas/record-import.schema.json +223 -0
  27. package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
  28. package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
  29. package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
  30. package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
  31. package/spec/output-schemas/search-result.schema.json +94 -0
  32. package/spec/output-schemas/search-results.schema.json +155 -0
  33. package/spec/project-profile.schema.json +66 -0
  34. package/src/app/context-format.ts +1 -1
  35. package/src/app/context-runtime-contract.ts +16 -7
  36. package/src/cli/commands/ask.ts +7 -0
  37. package/src/cli/commands/collection/index.ts +5 -0
  38. package/src/cli/commands/collection/policy.ts +126 -0
  39. package/src/cli/commands/context-build.ts +5 -0
  40. package/src/cli/commands/daemon.ts +45 -3
  41. package/src/cli/commands/egress-audit.ts +68 -0
  42. package/src/cli/commands/embed.ts +2 -0
  43. package/src/cli/commands/get.ts +12 -2
  44. package/src/cli/commands/index-cmd.ts +13 -0
  45. package/src/cli/commands/multi-get.ts +9 -2
  46. package/src/cli/commands/query.ts +12 -0
  47. package/src/cli/commands/replay.ts +13 -3
  48. package/src/cli/commands/shared.ts +28 -0
  49. package/src/cli/commands/update.ts +5 -0
  50. package/src/cli/commands/vsearch.ts +3 -1
  51. package/src/cli/program.ts +127 -0
  52. package/src/config/index.ts +9 -0
  53. package/src/config/project-profile.ts +2 -0
  54. package/src/config/types.ts +66 -0
  55. package/src/converters/adapters/browser-export/adapter.ts +199 -0
  56. package/src/converters/adapters/browser-export/formats.ts +358 -0
  57. package/src/converters/adapters/email/adapter.ts +429 -0
  58. package/src/converters/adapters/email/html.ts +162 -0
  59. package/src/converters/adapters/email/mime.ts +454 -0
  60. package/src/converters/adapters/email/parameters.ts +77 -0
  61. package/src/converters/adapters/ical/adapter.ts +475 -0
  62. package/src/converters/adapters/ical/recurrence.ts +186 -0
  63. package/src/converters/adapters/jsonl/adapter.ts +238 -0
  64. package/src/converters/adapters/jsonl/config.ts +105 -0
  65. package/src/converters/adapters/shared/html-text.ts +165 -0
  66. package/src/converters/adapters/shared/record-utils.ts +79 -0
  67. package/src/converters/adapters/shared/utf8-lines.ts +141 -0
  68. package/src/converters/adapters/transcript/adapter.ts +295 -0
  69. package/src/converters/adapters/transcript/json.ts +184 -0
  70. package/src/converters/adapters/transcript/model.ts +171 -0
  71. package/src/converters/adapters/transcript/text.ts +58 -0
  72. package/src/converters/adapters/transcript/timed.ts +152 -0
  73. package/src/converters/index.ts +11 -1
  74. package/src/converters/mime.ts +8 -0
  75. package/src/converters/pipeline.ts +15 -1
  76. package/src/converters/registry.ts +37 -1
  77. package/src/converters/types.ts +136 -0
  78. package/src/core/browser-clip.ts +16 -1
  79. package/src/core/collection-egress-policy-projection.ts +28 -0
  80. package/src/core/collection-egress-policy-service.ts +443 -0
  81. package/src/core/collection-egress-policy-validation.ts +242 -0
  82. package/src/core/config-mutation.ts +31 -19
  83. package/src/core/context-capsule-schema.ts +198 -1
  84. package/src/core/context-capsule-validation.ts +4 -4
  85. package/src/core/context-capsule-verification.ts +5 -1
  86. package/src/core/context-capsule.ts +177 -112
  87. package/src/core/context-evidence.ts +93 -15
  88. package/src/core/destination-classifier.ts +402 -0
  89. package/src/core/document-capabilities.ts +12 -0
  90. package/src/core/egress-audit.ts +239 -0
  91. package/src/core/egress-authorization.ts +50 -0
  92. package/src/core/egress-enforcement.ts +264 -0
  93. package/src/core/egress-policy.ts +440 -0
  94. package/src/core/egress-provenance.ts +336 -0
  95. package/src/core/job-manager.ts +37 -0
  96. package/src/core/network-boundary-inventory.ts +261 -0
  97. package/src/core/project-profile-apply-state.ts +5 -0
  98. package/src/core/project-profile.ts +4 -0
  99. package/src/core/record-metadata.ts +49 -0
  100. package/src/core/retrieval-qrels.ts +6 -0
  101. package/src/core/retrieval-replay.ts +18 -4
  102. package/src/core/retrieval-trace-export.ts +23 -4
  103. package/src/core/retrieval-trace-management-helpers.ts +1 -0
  104. package/src/core/retrieval-trace-management-types.ts +3 -0
  105. package/src/core/retrieval-trace-management.ts +9 -1
  106. package/src/core/retrieval-trace-session.ts +94 -0
  107. package/src/core/retrieval-trace.ts +22 -0
  108. package/src/ingestion/record-adapter-canonical.ts +433 -0
  109. package/src/ingestion/record-adapter.ts +448 -0
  110. package/src/ingestion/record-container.ts +704 -0
  111. package/src/ingestion/record-path.ts +20 -0
  112. package/src/ingestion/record-sync.ts +70 -0
  113. package/src/ingestion/sync.ts +243 -36
  114. package/src/ingestion/types.ts +71 -1
  115. package/src/ingestion/walker.ts +31 -11
  116. package/src/llm/errors.ts +10 -0
  117. package/src/llm/http-inference.ts +175 -0
  118. package/src/llm/http-policy.ts +537 -0
  119. package/src/llm/httpEmbedding.ts +47 -28
  120. package/src/llm/httpGeneration.ts +31 -18
  121. package/src/llm/httpRerank.ts +30 -18
  122. package/src/llm/index.ts +1 -0
  123. package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
  124. package/src/llm/pinned-http-connection.ts +177 -0
  125. package/src/mcp/context.ts +53 -1
  126. package/src/mcp/http-egress.ts +220 -0
  127. package/src/mcp/http-security.ts +32 -21
  128. package/src/mcp/http-session.ts +13 -2
  129. package/src/mcp/http-transport.ts +96 -6
  130. package/src/mcp/sync-egress.ts +24 -0
  131. package/src/mcp/tools/add-collection.ts +4 -0
  132. package/src/mcp/tools/ask.ts +1 -1
  133. package/src/mcp/tools/context.ts +9 -2
  134. package/src/mcp/tools/egress.ts +247 -0
  135. package/src/mcp/tools/embed.ts +1 -0
  136. package/src/mcp/tools/get.ts +10 -2
  137. package/src/mcp/tools/index-cmd.ts +7 -0
  138. package/src/mcp/tools/index.ts +98 -0
  139. package/src/mcp/tools/multi-get.ts +9 -2
  140. package/src/mcp/tools/query.ts +10 -0
  141. package/src/mcp/tools/remove-collection.ts +4 -0
  142. package/src/mcp/tools/sync.ts +6 -0
  143. package/src/mcp/tools/trace.ts +7 -1
  144. package/src/mcp/tools/vsearch.ts +1 -0
  145. package/src/mcp/tools/workspace-write.ts +2 -0
  146. package/src/pipeline/egress-lineage.ts +124 -0
  147. package/src/pipeline/filters.ts +1 -1
  148. package/src/pipeline/graph-retrieval.ts +7 -4
  149. package/src/pipeline/hybrid.ts +21 -4
  150. package/src/pipeline/result-context.ts +12 -4
  151. package/src/pipeline/search.ts +23 -4
  152. package/src/pipeline/types.ts +6 -0
  153. package/src/pipeline/vsearch.ts +67 -28
  154. package/src/publish/artifact.ts +31 -3
  155. package/src/publish/export-service.ts +25 -0
  156. package/src/sdk/client.ts +119 -2
  157. package/src/sdk/documents.ts +13 -5
  158. package/src/sdk/embed.ts +1 -0
  159. package/src/sdk/types.ts +33 -0
  160. package/src/serve/browse-tree.ts +4 -2
  161. package/src/serve/clipper-capture.ts +5 -1
  162. package/src/serve/closed-json.ts +61 -0
  163. package/src/serve/config-sync.ts +4 -1
  164. package/src/serve/context.ts +1 -0
  165. package/src/serve/doc-events.ts +110 -39
  166. package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
  167. package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
  168. package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
  169. package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
  170. package/src/serve/public/globals.built.css +1 -1
  171. package/src/serve/public/pages/Collections.tsx +3 -0
  172. package/src/serve/resident-request.ts +80 -3
  173. package/src/serve/resident-runtime.ts +63 -1
  174. package/src/serve/routes/api.ts +237 -65
  175. package/src/serve/routes/clipper.ts +64 -1
  176. package/src/serve/routes/mcp.ts +6 -0
  177. package/src/serve/routes/traces.ts +27 -4
  178. package/src/serve/server.ts +118 -7
  179. package/src/store/migrations/022-record-export-lineage.ts +42 -0
  180. package/src/store/migrations/023-collection-egress-policy.ts +61 -0
  181. package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
  182. package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
  183. package/src/store/migrations/index.ts +8 -0
  184. package/src/store/retrieval-trace-codec.ts +15 -0
  185. package/src/store/sqlite/adapter.ts +284 -14
  186. package/src/store/sqlite/change-journal-store.ts +41 -2
  187. package/src/store/sqlite/egress-audit-store.ts +485 -0
  188. package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
  189. package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
  190. package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
  191. package/src/store/sqlite/retrieval-trace-store.ts +104 -5
  192. package/src/store/types.ts +166 -1
  193. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip +0 -0
  194. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
  195. package/browser-extension/dist/chunk-vn5f663b.js +0 -50
@@ -14,6 +14,7 @@ import type {
14
14
  Collection,
15
15
  CollectionModelOverrides,
16
16
  Config,
17
+ EgressPolicy,
17
18
  ModelPreset,
18
19
  } from "../../config/types";
19
20
  import type { JobManager } from "../../core/job-manager";
@@ -49,6 +50,13 @@ import {
49
50
  normalizeContentTypes,
50
51
  } from "../../config";
51
52
  import { type PublicCaptureInput } from "../../core/capture";
53
+ import { projectCollectionEgressPolicy } from "../../core/collection-egress-policy-projection";
54
+ import {
55
+ CollectionEgressPolicyService,
56
+ type CollectionEgressPolicyState,
57
+ type EgressRelaxationConfirmation,
58
+ } from "../../core/collection-egress-policy-service";
59
+ import { parsePolicySetBody } from "../../core/collection-egress-policy-validation";
52
60
  import {
53
61
  type ConnectorVerificationCode,
54
62
  getConnectorVerificationRemediation,
@@ -58,6 +66,7 @@ import {
58
66
  deriveEditableCopyRelPath,
59
67
  getDocumentCapabilities,
60
68
  } from "../../core/document-capabilities";
69
+ import { EgressAuditService } from "../../core/egress-audit";
61
70
  import {
62
71
  atomicWrite,
63
72
  copyFilePath,
@@ -93,6 +102,7 @@ import {
93
102
  ProjectAffinityInputError,
94
103
  resolveRemoteProjectAffinity,
95
104
  } from "../../core/project-affinity-surface";
105
+ import { projectRecordEvidenceMetadata } from "../../core/record-metadata";
96
106
  import {
97
107
  retrievalTraceFilters,
98
108
  startRetrievalTraceRequest,
@@ -143,6 +153,7 @@ import {
143
153
  executeResidentCapturePlan,
144
154
  planResidentCapture,
145
155
  } from "../capture-service";
156
+ import { parseClosedJson } from "../closed-json";
146
157
  import { applyConfigChange, applyConfigChangeTyped } from "../config-sync";
147
158
  import {
148
159
  getConnectorStatuses,
@@ -175,6 +186,12 @@ export interface ContextHolder {
175
186
  jobManager?: JobManager;
176
187
  markContentMutation?: () => void;
177
188
  markIndexMutation?: () => void;
189
+ invalidateEgressPolicy?: () => Promise<{
190
+ policyEpoch: string;
191
+ queuedJobsInvalidated: number;
192
+ sessionsInvalidated: number;
193
+ staleWorkMustRetry: true;
194
+ }>;
178
195
  startBackgroundWork?: (
179
196
  operation: (signal: AbortSignal) => Promise<void>
180
197
  ) => boolean;
@@ -360,6 +377,11 @@ export interface UpdateCollectionRequestBody {
360
377
  };
361
378
  }
362
379
 
380
+ export interface UpdateCollectionEgressPolicyRequestBody {
381
+ policy: EgressPolicy;
382
+ confirmation?: EgressRelaxationConfirmation;
383
+ }
384
+
363
385
  export interface ClearCollectionEmbeddingsRequestBody {
364
386
  mode?: "stale" | "all";
365
387
  }
@@ -386,6 +408,7 @@ export interface CollectionResponse {
386
408
  gen: "override" | "preset" | "default";
387
409
  };
388
410
  activePresetId: string;
411
+ egressPolicy: CollectionEgressPolicyState | null;
389
412
  }
390
413
 
391
414
  function serializeCollection(
@@ -405,6 +428,12 @@ function serializeCollection(
405
428
  effectiveModels: getCollectionEffectiveModels(config, collection.name),
406
429
  modelSources: getCollectionModelSources(config, collection.name),
407
430
  activePresetId: modelConfig.activePreset,
431
+ egressPolicy: (() => {
432
+ const result = new CollectionEgressPolicyService({
433
+ getConfig: () => config,
434
+ }).get(collection.name);
435
+ return result.ok ? result.value : null;
436
+ })(),
408
437
  };
409
438
  }
410
439
 
@@ -607,6 +636,7 @@ function readRequestedUriFromUrl(req: Request): string | undefined {
607
636
 
608
637
  interface SourceMeta {
609
638
  absPath?: string;
639
+ relPath: string;
610
640
  mime: string;
611
641
  ext: string;
612
642
  modifiedAt?: string;
@@ -625,7 +655,11 @@ function getCollectionByName(
625
655
 
626
656
  async function resolveAbsoluteDocPath(
627
657
  collections: Config["collections"],
628
- doc: { collection: string; relPath: string }
658
+ doc: {
659
+ collection: string;
660
+ relPath: string;
661
+ recordSourcePath?: string | null;
662
+ }
629
663
  ): Promise<{
630
664
  collection: Config["collections"][number];
631
665
  fullPath: string;
@@ -638,7 +672,7 @@ async function resolveAbsoluteDocPath(
638
672
  const nodePath = await import("node:path"); // no bun equivalent
639
673
  let safeRelPath: string;
640
674
  try {
641
- safeRelPath = validateRelPath(doc.relPath);
675
+ safeRelPath = validateRelPath(doc.recordSourcePath ?? doc.relPath);
642
676
  } catch {
643
677
  return null;
644
678
  }
@@ -763,6 +797,7 @@ async function buildSourceMeta(
763
797
  doc: {
764
798
  collection: string;
765
799
  relPath: string;
800
+ recordSourcePath?: string | null;
766
801
  sourceMime: string;
767
802
  sourceExt: string;
768
803
  sourceMtime?: string | null;
@@ -770,9 +805,14 @@ async function buildSourceMeta(
770
805
  sourceHash?: string;
771
806
  }
772
807
  ): Promise<SourceMeta> {
773
- const resolved = await resolveAbsoluteDocPath(collections, doc);
808
+ const relPath = doc.recordSourcePath ?? doc.relPath;
809
+ const resolved = await resolveAbsoluteDocPath(collections, {
810
+ collection: doc.collection,
811
+ relPath,
812
+ });
774
813
  return {
775
814
  absPath: resolved?.fullPath,
815
+ relPath,
776
816
  mime: doc.sourceMime,
777
817
  ext: doc.sourceExt,
778
818
  modifiedAt: doc.sourceMtime ?? undefined,
@@ -781,6 +821,20 @@ async function buildSourceMeta(
781
821
  };
782
822
  }
783
823
 
824
+ const capabilitiesForDocument = (
825
+ doc: Pick<
826
+ DocumentRow,
827
+ "sourceExt" | "sourceMime" | "mirrorHash" | "recordKey"
828
+ >,
829
+ contentAvailable = doc.mirrorHash !== null
830
+ ) =>
831
+ getDocumentCapabilities({
832
+ sourceExt: doc.sourceExt,
833
+ sourceMime: doc.sourceMime,
834
+ contentAvailable,
835
+ recordKey: doc.recordKey,
836
+ });
837
+
784
838
  async function resolveDocumentReference(
785
839
  store: Pick<SqliteAdapter, "getDocumentByDocid" | "getDocumentByUri">,
786
840
  docId: string,
@@ -975,6 +1029,150 @@ export async function handleCollections(config: Config): Promise<Response> {
975
1029
  );
976
1030
  }
977
1031
 
1032
+ const collectionPolicyService = (
1033
+ ctxHolder: ContextHolder,
1034
+ store: SqliteAdapter,
1035
+ collection: string
1036
+ ): CollectionEgressPolicyService =>
1037
+ new CollectionEgressPolicyService({
1038
+ getConfig: () => ctxHolder.config,
1039
+ mutateConfig: (mutate) =>
1040
+ applyConfigChangeTyped(
1041
+ ctxHolder,
1042
+ store,
1043
+ (config) => mutate(config),
1044
+ undefined,
1045
+ {
1046
+ projectStore: (targetStore, config) =>
1047
+ projectCollectionEgressPolicy(targetStore, config, collection),
1048
+ }
1049
+ ),
1050
+ onPolicyChanged: async () =>
1051
+ (await ctxHolder.invalidateEgressPolicy?.()) ?? {
1052
+ policyEpoch: "egress-epoch-standalone",
1053
+ queuedJobsInvalidated: 0,
1054
+ sessionsInvalidated: 0,
1055
+ staleWorkMustRetry: true,
1056
+ },
1057
+ });
1058
+
1059
+ export function handleCollectionEgressPolicy(
1060
+ ctxHolder: ContextHolder,
1061
+ name: string
1062
+ ): Response {
1063
+ const state = new CollectionEgressPolicyService({
1064
+ getConfig: () => ctxHolder.config,
1065
+ }).get(name);
1066
+ if (state.ok) return jsonResponse(state.value);
1067
+ return errorResponse(
1068
+ state.code === "NOT_FOUND" ? "NOT_FOUND" : "VALIDATION",
1069
+ state.error,
1070
+ state.code === "NOT_FOUND" ? 404 : 400
1071
+ );
1072
+ }
1073
+
1074
+ export async function handleUpdateCollectionEgressPolicy(
1075
+ ctxHolder: ContextHolder,
1076
+ store: SqliteAdapter,
1077
+ name: string,
1078
+ req: Request
1079
+ ): Promise<Response> {
1080
+ const parsed = await parseClosedJson(req);
1081
+ if (!parsed.ok) return errorResponse("VALIDATION", parsed.error, 400);
1082
+ const input = parsePolicySetBody(parsed.value, name);
1083
+ if (!input.ok) return errorResponse("VALIDATION", input.error, 400);
1084
+ const result = await collectionPolicyService(ctxHolder, store, name).set(
1085
+ input.value
1086
+ );
1087
+ if (!result.ok) {
1088
+ const status =
1089
+ result.code === "NOT_FOUND"
1090
+ ? 404
1091
+ : result.code === "EGRESS_RELAXATION_CONFIRMATION_REQUIRED"
1092
+ ? 409
1093
+ : 400;
1094
+ return errorResponse(result.code, result.error, status);
1095
+ }
1096
+ return jsonResponse(result.value);
1097
+ }
1098
+
1099
+ export async function handleCollectionEgressCheck(
1100
+ ctxHolder: ContextHolder,
1101
+ req: Request
1102
+ ): Promise<Response> {
1103
+ const parsed = await parseClosedJson(req);
1104
+ if (!parsed.ok) return errorResponse("VALIDATION", parsed.error, 400);
1105
+ const result = new CollectionEgressPolicyService({
1106
+ getConfig: () => ctxHolder.config,
1107
+ }).check(parsed.value);
1108
+ return result.ok
1109
+ ? jsonResponse(result.value)
1110
+ : errorResponse("VALIDATION", result.error, 400);
1111
+ }
1112
+
1113
+ export async function handleEgressAuditList(
1114
+ store: SqliteAdapter,
1115
+ req: Request
1116
+ ): Promise<Response> {
1117
+ const url = new URL(req.url);
1118
+ const limit = url.searchParams.has("limit")
1119
+ ? Number(url.searchParams.get("limit"))
1120
+ : undefined;
1121
+ const result = await new EgressAuditService(store).list({
1122
+ limit,
1123
+ cursor: url.searchParams.get("cursor") ?? undefined,
1124
+ });
1125
+ return result.ok
1126
+ ? jsonResponse(result.value)
1127
+ : errorResponse(result.error.code, result.error.message);
1128
+ }
1129
+
1130
+ export async function handleEgressAuditShow(
1131
+ store: SqliteAdapter,
1132
+ auditId: string
1133
+ ): Promise<Response> {
1134
+ const result = await new EgressAuditService(store).show(auditId);
1135
+ return result.ok
1136
+ ? jsonResponse(result.value)
1137
+ : errorResponse(
1138
+ result.error.code,
1139
+ result.error.message,
1140
+ result.error.code === "NOT_FOUND" ? 404 : 400
1141
+ );
1142
+ }
1143
+
1144
+ export async function handleEgressAuditStatus(
1145
+ store: SqliteAdapter
1146
+ ): Promise<Response> {
1147
+ const result = await new EgressAuditService(store).status();
1148
+ return result.ok
1149
+ ? jsonResponse(result.value)
1150
+ : errorResponse(result.error.code, result.error.message);
1151
+ }
1152
+
1153
+ export async function handleEgressAuditDelete(
1154
+ store: SqliteAdapter,
1155
+ auditId: string
1156
+ ): Promise<Response> {
1157
+ const result = await new EgressAuditService(store).delete(auditId);
1158
+ return result.ok
1159
+ ? jsonResponse(result.value)
1160
+ : errorResponse(
1161
+ result.error.code,
1162
+ result.error.message,
1163
+ result.error.code === "NOT_FOUND" ? 404 : 400
1164
+ );
1165
+ }
1166
+
1167
+ export async function handleEgressAuditPurge(
1168
+ store: SqliteAdapter
1169
+ ): Promise<Response> {
1170
+ const result = await new EgressAuditService(store).purge();
1171
+ return result.ok
1172
+ ? jsonResponse(result.value)
1173
+ : errorResponse(result.error.code, result.error.message);
1174
+ }
1175
+
978
1176
  /**
979
1177
  * POST /api/publish/export
980
1178
  * Build a gno.sh-compatible publish artifact for a collection or single doc.
@@ -1140,6 +1338,7 @@ export async function handleCreateCollection(
1140
1338
  if (!collection) {
1141
1339
  return errorResponse("RUNTIME", "Collection not found after add", 500);
1142
1340
  }
1341
+ await ctxHolder.invalidateEgressPolicy?.();
1143
1342
  const jobResult = await startJob(
1144
1343
  "add",
1145
1344
  async (): Promise<SyncResult> => {
@@ -1255,6 +1454,7 @@ export async function handleDeleteCollection(
1255
1454
  const status = statusMap[syncResult.code] ?? 500;
1256
1455
  return errorResponse(syncResult.code, syncResult.error, status);
1257
1456
  }
1457
+ await ctxHolder.invalidateEgressPolicy?.();
1258
1458
  ctxHolder.markContentMutation?.();
1259
1459
  ctxHolder.markIndexMutation?.();
1260
1460
  return jsonResponse({
@@ -1613,7 +1813,7 @@ export async function handleDocs(
1613
1813
  uri: doc.uri,
1614
1814
  title: doc.title,
1615
1815
  collection: doc.collection,
1616
- relPath: doc.relPath,
1816
+ relPath: doc.recordSourcePath ?? doc.relPath,
1617
1817
  sourceExt: doc.sourceExt,
1618
1818
  sourceMime: doc.sourceMime,
1619
1819
  updatedAt: doc.updatedAt,
@@ -1790,12 +1990,10 @@ export async function handleDoc(
1790
1990
  }
1791
1991
 
1792
1992
  const contentAvailable = content !== null;
1793
- const capabilities = getDocumentCapabilities({
1794
- sourceExt: doc.sourceExt,
1795
- sourceMime: doc.sourceMime,
1796
- contentAvailable,
1797
- });
1993
+ const capabilities = capabilitiesForDocument(doc, contentAvailable);
1798
1994
  const source = await buildSourceMeta(config.collections, doc);
1995
+ const record = projectRecordEvidenceMetadata(doc);
1996
+ const relPath = doc.recordSourcePath ?? doc.relPath;
1799
1997
 
1800
1998
  const responseData = {
1801
1999
  docid: doc.docid,
@@ -1804,9 +2002,10 @@ export async function handleDoc(
1804
2002
  content,
1805
2003
  contentAvailable,
1806
2004
  collection: doc.collection,
1807
- relPath: doc.relPath,
2005
+ relPath,
1808
2006
  tags,
1809
2007
  source,
2008
+ record,
1810
2009
  capabilities,
1811
2010
  };
1812
2011
  const continuationTraceId = request
@@ -2079,6 +2278,15 @@ export async function handleRenameDoc(
2079
2278
  }
2080
2279
 
2081
2280
  const doc = docResult.value;
2281
+ const capabilities = capabilitiesForDocument(doc);
2282
+ if (!capabilities.editable) {
2283
+ return errorResponse(
2284
+ "READ_ONLY",
2285
+ capabilities.reason ??
2286
+ "This document cannot be renamed in place from GNO.",
2287
+ 409
2288
+ );
2289
+ }
2082
2290
  const resolvedDocPath = await resolveAbsoluteDocPath(
2083
2291
  ctxHolder.config.collections,
2084
2292
  doc
@@ -2096,20 +2304,6 @@ export async function handleRenameDoc(
2096
2304
  return errorResponse("FILE_NOT_FOUND", "Source file no longer exists", 404);
2097
2305
  }
2098
2306
 
2099
- const capabilities = getDocumentCapabilities({
2100
- sourceExt: doc.sourceExt,
2101
- sourceMime: doc.sourceMime,
2102
- contentAvailable: doc.mirrorHash !== null,
2103
- });
2104
- if (!capabilities.editable) {
2105
- return errorResponse(
2106
- "READ_ONLY",
2107
- capabilities.reason ??
2108
- "This document cannot be renamed in place from GNO.",
2109
- 409
2110
- );
2111
- }
2112
-
2113
2307
  const nodePath = await import("node:path"); // no bun equivalent
2114
2308
  const directory = nodePath.dirname(doc.relPath);
2115
2309
  const currentExt = nodePath.extname(doc.relPath);
@@ -2206,11 +2400,7 @@ export async function handleRefactorPlan(
2206
2400
  }
2207
2401
 
2208
2402
  const doc = docResult.value;
2209
- const capabilities = getDocumentCapabilities({
2210
- sourceExt: doc.sourceExt,
2211
- sourceMime: doc.sourceMime,
2212
- contentAvailable: doc.mirrorHash !== null,
2213
- });
2403
+ const capabilities = capabilitiesForDocument(doc);
2214
2404
  if (!capabilities.editable) {
2215
2405
  return errorResponse(
2216
2406
  "READ_ONLY",
@@ -2333,11 +2523,7 @@ export async function handleTrashDoc(
2333
2523
  }
2334
2524
  const { collection, fullPath } = resolvedDocPath;
2335
2525
 
2336
- const capabilities = getDocumentCapabilities({
2337
- sourceExt: doc.sourceExt,
2338
- sourceMime: doc.sourceMime,
2339
- contentAvailable: doc.mirrorHash !== null,
2340
- });
2526
+ const capabilities = capabilitiesForDocument(doc);
2341
2527
  if (!capabilities.editable) {
2342
2528
  return errorResponse(
2343
2529
  "READ_ONLY",
@@ -2432,11 +2618,7 @@ export async function handleMoveDoc(
2432
2618
  return errorResponse("NOT_FOUND", "Document not found", 404);
2433
2619
  }
2434
2620
  const doc = docResult.value;
2435
- const capabilities = getDocumentCapabilities({
2436
- sourceExt: doc.sourceExt,
2437
- sourceMime: doc.sourceMime,
2438
- contentAvailable: doc.mirrorHash !== null,
2439
- });
2621
+ const capabilities = capabilitiesForDocument(doc);
2440
2622
  if (!capabilities.editable) {
2441
2623
  return errorResponse(
2442
2624
  "READ_ONLY",
@@ -2563,11 +2745,7 @@ export async function handleDuplicateDoc(
2563
2745
  return errorResponse("NOT_FOUND", "Document not found", 404);
2564
2746
  }
2565
2747
  const doc = docResult.value;
2566
- const capabilities = getDocumentCapabilities({
2567
- sourceExt: doc.sourceExt,
2568
- sourceMime: doc.sourceMime,
2569
- contentAvailable: doc.mirrorHash !== null,
2570
- });
2748
+ const capabilities = capabilitiesForDocument(doc);
2571
2749
  if (!capabilities.editable) {
2572
2750
  return errorResponse(
2573
2751
  "READ_ONLY",
@@ -2837,6 +3015,15 @@ export async function handleUpdateDoc(
2837
3015
  }
2838
3016
 
2839
3017
  const doc = docResult.value;
3018
+ const capabilities = capabilitiesForDocument(doc);
3019
+ if (hasContent && !capabilities.editable) {
3020
+ return errorResponse(
3021
+ "READ_ONLY",
3022
+ capabilities.reason ??
3023
+ "This document cannot be edited in place. Create an editable markdown copy instead.",
3024
+ 409
3025
+ );
3026
+ }
2840
3027
 
2841
3028
  const resolvedDocPath = await resolveAbsoluteDocPath(
2842
3029
  ctxHolder.config.collections,
@@ -2886,20 +3073,6 @@ export async function handleUpdateDoc(
2886
3073
  }
2887
3074
  }
2888
3075
 
2889
- const capabilities = getDocumentCapabilities({
2890
- sourceExt: doc.sourceExt,
2891
- sourceMime: doc.sourceMime,
2892
- contentAvailable: doc.mirrorHash !== null,
2893
- });
2894
- if (hasContent && !capabilities.editable) {
2895
- return errorResponse(
2896
- "READ_ONLY",
2897
- capabilities.reason ??
2898
- "This document cannot be edited in place. Create an editable markdown copy instead.",
2899
- 409
2900
- );
2901
- }
2902
-
2903
3076
  let writeBack: "applied" | "skipped_unsupported" | undefined;
2904
3077
 
2905
3078
  try {
@@ -3044,11 +3217,7 @@ export async function handleCreateEditableCopy(
3044
3217
 
3045
3218
  const doc = docResult.value;
3046
3219
  const contentAvailable = doc.mirrorHash !== null;
3047
- const capabilities = getDocumentCapabilities({
3048
- sourceExt: doc.sourceExt,
3049
- sourceMime: doc.sourceMime,
3050
- contentAvailable,
3051
- });
3220
+ const capabilities = capabilitiesForDocument(doc, contentAvailable);
3052
3221
  if (capabilities.editable) {
3053
3222
  return errorResponse(
3054
3223
  "VALIDATION",
@@ -3094,12 +3263,15 @@ export async function handleCreateEditableCopy(
3094
3263
  const existingRelPaths = listResult.ok
3095
3264
  ? listResult.value.map((entry) => entry.relPath)
3096
3265
  : [];
3097
- relPath = deriveEditableCopyRelPath(doc.relPath, existingRelPaths);
3266
+ relPath = deriveEditableCopyRelPath(
3267
+ doc.recordSourcePath ?? doc.relPath,
3268
+ existingRelPaths
3269
+ );
3098
3270
  }
3099
3271
 
3100
3272
  const title =
3101
3273
  doc.title ??
3102
- doc.relPath
3274
+ (doc.recordSourcePath ?? doc.relPath)
3103
3275
  .split("/")
3104
3276
  .pop()
3105
3277
  ?.replace(/\.[^.]+$/, "") ??
@@ -1,10 +1,16 @@
1
1
  /** Dedicated loopback browser-clipper HTTP gateway. */
2
2
 
3
+ import type { EgressLineage } from "../../core/egress-provenance";
3
4
  import type { HttpMcpPeerServer } from "../../mcp/http-security";
4
5
  import type { SqliteAdapter } from "../../store/sqlite/adapter";
5
6
  import type { ContextHolder } from "./api";
6
7
 
7
8
  import { prepareBrowserClip } from "../../core/browser-clip";
9
+ import {
10
+ enforceCollectionEgressWithAudit,
11
+ EGRESS_DENIED_MESSAGE,
12
+ EgressDeniedError,
13
+ } from "../../core/egress-enforcement";
8
14
  import {
9
15
  planResidentCapture,
10
16
  type ResidentCapturePlanResult,
@@ -83,6 +89,40 @@ const pollStatusCode = (result: ClipperPairPollResult): number => {
83
89
  return 200;
84
90
  };
85
91
 
92
+ const enforceClipWrite = async (
93
+ ctxHolder: ContextHolder,
94
+ store: SqliteAdapter,
95
+ body: unknown
96
+ ): Promise<EgressLineage | undefined> => {
97
+ const record =
98
+ typeof body === "object" && body !== null
99
+ ? (body as Record<string, unknown>)
100
+ : null;
101
+ const payload =
102
+ typeof record?.payload === "object" && record.payload !== null
103
+ ? (record.payload as Record<string, unknown>)
104
+ : record;
105
+ const destination =
106
+ typeof payload?.destination === "object" && payload.destination !== null
107
+ ? (payload.destination as Record<string, unknown>)
108
+ : null;
109
+ const collection =
110
+ typeof destination?.collection === "string"
111
+ ? destination.collection
112
+ : undefined;
113
+ if (!collection) return undefined;
114
+ const { lineage } = await enforceCollectionEgressWithAudit({
115
+ collections: ctxHolder.config.collections,
116
+ collectionNames: [collection],
117
+ action: "clip_write",
118
+ destinationZone: "loopback",
119
+ caller: { authenticated: true, operationAuthorized: true },
120
+ contentClass: "source",
121
+ store,
122
+ });
123
+ return lineage;
124
+ };
125
+
86
126
  export function createClipperRouteGateway(
87
127
  ctxHolder: ContextHolder,
88
128
  store: SqliteAdapter,
@@ -279,7 +319,12 @@ export function createClipperRouteGateway(
279
319
  if (!authenticated.ok) return authenticated.response;
280
320
  const { admission, grant } = authenticated.value;
281
321
  try {
282
- const prepared = prepareBrowserClip(admission.body);
322
+ const egressLineage = await enforceClipWrite(
323
+ ctxHolder,
324
+ store,
325
+ admission.body
326
+ );
327
+ const prepared = prepareBrowserClip(admission.body, { egressLineage });
283
328
  const planned = await planResidentCapture(
284
329
  ctxHolder,
285
330
  store,
@@ -303,6 +348,12 @@ export function createClipperRouteGateway(
303
348
  admission.origin
304
349
  );
305
350
  } catch (error) {
351
+ if (error instanceof EgressDeniedError) {
352
+ return withClipperCors(
353
+ clipperErrorResponse("EGRESS_DENIED", EGRESS_DENIED_MESSAGE, 403),
354
+ admission.origin
355
+ );
356
+ }
306
357
  return withClipperCors(
307
358
  clipperErrorResponse(
308
359
  "CLIPPER_INVALID_REQUEST",
@@ -321,6 +372,11 @@ export function createClipperRouteGateway(
321
372
  if (!authenticated.ok) return authenticated.response;
322
373
  const { admission, grant } = authenticated.value;
323
374
  try {
375
+ const egressLineage = await enforceClipWrite(
376
+ ctxHolder,
377
+ store,
378
+ admission.body
379
+ );
324
380
  return withClipperCors(
325
381
  await executeClipperCapture({
326
382
  request,
@@ -330,10 +386,17 @@ export function createClipperRouteGateway(
330
386
  context: ctxHolder,
331
387
  store,
332
388
  pairing,
389
+ egressLineage,
333
390
  }),
334
391
  admission.origin
335
392
  );
336
393
  } catch (error) {
394
+ if (error instanceof EgressDeniedError) {
395
+ return withClipperCors(
396
+ clipperErrorResponse("EGRESS_DENIED", EGRESS_DENIED_MESSAGE, 403),
397
+ admission.origin
398
+ );
399
+ }
337
400
  return withClipperCors(
338
401
  clipperErrorResponse(
339
402
  "CLIPPER_CAPTURE_FAILED",
@@ -44,6 +44,9 @@ export async function createMcpHttpGateway(
44
44
  (runtime as Partial<ResidentRuntime>).setTransportStatusProvider?.(() =>
45
45
  transport.getStatus()
46
46
  );
47
+ (runtime as Partial<ResidentRuntime>).setPolicySessionInvalidator?.(() =>
48
+ transport.invalidateAuthenticatedSessions()
49
+ );
47
50
 
48
51
  const route: McpHttpRoute = async (request, server) => {
49
52
  const authorization = await security.authorize(request, server);
@@ -52,8 +55,10 @@ export async function createMcpHttpGateway(
52
55
  // POST responses and GET streams may both be long-lived SSE responses.
53
56
  server.timeout(request, 0);
54
57
  return transport.handleRequest(authorization.value.request, {
58
+ authenticated: authorization.value.authenticated,
55
59
  identity: authorization.value.identity,
56
60
  parsedBody: authorization.value.parsedBody,
61
+ peerClassification: authorization.value.peerClassification,
57
62
  });
58
63
  };
59
64
 
@@ -64,6 +69,7 @@ export async function createMcpHttpGateway(
64
69
  close: async () => {
65
70
  await transport.close();
66
71
  (runtime as Partial<ResidentRuntime>).setTransportStatusProvider?.(null);
72
+ (runtime as Partial<ResidentRuntime>).setPolicySessionInvalidator?.(null);
67
73
  },
68
74
  };
69
75
  }