mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-08-11 05:50:21 +02:00
feat: support pentests with Codex subscription auth (#419)
This commit is contained in:
@@ -9,7 +9,7 @@ import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { ensureImage, ensureInfra, randomSuffix, spawnWorker } from '../docker.js';
|
||||
import { buildEnvFlags, loadEnv, validateCredentials } from '../env.js';
|
||||
import { buildEnvFlags, loadEnv, resolveHostPiAuthPath, shouldUsePiAuth, validateCredentials } from '../env.js';
|
||||
import { getWorkspacesDir, initHome } from '../home.js';
|
||||
import { isLocal } from '../mode.js';
|
||||
import { resolveModelSpec } from '../model-spec.js';
|
||||
@@ -135,6 +135,7 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
workspace,
|
||||
...(args.pipelineTesting && { pipelineTesting: true }),
|
||||
...(args.debug && { debug: true }),
|
||||
...(shouldUsePiAuth() && { piAuthHostPath: resolveHostPiAuthPath() }),
|
||||
});
|
||||
|
||||
// 14. Bail if `docker run -d` itself fails (mount error, image missing, etc.)
|
||||
|
||||
@@ -12,6 +12,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { envBool, PI_AUTH_CONTAINER_PATH } from './env.js';
|
||||
import { getMode, isDevMode } from './mode.js';
|
||||
import { INTERNAL_DIR } from './paths.js';
|
||||
|
||||
@@ -203,7 +204,7 @@ function shouldSkipHostsName(name: string, hostname: string): boolean {
|
||||
* `host-gateway` so they target the host's loopback instead of the container's.
|
||||
*/
|
||||
function forwardEtcHostsFlags(): string[] {
|
||||
if (process.env.SHANNON_FORWARD_HOSTS === 'false') return [];
|
||||
if (!envBool('SHANNON_FORWARD_HOSTS', true)) return [];
|
||||
if (os.platform() === 'win32') return [];
|
||||
|
||||
let content: string;
|
||||
@@ -255,6 +256,7 @@ export interface WorkerOptions {
|
||||
workspace: string;
|
||||
pipelineTesting?: boolean;
|
||||
debug?: boolean;
|
||||
piAuthHostPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,6 +307,11 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
||||
args.push('-v', `${opts.outputDir}:/app/output`);
|
||||
}
|
||||
|
||||
// Reuse the host's pi credentials: mount only the auth file, allowing token refreshes to persist.
|
||||
if (opts.piAuthHostPath) {
|
||||
args.push('-v', `${opts.piAuthHostPath}:${PI_AUTH_CONTAINER_PATH}`);
|
||||
}
|
||||
|
||||
// Environment
|
||||
args.push(...opts.envFlags);
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
* NPX mode: fills gaps from ~/.shannon/config.toml (no .env).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import dotenv from 'dotenv';
|
||||
import { resolveConfig } from './config/resolver.js';
|
||||
import { getMode } from './mode.js';
|
||||
@@ -41,6 +44,34 @@ function providerForwardVars(providerId: string): readonly string[] {
|
||||
return [...PROVIDER_API_KEY_ENV[providerId], ...PROVIDER_EXTRA_ENV[providerId]];
|
||||
}
|
||||
|
||||
/** Parse a user-facing boolean env var: `1`/`true` (any case) true, `0`/`false`/empty false, else the default. */
|
||||
export function envBool(name: string, defaultValue: boolean): boolean {
|
||||
const raw = process.env[name]?.trim().toLowerCase();
|
||||
if (raw === undefined || raw === '') return defaultValue;
|
||||
if (raw === '1' || raw === 'true') return true;
|
||||
if (raw === '0' || raw === 'false') return false;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const USE_PI_AUTH_ENV = 'SHANNON_USE_PI_AUTH';
|
||||
|
||||
/** Where the host's auth.json is mounted: pi's standard location (worker HOME is /tmp), read natively. */
|
||||
export const PI_AUTH_CONTAINER_PATH = '/tmp/.pi/agent/auth.json';
|
||||
|
||||
/** Host path to pi's credential file. */
|
||||
export function resolveHostPiAuthPath(): string {
|
||||
return path.join(os.homedir(), '.pi', 'agent', 'auth.json');
|
||||
}
|
||||
|
||||
export function piAuthFlagEnabled(): boolean {
|
||||
return envBool(USE_PI_AUTH_ENV, false);
|
||||
}
|
||||
|
||||
/** Opted into pi auth via the flag, and the auth file exists to mount. */
|
||||
export function shouldUsePiAuth(): boolean {
|
||||
return piAuthFlagEnabled() && fs.existsSync(resolveHostPiAuthPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load credentials into process.env.
|
||||
* Local mode: loads ./.env via dotenv.
|
||||
@@ -110,6 +141,18 @@ export function validateCredentials(): CredentialValidation {
|
||||
return { valid: false, error: spec };
|
||||
}
|
||||
|
||||
// Pi-auth: skip the API-key checks, but the host auth file must exist to mount.
|
||||
if (piAuthFlagEnabled()) {
|
||||
const authPath = resolveHostPiAuthPath();
|
||||
if (!fs.existsSync(authPath)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `${USE_PI_AUTH_ENV} is set but no pi credentials were found at ${authPath}. Authenticate with pi first.`,
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 2. The selected provider must have a credential
|
||||
if (!hasCredential(spec.providerId)) {
|
||||
const requirement = isCuratedProvider(spec.providerId)
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
* built over an in-memory credential store primed from the environment.
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Api, Credential, CredentialInfo, CredentialStore, Model } from '@earendil-works/pi-ai';
|
||||
import { ModelRuntime } from '@earendil-works/pi-coding-agent';
|
||||
import { getAgentDir, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
||||
|
||||
/**
|
||||
* Providers Shannon curates with their own credential variables, config sections,
|
||||
@@ -203,12 +205,29 @@ class RuntimeCredentialStore implements CredentialStore {
|
||||
}
|
||||
}
|
||||
|
||||
/** The file pi reads credentials from: the agent dir's auth.json. */
|
||||
function piAuthPath(): string {
|
||||
return path.join(getAgentDir(), 'auth.json');
|
||||
}
|
||||
|
||||
/** Whether the host's pi credentials are mounted (auth.json present in the agent dir). */
|
||||
export function piAuthPresent(): boolean {
|
||||
return existsSync(piAuthPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a ModelRuntime whose only credential is the one supplied. Model catalogs
|
||||
* stay offline (`allowModelNetwork` defaults to false) so a scan never blocks on
|
||||
* a catalog refresh.
|
||||
*
|
||||
* When the host's pi auth.json is present, the runtime reads it instead: pi's
|
||||
* disk-backed store resolves the credential. The mount is writable so OAuth
|
||||
* refreshes persist to the host for subsequent runs.
|
||||
*/
|
||||
export async function createModelRuntime(providerId: string, apiKey: string | undefined): Promise<ModelRuntime> {
|
||||
if (piAuthPresent()) {
|
||||
return ModelRuntime.create({ authPath: piAuthPath() });
|
||||
}
|
||||
return ModelRuntime.create({ credentials: new RuntimeCredentialStore(providerId, apiKey) });
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
type ModelSpec,
|
||||
type OpenAiFormat,
|
||||
PI_CATALOG_URL,
|
||||
piAuthPresent,
|
||||
resolveGatewayFormat,
|
||||
resolveModel,
|
||||
resolveModelSpec,
|
||||
@@ -296,6 +297,7 @@ function credentialHint(providerId: string): string {
|
||||
/** Human-readable label for which credential path a run is using. */
|
||||
function describeAuth(providerId: string, baseUrl: string | undefined): string {
|
||||
if (baseUrl) return `custom endpoint (${baseUrl})`;
|
||||
if (piAuthPresent()) return `${providerId} credentials from pi auth.json`;
|
||||
if (providerId === 'amazon-bedrock') return 'Bedrock bearer token';
|
||||
return `${providerId} API key`;
|
||||
}
|
||||
@@ -341,9 +343,11 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
||||
);
|
||||
}
|
||||
|
||||
// With a mounted pi auth.json the env-var checks don't apply — step 5's probe validates it.
|
||||
const isBedrock = spec.providerId === 'amazon-bedrock';
|
||||
const missing = isBedrock ? ['AWS_REGION', 'AWS_BEARER_TOKEN_BEDROCK'].filter((n) => !process.env[n]) : [];
|
||||
if (missing.length > 0 || (!isBedrock && !credentials.apiKey)) {
|
||||
const missing =
|
||||
isBedrock && !piAuthPresent() ? ['AWS_REGION', 'AWS_BEARER_TOKEN_BEDROCK'].filter((n) => !process.env[n]) : [];
|
||||
if (!piAuthPresent() && (missing.length > 0 || (!isBedrock && !credentials.apiKey))) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`No credentials found for provider "${spec.providerId}". Set ${credentialHint(spec.providerId)} in .env.`,
|
||||
|
||||
Reference in New Issue
Block a user