@bothat-io/molenkopf 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/SECURITY.md +36 -0
- package/bin/launcher.js +76 -0
- package/bin/molenkopf.js +4 -0
- package/docs/DEPLOYMENT.md +104 -0
- package/docs/MOLENKOPF_PLUGIN_API.md +113 -0
- package/docs/MOLENKOPF_PROVIDER_ENV.md +123 -0
- package/docs/MOLENKOPF_USAGE.md +195 -0
- package/docs/PRODUCT_INTENT.md +36 -0
- package/docs/THREAT_MODEL.md +94 -0
- package/molenkopf.config.example.json +68 -0
- package/package.json +98 -0
- package/packages/core/src/auth/password.ts +47 -0
- package/packages/core/src/auth/session.ts +64 -0
- package/packages/core/src/ci/ci-mode.ts +71 -0
- package/packages/core/src/compression/content-classifier.ts +25 -0
- package/packages/core/src/compression/context-compressor.ts +48 -0
- package/packages/core/src/compression/json-compressor.ts +54 -0
- package/packages/core/src/compression/log-compressor.ts +32 -0
- package/packages/core/src/compression/operational-block-compressor.ts +43 -0
- package/packages/core/src/compression/stacktrace-compressor.ts +23 -0
- package/packages/core/src/config/config-policies.ts +146 -0
- package/packages/core/src/config/molenkopf-config.ts +137 -0
- package/packages/core/src/config/provider-config.ts +139 -0
- package/packages/core/src/events/event-bus.ts +88 -0
- package/packages/core/src/identity/api-keys.ts +149 -0
- package/packages/core/src/identity/budget.ts +51 -0
- package/packages/core/src/identity/db.ts +68 -0
- package/packages/core/src/identity/identity-store.ts +175 -0
- package/packages/core/src/identity/identity-validation.ts +102 -0
- package/packages/core/src/identity/key-permissions.ts +18 -0
- package/packages/core/src/identity/pricing.ts +11 -0
- package/packages/core/src/identity/types.ts +87 -0
- package/packages/core/src/identity/usage-snapshot.ts +116 -0
- package/packages/core/src/manifest/audit-activity.ts +74 -0
- package/packages/core/src/manifest/audit-metrics.ts +7 -0
- package/packages/core/src/manifest/audit-safety.ts +113 -0
- package/packages/core/src/manifest/audit-store.ts +189 -0
- package/packages/core/src/manifest/audit-summary.ts +184 -0
- package/packages/core/src/manifest/usage-meter.ts +105 -0
- package/packages/core/src/memory/memory-extractor.ts +57 -0
- package/packages/core/src/memory/memory-graph.ts +55 -0
- package/packages/core/src/pipeline/json-string-spans.ts +143 -0
- package/packages/core/src/pipeline/openai-request-rewriter.ts +66 -0
- package/packages/core/src/plugins/builtin-plugin-descriptors.ts +10 -0
- package/packages/core/src/plugins/builtin-plugin-modules.ts +9 -0
- package/packages/core/src/plugins/plugin-api.ts +96 -0
- package/packages/core/src/plugins/plugin-catalog.ts +42 -0
- package/packages/core/src/plugins/plugin-descriptor.ts +51 -0
- package/packages/core/src/plugins/plugin-sdk.ts +47 -0
- package/packages/core/src/plugins/static-pipeline.ts +5 -0
- package/packages/core/src/profiles/profile-router.ts +45 -0
- package/packages/core/src/providers/provider-catalog.ts +186 -0
- package/packages/core/src/routing/distribution.ts +31 -0
- package/packages/core/src/security/secret-redactor.ts +139 -0
- package/packages/core/src/security/target-policy.ts +61 -0
- package/packages/core/src/storage/local-paths.ts +6 -0
- package/packages/core/src/storage/private-state.ts +30 -0
- package/packages/core/src/storage/purge-dir.ts +10 -0
- package/packages/core/src/store/retrieval-store.ts +114 -0
- package/packages/core/src/utils/hash.ts +9 -0
- package/packages/core/src/utils/text.ts +18 -0
- package/packages/core/src/utils/tokens.ts +3 -0
- package/packages/dashboard/dist/assets/index-B_aSPgHx.js +11 -0
- package/packages/dashboard/dist/assets/index-D6z2TEL2.css +1 -0
- package/packages/dashboard/dist/favicon.png +0 -0
- package/packages/dashboard/dist/index.html +15 -0
- package/packages/dashboard/dist/molenkopf-logo.png +0 -0
- package/packages/dashboard/public/favicon.png +0 -0
- package/packages/dashboard/public/molenkopf-logo.png +0 -0
- package/packages/plugins/context-compressor-plugin/descriptor.ts +19 -0
- package/packages/plugins/context-compressor-plugin/page.html +191 -0
- package/packages/plugins/context-compressor-plugin/plugin.ts +40 -0
- package/packages/plugins/obsidian-graph-plugin/descriptor.ts +19 -0
- package/packages/plugins/obsidian-graph-plugin/page.html +68 -0
- package/packages/plugins/obsidian-graph-plugin/plugin.ts +27 -0
- package/packages/plugins/shared/audit-projects.ts +32 -0
- package/packages/proxy/src/cli/args.ts +34 -0
- package/packages/proxy/src/cli/config-loader.ts +43 -0
- package/packages/proxy/src/cli/env-file.ts +43 -0
- package/packages/proxy/src/cli/main.ts +132 -0
- package/packages/proxy/src/cli/profile-server.ts +176 -0
- package/packages/proxy/src/cli/target.ts +7 -0
- package/packages/proxy/src/http/agent-drafts.ts +103 -0
- package/packages/proxy/src/http/agent-router.ts +69 -0
- package/packages/proxy/src/http/audit-view.ts +15 -0
- package/packages/proxy/src/http/auth-state.ts +44 -0
- package/packages/proxy/src/http/budget-gate.ts +45 -0
- package/packages/proxy/src/http/budget-warnings.ts +7 -0
- package/packages/proxy/src/http/cli-stream-response.ts +51 -0
- package/packages/proxy/src/http/client-identity.ts +51 -0
- package/packages/proxy/src/http/communication-graph.ts +139 -0
- package/packages/proxy/src/http/control-plane-guard.ts +56 -0
- package/packages/proxy/src/http/dashboard-assets.ts +115 -0
- package/packages/proxy/src/http/encoded-usage-meter.ts +32 -0
- package/packages/proxy/src/http/header-utils.ts +65 -0
- package/packages/proxy/src/http/identity-id.ts +11 -0
- package/packages/proxy/src/http/local-api-agent-actions.ts +17 -0
- package/packages/proxy/src/http/local-api-auth.ts +120 -0
- package/packages/proxy/src/http/local-api-consumer-actions.ts +20 -0
- package/packages/proxy/src/http/local-api-identity.ts +194 -0
- package/packages/proxy/src/http/local-api-io.ts +82 -0
- package/packages/proxy/src/http/local-api-keys.ts +126 -0
- package/packages/proxy/src/http/local-api-pipeline.ts +41 -0
- package/packages/proxy/src/http/local-api-plugin-actions.ts +31 -0
- package/packages/proxy/src/http/local-api-provider-actions.ts +181 -0
- package/packages/proxy/src/http/local-api-retention.ts +28 -0
- package/packages/proxy/src/http/local-api-runtime-auth.ts +119 -0
- package/packages/proxy/src/http/local-api-scope.ts +47 -0
- package/packages/proxy/src/http/local-api-state.ts +180 -0
- package/packages/proxy/src/http/local-api.ts +166 -0
- package/packages/proxy/src/http/password-policy.ts +5 -0
- package/packages/proxy/src/http/plugin-data.ts +38 -0
- package/packages/proxy/src/http/plugin-host.ts +87 -0
- package/packages/proxy/src/http/plugin-modules.ts +1 -0
- package/packages/proxy/src/http/plugin-page-loader.ts +24 -0
- package/packages/proxy/src/http/plugin-pipeline.ts +125 -0
- package/packages/proxy/src/http/provider-access.ts +33 -0
- package/packages/proxy/src/http/provider-http-test.ts +133 -0
- package/packages/proxy/src/http/provider-input.ts +39 -0
- package/packages/proxy/src/http/provider-routing-snapshot.ts +28 -0
- package/packages/proxy/src/http/provider-test.ts +149 -0
- package/packages/proxy/src/http/proxy-identity.ts +78 -0
- package/packages/proxy/src/http/public-bind.ts +8 -0
- package/packages/proxy/src/http/request-finish.ts +62 -0
- package/packages/proxy/src/http/request-path.ts +8 -0
- package/packages/proxy/src/http/request-policy.ts +46 -0
- package/packages/proxy/src/http/runtime-auth-proof.ts +55 -0
- package/packages/proxy/src/http/runtime-auth-registry.ts +105 -0
- package/packages/proxy/src/http/runtime-settings.ts +199 -0
- package/packages/proxy/src/http/runtime-state.ts +198 -0
- package/packages/proxy/src/http/server-io.ts +80 -0
- package/packages/proxy/src/http/server-types.ts +17 -0
- package/packages/proxy/src/http/server.ts +190 -0
- package/packages/proxy/src/http/session-secret.ts +19 -0
- package/packages/proxy/src/http/streaming-proxy.ts +88 -0
- package/packages/proxy/src/http/usage-accounting.ts +100 -0
- package/packages/proxy/src/http/usage-restore.ts +15 -0
- package/packages/proxy/src/runtime/cli-diagnostics.ts +64 -0
- package/packages/proxy/src/runtime/cli-env.ts +22 -0
- package/packages/proxy/src/runtime/cli-executor.ts +134 -0
- package/packages/proxy/src/runtime/cli-provider.ts +162 -0
- package/packages/proxy/src/runtime/cli-request.ts +79 -0
- package/packages/proxy/src/runtime/codex-runtime-config.ts +37 -0
- package/packages/proxy/src/runtime/runtime-profile.ts +170 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export type JsonStringSpan = { start: number; end: number; value: string; key?: string };
|
|
2
|
+
export type JsonStringReplacement = { start: number; end: number; value: string };
|
|
3
|
+
|
|
4
|
+
type Frame = { type: "object"; state: "key" | "colon" | "value" | "comma"; key?: string } | { type: "array"; state: "value" | "comma" };
|
|
5
|
+
const MAX_SCAN_DEPTH = 1000;
|
|
6
|
+
|
|
7
|
+
export function replaceJsonStrings(text: string, replacements: JsonStringReplacement[]): string {
|
|
8
|
+
if (!replacements.length) return text;
|
|
9
|
+
const ordered = [...replacements].sort((a, b) => a.start - b.start);
|
|
10
|
+
let out = "";
|
|
11
|
+
let offset = 0;
|
|
12
|
+
for (const item of ordered) {
|
|
13
|
+
out += text.slice(offset, item.start) + JSON.stringify(item.value);
|
|
14
|
+
offset = item.end;
|
|
15
|
+
}
|
|
16
|
+
return out + text.slice(offset);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function scanJsonStringValues(text: string): JsonStringSpan[] | undefined {
|
|
20
|
+
if (exceedsScanDepth(text)) return [];
|
|
21
|
+
try {
|
|
22
|
+
JSON.parse(text);
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const spans: JsonStringSpan[] = [];
|
|
27
|
+
const stack: Frame[] = [];
|
|
28
|
+
let i = skipWs(text, 0);
|
|
29
|
+
consumeValue(undefined);
|
|
30
|
+
return spans;
|
|
31
|
+
|
|
32
|
+
function consumeValue(key: string | undefined) {
|
|
33
|
+
i = skipWs(text, i);
|
|
34
|
+
const char = text[i];
|
|
35
|
+
if (char === '"') {
|
|
36
|
+
const token = readString(text, i);
|
|
37
|
+
spans.push({ start: i, end: token.end, value: token.value, key });
|
|
38
|
+
i = token.end;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (char === "{") return consumeObject();
|
|
42
|
+
if (char === "[") return consumeArray();
|
|
43
|
+
while (i < text.length && !/[\s,\]}]/.test(text[i])) i++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function consumeObject() {
|
|
47
|
+
stack.push({ type: "object", state: "key" });
|
|
48
|
+
i++;
|
|
49
|
+
while (i < text.length) {
|
|
50
|
+
i = skipWs(text, i);
|
|
51
|
+
const frame = stack[stack.length - 1];
|
|
52
|
+
if (!frame || frame.type !== "object") return;
|
|
53
|
+
if (text[i] === "}") {
|
|
54
|
+
stack.pop();
|
|
55
|
+
i++;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (frame.state === "comma") {
|
|
59
|
+
if (text[i] === ",") {
|
|
60
|
+
frame.state = "key";
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (frame.state === "key") {
|
|
67
|
+
const token = readString(text, i);
|
|
68
|
+
frame.key = token.value;
|
|
69
|
+
frame.state = "colon";
|
|
70
|
+
i = token.end;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (frame.state === "colon") {
|
|
74
|
+
if (text[i] !== ":") return;
|
|
75
|
+
frame.state = "value";
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
consumeValue(frame.key);
|
|
80
|
+
frame.state = "comma";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function consumeArray() {
|
|
85
|
+
stack.push({ type: "array", state: "value" });
|
|
86
|
+
i++;
|
|
87
|
+
while (i < text.length) {
|
|
88
|
+
i = skipWs(text, i);
|
|
89
|
+
const frame = stack[stack.length - 1];
|
|
90
|
+
if (!frame || frame.type !== "array") return;
|
|
91
|
+
if (text[i] === "]") {
|
|
92
|
+
stack.pop();
|
|
93
|
+
i++;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (frame.state === "comma") {
|
|
97
|
+
if (text[i] === ",") {
|
|
98
|
+
frame.state = "value";
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
consumeValue(undefined);
|
|
105
|
+
frame.state = "comma";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function skipWs(text: string, index: number): number {
|
|
111
|
+
while (index < text.length && /\s/.test(text[index])) index++;
|
|
112
|
+
return index;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readString(text: string, start: number): { value: string; end: number } {
|
|
116
|
+
let end = start + 1;
|
|
117
|
+
while (end < text.length) {
|
|
118
|
+
if (text[end] === '"' && !isEscaped(text, end)) break;
|
|
119
|
+
end++;
|
|
120
|
+
}
|
|
121
|
+
end++;
|
|
122
|
+
return { value: JSON.parse(text.slice(start, end)), end };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isEscaped(text: string, quoteIndex: number): boolean {
|
|
126
|
+
let slashes = 0;
|
|
127
|
+
for (let i = quoteIndex - 1; i >= 0 && text[i] === "\\"; i--) slashes++;
|
|
128
|
+
return slashes % 2 === 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function exceedsScanDepth(text: string): boolean {
|
|
132
|
+
let depth = 0;
|
|
133
|
+
let inString = false;
|
|
134
|
+
for (let i = 0; i < text.length; i++) {
|
|
135
|
+
const char = text[i];
|
|
136
|
+
if (char === '"' && !isEscaped(text, i)) inString = !inString;
|
|
137
|
+
if (inString) continue;
|
|
138
|
+
if (char === "{" || char === "[") depth++;
|
|
139
|
+
else if (char === "}" || char === "]") depth--;
|
|
140
|
+
if (depth > MAX_SCAN_DEPTH) return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { compressContext } from "../compression/context-compressor.ts";
|
|
2
|
+
import { redactSecrets } from "../security/secret-redactor.ts";
|
|
3
|
+
import { RetrievalStore } from "../store/retrieval-store.ts";
|
|
4
|
+
import { estimateTokens } from "../utils/tokens.ts";
|
|
5
|
+
import { replaceJsonStrings, scanJsonStringValues, type JsonStringReplacement } from "./json-string-spans.ts";
|
|
6
|
+
|
|
7
|
+
export type RewriteAudit = {
|
|
8
|
+
compressedItems: number;
|
|
9
|
+
estimatedOriginalTokens: number;
|
|
10
|
+
estimatedCompressedTokens: number;
|
|
11
|
+
estimatedSavedTokens: number;
|
|
12
|
+
redactedSecrets: number;
|
|
13
|
+
retrievalIds: string[];
|
|
14
|
+
compressorsUsed: string[];
|
|
15
|
+
warnings: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CompressResult = { body: string; compressedItems: number; savedTokens: number; redactedSecrets: number; retrievalIds: string[]; compressorsUsed: string[] };
|
|
19
|
+
|
|
20
|
+
export async function rewriteOpenAiJsonBody(body: string, store: RetrievalStore, requestId?: string, options: { compress?: boolean } = {}): Promise<{ body: string; audit: RewriteAudit }> {
|
|
21
|
+
const redacted = redactSecrets(body);
|
|
22
|
+
const compressed = await compressJsonBody(redacted.text, store, requestId, options.compress !== false);
|
|
23
|
+
const audit = emptyAudit(body, redacted.redactions.length + compressed.redactedSecrets);
|
|
24
|
+
audit.compressedItems = compressed.compressedItems;
|
|
25
|
+
audit.retrievalIds = compressed.retrievalIds;
|
|
26
|
+
audit.compressorsUsed = compressed.compressorsUsed;
|
|
27
|
+
audit.estimatedCompressedTokens = estimateTokens(compressed.body);
|
|
28
|
+
audit.estimatedSavedTokens = compressed.savedTokens;
|
|
29
|
+
return { body: compressed.body, audit };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Compression only (no redaction), so the middleware pipeline can run redaction
|
|
33
|
+
// and compression as separate, individually-toggleable steps.
|
|
34
|
+
export async function compressJsonBody(text: string, store: RetrievalStore, requestId?: string, compress = true): Promise<CompressResult> {
|
|
35
|
+
const acc: CompressResult = { body: text, compressedItems: 0, savedTokens: 0, redactedSecrets: 0, retrievalIds: [], compressorsUsed: [] };
|
|
36
|
+
if (!compress) return acc;
|
|
37
|
+
const spans = scanJsonStringValues(text);
|
|
38
|
+
if (!spans) return acc;
|
|
39
|
+
const replacements: JsonStringReplacement[] = [];
|
|
40
|
+
for (const span of spans) {
|
|
41
|
+
if (span.value.length < 2000) continue;
|
|
42
|
+
const result = await compressContext(span.value, store, requestId);
|
|
43
|
+
acc.redactedSecrets += result.redactedSecrets;
|
|
44
|
+
if (!result.compressed) continue;
|
|
45
|
+
acc.compressedItems++;
|
|
46
|
+
acc.savedTokens += Math.max(0, estimateTokens(span.value) - estimateTokens(result.text));
|
|
47
|
+
if (result.retrievalId) acc.retrievalIds.push(result.retrievalId);
|
|
48
|
+
if (result.compressorName) acc.compressorsUsed.push(result.compressorName);
|
|
49
|
+
replacements.push({ start: span.start, end: span.end, value: result.text });
|
|
50
|
+
}
|
|
51
|
+
if (replacements.length) acc.body = replaceJsonStrings(text, replacements);
|
|
52
|
+
return acc;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function emptyAudit(body: string, redactedSecrets: number): RewriteAudit {
|
|
56
|
+
return {
|
|
57
|
+
compressedItems: 0,
|
|
58
|
+
estimatedOriginalTokens: estimateTokens(body),
|
|
59
|
+
estimatedCompressedTokens: estimateTokens(body),
|
|
60
|
+
estimatedSavedTokens: 0,
|
|
61
|
+
redactedSecrets,
|
|
62
|
+
retrievalIds: [],
|
|
63
|
+
compressorsUsed: [],
|
|
64
|
+
warnings: []
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { descriptor as contextCompressorDescriptor } from "../../../plugins/context-compressor-plugin/descriptor.ts";
|
|
2
|
+
import { descriptor as obsidianGraphDescriptor } from "../../../plugins/obsidian-graph-plugin/descriptor.ts";
|
|
3
|
+
import type { PluginDescriptor } from "./plugin-descriptor.ts";
|
|
4
|
+
|
|
5
|
+
export { contextCompressorDescriptor, obsidianGraphDescriptor };
|
|
6
|
+
|
|
7
|
+
export const builtinPluginDescriptors: PluginDescriptor[] = [
|
|
8
|
+
contextCompressorDescriptor,
|
|
9
|
+
obsidianGraphDescriptor
|
|
10
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { plugin as contextCompressorPlugin } from "../../../plugins/context-compressor-plugin/plugin.ts";
|
|
2
|
+
import { plugin as obsidianGraphPlugin } from "../../../plugins/obsidian-graph-plugin/plugin.ts";
|
|
3
|
+
import type { MolenkopfPluginModule } from "./plugin-api.ts";
|
|
4
|
+
import { contextCompressorDescriptor, obsidianGraphDescriptor } from "./builtin-plugin-descriptors.ts";
|
|
5
|
+
|
|
6
|
+
export const builtinPluginModules: Record<string, MolenkopfPluginModule> = {
|
|
7
|
+
[contextCompressorDescriptor.id]: contextCompressorPlugin,
|
|
8
|
+
[obsidianGraphDescriptor.id]: obsidianGraphPlugin
|
|
9
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AuditManifest } from "../manifest/audit-store.ts";
|
|
2
|
+
import type { MemoryGraph } from "../memory/memory-graph.ts";
|
|
3
|
+
|
|
4
|
+
export type PluginJson = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export type PluginBlock = {
|
|
7
|
+
status: number;
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type PluginUsage = {
|
|
12
|
+
requests: number;
|
|
13
|
+
inputTokens: number;
|
|
14
|
+
outputTokens: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type PluginMetrics = {
|
|
18
|
+
redactedSecrets: number;
|
|
19
|
+
compressedItems: number;
|
|
20
|
+
savedTokens: number;
|
|
21
|
+
retrievalIds: string[];
|
|
22
|
+
compressorsUsed: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PluginLifecycleContext = {
|
|
26
|
+
pluginId: string;
|
|
27
|
+
dataDir?: string;
|
|
28
|
+
now: () => Date;
|
|
29
|
+
note: (message: string) => void;
|
|
30
|
+
port?: number;
|
|
31
|
+
reason?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type PluginRequestContext = {
|
|
35
|
+
requestId: string;
|
|
36
|
+
method: string;
|
|
37
|
+
path: string;
|
|
38
|
+
consumerId: string;
|
|
39
|
+
providerId: string;
|
|
40
|
+
body: string;
|
|
41
|
+
usageOf: (consumerId: string) => PluginUsage;
|
|
42
|
+
note: (message: string) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type PluginRequestResult = Partial<PluginMetrics> & {
|
|
46
|
+
body?: string;
|
|
47
|
+
providerId?: string;
|
|
48
|
+
block?: PluginBlock;
|
|
49
|
+
notes?: string[];
|
|
50
|
+
audit?: PluginJson[];
|
|
51
|
+
events?: PluginJson[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type PluginAuditContext = {
|
|
55
|
+
requestId: string;
|
|
56
|
+
providerId: string;
|
|
57
|
+
statusCode: number;
|
|
58
|
+
manifest: PluginJson;
|
|
59
|
+
note: (message: string) => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type PluginEventContext = {
|
|
63
|
+
event: string;
|
|
64
|
+
data: PluginJson;
|
|
65
|
+
emit: (event: string, data: PluginJson) => void;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type PluginDataContext = {
|
|
69
|
+
canManage: boolean;
|
|
70
|
+
userId?: string;
|
|
71
|
+
teamIds: string[];
|
|
72
|
+
scope: string;
|
|
73
|
+
plugin: PluginJson;
|
|
74
|
+
scopes: string[];
|
|
75
|
+
manifests: AuditManifest[];
|
|
76
|
+
memoryGraph?: MemoryGraph;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type PluginRuntimeContext = {
|
|
80
|
+
pluginId: string;
|
|
81
|
+
dataDir?: string;
|
|
82
|
+
storage?: unknown;
|
|
83
|
+
now: () => Date;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type MolenkopfPluginModule = {
|
|
87
|
+
onBoot?: (ctx: PluginLifecycleContext, runtime: PluginRuntimeContext) => void | Promise<void>;
|
|
88
|
+
onStart?: (ctx: PluginLifecycleContext, runtime: PluginRuntimeContext) => void | Promise<void>;
|
|
89
|
+
onEnable?: (ctx: PluginLifecycleContext, runtime: PluginRuntimeContext) => void | Promise<void>;
|
|
90
|
+
onDisable?: (ctx: PluginLifecycleContext, runtime: PluginRuntimeContext) => void | Promise<void>;
|
|
91
|
+
onRequest?: (ctx: PluginRequestContext, runtime: PluginRuntimeContext) => PluginRequestResult | void | Promise<PluginRequestResult | void>;
|
|
92
|
+
onAudit?: (ctx: PluginAuditContext, runtime: PluginRuntimeContext) => void | Promise<void>;
|
|
93
|
+
onEvent?: (ctx: PluginEventContext, runtime: PluginRuntimeContext) => void | Promise<void>;
|
|
94
|
+
getData?: (ctx: PluginDataContext, runtime: PluginRuntimeContext) => PluginJson | Promise<PluginJson>;
|
|
95
|
+
onStop?: (ctx: PluginLifecycleContext, runtime: PluginRuntimeContext) => void | Promise<void>;
|
|
96
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { builtinPluginDescriptors, type PluginCategory, type PluginDataScope, type PluginRuntimeHook, type PluginTrafficAccess, type PluginType } from "./plugin-descriptor.ts";
|
|
2
|
+
import type { PluginPermission } from "./plugin-sdk.ts";
|
|
3
|
+
|
|
4
|
+
export type MolenkopfPlugin = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type: PluginType;
|
|
8
|
+
category: PluginCategory;
|
|
9
|
+
description: string;
|
|
10
|
+
traffic: PluginTrafficAccess;
|
|
11
|
+
enabledByDefault: boolean;
|
|
12
|
+
canToggle: boolean;
|
|
13
|
+
permissions: PluginPermission[];
|
|
14
|
+
hooks: PluginRuntimeHook[];
|
|
15
|
+
modulePath?: string;
|
|
16
|
+
pagePath?: string;
|
|
17
|
+
dataPath?: string;
|
|
18
|
+
dataScopes?: PluginDataScope[];
|
|
19
|
+
pipelineIndex?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const pluginCatalog: MolenkopfPlugin[] = builtinPluginDescriptors.map((plugin) => ({
|
|
23
|
+
id: plugin.id,
|
|
24
|
+
name: plugin.name,
|
|
25
|
+
type: plugin.type,
|
|
26
|
+
category: plugin.category,
|
|
27
|
+
description: plugin.description,
|
|
28
|
+
traffic: { reads: [...plugin.traffic.reads], mutates: [...plugin.traffic.mutates] },
|
|
29
|
+
permissions: [...plugin.permissions],
|
|
30
|
+
hooks: [...plugin.hooks],
|
|
31
|
+
modulePath: plugin.modulePath,
|
|
32
|
+
enabledByDefault: plugin.toggle.defaultEnabled,
|
|
33
|
+
canToggle: plugin.toggle.canDisable,
|
|
34
|
+
pagePath: plugin.workspace?.pagePath,
|
|
35
|
+
dataPath: plugin.workspace?.dataPath,
|
|
36
|
+
dataScopes: plugin.workspace ? [...plugin.workspace.dataScopes] : undefined,
|
|
37
|
+
pipelineIndex: plugin.pipelineIndex
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
export function findPlugin(id: string): MolenkopfPlugin | undefined {
|
|
41
|
+
return pluginCatalog.find((plugin) => plugin.id === id);
|
|
42
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PluginPermission } from "./plugin-sdk.ts";
|
|
2
|
+
import { builtinPluginDescriptors as descriptors } from "./builtin-plugin-descriptors.ts";
|
|
3
|
+
import { staticPluginPipeline } from "./static-pipeline.ts";
|
|
4
|
+
|
|
5
|
+
export type PluginCategory = "safety" | "compression" | "storage" | "events" | "routing" | "visualization";
|
|
6
|
+
export type PluginDataScope = "metrics" | "audit-summary" | "requests" | "memory-graph";
|
|
7
|
+
export type PluginType = "observer" | "classifier" | "redactor" | "transformer" | "retriever" | "router" | "auditor" | "stream-filter";
|
|
8
|
+
export type PluginTrafficMutation = "none" | "mask" | "transform" | "augment-context" | "route" | "block" | "audit-log" | "event-filter";
|
|
9
|
+
export type PluginRuntimeHook =
|
|
10
|
+
| "request:metadata"
|
|
11
|
+
| "request:body:rewrite"
|
|
12
|
+
| "audit:manifest"
|
|
13
|
+
| "events:lifecycle"
|
|
14
|
+
| "provider:route"
|
|
15
|
+
| "workspace:local-page";
|
|
16
|
+
|
|
17
|
+
export type PluginTogglePolicy = {
|
|
18
|
+
defaultEnabled: boolean;
|
|
19
|
+
canDisable: true;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type PluginWorkspace = {
|
|
23
|
+
pagePath: string;
|
|
24
|
+
dataPath: string;
|
|
25
|
+
dataScopes: PluginDataScope[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type PluginTrafficAccess = {
|
|
29
|
+
reads: ("metadata" | "redacted-body" | "body" | "audit" | "events")[];
|
|
30
|
+
mutates: PluginTrafficMutation[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type PluginDescriptor = {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
type: PluginType;
|
|
37
|
+
category: PluginCategory;
|
|
38
|
+
description: string;
|
|
39
|
+
traffic: PluginTrafficAccess;
|
|
40
|
+
permissions: PluginPermission[];
|
|
41
|
+
hooks: PluginRuntimeHook[];
|
|
42
|
+
toggle: PluginTogglePolicy;
|
|
43
|
+
modulePath?: string;
|
|
44
|
+
workspace?: PluginWorkspace;
|
|
45
|
+
pipelineIndex?: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const builtinPluginDescriptors: PluginDescriptor[] = descriptors.map((plugin) => {
|
|
49
|
+
const pipelineIndex = (staticPluginPipeline as readonly string[]).indexOf(plugin.id);
|
|
50
|
+
return pipelineIndex >= 0 ? { ...plugin, pipelineIndex } : plugin;
|
|
51
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { MolenkopfPluginModule } from "./plugin-api.ts";
|
|
2
|
+
|
|
3
|
+
export type PluginPermission =
|
|
4
|
+
| "metadata:read"
|
|
5
|
+
| "body:read"
|
|
6
|
+
| "body:write"
|
|
7
|
+
| "audit:read"
|
|
8
|
+
| "audit:write"
|
|
9
|
+
| "events:write"
|
|
10
|
+
| "provider:write";
|
|
11
|
+
|
|
12
|
+
export type LocalPlugin = {
|
|
13
|
+
name: string;
|
|
14
|
+
permissions: PluginPermission[];
|
|
15
|
+
module: MolenkopfPluginModule;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createLocalPluginRegistry() {
|
|
19
|
+
const plugins = new Map<string, LocalPlugin>();
|
|
20
|
+
return {
|
|
21
|
+
register(plugin: LocalPlugin) {
|
|
22
|
+
if (!/^[a-z0-9-]+$/.test(plugin.name)) throw new Error("invalid plugin name");
|
|
23
|
+
if (plugin.permissions.length === 0) throw new Error("plugin permissions required");
|
|
24
|
+
if (!hasRuntimeHook(plugin.module)) throw new Error("plugin runtime hook required");
|
|
25
|
+
if (plugins.has(plugin.name)) throw new Error(`duplicate plugin: ${plugin.name}`);
|
|
26
|
+
plugins.set(plugin.name, deepFreeze({ ...plugin, permissions: [...plugin.permissions], module: { ...plugin.module } }));
|
|
27
|
+
},
|
|
28
|
+
registerRemote(_url: string): never {
|
|
29
|
+
throw new Error("remote plugins are disabled");
|
|
30
|
+
},
|
|
31
|
+
get(name: string) {
|
|
32
|
+
return plugins.get(name);
|
|
33
|
+
},
|
|
34
|
+
names() {
|
|
35
|
+
return [...plugins.keys()];
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deepFreeze<T extends object>(value: T): T {
|
|
41
|
+
for (const item of Object.values(value)) if (item && typeof item === "object") deepFreeze(item as object);
|
|
42
|
+
return Object.freeze(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasRuntimeHook(module: MolenkopfPluginModule): boolean {
|
|
46
|
+
return Boolean(module.onBoot || module.onStart || module.onEnable || module.onDisable || module.onRequest || module.onAudit || module.onEvent || module.getData || module.onStop);
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type UpstreamProfile = {
|
|
2
|
+
name: string;
|
|
3
|
+
target: string;
|
|
4
|
+
healthy?: boolean;
|
|
5
|
+
envKey?: string;
|
|
6
|
+
budgetTokens?: number;
|
|
7
|
+
usedTokens?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RoutingConfig = {
|
|
11
|
+
mode: "fixed" | "manual" | "failover";
|
|
12
|
+
profile?: string;
|
|
13
|
+
profiles: UpstreamProfile[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function chooseProfile(config: RoutingConfig): UpstreamProfile {
|
|
17
|
+
if (config.mode === "failover") {
|
|
18
|
+
const profile = config.profiles.find((item) => item.healthy !== false && hasBudget(item));
|
|
19
|
+
if (profile) return profile;
|
|
20
|
+
throw new Error("no healthy profile with remaining budget");
|
|
21
|
+
}
|
|
22
|
+
const name = config.profile;
|
|
23
|
+
if (!name) throw new Error(`explicit profile required for ${config.mode} routing`);
|
|
24
|
+
const profile = config.profiles.find((item) => item.name === name);
|
|
25
|
+
if (!profile) throw new Error(`unknown profile: ${name}`);
|
|
26
|
+
if (!hasBudget(profile)) throw new Error(`budget exhausted: ${profile.name}`);
|
|
27
|
+
return profile;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readCredential(profile: Pick<UpstreamProfile, "envKey">, env: Record<string, string | undefined> = process.env): string | undefined {
|
|
31
|
+
return profile.envKey ? env[profile.envKey] : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function healthSummary(profiles: UpstreamProfile[]) {
|
|
35
|
+
return profiles.map((item) => ({
|
|
36
|
+
name: item.name,
|
|
37
|
+
target: item.target,
|
|
38
|
+
healthy: item.healthy !== false,
|
|
39
|
+
budgetRemaining: item.budgetTokens === undefined ? undefined : Math.max(0, item.budgetTokens - (item.usedTokens ?? 0))
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasBudget(profile: UpstreamProfile): boolean {
|
|
44
|
+
return profile.budgetTokens === undefined || (profile.usedTokens ?? 0) < profile.budgetTokens;
|
|
45
|
+
}
|