@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,28 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { AuditStore } from "../../../core/src/manifest/audit-store.ts";
|
|
3
|
+
import { RetrievalStore } from "../../../core/src/store/retrieval-store.ts";
|
|
4
|
+
import type { RuntimeState } from "./runtime-state.ts";
|
|
5
|
+
import { readJson, writeJson } from "./local-api-io.ts";
|
|
6
|
+
|
|
7
|
+
type PurgeScope = "audit" | "retrieval" | "all";
|
|
8
|
+
|
|
9
|
+
export async function purgeRetention(req: IncomingMessage, res: ServerResponse, audit: AuditStore, state: RuntimeState) {
|
|
10
|
+
const body = await readJson(req);
|
|
11
|
+
const scope = parseScope(body.scope);
|
|
12
|
+
if (!scope) return writeJson(res, 400, { error: "invalid_purge_scope" });
|
|
13
|
+
const purged = { audit: false, retrieval: false };
|
|
14
|
+
if (scope === "audit" || scope === "all") {
|
|
15
|
+
await audit.purgeAll();
|
|
16
|
+
state.latest = undefined;
|
|
17
|
+
purged.audit = true;
|
|
18
|
+
}
|
|
19
|
+
if (scope === "retrieval" || scope === "all") {
|
|
20
|
+
await new RetrievalStore(state.dataDir).purgeAll();
|
|
21
|
+
purged.retrieval = true;
|
|
22
|
+
}
|
|
23
|
+
writeJson(res, 200, { ok: true, purged });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseScope(value: unknown): PurgeScope | undefined {
|
|
27
|
+
return value === "audit" || value === "retrieval" || value === "all" ? value : undefined;
|
|
28
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { rename, rm } from "node:fs/promises";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { viewRuntimeProfile, type ProviderConfig } from "../../../core/src/providers/provider-catalog.ts";
|
|
6
|
+
import { defaultDataDir } from "../../../core/src/storage/local-paths.ts";
|
|
7
|
+
import { ensurePrivateDir } from "../../../core/src/storage/private-state.ts";
|
|
8
|
+
import type { RuntimeState } from "./runtime-state.ts";
|
|
9
|
+
import { buildProviderStatus } from "./local-api-state.ts";
|
|
10
|
+
import { readJson, writeJson } from "./local-api-io.ts";
|
|
11
|
+
import { persistRuntimeAuthProvider, runtimeAuthProvider, writeRuntimeAuthFiles } from "./runtime-auth-registry.ts";
|
|
12
|
+
import { persistRuntimeSettings } from "./runtime-settings.ts";
|
|
13
|
+
import { runtimeProfileFromImport, writeRuntimeProfileFiles } from "../runtime/runtime-profile.ts";
|
|
14
|
+
import { consumeRuntimeAuthProof } from "./runtime-auth-proof.ts";
|
|
15
|
+
|
|
16
|
+
const ID_RE = /^[a-z0-9][a-z0-9._:-]{0,63}$/i;
|
|
17
|
+
const AUTH_IMPORT_BYTES = 256 * 1024;
|
|
18
|
+
|
|
19
|
+
export async function importProviderAuth(req: IncomingMessage, res: ServerResponse, state: RuntimeState) {
|
|
20
|
+
const body = await readJson(req, AUTH_IMPORT_BYTES);
|
|
21
|
+
const runtime = runtimeValue(body.runtime);
|
|
22
|
+
if (!runtime) return writeJson(res, 400, { error: "invalid_runtime" });
|
|
23
|
+
const authJson = authJsonText(body);
|
|
24
|
+
if (!authJson) return writeJson(res, 400, { error: "missing_auth_json" });
|
|
25
|
+
const parsedAuth = parseJsonObject(authJson);
|
|
26
|
+
if (!parsedAuth) return writeJson(res, 400, { error: "invalid_auth_json" });
|
|
27
|
+
const explicitId = typeof body.id === "string" ? body.id.trim() : "";
|
|
28
|
+
const id = explicitId || uniqueGeneratedId(state, `${runtime}-import-${randomSuffix()}`);
|
|
29
|
+
if (!ID_RE.test(id)) return writeJson(res, 400, { error: "invalid_provider_id" });
|
|
30
|
+
if (id.toLowerCase() === "default") return writeJson(res, 400, { error: "reserved_provider_id" });
|
|
31
|
+
if (state.providers.some((item) => item.id === id)) return writeJson(res, 409, { error: "provider_exists" });
|
|
32
|
+
let runtimeProfile;
|
|
33
|
+
try {
|
|
34
|
+
runtimeProfile = runtimeProfileFromImport(body, runtime);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return writeJson(res, 400, { error: error instanceof Error ? error.message : "invalid_runtime_profile" });
|
|
37
|
+
}
|
|
38
|
+
if (!consumeRuntimeAuthProof(state, body)) return writeJson(res, 409, { error: "invalid_runtime_auth_proof" });
|
|
39
|
+
|
|
40
|
+
const root = state.dataDir ?? defaultDataDir();
|
|
41
|
+
const authRoot = join(root, "runtime-auth");
|
|
42
|
+
const authDir = join(authRoot, id);
|
|
43
|
+
const stagingDir = join(authRoot, `.staging-${id}-${randomSuffix()}`);
|
|
44
|
+
const ref = `runtime-auth:${id}`;
|
|
45
|
+
const name = importedName(body, runtime, id);
|
|
46
|
+
const provider = runtimeAuthProvider(id, name, runtime, authDir, ref, runtimeProfile.profile);
|
|
47
|
+
const snapshot = {
|
|
48
|
+
providers: [...state.providers],
|
|
49
|
+
providerWeights: { ...state.providerWeights },
|
|
50
|
+
activeProviderId: state.activeProviderId,
|
|
51
|
+
routingMode: state.routingMode,
|
|
52
|
+
providerSelectedAt: state.providerSelectedAt
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
await ensurePrivateDir(authRoot);
|
|
56
|
+
await writeRuntimeAuthFiles(stagingDir, runtime, authJson.endsWith("\n") ? authJson : `${authJson}\n`);
|
|
57
|
+
await writeRuntimeProfileFiles(stagingDir, runtimeProfile);
|
|
58
|
+
await rename(stagingDir, authDir);
|
|
59
|
+
state.providers.push(provider);
|
|
60
|
+
state.providerWeights[id] = 1;
|
|
61
|
+
if (body.activate !== false) {
|
|
62
|
+
state.activeProviderId = id;
|
|
63
|
+
state.routingMode = "manual";
|
|
64
|
+
state.providerSelectedAt = new Date().toISOString();
|
|
65
|
+
}
|
|
66
|
+
await persistRuntimeAuthProvider(root, provider, body.activate !== false, state.routingMode);
|
|
67
|
+
await persistRuntimeSettings(state);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
state.providers = snapshot.providers;
|
|
70
|
+
state.providerWeights = snapshot.providerWeights;
|
|
71
|
+
state.activeProviderId = snapshot.activeProviderId;
|
|
72
|
+
state.routingMode = snapshot.routingMode;
|
|
73
|
+
state.providerSelectedAt = snapshot.providerSelectedAt;
|
|
74
|
+
await rm(authDir, { recursive: true, force: true });
|
|
75
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
writeJson(res, 200, { imported: { id, name, runtime, runtimeAuthConfigured: true, profile: viewRuntimeProfile(runtimeProfile.profile), active: state.activeProviderId === id }, providers: buildProviderStatus(state) });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runtimeValue(value: unknown): ProviderConfig["runtime"] | undefined {
|
|
82
|
+
return value === "claude" || value === "codex" ? value : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function authJsonText(body: Record<string, unknown>): string {
|
|
86
|
+
if (typeof body.authJson === "string" && body.authJson.trim()) return body.authJson.trim();
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseJsonObject(text: string): Record<string, unknown> | undefined {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(text);
|
|
93
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function randomSuffix(): string {
|
|
100
|
+
return randomBytes(3).toString("hex");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function uniqueGeneratedId(state: RuntimeState, base: string): string {
|
|
104
|
+
if (!state.providers.some((item) => item.id === base)) return base;
|
|
105
|
+
for (let index = 2; index < 100; index++) {
|
|
106
|
+
const id = `${base}-${index}`;
|
|
107
|
+
if (!state.providers.some((item) => item.id === id)) return id;
|
|
108
|
+
}
|
|
109
|
+
return `${base}-${Date.now().toString(36)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function importedName(body: Record<string, unknown>, runtime: "claude" | "codex", id: string): string {
|
|
113
|
+
if (typeof body.name === "string" && body.name.trim()) return body.name.trim().slice(0, 80);
|
|
114
|
+
return `${label(runtime)} imported account ${id.slice(-6)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function label(runtime: "claude" | "codex"): string {
|
|
118
|
+
return runtime === "codex" ? "Codex" : "Claude";
|
|
119
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AuditManifest } from "../../../core/src/manifest/audit-store.ts";
|
|
2
|
+
import type { RuntimeState } from "./runtime-state.ts";
|
|
3
|
+
import { authRequired, canManage, type AuthUser } from "./auth-state.ts";
|
|
4
|
+
|
|
5
|
+
export function filterAuditForUser(state: RuntimeState, user: AuthUser | undefined, manifests: AuditManifest[]): AuditManifest[] {
|
|
6
|
+
const allowed = auditFilterForUser(state, user);
|
|
7
|
+
return allowed ? manifests.filter(allowed) : manifests;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function auditFilterForUser(state: RuntimeState, user: AuthUser | undefined): ((manifest: AuditManifest) => boolean) | undefined {
|
|
11
|
+
if (canReadAllLocal(state, user)) return undefined;
|
|
12
|
+
if (!user) return () => false;
|
|
13
|
+
const teams = readableTeamIds(state, user);
|
|
14
|
+
const keys = ownedKeyIds(state, user.id);
|
|
15
|
+
return (manifest) => manifestAllowed(manifest, user, teams, keys);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function consumerAllowed(state: RuntimeState, user: AuthUser | undefined, id: string): boolean {
|
|
19
|
+
if (canReadAllLocal(state, user)) return true;
|
|
20
|
+
if (!user) return false;
|
|
21
|
+
if (id === `user:${user.id}`) return true;
|
|
22
|
+
return ownedKeyIds(state, user.id).has(id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function canReadAllLocal(state: RuntimeState, user: AuthUser | undefined): boolean {
|
|
26
|
+
return !authRequired(state) || canManage(state, user);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function manifestAllowed(manifest: AuditManifest, user: AuthUser, teams: Set<string>, keys: Set<string>): boolean {
|
|
30
|
+
const client = manifest.client;
|
|
31
|
+
if (!client) return false;
|
|
32
|
+
if (client.userId === user.id || client.id === `user:${user.id}`) return true;
|
|
33
|
+
if (client.keyId && keys.has(client.keyId)) return true;
|
|
34
|
+
return Boolean(client.teamIds?.some((id) => teams.has(id)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readableTeamIds(state: RuntimeState, user: AuthUser): Set<string> {
|
|
38
|
+
const ids = new Set(user.teamIds);
|
|
39
|
+
for (const team of Object.values(state.identity?.data.teams ?? {})) {
|
|
40
|
+
if (team.managerIds.includes(user.id)) ids.add(team.id);
|
|
41
|
+
}
|
|
42
|
+
return ids;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ownedKeyIds(state: RuntimeState, userId: string): Set<string> {
|
|
46
|
+
return new Set(Object.values(state.identity?.data.keys ?? {}).filter((key) => key.ownerUserId === userId).map((key) => key.id));
|
|
47
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { pluginCatalog, type MolenkopfPlugin } from "../../../core/src/plugins/plugin-catalog.ts";
|
|
2
|
+
import { staticPluginPipeline } from "../../../core/src/plugins/static-pipeline.ts";
|
|
3
|
+
import { viewProviders } from "../../../core/src/providers/provider-catalog.ts";
|
|
4
|
+
import { summarizeAudit } from "../../../core/src/manifest/audit-summary.ts";
|
|
5
|
+
import { weightShares } from "../../../core/src/routing/distribution.ts";
|
|
6
|
+
import { activeProvider, CONTROL_PLANE_LIMITS, distributionEligible, emptyUsage, providerWeight, type RuntimeState } from "./runtime-state.ts";
|
|
7
|
+
import { canManage, providerAllowed, type AuthUser } from "./auth-state.ts";
|
|
8
|
+
import { listAgentDrafts } from "./agent-drafts.ts";
|
|
9
|
+
import { orderIndex, redactionBeforeCompression } from "./local-api-pipeline.ts";
|
|
10
|
+
import { consumerAllowed } from "./local-api-scope.ts";
|
|
11
|
+
|
|
12
|
+
function hostOf(target: string): string {
|
|
13
|
+
if (target.startsWith("cli://")) return target;
|
|
14
|
+
try { return new URL(target).host; } catch { return target; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const BUILT_IN_PROVIDER_IDS = new Set(["openai-env", "anthropic-env", "ollama-local", "lmstudio-local"]);
|
|
18
|
+
|
|
19
|
+
export function buildStatus(state: RuntimeState) {
|
|
20
|
+
const provider = activeProvider(state);
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
startedAt: state.startedAt,
|
|
24
|
+
target: provider.target,
|
|
25
|
+
targetHost: hostOf(provider.target),
|
|
26
|
+
activeProviderId: provider.id,
|
|
27
|
+
bindHost: state.host,
|
|
28
|
+
port: state.port,
|
|
29
|
+
requests: state.requests,
|
|
30
|
+
compressedItems: state.compressedItems,
|
|
31
|
+
latestStatusCode: state.latest?.statusCode,
|
|
32
|
+
routingMode: state.routingMode,
|
|
33
|
+
settingsLoadWarning: state.settingsLoadWarning,
|
|
34
|
+
pipeline: staticPluginPipeline,
|
|
35
|
+
remotePluginLoading: false
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildPluginStatus(state: RuntimeState) {
|
|
40
|
+
const items = pluginCatalog.map((plugin) => ({ ...pluginView(plugin, state), order: plugin.pipelineIndex !== undefined ? orderIndex(state, plugin.id) : undefined }));
|
|
41
|
+
return {
|
|
42
|
+
items,
|
|
43
|
+
staticPipeline: items.filter((item) => item.pipelineIndex !== undefined),
|
|
44
|
+
pipelineSafe: redactionBeforeCompression(state),
|
|
45
|
+
remotePlugins: { enabled: false, reason: "remote plugin loading disabled" }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildProviderStatus(state: RuntimeState, user?: AuthUser) {
|
|
50
|
+
const views = viewProviders(state.providers, state.activeProviderId);
|
|
51
|
+
const shares = weightShares(state.providers.filter((item) => item.id !== "default" && distributionEligible(item)).map((item) => ({ id: item.id, weight: providerWeight(state, item.id) })));
|
|
52
|
+
const enriched = views
|
|
53
|
+
.filter((view) => providerAllowed(state, user, view.id))
|
|
54
|
+
.map((view) => ({
|
|
55
|
+
...view,
|
|
56
|
+
weight: providerWeight(state, view.id),
|
|
57
|
+
sharePercent: shares[view.id] ?? 0,
|
|
58
|
+
usage: state.usageByProvider[view.id] ?? emptyUsage()
|
|
59
|
+
}));
|
|
60
|
+
const configured = enriched.filter((item) => item.id !== "default" && (item.enabled !== false || !BUILT_IN_PROVIDER_IDS.has(item.id)));
|
|
61
|
+
const items = enriched.slice(0, CONTROL_PLANE_LIMITS.providerItems);
|
|
62
|
+
return {
|
|
63
|
+
activeProviderId: state.activeProviderId,
|
|
64
|
+
selectedAt: state.providerSelectedAt,
|
|
65
|
+
routingMode: state.routingMode,
|
|
66
|
+
activeProvider: enriched.find((item) => item.active),
|
|
67
|
+
configuredCount: configured.length,
|
|
68
|
+
hasConfiguredProviders: configured.length > 0,
|
|
69
|
+
configuredItems: configured.slice(0, CONTROL_PLANE_LIMITS.providerItems),
|
|
70
|
+
items,
|
|
71
|
+
hasMore: enriched.length > items.length
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildAgentStatus(state: RuntimeState) {
|
|
76
|
+
return {
|
|
77
|
+
items: listAgentDrafts(state),
|
|
78
|
+
configured: state.configAgents.map((agent) => ({
|
|
79
|
+
id: agent.id,
|
|
80
|
+
providerId: agent.providerId,
|
|
81
|
+
enabled: agent.enabled !== false,
|
|
82
|
+
profileId: agent.profileId,
|
|
83
|
+
pluginPolicyId: agent.pluginPolicyId,
|
|
84
|
+
allowedModels: agent.allowedModels,
|
|
85
|
+
defaultModel: agent.defaultModel,
|
|
86
|
+
enabledPluginIds: agent.enabledPluginIds
|
|
87
|
+
})),
|
|
88
|
+
limit: CONTROL_PLANE_LIMITS.agentDrafts,
|
|
89
|
+
hasMore: state.agentDrafts.length > CONTROL_PLANE_LIMITS.agentDrafts,
|
|
90
|
+
tokenPolicy: "hash-only; raw token values rejected"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildConsumers(state: RuntimeState, user?: AuthUser) {
|
|
95
|
+
const ids = new Set<string>([...Object.keys(state.usageByUser), ...Object.keys(state.usageByAgent), ...Object.keys(state.consumerBudgets)]);
|
|
96
|
+
const items = [...ids].filter((id) => consumerAllowed(state, user, id)).map((id) => ({
|
|
97
|
+
id,
|
|
98
|
+
label: id,
|
|
99
|
+
usage: state.usageByUser[id] ?? state.usageByAgent[id] ?? { requests: 0, inputTokens: 0, outputTokens: 0 },
|
|
100
|
+
budget: state.consumerBudgets[id]
|
|
101
|
+
})).sort((a, b) => (b.usage.inputTokens + b.usage.outputTokens) - (a.usage.inputTokens + a.usage.outputTokens));
|
|
102
|
+
return { items: items.slice(0, 100) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildConfig(state: RuntimeState, user?: AuthUser) {
|
|
106
|
+
if (!canManage(state, user)) return buildUserConfig(state);
|
|
107
|
+
const provider = activeProvider(state);
|
|
108
|
+
return {
|
|
109
|
+
dashboardPath: "/__molenkopf/dashboard",
|
|
110
|
+
localApi: [
|
|
111
|
+
"/__molenkopf/health", "/__molenkopf/status", "/__molenkopf/stats", "/__molenkopf/plugins",
|
|
112
|
+
"/__molenkopf/providers", "/__molenkopf/agents", "/__molenkopf/config", "/__molenkopf/events",
|
|
113
|
+
"/__molenkopf/requests", "/__molenkopf/requests/latest", "/__molenkopf/audit/summary",
|
|
114
|
+
"/__molenkopf/plugins/:id/data"
|
|
115
|
+
],
|
|
116
|
+
target: provider.target,
|
|
117
|
+
targetHost: hostOf(provider.target),
|
|
118
|
+
bindHost: state.host,
|
|
119
|
+
port: state.port,
|
|
120
|
+
configSource: state.configSource,
|
|
121
|
+
settingsLoadWarning: state.settingsLoadWarning,
|
|
122
|
+
routing: { mode: state.routingMode, activeProfile: state.activeProviderId, profiles: viewProviders(state.providers, state.activeProviderId) },
|
|
123
|
+
agentAccess: { mode: "draft metadata", tokenStorage: "hash only; raw values rejected", providerBinding: "provider profile per agent", draftPath: "/__molenkopf/agents/draft", drafts: state.agentDrafts.length },
|
|
124
|
+
credentialPolicy: "file config uses credential refs; UI-added credentials and imported runtime auth stay local and are not displayed",
|
|
125
|
+
remotePluginLoading: false
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildUserConfig(state: RuntimeState) {
|
|
130
|
+
return {
|
|
131
|
+
dashboardPath: "/__molenkopf/dashboard",
|
|
132
|
+
localApi: ["/__molenkopf/health", "/__molenkopf/me", "/__molenkopf/usage", "/__molenkopf/keys"],
|
|
133
|
+
bindHost: state.host,
|
|
134
|
+
port: state.port,
|
|
135
|
+
credentialPolicy: "credential values are not displayed",
|
|
136
|
+
remotePluginLoading: false
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function buildStats(state: RuntimeState) {
|
|
141
|
+
return {
|
|
142
|
+
requests: state.requests,
|
|
143
|
+
compressedItems: state.compressedItems,
|
|
144
|
+
startedAt: state.startedAt,
|
|
145
|
+
host: state.host,
|
|
146
|
+
port: state.port,
|
|
147
|
+
latest: boundLatest(state.latest),
|
|
148
|
+
pluginEnabled: state.pluginEnabled,
|
|
149
|
+
activeProviderId: state.activeProviderId,
|
|
150
|
+
settingsLoadWarning: state.settingsLoadWarning,
|
|
151
|
+
providers: buildProviderStatus(state),
|
|
152
|
+
agentDrafts: buildAgentStatus(state),
|
|
153
|
+
auditSummary: state.latest ? summarizeAudit([state.latest]) : summarizeAudit([]),
|
|
154
|
+
communicationGraph: boundGraph(state.communicationGraph)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function pluginView(plugin: MolenkopfPlugin, state: RuntimeState) {
|
|
159
|
+
const enabled = state.pluginEnabled[plugin.id] ?? plugin.enabledByDefault;
|
|
160
|
+
const lifecycle = state.pluginLifecycle[plugin.id];
|
|
161
|
+
return {
|
|
162
|
+
...plugin,
|
|
163
|
+
enabled,
|
|
164
|
+
status: enabled ? "enabled" : "disabled",
|
|
165
|
+
lifecycleStatus: lifecycle?.status ?? (enabled ? "enabled" : "disabled"),
|
|
166
|
+
lifecycleError: lifecycle?.error,
|
|
167
|
+
updatedAt: state.pluginUpdatedAt[plugin.id],
|
|
168
|
+
source: state.pluginUpdatedAt[plugin.id] ? "local" : "default"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function boundGraph(graph: RuntimeState["communicationGraph"]) {
|
|
173
|
+
const nodes = graph.nodes.slice(0, CONTROL_PLANE_LIMITS.graphNodes);
|
|
174
|
+
const edges = graph.edges.slice(0, CONTROL_PLANE_LIMITS.graphEdges);
|
|
175
|
+
return { nodes, edges, hasMore: nodes.length < graph.nodes.length || edges.length < graph.edges.length };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function boundLatest(latest: RuntimeState["latest"]) {
|
|
179
|
+
return latest ? { ...latest, retrievalIds: latest.retrievalIds.slice(0, 10), warnings: latest.warnings.slice(0, 10) } : undefined;
|
|
180
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { AuditCursorError, type AuditStore } from "../../../core/src/manifest/audit-store.ts";
|
|
3
|
+
import type { EventBus } from "../../../core/src/events/event-bus.ts";
|
|
4
|
+
import { summarizeAudit } from "../../../core/src/manifest/audit-summary.ts";
|
|
5
|
+
import { loadPluginPage } from "./plugin-page-loader.ts";
|
|
6
|
+
import { type RuntimeState } from "./runtime-state.ts";
|
|
7
|
+
import { buildPluginData } from "./plugin-data.ts";
|
|
8
|
+
import { buildAgentStatus, buildConfig, buildConsumers, buildPluginStatus, buildProviderStatus, buildStats, buildStatus } from "./local-api-state.ts";
|
|
9
|
+
import { auditView, auditViews } from "./audit-view.ts";
|
|
10
|
+
import { LocalApiError, writeHtml, writeJson } from "./local-api-io.ts";
|
|
11
|
+
import { saveAgentDraft } from "./local-api-agent-actions.ts";
|
|
12
|
+
import { setConsumerBudget } from "./local-api-consumer-actions.ts";
|
|
13
|
+
import { addProvider, removeProvider, selectProvider, setProviderWeight, setProviderWeights, setRoutingMode, updateProvider } from "./local-api-provider-actions.ts";
|
|
14
|
+
import { togglePlugin } from "./local-api-plugin-actions.ts";
|
|
15
|
+
import { authRequired, canManage, currentUser, type AuthUser } from "./auth-state.ts";
|
|
16
|
+
import { login, logout, me, setupAdmin } from "./local-api-auth.ts";
|
|
17
|
+
import { reorderPlugin } from "./local-api-pipeline.ts";
|
|
18
|
+
import { issueKeyHandler, listKeysHandler, revokeKeyHandler, updateKeyHandler, usageHandler } from "./local-api-keys.ts";
|
|
19
|
+
import { listIdentity, putIdentityTeam, putIdentityUser, removeIdentityTeam, removeIdentityUser } from "./local-api-identity.ts";
|
|
20
|
+
import { importProviderAuth } from "./local-api-runtime-auth.ts";
|
|
21
|
+
import { testProvider, testRuntimeProvider } from "./provider-test.ts";
|
|
22
|
+
import { checkControlPlaneWrite } from "./control-plane-guard.ts";
|
|
23
|
+
import { auditFilterForUser } from "./local-api-scope.ts";
|
|
24
|
+
import { purgeRetention } from "./local-api-retention.ts";
|
|
25
|
+
import type { PluginHost } from "./plugin-host.ts";
|
|
26
|
+
|
|
27
|
+
const DEV_REVISION_PATH = "/__molenkopf/dev/revision";
|
|
28
|
+
const PUBLIC_PATHS = new Set(["/__molenkopf/health", "/__molenkopf/login", "/__molenkopf/logout", "/__molenkopf/me", "/__molenkopf/setup-admin", DEV_REVISION_PATH]);
|
|
29
|
+
const BOOTSTRAP_PATHS = new Set(["/__molenkopf/health", "/__molenkopf/me", "/__molenkopf/setup-admin"]);
|
|
30
|
+
const ADMIN_READ_PATHS = new Set(["/__molenkopf/status", "/__molenkopf/plugins", "/__molenkopf/providers", "/__molenkopf/agents", "/__molenkopf/stats", "/__molenkopf/events"]);
|
|
31
|
+
const MANAGE_PATHS = new Set([
|
|
32
|
+
"/__molenkopf/plugins/toggle", "/__molenkopf/plugins/reorder", "/__molenkopf/providers/select", "/__molenkopf/providers/weight", "/__molenkopf/providers/weights",
|
|
33
|
+
"/__molenkopf/providers/add", "/__molenkopf/providers/import-auth", "/__molenkopf/providers/test", "/__molenkopf/providers/test-runtime", "/__molenkopf/providers/update", "/__molenkopf/providers/remove",
|
|
34
|
+
"/__molenkopf/routing/mode", "/__molenkopf/consumers/budget", "/__molenkopf/agents/draft",
|
|
35
|
+
"/__molenkopf/identity/users", "/__molenkopf/identity/users/remove",
|
|
36
|
+
"/__molenkopf/identity/teams", "/__molenkopf/identity/teams/remove", "/__molenkopf/retention/purge"
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export async function handleLocalRequest(req: IncomingMessage, res: ServerResponse, audit: AuditStore, events: EventBus, state: RuntimeState, pluginHost: PluginHost) {
|
|
40
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
41
|
+
const path = url.pathname;
|
|
42
|
+
try {
|
|
43
|
+
const guard = checkControlPlaneWrite(req, path, state);
|
|
44
|
+
if (guard.ok === false) return writeJson(res, guard.status, { error: guard.error });
|
|
45
|
+
const open = !authRequired(state);
|
|
46
|
+
const user = open ? undefined : currentUser(state, req.headers.cookie ?? null);
|
|
47
|
+
if (open && !BOOTSTRAP_PATHS.has(path)) return writeJson(res, 401, { error: "setup_required" });
|
|
48
|
+
if (!open && !PUBLIC_PATHS.has(path) && !user) return writeJson(res, 401, { error: "unauthorized" });
|
|
49
|
+
if (!open && MANAGE_PATHS.has(path) && !canManage(state, user)) return writeJson(res, 403, { error: "forbidden" });
|
|
50
|
+
if (!open && ADMIN_READ_PATHS.has(path) && !canManage(state, user)) return writeJson(res, 403, { error: "forbidden" });
|
|
51
|
+
if (!methodAllowed(req.method ?? "GET", path)) return writeJson(res, 405, { error: "method_not_allowed" });
|
|
52
|
+
if (path === "/__molenkopf/login") return login(req, res, state);
|
|
53
|
+
if (path === "/__molenkopf/setup-admin") return setupAdmin(req, res, state);
|
|
54
|
+
if (path === "/__molenkopf/plugins/reorder") return reorderPlugin(req, res, state);
|
|
55
|
+
if (path === "/__molenkopf/logout") return logout(req, res);
|
|
56
|
+
if (path === "/__molenkopf/me") return me(req, res, state);
|
|
57
|
+
if (path === "/__molenkopf/health") return writeJson(res, 200, { ok: true });
|
|
58
|
+
if (path === DEV_REVISION_PATH) return writeDevRevision(res);
|
|
59
|
+
if (path === "/__molenkopf/status") return writeJson(res, 200, buildStatus(state));
|
|
60
|
+
if (path === "/__molenkopf/plugins") return writeJson(res, 200, buildPluginStatus(state));
|
|
61
|
+
if (path === "/__molenkopf/providers") return writeJson(res, 200, buildProviderStatus(state, user));
|
|
62
|
+
if (path === "/__molenkopf/agents") return writeJson(res, 200, buildAgentStatus(state));
|
|
63
|
+
if (path === "/__molenkopf/config") return writeJson(res, 200, buildConfig(state, user));
|
|
64
|
+
if (path === "/__molenkopf/stats") return writeJson(res, 200, buildStats(state));
|
|
65
|
+
if (path === "/__molenkopf/requests/latest") return audit.listPage({ limit: 1, newestFirst: true, filter: auditFilterForUser(state, user) }).then((page) => writeJson(res, 200, auditViews(page.items).at(0) ?? {}));
|
|
66
|
+
if (path === "/__molenkopf/requests") {
|
|
67
|
+
const cursor = url.searchParams.get("cursor") ?? undefined;
|
|
68
|
+
try {
|
|
69
|
+
const page = await audit.listPage({ limit: 200, cursor, newestFirst: true, filter: auditFilterForUser(state, user) });
|
|
70
|
+
return writeJson(res, 200, auditViews(page.items));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof AuditCursorError) return writeJson(res, 400, { error: "invalid_cursor" });
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (path === "/__molenkopf/audit/summary") return audit.listPage({ limit: 1000, newestFirst: true, filter: auditFilterForUser(state, user) }).then((page) => writeJson(res, 200, summarizeAudit(auditViews(page.items))));
|
|
77
|
+
if (path === "/__molenkopf/consumers") return writeJson(res, 200, buildConsumers(state, user));
|
|
78
|
+
if (path === "/__molenkopf/events") return streamEvents(res, events);
|
|
79
|
+
if (path === "/__molenkopf/plugins/toggle") return togglePlugin(req, res, state, pluginHost);
|
|
80
|
+
if (path === "/__molenkopf/providers/select") return selectProvider(req, res, state);
|
|
81
|
+
if (path === "/__molenkopf/providers/weight") return setProviderWeight(req, res, state);
|
|
82
|
+
if (path === "/__molenkopf/providers/weights") return setProviderWeights(req, res, state);
|
|
83
|
+
if (path === "/__molenkopf/providers/add") return addProvider(req, res, state);
|
|
84
|
+
if (path === "/__molenkopf/providers/import-auth") return importProviderAuth(req, res, state);
|
|
85
|
+
if (path === "/__molenkopf/providers/test") return testProvider(req, res, state);
|
|
86
|
+
if (path === "/__molenkopf/providers/test-runtime") return testRuntimeProvider(req, res, state);
|
|
87
|
+
if (path === "/__molenkopf/providers/update") return updateProvider(req, res, state);
|
|
88
|
+
if (path === "/__molenkopf/providers/remove") return removeProvider(req, res, state);
|
|
89
|
+
if (path === "/__molenkopf/routing/mode") return setRoutingMode(req, res, state);
|
|
90
|
+
if (path === "/__molenkopf/consumers/budget") return setConsumerBudget(req, res, state);
|
|
91
|
+
if (path === "/__molenkopf/agents/draft") return saveAgentDraft(req, res, state);
|
|
92
|
+
if (path === "/__molenkopf/keys") return req.method === "POST" ? issueKeyHandler(req, res, state, user) : listKeysHandler(req, res, state, user);
|
|
93
|
+
if (path === "/__molenkopf/keys/update") return updateKeyHandler(req, res, state, user);
|
|
94
|
+
if (path === "/__molenkopf/keys/revoke") return revokeKeyHandler(req, res, state, user);
|
|
95
|
+
if (path === "/__molenkopf/usage") return usageHandler(req, res, state, user);
|
|
96
|
+
if (path === "/__molenkopf/identity") {
|
|
97
|
+
if (!open && !canManage(state, user)) return writeJson(res, 403, { error: "forbidden" });
|
|
98
|
+
return listIdentity(req, res, state);
|
|
99
|
+
}
|
|
100
|
+
if (path === "/__molenkopf/identity/users") return putIdentityUser(req, res, state);
|
|
101
|
+
if (path === "/__molenkopf/identity/users/remove") return removeIdentityUser(req, res, state);
|
|
102
|
+
if (path === "/__molenkopf/identity/teams") return putIdentityTeam(req, res, state);
|
|
103
|
+
if (path === "/__molenkopf/identity/teams/remove") return removeIdentityTeam(req, res, state);
|
|
104
|
+
if (path === "/__molenkopf/retention/purge") return purgeRetention(req, res, audit, state);
|
|
105
|
+
const pluginData = path.match(/^\/__molenkopf\/plugins\/([^/]+)\/data$/);
|
|
106
|
+
if (pluginData && !canManage(state, user)) return writeJson(res, 403, { error: "forbidden" });
|
|
107
|
+
if (pluginData) return writePluginData(res, pluginData[1], audit, state, user, pluginHost);
|
|
108
|
+
const pluginPage = path.match(/^\/__molenkopf\/plugins\/([^/]+)\/page$/);
|
|
109
|
+
if (pluginPage && !canManage(state, user)) return writeJson(res, 403, { error: "forbidden" });
|
|
110
|
+
if (pluginPage) return writePluginPage(res, pluginPage[1]);
|
|
111
|
+
writeJson(res, 404, { error: "not_found" });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof LocalApiError) return writeJson(res, error.status, { error: error.code });
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const GET_ONLY = new Set([
|
|
119
|
+
"/__molenkopf/health", "/__molenkopf/me", DEV_REVISION_PATH, "/__molenkopf/status", "/__molenkopf/plugins", "/__molenkopf/providers", "/__molenkopf/agents",
|
|
120
|
+
"/__molenkopf/config", "/__molenkopf/stats", "/__molenkopf/requests/latest", "/__molenkopf/requests", "/__molenkopf/audit/summary", "/__molenkopf/consumers",
|
|
121
|
+
"/__molenkopf/events", "/__molenkopf/usage", "/__molenkopf/identity"
|
|
122
|
+
]);
|
|
123
|
+
const POST_ONLY = new Set([
|
|
124
|
+
"/__molenkopf/login", "/__molenkopf/logout", "/__molenkopf/setup-admin", "/__molenkopf/plugins/reorder", "/__molenkopf/plugins/toggle",
|
|
125
|
+
"/__molenkopf/providers/select", "/__molenkopf/providers/weight", "/__molenkopf/providers/weights", "/__molenkopf/providers/add",
|
|
126
|
+
"/__molenkopf/providers/import-auth", "/__molenkopf/providers/test", "/__molenkopf/providers/test-runtime", "/__molenkopf/providers/update",
|
|
127
|
+
"/__molenkopf/providers/remove", "/__molenkopf/routing/mode", "/__molenkopf/consumers/budget", "/__molenkopf/agents/draft", "/__molenkopf/keys/update",
|
|
128
|
+
"/__molenkopf/keys/revoke", "/__molenkopf/identity/users", "/__molenkopf/identity/users/remove", "/__molenkopf/identity/teams",
|
|
129
|
+
"/__molenkopf/identity/teams/remove", "/__molenkopf/retention/purge"
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
function methodAllowed(method: string, path: string): boolean {
|
|
133
|
+
const upper = method.toUpperCase();
|
|
134
|
+
if (path === "/__molenkopf/keys") return upper === "GET" || upper === "POST";
|
|
135
|
+
if (GET_ONLY.has(path)) return upper === "GET";
|
|
136
|
+
if (POST_ONLY.has(path)) return upper === "POST";
|
|
137
|
+
if (/^\/__molenkopf\/plugins\/[^/]+\/(?:data|page)$/.test(path)) return upper === "GET";
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function writeDevRevision(res: ServerResponse) {
|
|
142
|
+
if (process.env.MOLENKOPF_PROFILE !== "dev") return writeJson(res, 404, { error: "not_found" });
|
|
143
|
+
return writeJson(res, 200, { revision: process.env.MOLENKOPF_DEV_REVISION || "dev" });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function writePluginPage(res: ServerResponse, id: string) {
|
|
147
|
+
const html = loadPluginPage(id);
|
|
148
|
+
if (!html) return writeJson(res, 404, { error: "plugin_page_not_found" });
|
|
149
|
+
writeHtml(res, html);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function writePluginData(res: ServerResponse, id: string, audit: AuditStore, state: RuntimeState, user: AuthUser | undefined, pluginHost: PluginHost) {
|
|
153
|
+
const result = await buildPluginData(id, audit, state, user, pluginHost);
|
|
154
|
+
writeJson(res, result.status, result.payload);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function streamEvents(res: ServerResponse, events: EventBus) {
|
|
158
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
159
|
+
res.write(": connected\n\n");
|
|
160
|
+
const heartbeat = setInterval(() => res.write(": heartbeat\n\n"), 25000);
|
|
161
|
+
const unsubscribe = events.subscribe((event) => res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`));
|
|
162
|
+
res.on("close", () => {
|
|
163
|
+
clearInterval(heartbeat);
|
|
164
|
+
unsubscribe();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AuditStore, AuditManifest } from "../../../core/src/manifest/audit-store.ts";
|
|
2
|
+
import { findPlugin } from "../../../core/src/plugins/plugin-catalog.ts";
|
|
3
|
+
import { pluginView } from "./local-api-state.ts";
|
|
4
|
+
import { type RuntimeState } from "./runtime-state.ts";
|
|
5
|
+
import { auditViews } from "./audit-view.ts";
|
|
6
|
+
import { auditFilterForUser } from "./local-api-scope.ts";
|
|
7
|
+
import { canManage, type AuthUser } from "./auth-state.ts";
|
|
8
|
+
import { auditPath } from "./request-path.ts";
|
|
9
|
+
import type { PluginHost } from "./plugin-host.ts";
|
|
10
|
+
|
|
11
|
+
type PluginDataResult = { status: number; payload: unknown };
|
|
12
|
+
|
|
13
|
+
export async function buildPluginData(id: string, audit: AuditStore, state: RuntimeState, user: AuthUser | undefined, host: PluginHost): Promise<PluginDataResult> {
|
|
14
|
+
const plugin = findPlugin(id);
|
|
15
|
+
if (!plugin) return { status: 404, payload: { error: "unknown_plugin" } };
|
|
16
|
+
if (!plugin.dataPath) return { status: 404, payload: { error: "plugin_data_not_found" } };
|
|
17
|
+
const result = await host.data(id, {
|
|
18
|
+
canManage: canManage(state, user),
|
|
19
|
+
userId: user?.id,
|
|
20
|
+
teamIds: user?.teamIds ?? [],
|
|
21
|
+
scope: "data",
|
|
22
|
+
plugin: pluginView(plugin, state) as unknown as Record<string, unknown>,
|
|
23
|
+
scopes: plugin.dataScopes ?? [],
|
|
24
|
+
manifests: await scopedManifests(audit, state, user),
|
|
25
|
+
memoryGraph: state.memoryGraph
|
|
26
|
+
});
|
|
27
|
+
return result.ok === true ? { status: 200, payload: result.payload } : { status: result.status, payload: { error: result.error } };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function scopedManifests(audit: AuditStore, state: RuntimeState, user?: AuthUser): Promise<AuditManifest[]> {
|
|
31
|
+
const page = await audit.listPage({ limit: 200, newestFirst: true, filter: auditFilterForUser(state, user) });
|
|
32
|
+
return auditViews(page.items).filter(isAgentTraffic);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isAgentTraffic(manifest: AuditManifest): boolean {
|
|
36
|
+
const path = auditPath(manifest.path);
|
|
37
|
+
return !path.startsWith("/.well-known/appspecific/") && path !== "/favicon.ico";
|
|
38
|
+
}
|