@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.
- package/README.md +46 -20
- package/assets/skill/SKILL.md +59 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +6 -6
- package/package.json +2 -2
- package/spec/cli.md +82 -5
- package/spec/db/schema.sql +31 -0
- package/spec/mcp.md +71 -0
- package/spec/output-schemas/ask.schema.json +156 -1
- package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
- package/spec/output-schemas/collection-egress-check.schema.json +91 -0
- package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
- package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +183 -3
- package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
- package/spec/output-schemas/egress-audit-management.schema.json +88 -0
- package/spec/output-schemas/get.schema.json +94 -0
- package/spec/output-schemas/mcp-http-error.schema.json +113 -2
- package/spec/output-schemas/mcp-job-status.schema.json +28 -0
- package/spec/output-schemas/multi-get.schema.json +128 -0
- package/spec/output-schemas/publish-artifact.schema.json +32 -0
- package/spec/output-schemas/record-import.schema.json +223 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
- package/spec/output-schemas/search-result.schema.json +94 -0
- package/spec/output-schemas/search-results.schema.json +155 -0
- package/spec/project-profile.schema.json +66 -0
- package/src/app/context-format.ts +1 -1
- package/src/app/context-runtime-contract.ts +16 -7
- package/src/cli/commands/ask.ts +7 -0
- package/src/cli/commands/collection/index.ts +5 -0
- package/src/cli/commands/collection/policy.ts +126 -0
- package/src/cli/commands/context-build.ts +5 -0
- package/src/cli/commands/daemon.ts +45 -3
- package/src/cli/commands/egress-audit.ts +68 -0
- package/src/cli/commands/embed.ts +2 -0
- package/src/cli/commands/get.ts +12 -2
- package/src/cli/commands/index-cmd.ts +13 -0
- package/src/cli/commands/multi-get.ts +9 -2
- package/src/cli/commands/query.ts +12 -0
- package/src/cli/commands/replay.ts +13 -3
- package/src/cli/commands/shared.ts +28 -0
- package/src/cli/commands/update.ts +5 -0
- package/src/cli/commands/vsearch.ts +3 -1
- package/src/cli/program.ts +127 -0
- package/src/config/index.ts +9 -0
- package/src/config/project-profile.ts +2 -0
- package/src/config/types.ts +66 -0
- package/src/converters/adapters/browser-export/adapter.ts +199 -0
- package/src/converters/adapters/browser-export/formats.ts +358 -0
- package/src/converters/adapters/email/adapter.ts +429 -0
- package/src/converters/adapters/email/html.ts +162 -0
- package/src/converters/adapters/email/mime.ts +454 -0
- package/src/converters/adapters/email/parameters.ts +77 -0
- package/src/converters/adapters/ical/adapter.ts +475 -0
- package/src/converters/adapters/ical/recurrence.ts +186 -0
- package/src/converters/adapters/jsonl/adapter.ts +238 -0
- package/src/converters/adapters/jsonl/config.ts +105 -0
- package/src/converters/adapters/shared/html-text.ts +165 -0
- package/src/converters/adapters/shared/record-utils.ts +79 -0
- package/src/converters/adapters/shared/utf8-lines.ts +141 -0
- package/src/converters/adapters/transcript/adapter.ts +295 -0
- package/src/converters/adapters/transcript/json.ts +184 -0
- package/src/converters/adapters/transcript/model.ts +171 -0
- package/src/converters/adapters/transcript/text.ts +58 -0
- package/src/converters/adapters/transcript/timed.ts +152 -0
- package/src/converters/index.ts +11 -1
- package/src/converters/mime.ts +8 -0
- package/src/converters/pipeline.ts +15 -1
- package/src/converters/registry.ts +37 -1
- package/src/converters/types.ts +136 -0
- package/src/core/browser-clip.ts +16 -1
- package/src/core/collection-egress-policy-projection.ts +28 -0
- package/src/core/collection-egress-policy-service.ts +443 -0
- package/src/core/collection-egress-policy-validation.ts +242 -0
- package/src/core/config-mutation.ts +31 -19
- package/src/core/context-capsule-schema.ts +198 -1
- package/src/core/context-capsule-validation.ts +4 -4
- package/src/core/context-capsule-verification.ts +5 -1
- package/src/core/context-capsule.ts +177 -112
- package/src/core/context-evidence.ts +93 -15
- package/src/core/destination-classifier.ts +402 -0
- package/src/core/document-capabilities.ts +12 -0
- package/src/core/egress-audit.ts +239 -0
- package/src/core/egress-authorization.ts +50 -0
- package/src/core/egress-enforcement.ts +264 -0
- package/src/core/egress-policy.ts +440 -0
- package/src/core/egress-provenance.ts +336 -0
- package/src/core/job-manager.ts +37 -0
- package/src/core/network-boundary-inventory.ts +261 -0
- package/src/core/project-profile-apply-state.ts +5 -0
- package/src/core/project-profile.ts +4 -0
- package/src/core/record-metadata.ts +49 -0
- package/src/core/retrieval-qrels.ts +6 -0
- package/src/core/retrieval-replay.ts +18 -4
- package/src/core/retrieval-trace-export.ts +23 -4
- package/src/core/retrieval-trace-management-helpers.ts +1 -0
- package/src/core/retrieval-trace-management-types.ts +3 -0
- package/src/core/retrieval-trace-management.ts +9 -1
- package/src/core/retrieval-trace-session.ts +94 -0
- package/src/core/retrieval-trace.ts +22 -0
- package/src/ingestion/record-adapter-canonical.ts +433 -0
- package/src/ingestion/record-adapter.ts +448 -0
- package/src/ingestion/record-container.ts +704 -0
- package/src/ingestion/record-path.ts +20 -0
- package/src/ingestion/record-sync.ts +70 -0
- package/src/ingestion/sync.ts +243 -36
- package/src/ingestion/types.ts +71 -1
- package/src/ingestion/walker.ts +31 -11
- package/src/llm/errors.ts +10 -0
- package/src/llm/http-inference.ts +175 -0
- package/src/llm/http-policy.ts +537 -0
- package/src/llm/httpEmbedding.ts +47 -28
- package/src/llm/httpGeneration.ts +31 -18
- package/src/llm/httpRerank.ts +30 -18
- package/src/llm/index.ts +1 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
- package/src/llm/pinned-http-connection.ts +177 -0
- package/src/mcp/context.ts +53 -1
- package/src/mcp/http-egress.ts +220 -0
- package/src/mcp/http-security.ts +32 -21
- package/src/mcp/http-session.ts +13 -2
- package/src/mcp/http-transport.ts +96 -6
- package/src/mcp/sync-egress.ts +24 -0
- package/src/mcp/tools/add-collection.ts +4 -0
- package/src/mcp/tools/ask.ts +1 -1
- package/src/mcp/tools/context.ts +9 -2
- package/src/mcp/tools/egress.ts +247 -0
- package/src/mcp/tools/embed.ts +1 -0
- package/src/mcp/tools/get.ts +10 -2
- package/src/mcp/tools/index-cmd.ts +7 -0
- package/src/mcp/tools/index.ts +98 -0
- package/src/mcp/tools/multi-get.ts +9 -2
- package/src/mcp/tools/query.ts +10 -0
- package/src/mcp/tools/remove-collection.ts +4 -0
- package/src/mcp/tools/sync.ts +6 -0
- package/src/mcp/tools/trace.ts +7 -1
- package/src/mcp/tools/vsearch.ts +1 -0
- package/src/mcp/tools/workspace-write.ts +2 -0
- package/src/pipeline/egress-lineage.ts +124 -0
- package/src/pipeline/filters.ts +1 -1
- package/src/pipeline/graph-retrieval.ts +7 -4
- package/src/pipeline/hybrid.ts +21 -4
- package/src/pipeline/result-context.ts +12 -4
- package/src/pipeline/search.ts +23 -4
- package/src/pipeline/types.ts +6 -0
- package/src/pipeline/vsearch.ts +67 -28
- package/src/publish/artifact.ts +31 -3
- package/src/publish/export-service.ts +25 -0
- package/src/sdk/client.ts +119 -2
- package/src/sdk/documents.ts +13 -5
- package/src/sdk/embed.ts +1 -0
- package/src/sdk/types.ts +33 -0
- package/src/serve/browse-tree.ts +4 -2
- package/src/serve/clipper-capture.ts +5 -1
- package/src/serve/closed-json.ts +61 -0
- package/src/serve/config-sync.ts +4 -1
- package/src/serve/context.ts +1 -0
- package/src/serve/doc-events.ts +110 -39
- package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
- package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
- package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
- package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Collections.tsx +3 -0
- package/src/serve/resident-request.ts +80 -3
- package/src/serve/resident-runtime.ts +63 -1
- package/src/serve/routes/api.ts +237 -65
- package/src/serve/routes/clipper.ts +64 -1
- package/src/serve/routes/mcp.ts +6 -0
- package/src/serve/routes/traces.ts +27 -4
- package/src/serve/server.ts +118 -7
- package/src/store/migrations/022-record-export-lineage.ts +42 -0
- package/src/store/migrations/023-collection-egress-policy.ts +61 -0
- package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
- package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
- package/src/store/migrations/index.ts +8 -0
- package/src/store/retrieval-trace-codec.ts +15 -0
- package/src/store/sqlite/adapter.ts +284 -14
- package/src/store/sqlite/change-journal-store.ts +41 -2
- package/src/store/sqlite/egress-audit-store.ts +485 -0
- package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
- package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
- package/src/store/sqlite/retrieval-trace-store.ts +104 -5
- package/src/store/types.ts +166 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-vn5f663b.js +0 -50
package/src/serve/routes/api.ts
CHANGED
|
@@ -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: {
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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",
|
package/src/serve/routes/mcp.ts
CHANGED
|
@@ -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
|
}
|