feat: support pentests with Codex subscription auth (#419)

This commit is contained in:
ezl-keygraph
2026-08-10 15:27:19 +05:30
committed by GitHub
parent 760a140228
commit d4cc2ab974
11 changed files with 136 additions and 10 deletions
+5
View File
@@ -48,3 +48,8 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
# --- Misc --------------------------------------------------------------------
# Forward /etc/hosts entries into the worker container.
# SHANNON_FORWARD_HOSTS=false
# See the guide below to use an OpenAI subscription
# https://github.com/KeygraphHQ/shannon/blob/main/docs/ai-providers.md#openai-codex-chatgpt-pluspro-subscription
# SHANNON_USE_PI_AUTH=1
# SHANNON_AI_MODEL=openai-codex:gpt-5.5
+1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -107,12 +107,12 @@ RUN ln -s /app/apps/worker/dist/scripts/save-deliverable.js /usr/local/bin/save-
# Create directories for session data and ensure proper permissions
RUN mkdir -p /app/sessions /app/repos /app/workspaces && \
mkdir -p /tmp/.cache /tmp/.config /tmp/.npm && \
mkdir -p /tmp/.cache /tmp/.config /tmp/.npm /tmp/.pi/agent && \
chmod 777 /app && \
chmod 777 /tmp/.cache && \
chmod 777 /tmp/.config && \
chmod 777 /tmp/.npm && \
chown -R pentest:pentest /app /tmp/.claude
chown -R pentest:pentest /app /tmp/.claude /tmp/.pi
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
+4 -1
View File
@@ -94,7 +94,10 @@ Shannon pulls the worker image from Docker Hub, starts the required local infras
For source builds, authenticated scans, provider-specific setup, and platform notes, see [Documentation](#documentation).
> [!TIP]
> **Prefer to run on your Claude Code subscription instead of API credits?** The [`shannon-v1`](https://github.com/KeygraphHQ/shannon/tree/shannon-v1) branch is the last release built on the Claude Agent SDK, so it accepts a Claude Code OAuth token. Generate one with `claude setup-token`, then run `npx @keygraph/shannon@1.9.0 setup` and pick **OAuth Token**. Pentests then cost nothing beyond your existing subscription.
> **Prefer to use a subscription instead of API credits?**
>
> - **OpenAI Codex:** The latest version of Shannon supports ChatGPT Plus and Pro subscriptions. Follow the [OpenAI Codex subscription setup guide](docs/ai-providers.md#openai-codex-chatgpt-pluspro-subscription) to get started.
> - **Claude Code:** The latest version of Shannon does not support Claude Code subscriptions. Follow the [Claude Code subscription setup guide](docs/ai-providers.md#claude-code-subscription) to use version `1.9.0`, which is the final release built on the Claude Agent SDK.
## Key Capabilities
+2 -1
View File
@@ -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.)
+8 -1
View File
@@ -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);
+43
View File
@@ -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)
+20 -1
View File
@@ -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) });
}
+6 -2
View File
@@ -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.`,
+44 -1
View File
@@ -144,12 +144,55 @@ The variable is rejected in preflight where it cannot take effect: with a non-`o
`npx @keygraph/shannon setup` covers this under **Custom Base URL**, which asks which API your gateway serves and configures the matching provider for you.
## OpenAI Codex (ChatGPT Plus/Pro subscription)
A ChatGPT Plus or Pro Codex subscription can run Shannon. Shannon reuses a login created by Pi.
Before running a pentest, review the [cyber safeguards requirements](#cyber-safeguards-do-this-before-your-first-scan).
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
2. Log in with your subscription using Pi's [subscription authentication guide](https://pi.dev/docs/latest/providers#subscriptions). This creates `~/.pi/agent/auth.json` with an `openai-codex` entry.
3. Select a Codex model and enable Pi authentication:
```bash
export SHANNON_USE_PI_AUTH=1
export SHANNON_AI_MODEL=openai-codex:gpt-5.5
```
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
Supported Codex models are `gpt-5.6-sol`, `gpt-5.5`, and `gpt-5.4`.
## Claude Code subscription
The latest version of Shannon does not support Claude Code subscriptions. The [`shannon-v1`](https://github.com/KeygraphHQ/shannon/tree/shannon-v1) branch is the final release built on the Claude Agent SDK and supports Claude Code OAuth.
Before running a pentest, review the [cyber safeguards requirements](#cyber-safeguards-do-this-before-your-first-scan).
1. Generate a Claude Code OAuth token:
```bash
claude setup-token
```
2. Run the setup flow for the final `shannon-v1` release:
```bash
npx @keygraph/shannon@1.9.0 setup
```
3. Select **OAuth Token** and enter the token generated by Claude Code.
4. Start the pentest with `npx @keygraph/shannon@1.9.0 start ...`.
These instructions apply only to `shannon-v1`.
## Validation
Checks run before a scan starts, so mistakes fail immediately rather than partway through a run:
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). A custom base URL exempts the model ID, since a gateway may serve its own names.
- **Credential presence** — always validated for the selected provider.
- **Credential presence** — validated for the selected provider, or read from Pi when `SHANNON_USE_PI_AUTH=1`.
- **Credential validity** — one minimal request against the model the scan will use, so a rejected key, an exhausted quota, or a model the account cannot reach fails before any agent runs. Bedrock included: its bearer token and region go through the same probe.
## Migrating from the three-tier configuration
+1 -1
View File
@@ -12,7 +12,7 @@ if [ -n "$TARGET_UID" ] && [ "$TARGET_UID" != "$CURRENT_UID" ]; then
groupadd -g "$TARGET_GID" pentest
useradd -u "$TARGET_UID" -g pentest -s /bin/bash -M pentest
chown -R pentest:pentest /app/sessions /app/workspaces /tmp/.claude
chown -R pentest:pentest /app/sessions /app/workspaces /tmp/.claude /tmp/.pi
fi
exec su -m pentest -c "exec $*"