@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,100 @@
|
|
|
1
|
+
import type { AuditManifest } from "../../../core/src/manifest/audit-store.ts";
|
|
2
|
+
import { costEur } from "../../../core/src/identity/pricing.ts";
|
|
3
|
+
import { budgetPeriodKey } from "../../../core/src/identity/budget.ts";
|
|
4
|
+
import type { BudgetPeriod } from "../../../core/src/identity/types.ts";
|
|
5
|
+
import { clientIdForAgent } from "./client-identity.ts";
|
|
6
|
+
import type { RuntimeState, UsagePeriodTotals, UsageTotals } from "./runtime-state.ts";
|
|
7
|
+
|
|
8
|
+
export function recordUsage(state: RuntimeState, manifest: AuditManifest): void {
|
|
9
|
+
const cost = costEur(state.identity?.data.pricing?.[manifest.providerId ?? ""], manifest.upstreamInputTokens ?? 0, manifest.upstreamOutputTokens ?? 0);
|
|
10
|
+
const at = new Date(manifest.timestamp);
|
|
11
|
+
if (manifest.providerId) accumulate(state.usageByProvider, manifest.providerId, manifest, cost, at);
|
|
12
|
+
const client = manifest.client;
|
|
13
|
+
if (client) {
|
|
14
|
+
if (client.userId) accumulate(state.usageByUser, userUsageKey(client.userId), manifest, cost, at);
|
|
15
|
+
else if (client.source === "user") accumulate(state.usageByUser, client.id, manifest, cost, at);
|
|
16
|
+
for (const id of agentUsageKeys(client)) accumulate(state.usageByAgent, id, manifest, cost, at);
|
|
17
|
+
if (client.keyId) accumulate(state.usageByKey, client.keyId, manifest, cost, at);
|
|
18
|
+
for (const teamId of client.teamIds ?? []) accumulate(state.usageByTeam, teamId, manifest, cost, at);
|
|
19
|
+
}
|
|
20
|
+
state.usageSnapshot?.schedule(state);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function userUsageKey(userId: string): string {
|
|
24
|
+
return `user:${userId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function keyTokensUsed(state: RuntimeState, keyId: string, period: BudgetPeriod = "total", now = new Date()): number {
|
|
28
|
+
return tokensOf(usageForPeriod(state.usageByKey[keyId], period, now));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function keyCostUsed(state: RuntimeState, keyId: string, period: BudgetPeriod = "total", now = new Date()): number {
|
|
32
|
+
return costOf(usageForPeriod(state.usageByKey[keyId], period, now));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function userTokensUsed(state: RuntimeState, userId: string, period: BudgetPeriod = "total", now = new Date()): number {
|
|
36
|
+
return tokensOf(usageForPeriod(state.usageByUser[userUsageKey(userId)], period, now));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function userCostUsed(state: RuntimeState, userId: string, period: BudgetPeriod = "total", now = new Date()): number {
|
|
40
|
+
return costOf(usageForPeriod(state.usageByUser[userUsageKey(userId)], period, now));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function teamTokensUsed(state: RuntimeState, teamId: string, period: BudgetPeriod = "total", now = new Date()): number {
|
|
44
|
+
return tokensOf(usageForPeriod(state.usageByTeam[teamId], period, now));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function teamCostUsed(state: RuntimeState, teamId: string, period: BudgetPeriod = "total", now = new Date()): number {
|
|
48
|
+
return costOf(usageForPeriod(state.usageByTeam[teamId], period, now));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function orgTokensUsed(state: RuntimeState, period: BudgetPeriod = "total", now = new Date()): number {
|
|
52
|
+
return Object.values(state.usageByUser).reduce((sum, usage) => sum + tokensOf(usageForPeriod(usage, period, now)), 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function orgCostUsed(state: RuntimeState, period: BudgetPeriod = "total", now = new Date()): number {
|
|
56
|
+
return Object.values(state.usageByUser).reduce((sum, usage) => sum + costOf(usageForPeriod(usage, period, now)), 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function agentTokensUsed(state: RuntimeState, clientId: string): number {
|
|
60
|
+
return tokensOf(state.usageByAgent[clientId] ?? state.usageByUser[clientId]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function agentUsageKeys(client: NonNullable<AuditManifest["client"]>): string[] {
|
|
64
|
+
const ids = new Set<string>();
|
|
65
|
+
if (client.source === "agent") ids.add(client.id);
|
|
66
|
+
if (client.agentId) ids.add(clientIdForAgent(client.agentId));
|
|
67
|
+
return [...ids];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function usageForPeriod(usage: UsageTotals | undefined, period: BudgetPeriod = "total", now = new Date()): UsagePeriodTotals {
|
|
71
|
+
if (!usage) return { requests: 0, inputTokens: 0, outputTokens: 0, costEur: 0 };
|
|
72
|
+
return period === "total" ? usage : usage.periods?.[budgetPeriodKey(period, now)] ?? { requests: 0, inputTokens: 0, outputTokens: 0, costEur: 0 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function accumulate(map: Record<string, UsageTotals>, key: string, manifest: AuditManifest, cost: number, at: Date): void {
|
|
76
|
+
const usage = map[key] ?? { requests: 0, inputTokens: 0, outputTokens: 0, costEur: 0 };
|
|
77
|
+
addUsage(usage, manifest, cost);
|
|
78
|
+
usage.periods ??= {};
|
|
79
|
+
for (const period of ["day", "week", "month"] as const) {
|
|
80
|
+
const key = budgetPeriodKey(period, at);
|
|
81
|
+
usage.periods[key] = addUsage(usage.periods[key] ?? { requests: 0, inputTokens: 0, outputTokens: 0, costEur: 0 }, manifest, cost);
|
|
82
|
+
}
|
|
83
|
+
map[key] = usage;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function addUsage(usage: UsagePeriodTotals, manifest: AuditManifest, cost: number): UsagePeriodTotals {
|
|
87
|
+
usage.requests++;
|
|
88
|
+
usage.inputTokens += manifest.upstreamInputTokens ?? 0;
|
|
89
|
+
usage.outputTokens += manifest.upstreamOutputTokens ?? 0;
|
|
90
|
+
usage.costEur = (usage.costEur ?? 0) + cost;
|
|
91
|
+
return usage;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function tokensOf(usage: UsageTotals | undefined): number {
|
|
95
|
+
return usage ? usage.inputTokens + usage.outputTokens : 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function costOf(usage: UsageTotals | undefined): number {
|
|
99
|
+
return usage?.costEur ?? 0;
|
|
100
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuditStore } from "../../../core/src/manifest/audit-store.ts";
|
|
2
|
+
import type { UsageSnapshotStore } from "../../../core/src/identity/usage-snapshot.ts";
|
|
3
|
+
import { recordUsage, type RuntimeState } from "./runtime-state.ts";
|
|
4
|
+
|
|
5
|
+
const USAGE_FIELDS = ["usageByAgent", "usageByUser", "usageByProvider", "usageByKey", "usageByTeam"] as const;
|
|
6
|
+
|
|
7
|
+
export async function restoreUsage(state: RuntimeState, snapshots: UsageSnapshotStore, audit: AuditStore): Promise<void> {
|
|
8
|
+
const snapshot = await snapshots.load();
|
|
9
|
+
if (snapshot) {
|
|
10
|
+
for (const field of USAGE_FIELDS) if (snapshot[field]) state[field] = snapshot[field] as RuntimeState[typeof field];
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
for (const manifest of await audit.list()) recordUsage(state, manifest);
|
|
14
|
+
await snapshots.save(state).catch(() => {});
|
|
15
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { redactSecrets } from "../../../core/src/security/secret-redactor.ts";
|
|
2
|
+
|
|
3
|
+
export type CliLifecycleState = "unknown" | "spawned" | "stdin_sent" | "first_output" | "timeout" | "killed" | "closed" | "malformed";
|
|
4
|
+
export type CliErrorClass = "auth_failure" | "permission_prompt" | "timeout" | "empty_output" | "exit_error" | "spawn_error" | "unknown";
|
|
5
|
+
|
|
6
|
+
export type CliErrorDiagnostics = {
|
|
7
|
+
class: CliErrorClass;
|
|
8
|
+
lifecycle: { state: CliLifecycleState; events: string[] };
|
|
9
|
+
permissionBlocked: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function cliErrorDiagnostics(error: unknown): CliErrorDiagnostics {
|
|
13
|
+
const message = safeMessage(error);
|
|
14
|
+
const events = lifecycleEvents(message);
|
|
15
|
+
const classification = classifyCliMessage(message);
|
|
16
|
+
return {
|
|
17
|
+
class: classification,
|
|
18
|
+
lifecycle: { state: lifecycleState(events, classification), events },
|
|
19
|
+
permissionBlocked: classification === "permission_prompt"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function safeCliMessage(error: unknown): string {
|
|
24
|
+
const message = safeMessage(error);
|
|
25
|
+
const classification = classifyCliMessage(message);
|
|
26
|
+
if (classification === "auth_failure") return "Local CLI authentication failed. Re-import a current auth.json or run the runtime login command, then test again.";
|
|
27
|
+
if (classification === "permission_prompt") return "Local CLI reported a permission prompt.";
|
|
28
|
+
return message;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function successfulCliLifecycle(): CliErrorDiagnostics["lifecycle"] {
|
|
32
|
+
return { state: "closed", events: ["spawned", "stdin_sent", "first_output", "closed"] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function classifyCliMessage(message: string): CliErrorClass {
|
|
36
|
+
if (/output_class:auth_failure|not logged in|please run \/login|authentication|credentials/i.test(message)) return "auth_failure";
|
|
37
|
+
if (/output_class:permission_prompt|requested permissions|haven't granted|permission[^.]{0,80}(denied|blocked|required)|requires permission|not allowed/i.test(message)) return "permission_prompt";
|
|
38
|
+
if (/timed out after/i.test(message)) return "timeout";
|
|
39
|
+
if (/returned empty output|malformed/i.test(message)) return "empty_output";
|
|
40
|
+
if (/failed:/i.test(message)) return "spawn_error";
|
|
41
|
+
if (/exited with/i.test(message)) return "exit_error";
|
|
42
|
+
return "unknown";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function lifecycleEvents(message: string): string[] {
|
|
46
|
+
const match = message.match(/lifecycle:\s*([^;]+)/i);
|
|
47
|
+
if (!match) return [];
|
|
48
|
+
return match[1].split("->").map((item) => item.trim().replace(/^\d+ms\s+/, "").slice(0, 120)).filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function lifecycleState(events: string[], classification: CliErrorClass): CliLifecycleState {
|
|
52
|
+
if (classification === "empty_output") return "malformed";
|
|
53
|
+
if (events.some((event) => event.startsWith("timeout"))) return "timeout";
|
|
54
|
+
if (events.some((event) => event.startsWith("kill"))) return "killed";
|
|
55
|
+
if (events.some((event) => event.startsWith("close"))) return "closed";
|
|
56
|
+
if (events.some((event) => event.includes("first byte"))) return "first_output";
|
|
57
|
+
if (events.some((event) => event === "stdin sent")) return "stdin_sent";
|
|
58
|
+
if (events.some((event) => event.startsWith("spawn"))) return "spawned";
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function safeMessage(error: unknown): string {
|
|
63
|
+
return redactSecrets(String(error instanceof Error ? error.message : error)).text.slice(0, 720);
|
|
64
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ProviderConfig } from "../../../core/src/providers/provider-catalog.ts";
|
|
2
|
+
|
|
3
|
+
const PRESERVED_ENV = new Set([
|
|
4
|
+
"APPDATA", "COMSPEC", "HOME", "HOMEDRIVE", "HOMEPATH", "LANG", "LC_ALL", "LC_CTYPE",
|
|
5
|
+
"LOCALAPPDATA", "LOGNAME", "NUMBER_OF_PROCESSORS", "OS", "PATH", "PATHEXT", "PROGRAMDATA",
|
|
6
|
+
"PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMW6432", "PROCESSOR_ARCHITECTURE", "SHELL",
|
|
7
|
+
"SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "TERM", "TMP", "TMPDIR", "USER", "USERDOMAIN",
|
|
8
|
+
"USERNAME", "USERPROFILE", "WINDIR", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function cliEnv(provider: ProviderConfig, source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
12
|
+
const env: NodeJS.ProcessEnv = {};
|
|
13
|
+
for (const [key, value] of Object.entries(source)) {
|
|
14
|
+
if (value !== undefined && PRESERVED_ENV.has(key.toUpperCase())) env[key] = value;
|
|
15
|
+
}
|
|
16
|
+
if (provider.runtimeAuthDir && provider.runtime === "codex") env.CODEX_HOME = provider.runtimeAuthDir;
|
|
17
|
+
if (provider.runtimeAuthDir && provider.runtime === "claude") {
|
|
18
|
+
env.CLAUDE_CONFIG_DIR = provider.runtimeAuthDir;
|
|
19
|
+
env.CLAUDE_HOME = provider.runtimeAuthDir;
|
|
20
|
+
}
|
|
21
|
+
return env;
|
|
22
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ProviderConfig } from "../../../core/src/providers/provider-catalog.ts";
|
|
5
|
+
import { cliArgs } from "./cli-request.ts";
|
|
6
|
+
import { cliEnv } from "./cli-env.ts";
|
|
7
|
+
|
|
8
|
+
export function executeCliProvider(provider: ProviderConfig, prompt: string, runModel?: string): Promise<string> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const baseArgs = cliArgs(provider, runModel);
|
|
11
|
+
const args = provider.cliInputMode === "argument" ? [...baseArgs, prompt] : baseArgs;
|
|
12
|
+
const spec = cliSpawnSpec(provider.cliCommand ?? "claude", args);
|
|
13
|
+
const timeoutMs = provider.cliTimeoutMs ?? 120000;
|
|
14
|
+
const lifecycle = newLifecycle(provider);
|
|
15
|
+
const child = spawn(spec.command, spec.args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: cliEnv(provider) });
|
|
16
|
+
const stdout: Buffer[] = [], stderr: Buffer[] = [];
|
|
17
|
+
const limits = outputLimits();
|
|
18
|
+
let settled = false, killTimer: NodeJS.Timeout | undefined, stdoutSeen = false, stderrSeen = false;
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
lifecycle.add(`timeout ${timeoutMs}ms`);
|
|
21
|
+
lifecycle.add(`kill SIGTERM ${child.kill("SIGTERM") ? "sent" : "failed"}`);
|
|
22
|
+
killTimer = setTimeout(() => lifecycle.add(`kill SIGKILL ${child.kill("SIGKILL") ? "sent" : "failed"}`), 250);
|
|
23
|
+
killTimer.unref?.();
|
|
24
|
+
fail(new Error(`local cli provider timed out after ${timeoutMs}ms${detail(lifecycle.items, stdout, stderr)}`), false);
|
|
25
|
+
}, timeoutMs);
|
|
26
|
+
|
|
27
|
+
child.stdout.on("data", (chunk) => {
|
|
28
|
+
if (!stdoutSeen) { stdoutSeen = true; lifecycle.add("stdout first byte"); }
|
|
29
|
+
if (!capture(stdout, Buffer.from(chunk), limits.streamBytes)) overflow("stdout");
|
|
30
|
+
});
|
|
31
|
+
child.stderr.on("data", (chunk) => {
|
|
32
|
+
if (!stderrSeen) { stderrSeen = true; lifecycle.add("stderr first byte"); }
|
|
33
|
+
if (!capture(stderr, Buffer.from(chunk), limits.streamBytes)) overflow("stderr");
|
|
34
|
+
});
|
|
35
|
+
child.on("error", (error) => fail(new Error(`local cli provider failed: ${error.message}${detail(lifecycle.items, stdout, stderr)}`), true));
|
|
36
|
+
child.on("close", (code, signal) => {
|
|
37
|
+
lifecycle.add(`close code=${code ?? "unknown"} signal=${signal ?? "none"}`);
|
|
38
|
+
if (settled) { if (killTimer) clearTimeout(killTimer); return; }
|
|
39
|
+
if (code !== 0) return fail(new Error(exitMessage(code, stdout, stderr, lifecycle.items)), true);
|
|
40
|
+
finish(true);
|
|
41
|
+
const output = Buffer.concat(stdout).toString("utf8").trim();
|
|
42
|
+
if (!output) return reject(new Error(`local cli provider returned empty output${detail(lifecycle.items, stdout, stderr)}`));
|
|
43
|
+
resolve(output);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
lifecycle.add("stdin sent");
|
|
47
|
+
if (provider.cliInputMode !== "argument") child.stdin.end(prompt);
|
|
48
|
+
else child.stdin.end();
|
|
49
|
+
|
|
50
|
+
function fail(error: Error, clearKill: boolean): void {
|
|
51
|
+
if (settled) return;
|
|
52
|
+
finish(clearKill);
|
|
53
|
+
reject(error);
|
|
54
|
+
}
|
|
55
|
+
function finish(clearKill: boolean): void {
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
if (clearKill && killTimer) clearTimeout(killTimer);
|
|
59
|
+
}
|
|
60
|
+
function overflow(stream: "stdout" | "stderr"): void {
|
|
61
|
+
lifecycle.add(`${stream} overflow`);
|
|
62
|
+
child.kill("SIGTERM");
|
|
63
|
+
fail(new Error(`local cli provider output exceeded ${limits.streamBytes} bytes${detail(lifecycle.items, stdout, stderr)}`), true);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function newLifecycle(provider: ProviderConfig) {
|
|
69
|
+
const started = Date.now();
|
|
70
|
+
const items = [`0ms spawn runtime=${provider.runtime ?? "cli"}`];
|
|
71
|
+
return { items, add: (event: string) => items.push(`${Date.now() - started}ms ${event}`) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function exitMessage(code: number | null, stdout: Buffer[], stderr: Buffer[], events: string[]): string {
|
|
75
|
+
return `local cli provider exited with ${code ?? "unknown"}${detail(events, stdout, stderr)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detail(events: string[], stdout: Buffer[], stderr: Buffer[]): string {
|
|
79
|
+
const outputClass = cliOutputClass(stdout, stderr);
|
|
80
|
+
return `; lifecycle: ${events.join(" -> ")}${outputClass ? `; output_class:${outputClass}` : ""}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cliOutputClass(stdout: Buffer[], stderr: Buffer[]): string {
|
|
84
|
+
if (stdout.some((item) => item.length === 0) || stderr.some((item) => item.length === 0)) return "overflow";
|
|
85
|
+
const raw = [Buffer.concat(stdout).toString("utf8"), Buffer.concat(stderr).toString("utf8")].join("\n");
|
|
86
|
+
if (/requested permissions|haven't granted|permission[^.]{0,80}(denied|blocked|required)|requires permission|not allowed/i.test(raw)) return "permission_prompt";
|
|
87
|
+
if (/not logged in|please run \/login|auth|authentication|credentials/i.test(raw)) return "auth_failure";
|
|
88
|
+
return raw.trim() ? "present" : "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function capture(chunks: Buffer[], chunk: Buffer, limit: number): boolean {
|
|
92
|
+
const used = chunks.reduce((sum, item) => sum + item.length, 0);
|
|
93
|
+
const remaining = limit - used;
|
|
94
|
+
if (remaining <= 0) return false;
|
|
95
|
+
if (chunk.length <= remaining) {
|
|
96
|
+
chunks.push(chunk);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
chunks.push(chunk.subarray(0, remaining), Buffer.alloc(0));
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function outputLimits(): { streamBytes: number } {
|
|
104
|
+
const configured = Number(process.env.MOLENKOPF_CLI_OUTPUT_LIMIT_BYTES);
|
|
105
|
+
return { streamBytes: Number.isInteger(configured) && configured > 0 ? configured : 1024 * 1024 };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cliSpawnSpec(command: string, args: string[]): { command: string; args: string[] } {
|
|
109
|
+
const resolved = resolveCliCommand(command);
|
|
110
|
+
if (process.platform === "win32" && /\.(?:cmd|bat)$/i.test(resolved)) return { command: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", resolved, ...args] };
|
|
111
|
+
if (process.platform === "win32" && /\.ps1$/i.test(resolved)) return { command: "powershell.exe", args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", resolved, ...args] };
|
|
112
|
+
return { command: resolved, args };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveCliCommand(command: string, env: Record<string, string | undefined> = process.env, platform = process.platform): string {
|
|
116
|
+
if (platform !== "win32" || hasExtension(command)) return command;
|
|
117
|
+
for (const candidate of windowsCandidates(command, env)) if (existsSync(candidate)) return candidate;
|
|
118
|
+
return command;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function windowsCandidates(command: string, env: Record<string, string | undefined>): string[] {
|
|
122
|
+
const exts = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
123
|
+
if (isPathLike(command)) return exts.map((ext) => `${command}${ext.toLowerCase()}`);
|
|
124
|
+
const pathValue = env.PATH ?? env.Path ?? env.path ?? "";
|
|
125
|
+
return pathValue.split(";").filter(Boolean).flatMap((dir) => exts.map((ext) => join(dir, `${command}${ext.toLowerCase()}`)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isPathLike(command: string): boolean {
|
|
129
|
+
return command.includes("/") || command.includes("\\");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasExtension(command: string): boolean {
|
|
133
|
+
return /\.[^\\/]+$/.test(command);
|
|
134
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { ProviderConfig } from "../../../core/src/providers/provider-catalog.ts";
|
|
2
|
+
import { estimateTokens } from "../../../core/src/utils/tokens.ts";
|
|
3
|
+
import { executeCliProvider } from "./cli-executor.ts";
|
|
4
|
+
import { cliRequest } from "./cli-request.ts";
|
|
5
|
+
|
|
6
|
+
export type CliProviderResult = {
|
|
7
|
+
status: number;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
body: Buffer;
|
|
10
|
+
usage?: { inputTokens?: number; outputTokens?: number };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function isCliProvider(provider: ProviderConfig): boolean {
|
|
14
|
+
return provider.kind === "cli" && (provider.runtime === "claude" || provider.runtime === "codex");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runCliProvider(provider: ProviderConfig, body: string, requestId: string, path = "/v1/responses"): Promise<CliProviderResult> {
|
|
18
|
+
const request = cliRequest(body, provider);
|
|
19
|
+
const prompt = request.prompt;
|
|
20
|
+
const output = await executeCliProvider(provider, prompt, request.runModel);
|
|
21
|
+
const usage = { inputTokens: estimateTokens(prompt), outputTokens: estimateTokens(output) };
|
|
22
|
+
const model = request.responseModel;
|
|
23
|
+
if (isAnthropicMessages(path) && wantsStream(body)) {
|
|
24
|
+
return {
|
|
25
|
+
status: 200,
|
|
26
|
+
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
|
|
27
|
+
body: Buffer.from(anthropicStream(requestId, model, output, usage)),
|
|
28
|
+
usage
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (isOpenAiResponses(path) && wantsStream(body)) {
|
|
32
|
+
return {
|
|
33
|
+
status: 200,
|
|
34
|
+
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
|
|
35
|
+
body: Buffer.from(openAiResponsesStream(requestId, model, output, usage)),
|
|
36
|
+
usage
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
status: 200,
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
body: Buffer.from(JSON.stringify(responseBody(requestId, model, output, path, usage))),
|
|
43
|
+
usage
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function responseBody(requestId: string, model: string, text: string, path: string, usage: { inputTokens?: number; outputTokens?: number }) {
|
|
48
|
+
const input = usage.inputTokens ?? 0, output = usage.outputTokens ?? 0;
|
|
49
|
+
if (isAnthropicMessages(path)) {
|
|
50
|
+
return {
|
|
51
|
+
id: `msg_${requestId.replaceAll("-", "")}`,
|
|
52
|
+
type: "message",
|
|
53
|
+
role: "assistant",
|
|
54
|
+
model,
|
|
55
|
+
content: [{ type: "text", text }],
|
|
56
|
+
stop_reason: "end_turn",
|
|
57
|
+
stop_sequence: null,
|
|
58
|
+
usage: { input_tokens: input, output_tokens: output }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
id: requestId,
|
|
63
|
+
object: "response",
|
|
64
|
+
model,
|
|
65
|
+
output_text: text,
|
|
66
|
+
output: [{
|
|
67
|
+
type: "message",
|
|
68
|
+
role: "assistant",
|
|
69
|
+
content: [{ type: "output_text", text }]
|
|
70
|
+
}],
|
|
71
|
+
usage: { input_tokens: input, output_tokens: output, prompt_tokens: input, completion_tokens: output }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isAnthropicMessages(path: string): boolean {
|
|
76
|
+
return path.split("?")[0] === "/v1/messages";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isOpenAiResponses(path: string): boolean {
|
|
80
|
+
return path.split("?")[0] === "/v1/responses";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function wantsStream(body: string): boolean {
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(body || "{}") as { stream?: unknown };
|
|
86
|
+
return data.stream === true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function anthropicStream(requestId: string, model: string, text: string, usage: { inputTokens?: number; outputTokens?: number }): string {
|
|
93
|
+
const id = `msg_${requestId.replaceAll("-", "")}`;
|
|
94
|
+
const input = usage.inputTokens ?? 0, output = usage.outputTokens ?? 0;
|
|
95
|
+
return [
|
|
96
|
+
sse("message_start", { type: "message_start", message: { id, type: "message", role: "assistant", model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: input, output_tokens: 0 } } }),
|
|
97
|
+
sse("content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }),
|
|
98
|
+
sse("content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text } }),
|
|
99
|
+
sse("content_block_stop", { type: "content_block_stop", index: 0 }),
|
|
100
|
+
sse("message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: output } }),
|
|
101
|
+
sse("message_stop", { type: "message_stop" })
|
|
102
|
+
].join("");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function openAiResponsesStream(requestId: string, model: string, text: string, usage: { inputTokens?: number; outputTokens?: number }): string {
|
|
106
|
+
return [
|
|
107
|
+
openAiResponsesStreamStart(requestId, model),
|
|
108
|
+
openAiResponsesStreamOutput(requestId, model, text, usage),
|
|
109
|
+
"data: [DONE]\n\n"
|
|
110
|
+
].join("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function openAiResponsesStreamStart(requestId: string, model: string): string {
|
|
114
|
+
const base = { id: requestId, object: "response", model, output: [] };
|
|
115
|
+
return [
|
|
116
|
+
sse("response.created", { type: "response.created", response: { ...base, status: "in_progress" } }),
|
|
117
|
+
sse("response.in_progress", { type: "response.in_progress", response: { ...base, status: "in_progress" } })
|
|
118
|
+
].join("");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function openAiResponsesStreamOutput(requestId: string, model: string, text: string, usage: { inputTokens?: number; outputTokens?: number }): string {
|
|
122
|
+
const input = usage.inputTokens ?? 0, output = usage.outputTokens ?? 0;
|
|
123
|
+
const itemId = `msg_${requestId.replaceAll("-", "")}`;
|
|
124
|
+
const base = { id: requestId, object: "response", model, output: [] };
|
|
125
|
+
const item = { id: itemId, type: "message", status: "completed", role: "assistant", content: [{ type: "output_text", text, annotations: [] }] };
|
|
126
|
+
const completed = {
|
|
127
|
+
...base,
|
|
128
|
+
status: "completed",
|
|
129
|
+
output: [item],
|
|
130
|
+
output_text: text,
|
|
131
|
+
usage: {
|
|
132
|
+
input_tokens: input,
|
|
133
|
+
output_tokens: output,
|
|
134
|
+
total_tokens: input + output,
|
|
135
|
+
prompt_tokens: input,
|
|
136
|
+
completion_tokens: output
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
return [
|
|
140
|
+
sse("response.output_item.added", { type: "response.output_item.added", output_index: 0, item: { ...item, status: "in_progress", content: [] } }),
|
|
141
|
+
sse("response.content_part.added", { type: "response.content_part.added", item_id: itemId, output_index: 0, content_index: 0, part: { type: "output_text", text: "", annotations: [] } }),
|
|
142
|
+
sse("response.output_text.delta", { type: "response.output_text.delta", item_id: itemId, output_index: 0, content_index: 0, delta: text }),
|
|
143
|
+
sse("response.output_text.done", { type: "response.output_text.done", item_id: itemId, output_index: 0, content_index: 0, text }),
|
|
144
|
+
sse("response.content_part.done", { type: "response.content_part.done", item_id: itemId, output_index: 0, content_index: 0, part: item.content[0] }),
|
|
145
|
+
sse("response.output_item.done", { type: "response.output_item.done", output_index: 0, item }),
|
|
146
|
+
sse("response.completed", { type: "response.completed", response: completed })
|
|
147
|
+
].join("");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function openAiResponsesStreamFailure(requestId: string, model: string): string {
|
|
151
|
+
return [
|
|
152
|
+
sse("response.failed", {
|
|
153
|
+
type: "response.failed",
|
|
154
|
+
response: { id: requestId, object: "response", model, status: "failed", error: { code: "proxy_error", message: "Local CLI provider failed." } }
|
|
155
|
+
}),
|
|
156
|
+
"data: [DONE]\n\n"
|
|
157
|
+
].join("");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sse(event: string, data: unknown): string {
|
|
161
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
162
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ProviderConfig } from "../../../core/src/providers/provider-catalog.ts";
|
|
2
|
+
|
|
3
|
+
type ParsedRequest = Record<string, unknown> | undefined;
|
|
4
|
+
|
|
5
|
+
export type CliRequestShape = {
|
|
6
|
+
prompt: string;
|
|
7
|
+
responseModel: string;
|
|
8
|
+
runModel?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function cliRequest(body: string, provider: ProviderConfig): CliRequestShape {
|
|
12
|
+
const parsed = parseBody(body);
|
|
13
|
+
const model = requestModel(parsed, provider);
|
|
14
|
+
return {
|
|
15
|
+
prompt: parsed ? promptFromJson(parsed) || body : body,
|
|
16
|
+
responseModel: model.responseModel,
|
|
17
|
+
runModel: model.runModel
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cliArgs(provider: ProviderConfig, runModel?: string): string[] {
|
|
22
|
+
const args = [...(provider.cliArgs ?? defaultArgs(provider))];
|
|
23
|
+
if (provider.runtime === "claude") {
|
|
24
|
+
ensureFlag(args, "--no-session-persistence");
|
|
25
|
+
if (runModel && !hasModelFlag(args)) args.push("--model", runModel);
|
|
26
|
+
}
|
|
27
|
+
if (provider.runtime === "codex") {
|
|
28
|
+
ensureFlag(args, "--ephemeral");
|
|
29
|
+
if (provider.runtimeAuthDir) ensureFlag(args, "--ignore-user-config");
|
|
30
|
+
if (runModel && !hasModelFlag(args)) args.push("-m", runModel);
|
|
31
|
+
}
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requestModel(parsed: ParsedRequest, provider: ProviderConfig): { responseModel: string; runModel?: string } {
|
|
36
|
+
const raw = typeof parsed?.model === "string" ? parsed.model.trim() : "";
|
|
37
|
+
const fallback = provider.runtime === "claude" ? "sonnet" : provider.runtime === "codex" ? "gpt-5" : provider.id;
|
|
38
|
+
if (!raw) return { responseModel: fallback };
|
|
39
|
+
return { responseModel: raw, runModel: raw };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultArgs(provider: ProviderConfig): string[] {
|
|
43
|
+
return provider.runtime === "codex" ? ["exec"] : ["--print"];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureFlag(args: string[], flag: string): void {
|
|
47
|
+
if (!args.includes(flag)) args.push(flag);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasModelFlag(args: string[]): boolean {
|
|
51
|
+
return args.some((arg) => arg === "-m" || arg === "--model" || arg.startsWith("--model="));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseBody(body: string): ParsedRequest {
|
|
55
|
+
if (!body.trim()) return {};
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(body);
|
|
58
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as ParsedRequest : undefined;
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function promptFromJson(record: Record<string, unknown>): string {
|
|
65
|
+
return collectText(record.input) || collectMessages(record.messages) || collectText(record.prompt);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function collectMessages(value: unknown): string {
|
|
69
|
+
if (!Array.isArray(value)) return "";
|
|
70
|
+
return value.map((item) => collectText((item as Record<string, unknown>)?.content)).filter(Boolean).join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectText(value: unknown): string {
|
|
74
|
+
if (typeof value === "string") return value;
|
|
75
|
+
if (Array.isArray(value)) return value.map(collectText).filter(Boolean).join("\n");
|
|
76
|
+
if (!value || typeof value !== "object") return "";
|
|
77
|
+
const record = value as Record<string, unknown>;
|
|
78
|
+
return collectText(record.text ?? record.input_text ?? record.output_text ?? record.content);
|
|
79
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RuntimeProfileConfig } from "../../../core/src/providers/provider-catalog.ts";
|
|
2
|
+
|
|
3
|
+
const CODEX_SANDBOX = new Set(["read-only", "workspace-write", "danger-full-access"]);
|
|
4
|
+
const CODEX_APPROVAL = new Set(["untrusted", "on-failure", "on-request", "never"]);
|
|
5
|
+
|
|
6
|
+
export function codexConfigSummary(toml: string): RuntimeProfileConfig {
|
|
7
|
+
const values = topLevelValues(toml);
|
|
8
|
+
const bypass = values.get("dangerously_bypass_approvals_and_sandbox") === "true";
|
|
9
|
+
return {
|
|
10
|
+
sandbox: bypass ? "danger-full-access" : enumText(values.get("sandbox_mode") || "", CODEX_SANDBOX, "invalid_sandbox"),
|
|
11
|
+
approval: bypass ? "never" : enumText(values.get("approval_policy") || "", CODEX_APPROVAL, "invalid_approval")
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function topLevelValues(toml: string): Map<string, string> {
|
|
16
|
+
const values = new Map<string, string>();
|
|
17
|
+
for (const rawLine of toml.split(/\r?\n/)) {
|
|
18
|
+
const line = rawLine.trim();
|
|
19
|
+
if (!line || line.startsWith("#")) continue;
|
|
20
|
+
if (line.startsWith("[")) break;
|
|
21
|
+
const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
|
|
22
|
+
if (match) values.set(match[1], cleanValue(match[2]));
|
|
23
|
+
}
|
|
24
|
+
return values;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cleanValue(value: string): string {
|
|
28
|
+
const trimmed = value.replace(/\s+#.*$/, "").trim();
|
|
29
|
+
const quoted = trimmed.match(/^"([^"]*)"$/) || trimmed.match(/^'([^']*)'$/);
|
|
30
|
+
return (quoted?.[1] ?? trimmed).replace(/_/g, "-");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function enumText(value: string, allowed: Set<string>, error: string): string | undefined {
|
|
34
|
+
if (!value) return undefined;
|
|
35
|
+
if (!allowed.has(value)) throw new Error(error);
|
|
36
|
+
return value;
|
|
37
|
+
}
|