create-quorum-router 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/bin/create-quorum-router.js +25 -4
- package/package.json +1 -1
- package/templates/basic/README.md +64 -6
- package/templates/basic/deno.json +1 -0
- package/templates/basic/router.config.example.json +11 -0
- package/templates/basic/src/auth_session.ts +1 -0
- package/templates/basic/src/best_route.ts +10 -1
- package/templates/basic/src/cli.ts +14 -2
- package/templates/basic/src/cost_aware.ts +150 -0
- package/templates/basic/src/provider_registry.ts +14 -7
- package/templates/basic/src/schema.ts +87 -0
- package/templates/basic/src/supabase.ts +462 -0
- package/templates/basic/src/trace.ts +3 -0
- package/templates/basic/src/wrapper_client.ts +46 -13
- package/templates/basic/supabase/migrations/20260701130000_workflow_access_audit.sql +125 -0
- package/templates/basic/supabase/migrations/20260712211500_workflow_access_audit_limits.sql +94 -0
|
@@ -1,3 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const SupabaseAuditModeSchema = z.enum([
|
|
4
|
+
"disabled",
|
|
5
|
+
"optional",
|
|
6
|
+
"required",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export type SupabaseAuditMode = z.infer<typeof SupabaseAuditModeSchema>;
|
|
10
|
+
|
|
11
|
+
export const SupabaseFeatureConfigSchema = z.object({
|
|
12
|
+
audit: z.object({
|
|
13
|
+
mode: SupabaseAuditModeSchema,
|
|
14
|
+
}).strict(),
|
|
15
|
+
}).strict();
|
|
16
|
+
|
|
17
|
+
export type SupabaseFeatureConfig = z.infer<
|
|
18
|
+
typeof SupabaseFeatureConfigSchema
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export const SelectiveFeatureConfigSchema = z.object({
|
|
22
|
+
supabase: SupabaseFeatureConfigSchema.optional(),
|
|
23
|
+
}).strict();
|
|
24
|
+
|
|
25
|
+
const SECRET_CONFIG_KEY =
|
|
26
|
+
/^(?:quorum_router_)?supabase_(?:url|anon_key|publishable_key|session_jwt|access_token|service_role_key|admin_key)$/i;
|
|
27
|
+
|
|
28
|
+
function normalizedConfigKey(key: string): string {
|
|
29
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-z0-9]+/gi, "_")
|
|
30
|
+
.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findSupabaseCredentialPath(
|
|
34
|
+
value: unknown,
|
|
35
|
+
path: Array<string | number> = [],
|
|
36
|
+
): Array<string | number> | undefined {
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
39
|
+
const found = findSupabaseCredentialPath(value[index], [...path, index]);
|
|
40
|
+
if (found) return found;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!value || typeof value !== "object") return undefined;
|
|
45
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
46
|
+
const normalized = normalizedConfigKey(key);
|
|
47
|
+
if (
|
|
48
|
+
SECRET_CONFIG_KEY.test(normalized) ||
|
|
49
|
+
(/supabase/.test(normalized) &&
|
|
50
|
+
/(secret|token|key|credential|password|url)/.test(normalized))
|
|
51
|
+
) {
|
|
52
|
+
return [...path, key];
|
|
53
|
+
}
|
|
54
|
+
const found = findSupabaseCredentialPath(nested, [...path, key]);
|
|
55
|
+
if (found) return found;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const GeneratedRouterConfigSchema = z.object({
|
|
61
|
+
features: SelectiveFeatureConfigSchema.optional(),
|
|
62
|
+
}).passthrough().superRefine((value, context) => {
|
|
63
|
+
const path = findSupabaseCredentialPath(value);
|
|
64
|
+
if (path) {
|
|
65
|
+
context.addIssue({
|
|
66
|
+
code: z.ZodIssueCode.custom,
|
|
67
|
+
path,
|
|
68
|
+
message: "Supabase runtime credentials belong in the environment only",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
1
73
|
export type AuthMode = "auto" | "wrapper" | "oauth" | "session" | "env";
|
|
2
74
|
export type InventorySource =
|
|
3
75
|
| "wrapper"
|
|
@@ -21,6 +93,7 @@ export type ModelInventoryEntry = {
|
|
|
21
93
|
notes: string[];
|
|
22
94
|
command?: string;
|
|
23
95
|
args_template?: string[];
|
|
96
|
+
prompt_transport?: "stdin" | "file";
|
|
24
97
|
invocation_model?: string;
|
|
25
98
|
};
|
|
26
99
|
|
|
@@ -82,6 +155,19 @@ export type PromptContextTrace = {
|
|
|
82
155
|
context_fetch_error?: string;
|
|
83
156
|
};
|
|
84
157
|
|
|
158
|
+
export type CostAwareTrace = {
|
|
159
|
+
enabled: boolean;
|
|
160
|
+
pricing_source: "configured_estimate_not_live_billing";
|
|
161
|
+
max_budget_usd?: number;
|
|
162
|
+
estimated_total_usd?: number;
|
|
163
|
+
selected_model_ids: string[];
|
|
164
|
+
excluded: Array<{
|
|
165
|
+
model_id: string;
|
|
166
|
+
estimated_cost_usd?: number;
|
|
167
|
+
reason: "missing_estimate" | "budget_exceeded";
|
|
168
|
+
}>;
|
|
169
|
+
};
|
|
170
|
+
|
|
85
171
|
export type DogfoodTrace = {
|
|
86
172
|
run_id: string;
|
|
87
173
|
timestamp: string;
|
|
@@ -99,6 +185,7 @@ export type DogfoodTrace = {
|
|
|
99
185
|
prompt_hash?: string;
|
|
100
186
|
prompt_summary?: string;
|
|
101
187
|
prompt_context?: PromptContextTrace;
|
|
188
|
+
cost_aware?: CostAwareTrace;
|
|
102
189
|
response_summary?: string;
|
|
103
190
|
schema_valid: boolean;
|
|
104
191
|
redaction_ok: boolean;
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
GeneratedRouterConfigSchema,
|
|
4
|
+
type SupabaseAuditMode,
|
|
5
|
+
} from "./schema.ts";
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = "router.config.json";
|
|
8
|
+
const AUDIT_RPC_PATH = "/rest/v1/rpc/insert_workflow_access_audit_batch";
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
10
|
+
const MAX_TIMEOUT_MS = 30_000;
|
|
11
|
+
|
|
12
|
+
const FORBIDDEN_SUPABASE_ENV_KEYS = new Set([
|
|
13
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
14
|
+
"QUORUM_ROUTER_SUPABASE_SERVICE_ROLE_KEY",
|
|
15
|
+
"SUPABASE_SERVICE_KEY",
|
|
16
|
+
"QUORUM_ROUTER_SUPABASE_SERVICE_KEY",
|
|
17
|
+
"SUPABASE_ADMIN_KEY",
|
|
18
|
+
"QUORUM_ROUTER_SUPABASE_ADMIN_KEY",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export type SupabaseStatusKind =
|
|
22
|
+
| "disabled"
|
|
23
|
+
| "configured"
|
|
24
|
+
| "partial"
|
|
25
|
+
| "forbidden-credential";
|
|
26
|
+
|
|
27
|
+
export type SupabaseStatus = {
|
|
28
|
+
kind: SupabaseStatusKind;
|
|
29
|
+
mode: SupabaseAuditMode;
|
|
30
|
+
ok: boolean;
|
|
31
|
+
configuredFields: number;
|
|
32
|
+
requiredFields: number;
|
|
33
|
+
forbiddenEnvKeys: string[];
|
|
34
|
+
detail: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type SupabaseAuditRecord = {
|
|
38
|
+
workflow_id: string;
|
|
39
|
+
route?: string;
|
|
40
|
+
decision: "allow" | "error";
|
|
41
|
+
metadata: {
|
|
42
|
+
command: string;
|
|
43
|
+
mode: string;
|
|
44
|
+
auth_mode: string;
|
|
45
|
+
provider_selection_honored: boolean;
|
|
46
|
+
fallback_used: boolean;
|
|
47
|
+
schema_valid: boolean;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type RouteAuditSource = {
|
|
52
|
+
run_id: string;
|
|
53
|
+
command: string;
|
|
54
|
+
mode: string;
|
|
55
|
+
auth_mode: string;
|
|
56
|
+
selected_provider?: string;
|
|
57
|
+
selected_model?: string;
|
|
58
|
+
provider_selection_honored: boolean;
|
|
59
|
+
fallback_used: boolean;
|
|
60
|
+
schema_valid: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type AuditRequestOptions = {
|
|
64
|
+
fetchFn?: typeof fetch;
|
|
65
|
+
signal?: AbortSignal;
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
configPath?: string;
|
|
68
|
+
allowInsecureLocalhostForTest?: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type RuntimeCredentials = {
|
|
72
|
+
url?: string;
|
|
73
|
+
anonKey?: string;
|
|
74
|
+
sessionJwt?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function envValue(...names: string[]): string | undefined {
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
const value = Deno.env.get(name)?.trim();
|
|
80
|
+
if (value) return value;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runtimeCredentials(): RuntimeCredentials {
|
|
86
|
+
return {
|
|
87
|
+
url: envValue("QUORUM_ROUTER_SUPABASE_URL", "SUPABASE_URL"),
|
|
88
|
+
anonKey: envValue(
|
|
89
|
+
"QUORUM_ROUTER_SUPABASE_ANON_KEY",
|
|
90
|
+
"SUPABASE_ANON_KEY",
|
|
91
|
+
),
|
|
92
|
+
sessionJwt: envValue(
|
|
93
|
+
"QUORUM_ROUTER_SUPABASE_SESSION_JWT",
|
|
94
|
+
"SUPABASE_SESSION_JWT",
|
|
95
|
+
),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function forbiddenSupabaseEnvKeys(): string[] {
|
|
100
|
+
return Object.keys(Deno.env.toObject()).filter((key) =>
|
|
101
|
+
FORBIDDEN_SUPABASE_ENV_KEYS.has(key.toUpperCase()) ||
|
|
102
|
+
/SUPABASE.*(SERVICE.*ROLE|SERVICE.*KEY|ADMIN.*KEY|JWT.*SECRET)/i.test(key)
|
|
103
|
+
).sort();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function decodeJwtPayload(value: string): Record<string, unknown> | undefined {
|
|
107
|
+
const payload = value.split(".")[1];
|
|
108
|
+
if (!payload) return undefined;
|
|
109
|
+
try {
|
|
110
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/")
|
|
111
|
+
.padEnd(Math.ceil(payload.length / 4) * 4, "=");
|
|
112
|
+
const parsed = JSON.parse(atob(normalized));
|
|
113
|
+
return parsed && typeof parsed === "object"
|
|
114
|
+
? parsed as Record<string, unknown>
|
|
115
|
+
: undefined;
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isPrivilegedCredential(value: string): boolean {
|
|
122
|
+
if (/^sb_secret_/i.test(value) || /service[_-]?role/i.test(value)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const role = decodeJwtPayload(value)?.role;
|
|
126
|
+
return typeof role === "string" &&
|
|
127
|
+
/^(service_role|supabase_admin)$/i.test(role);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertRuntimeCredentialBoundary(credentials: RuntimeCredentials) {
|
|
131
|
+
const forbiddenKeys = forbiddenSupabaseEnvKeys();
|
|
132
|
+
if (forbiddenKeys.length > 0) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Supabase audit forbidden-credential: runtime env keys present: ${
|
|
135
|
+
forbiddenKeys.join(", ")
|
|
136
|
+
}; values hidden`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
(credentials.anonKey && isPrivilegedCredential(credentials.anonKey)) ||
|
|
141
|
+
(credentials.sessionJwt && isPrivilegedCredential(credentials.sessionJwt))
|
|
142
|
+
) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
"Supabase audit forbidden-credential: service-role/admin credential detected; value hidden",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function loadSupabaseAuditMode(
|
|
150
|
+
path = CONFIG_PATH,
|
|
151
|
+
): Promise<SupabaseAuditMode> {
|
|
152
|
+
let text: string;
|
|
153
|
+
try {
|
|
154
|
+
text = await Deno.readTextFile(path);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof Deno.errors.NotFound) return "disabled";
|
|
157
|
+
throw new Error(`Supabase feature config unreadable: ${path}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let raw: unknown;
|
|
161
|
+
try {
|
|
162
|
+
raw = JSON.parse(text);
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error(`Supabase feature config invalid JSON: ${path}`);
|
|
165
|
+
}
|
|
166
|
+
const parsed = GeneratedRouterConfigSchema.safeParse(raw);
|
|
167
|
+
if (!parsed.success) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Supabase feature config invalid: ${
|
|
170
|
+
parsed.error.issues.map((issue) => issue.path.join(".") || "config")
|
|
171
|
+
.join(", ")
|
|
172
|
+
}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return parsed.data.features?.supabase?.audit.mode ?? "disabled";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function getSupabaseStatus(
|
|
179
|
+
configPath = CONFIG_PATH,
|
|
180
|
+
): Promise<SupabaseStatus> {
|
|
181
|
+
const mode = await loadSupabaseAuditMode(configPath);
|
|
182
|
+
const credentials = runtimeCredentials();
|
|
183
|
+
const forbiddenEnvKeys = forbiddenSupabaseEnvKeys();
|
|
184
|
+
if (
|
|
185
|
+
forbiddenEnvKeys.length > 0 ||
|
|
186
|
+
(credentials.anonKey && isPrivilegedCredential(credentials.anonKey)) ||
|
|
187
|
+
(credentials.sessionJwt && isPrivilegedCredential(credentials.sessionJwt))
|
|
188
|
+
) {
|
|
189
|
+
return {
|
|
190
|
+
kind: "forbidden-credential",
|
|
191
|
+
mode,
|
|
192
|
+
ok: false,
|
|
193
|
+
configuredFields: Object.values(credentials).filter(Boolean).length,
|
|
194
|
+
requiredFields: 3,
|
|
195
|
+
forbiddenEnvKeys,
|
|
196
|
+
detail: "service-role/admin runtime credential rejected; values hidden",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const configuredFields = Object.values(credentials).filter(Boolean).length;
|
|
201
|
+
if (configuredFields > 0 && configuredFields < 3) {
|
|
202
|
+
return {
|
|
203
|
+
kind: "partial",
|
|
204
|
+
mode,
|
|
205
|
+
ok: false,
|
|
206
|
+
configuredFields,
|
|
207
|
+
requiredFields: 3,
|
|
208
|
+
forbiddenEnvKeys: [],
|
|
209
|
+
detail:
|
|
210
|
+
"URL, publishable/anon key, and user session JWT must be set together; values hidden",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (mode === "disabled") {
|
|
214
|
+
return {
|
|
215
|
+
kind: "disabled",
|
|
216
|
+
mode,
|
|
217
|
+
ok: true,
|
|
218
|
+
configuredFields,
|
|
219
|
+
requiredFields: 3,
|
|
220
|
+
forbiddenEnvKeys: [],
|
|
221
|
+
detail: configuredFields === 3
|
|
222
|
+
? "credentials present but audit is disabled; values hidden"
|
|
223
|
+
: "Supabase audit is disabled and no credentials are required",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (configuredFields === 3) {
|
|
227
|
+
return {
|
|
228
|
+
kind: "configured",
|
|
229
|
+
mode,
|
|
230
|
+
ok: true,
|
|
231
|
+
configuredFields,
|
|
232
|
+
requiredFields: 3,
|
|
233
|
+
forbiddenEnvKeys: [],
|
|
234
|
+
detail:
|
|
235
|
+
"runtime URL, publishable/anon key, and user session JWT are configured; values hidden",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
kind: "partial",
|
|
240
|
+
mode,
|
|
241
|
+
ok: false,
|
|
242
|
+
configuredFields,
|
|
243
|
+
requiredFields: 3,
|
|
244
|
+
forbiddenEnvKeys: [],
|
|
245
|
+
detail:
|
|
246
|
+
"audit mode requires URL, publishable/anon key, and user session JWT; values hidden",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function runSupabaseStatus(): Promise<void> {
|
|
251
|
+
const status = await getSupabaseStatus();
|
|
252
|
+
console.log("QuorumRouter Supabase audit status");
|
|
253
|
+
console.log(`state: ${status.kind}`);
|
|
254
|
+
console.log(`audit_mode: ${status.mode}`);
|
|
255
|
+
console.log(
|
|
256
|
+
`runtime_credentials: ${status.configuredFields}/${status.requiredFields} configured; values hidden`,
|
|
257
|
+
);
|
|
258
|
+
if (status.forbiddenEnvKeys.length > 0) {
|
|
259
|
+
console.log(`forbidden_env_keys: ${status.forbiddenEnvKeys.join(", ")}`);
|
|
260
|
+
}
|
|
261
|
+
console.log(`detail: ${status.detail}`);
|
|
262
|
+
console.log("network_request_sent: false");
|
|
263
|
+
if (!status.ok) Deno.exitCode = 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function toSupabaseAuditRecord(
|
|
267
|
+
source: RouteAuditSource,
|
|
268
|
+
): SupabaseAuditRecord {
|
|
269
|
+
const route = source.selected_provider && source.selected_model
|
|
270
|
+
? `${source.selected_provider}/${source.selected_model}`
|
|
271
|
+
: source.selected_provider;
|
|
272
|
+
return {
|
|
273
|
+
workflow_id: source.run_id,
|
|
274
|
+
route,
|
|
275
|
+
decision: source.schema_valid ? "allow" : "error",
|
|
276
|
+
metadata: {
|
|
277
|
+
command: source.command,
|
|
278
|
+
mode: source.mode,
|
|
279
|
+
auth_mode: source.auth_mode,
|
|
280
|
+
provider_selection_honored: source.provider_selection_honored,
|
|
281
|
+
fallback_used: source.fallback_used,
|
|
282
|
+
schema_valid: source.schema_valid,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function auditEndpoint(
|
|
288
|
+
rawUrl: string,
|
|
289
|
+
allowInsecureLocalhostForTest = false,
|
|
290
|
+
): string {
|
|
291
|
+
let url: URL;
|
|
292
|
+
try {
|
|
293
|
+
url = new URL(rawUrl);
|
|
294
|
+
} catch {
|
|
295
|
+
throw new Error("Supabase audit invalid project URL; value hidden");
|
|
296
|
+
}
|
|
297
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" ||
|
|
298
|
+
url.hostname === "localhost";
|
|
299
|
+
const allowedProtocol = url.protocol === "https:" ||
|
|
300
|
+
(allowInsecureLocalhostForTest && loopback && url.protocol === "http:");
|
|
301
|
+
if (!allowedProtocol || url.username || url.password) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
"Supabase audit invalid project URL; HTTPS required; value hidden",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return new URL(AUDIT_RPC_PATH, url).toString();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function boundedTimeout(value: number | undefined): number {
|
|
310
|
+
if (!Number.isFinite(value) || (value ?? 0) <= 0) return DEFAULT_TIMEOUT_MS;
|
|
311
|
+
return Math.min(Math.floor(value!), MAX_TIMEOUT_MS);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function httpFailure(status: number): Error {
|
|
315
|
+
if (status === 401 || status === 403) {
|
|
316
|
+
return new Error(`Supabase audit auth failure (HTTP ${status})`);
|
|
317
|
+
}
|
|
318
|
+
if (status === 429) {
|
|
319
|
+
return new Error("Supabase audit rate limited (HTTP 429)");
|
|
320
|
+
}
|
|
321
|
+
if (status >= 500) {
|
|
322
|
+
return new Error(`Supabase audit server failure (HTTP ${status})`);
|
|
323
|
+
}
|
|
324
|
+
return new Error(`Supabase audit RPC rejected request (HTTP ${status})`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function sendSupabaseAuditRecord(
|
|
328
|
+
record: SupabaseAuditRecord,
|
|
329
|
+
credentials: Required<RuntimeCredentials>,
|
|
330
|
+
options: AuditRequestOptions = {},
|
|
331
|
+
): Promise<void> {
|
|
332
|
+
assertRuntimeCredentialBoundary(credentials);
|
|
333
|
+
const endpoint = auditEndpoint(
|
|
334
|
+
credentials.url,
|
|
335
|
+
options.allowInsecureLocalhostForTest,
|
|
336
|
+
);
|
|
337
|
+
if (options.signal?.aborted) {
|
|
338
|
+
throw new Error("Supabase audit request aborted");
|
|
339
|
+
}
|
|
340
|
+
const controller = new AbortController();
|
|
341
|
+
let timedOut = false;
|
|
342
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
343
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
344
|
+
const timeout = setTimeout(() => {
|
|
345
|
+
timedOut = true;
|
|
346
|
+
controller.abort();
|
|
347
|
+
}, boundedTimeout(options.timeoutMs));
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const response = await (options.fetchFn ?? fetch)(endpoint, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: {
|
|
353
|
+
"content-type": "application/json",
|
|
354
|
+
"prefer": "return=minimal",
|
|
355
|
+
"authorization": `Bearer ${credentials.sessionJwt}`,
|
|
356
|
+
"apikey": credentials.anonKey,
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify({ records: [record] }),
|
|
359
|
+
signal: controller.signal,
|
|
360
|
+
redirect: "error",
|
|
361
|
+
});
|
|
362
|
+
if (!response.ok) throw httpFailure(response.status);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (timedOut) throw new Error("Supabase audit request timed out");
|
|
365
|
+
if (controller.signal.aborted) {
|
|
366
|
+
throw new Error("Supabase audit request aborted");
|
|
367
|
+
}
|
|
368
|
+
if (error instanceof Error && error.message.startsWith("Supabase audit")) {
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
throw new Error("Supabase audit network failure");
|
|
372
|
+
} finally {
|
|
373
|
+
clearTimeout(timeout);
|
|
374
|
+
options.signal?.removeEventListener("abort", abort);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function preflightRequiredSupabaseAudit(
|
|
379
|
+
options: AuditRequestOptions = {},
|
|
380
|
+
): Promise<void> {
|
|
381
|
+
const mode = await loadSupabaseAuditMode(options.configPath);
|
|
382
|
+
if (mode !== "required") return;
|
|
383
|
+
const credentials = runtimeCredentials();
|
|
384
|
+
assertRuntimeCredentialBoundary(credentials);
|
|
385
|
+
const missing = Object.entries(credentials).filter(([, value]) => !value).map(
|
|
386
|
+
([key]) => key,
|
|
387
|
+
);
|
|
388
|
+
if (missing.length > 0) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Required Supabase audit incomplete runtime configuration: missing ${
|
|
391
|
+
missing.join(", ")
|
|
392
|
+
}; provider invocation blocked`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
auditEndpoint(
|
|
396
|
+
credentials.url!,
|
|
397
|
+
options.allowInsecureLocalhostForTest,
|
|
398
|
+
);
|
|
399
|
+
const claims = decodeJwtPayload(credentials.sessionJwt!);
|
|
400
|
+
if (
|
|
401
|
+
claims?.role !== "authenticated" ||
|
|
402
|
+
typeof claims.sub !== "string" || claims.sub.length === 0
|
|
403
|
+
) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
"Required Supabase audit needs an authenticated user session JWT; provider invocation blocked",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function auditRouteOutcome(
|
|
411
|
+
source: RouteAuditSource,
|
|
412
|
+
options: AuditRequestOptions = {},
|
|
413
|
+
): Promise<void> {
|
|
414
|
+
const credentials = runtimeCredentials();
|
|
415
|
+
assertRuntimeCredentialBoundary(credentials);
|
|
416
|
+
const mode = await loadSupabaseAuditMode(options.configPath);
|
|
417
|
+
if (mode === "disabled") return;
|
|
418
|
+
|
|
419
|
+
const missing = Object.entries(credentials).filter(([, value]) => !value)
|
|
420
|
+
.map(([key]) => key);
|
|
421
|
+
try {
|
|
422
|
+
if (missing.length > 0) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Supabase audit incomplete runtime configuration: missing ${
|
|
425
|
+
missing.join(", ")
|
|
426
|
+
}; values hidden`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
await sendSupabaseAuditRecord(
|
|
430
|
+
toSupabaseAuditRecord(source),
|
|
431
|
+
credentials as Required<RuntimeCredentials>,
|
|
432
|
+
options,
|
|
433
|
+
);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const message = error instanceof Error
|
|
436
|
+
? error.message
|
|
437
|
+
: "Supabase audit failed";
|
|
438
|
+
if (mode === "optional") {
|
|
439
|
+
console.warn(`WARNING: optional ${message}; route result preserved`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`Required ${message}; route result withheld`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export const SupabaseAuditPayloadSchema = z.object({
|
|
447
|
+
records: z.array(
|
|
448
|
+
z.object({
|
|
449
|
+
workflow_id: z.string().min(1),
|
|
450
|
+
route: z.string().optional(),
|
|
451
|
+
decision: z.enum(["allow", "error"]),
|
|
452
|
+
metadata: z.object({
|
|
453
|
+
command: z.string(),
|
|
454
|
+
mode: z.string(),
|
|
455
|
+
auth_mode: z.string(),
|
|
456
|
+
provider_selection_honored: z.boolean(),
|
|
457
|
+
fallback_used: z.boolean(),
|
|
458
|
+
schema_valid: z.boolean(),
|
|
459
|
+
}).strict(),
|
|
460
|
+
}).strict(),
|
|
461
|
+
).length(1),
|
|
462
|
+
}).strict();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentChatTurn,
|
|
3
3
|
AuthMode,
|
|
4
|
+
CostAwareTrace,
|
|
4
5
|
DogfoodTrace,
|
|
5
6
|
PromptContextTrace,
|
|
6
7
|
ProviderResult,
|
|
@@ -74,6 +75,7 @@ export async function buildTrace(args: {
|
|
|
74
75
|
requestedModel?: string;
|
|
75
76
|
providerSelectionHonored?: boolean;
|
|
76
77
|
fallbackUsed?: boolean;
|
|
78
|
+
costAware?: CostAwareTrace;
|
|
77
79
|
agentChatTurns?: AgentChatTurn[];
|
|
78
80
|
}): Promise<DogfoodTrace> {
|
|
79
81
|
const responseSummary = args.results?.map((r) =>
|
|
@@ -93,6 +95,7 @@ export async function buildTrace(args: {
|
|
|
93
95
|
selected_model: args.selected?.model ?? args.results?.[0]?.model,
|
|
94
96
|
provider_selection_honored: args.providerSelectionHonored ?? true,
|
|
95
97
|
fallback_used: args.fallbackUsed ?? false,
|
|
98
|
+
cost_aware: args.costAware,
|
|
96
99
|
prompt_hash: args.prompt ? await promptHash(args.prompt) : undefined,
|
|
97
100
|
prompt_summary: args.prompt ? summarize(args.prompt, 160) : undefined,
|
|
98
101
|
prompt_context: args.promptContext,
|
|
@@ -3,18 +3,21 @@ import { redact, summarize } from "./redact.ts";
|
|
|
3
3
|
|
|
4
4
|
export function buildWrapperArgs(
|
|
5
5
|
entry: ModelInventoryEntry,
|
|
6
|
-
prompt: string,
|
|
7
6
|
outPath: string,
|
|
7
|
+
promptPath?: string,
|
|
8
8
|
): string[] {
|
|
9
9
|
const args = (entry.args_template ?? []).map((arg) =>
|
|
10
|
-
arg === "
|
|
11
|
-
? prompt
|
|
12
|
-
: arg === "__CWD__"
|
|
10
|
+
arg === "__CWD__"
|
|
13
11
|
? Deno.cwd()
|
|
14
12
|
: arg === "__OUT__"
|
|
15
13
|
? outPath
|
|
14
|
+
: arg === "__PROMPT_FILE__"
|
|
15
|
+
? promptPath ?? ""
|
|
16
16
|
: arg
|
|
17
17
|
);
|
|
18
|
+
if (args.includes("__PROMPT__") || args.includes("__PROMPT_FILE__")) {
|
|
19
|
+
throw new Error("QuorumRouter blocked: unresolved prompt placeholder");
|
|
20
|
+
}
|
|
18
21
|
if (entry.command === "grok" && entry.invocation_model) {
|
|
19
22
|
if (entry.invocation_model === "grok-build") {
|
|
20
23
|
const withoutPlanMode: string[] = [];
|
|
@@ -166,28 +169,57 @@ export async function callWrapper(
|
|
|
166
169
|
`QuorumRouter blocked: ${entry.provider} has no command`,
|
|
167
170
|
);
|
|
168
171
|
}
|
|
169
|
-
await Deno.mkdir("out", {
|
|
170
|
-
|
|
172
|
+
await Deno.mkdir("out", { recursive: true, mode: 0o700 });
|
|
173
|
+
const outPath = await Deno.makeTempFile({
|
|
174
|
+
dir: "out",
|
|
175
|
+
prefix: "quorum-router-output-",
|
|
176
|
+
suffix: ".txt",
|
|
171
177
|
});
|
|
172
|
-
|
|
178
|
+
await Deno.chmod(outPath, 0o600);
|
|
179
|
+
const absoluteOutPath = await Deno.realPath(outPath);
|
|
180
|
+
const promptPath = entry.prompt_transport === "file"
|
|
181
|
+
? await Deno.makeTempFile({
|
|
182
|
+
dir: "out",
|
|
183
|
+
prefix: "quorum-router-prompt-",
|
|
184
|
+
suffix: ".txt",
|
|
185
|
+
})
|
|
186
|
+
: undefined;
|
|
187
|
+
if (promptPath) {
|
|
188
|
+
await Deno.chmod(promptPath, 0o600);
|
|
189
|
+
await Deno.writeTextFile(promptPath, prompt);
|
|
190
|
+
}
|
|
191
|
+
const absolutePromptPath = promptPath
|
|
192
|
+
? await Deno.realPath(promptPath)
|
|
193
|
+
: undefined;
|
|
173
194
|
try {
|
|
174
|
-
const runWrapper = () =>
|
|
175
|
-
new Deno.Command(entry.command!, {
|
|
176
|
-
args: buildWrapperArgs(
|
|
195
|
+
const runWrapper = async () => {
|
|
196
|
+
const child = new Deno.Command(entry.command!, {
|
|
197
|
+
args: buildWrapperArgs(
|
|
198
|
+
entry,
|
|
199
|
+
absoluteOutPath,
|
|
200
|
+
absolutePromptPath,
|
|
201
|
+
),
|
|
177
202
|
cwd: Deno.env.get("TMPDIR") || "/tmp",
|
|
178
203
|
clearEnv: true,
|
|
179
204
|
env: safeWrapperEnv(),
|
|
180
|
-
stdin: "null",
|
|
205
|
+
stdin: entry.prompt_transport === "stdin" ? "piped" : "null",
|
|
181
206
|
stdout: "piped",
|
|
182
207
|
stderr: "piped",
|
|
183
208
|
}).spawn();
|
|
184
|
-
|
|
209
|
+
if (entry.prompt_transport === "stdin") {
|
|
210
|
+
const writer = child.stdin.getWriter();
|
|
211
|
+
await writer.write(new TextEncoder().encode(prompt));
|
|
212
|
+
await writer.close();
|
|
213
|
+
}
|
|
214
|
+
return child;
|
|
215
|
+
};
|
|
216
|
+
let output = await outputWithTimeout(await runWrapper(), 120_000);
|
|
185
217
|
if (
|
|
186
218
|
output.code === 141 && output.stdout.length === 0 &&
|
|
187
219
|
output.stderr.length === 0
|
|
188
220
|
) {
|
|
189
221
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
190
|
-
output = await outputWithTimeout(runWrapper(), 120_000);
|
|
222
|
+
output = await outputWithTimeout(await runWrapper(), 120_000);
|
|
191
223
|
}
|
|
192
224
|
const stdout = new TextDecoder().decode(output.stdout);
|
|
193
225
|
const stderr = new TextDecoder().decode(output.stderr);
|
|
@@ -220,5 +252,6 @@ export async function callWrapper(
|
|
|
220
252
|
};
|
|
221
253
|
} finally {
|
|
222
254
|
await Deno.remove(outPath).catch(() => undefined);
|
|
255
|
+
if (promptPath) await Deno.remove(promptPath).catch(() => undefined);
|
|
223
256
|
}
|
|
224
257
|
}
|