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.
@@ -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 === "__PROMPT__"
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
- recursive: true,
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
- const outPath = `out/tmp-${crypto.randomUUID()}.txt`;
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(entry, prompt, outPath),
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
- let output = await outputWithTimeout(runWrapper(), 120_000);
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
  }