Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3acf3b5
Add a5c agent workflow template
tmuskal Aug 29, 2025
f17657e
Add remote agents configuration to config.yml
tmuskal Aug 29, 2025
b0419ab
Change runner environment to ubuntu-latest
tmuskal Aug 29, 2025
288220d
Update a5c.yml
tmuskal Aug 29, 2025
b3273d5
docs: add initial plan for GitHub Models provider\n\nBy: [developer-a…
Aug 29, 2025
4ccffdc
feat(core): add built-in 'github' provider for GitHub Models (Respons…
Aug 29, 2025
e8d1879
Change model to GPT-5 and update base URL
tmuskal Aug 29, 2025
98aa46e
Update GitHub Models base URL and wire API
tmuskal Aug 29, 2025
edd8764
Delete docs/dev/developer-agent/github-models-provider-20250829T07035…
tmuskal Aug 29, 2025
e1fc7f6
Merge pull request #2 from a5c-incubator/feat/github-models-provider
tmuskal Aug 29, 2025
53fdc12
docs: plan for sessions list/resume\n\nBy: developer-agent(https://ap…
Aug 29, 2025
7272826
feat(core,tui): add sessions list and resume (CLI + TUI)\n\n- core: p…
Aug 29, 2025
737671c
Delete package-lock.json
tmuskal Aug 29, 2025
dd2cd5c
Delete docs/dev/developer-agent/sessions-list-resume-20250829T090440.md
tmuskal Aug 29, 2025
0726169
Delete .a5c/config.yml
tmuskal Aug 29, 2025
8911215
Delete .github/workflows/a5c.yml
tmuskal Aug 29, 2025
014b5ce
Revert "✨ Add GitHub Models provider (Responses API)"
tmuskal Aug 29, 2025
0df707f
Merge pull request #30 from a5c-incubator/revert-2-feat/github-models…
tmuskal Aug 29, 2025
461496b
Merge branch 'main' into feat/sessions-list-resume
tmuskal Aug 29, 2025
6832f36
Merge branch 'openai:main' into main
tmuskal Aug 29, 2025
b2a0bb6
fix(tui,core): address requested changes\n\n- TUI: wrap stderr prints…
Aug 29, 2025
5842ffb
Merge branch 'main' into feat/sessions-list-resume
tmuskal Aug 29, 2025
5fe6ef3
✨ CLI: add /usage command
github-actions[bot] Aug 29, 2025
1b5bc21
Update a5c.yml
tmuskal Aug 29, 2025
83efd6d
Resolve merge: keep workflow from main; integrate sessions changes
Aug 29, 2025
38b19ee
style(core): organize imports in sessions.rs (rustfmt)
Aug 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/a5c.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Template for a5c agent workflow aligned to pnpm/Corepack.
# Maintainers: this template is authoritative. Please sync into
# .github/workflows/a5c.yml so the active workflow uses pnpm caching:
# - actions/setup-node@v4 with `cache: pnpm`
# - `cache-dependency-path: pnpm-lock.yaml`

name: a5c

env:
DISABLE_AUTOUPDATER: 1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AZURE_OPENAI_PROJECT_NAME: ${{ vars.AZURE_OPENAI_PROJECT_NAME || '' }}
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY || '' }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || '' }}
DEBUG: ${{ github.event.inputs.debug || 'false' }}
GITHUB_TOKEN: ${{ secrets.A5C_AGENT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
# Optional: supply a GitHub App token for validator checks
# A5C_AGENT_GITHUB_TOKEN: ${{ secrets.A5C_AGENT_GITHUB_TOKEN }}
# Feature flags for validator checks behavior
# A5C_VALIDATOR_ENABLE_CHECKS: "true" # set false to disable Check Runs
# A5C_VALIDATOR_USE_STATUSES: "false" # if supported, use commit statuses instead of checks
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_GUILD_ID: ${{ vars.DISCORD_GUILD_ID }}
A5C_CLI_TOOL: ${{ vars.A5C_CLI_TOOL }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN || '' }}
SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET || '' }}
SLACK_APP_TOKEN: ${{ secrets.SLACK_APP_TOKEN || '' }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN || '' }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID || '' }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID || '' }}
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN || '' }}
SUPABASE_ORG_ID: ${{ vars.SUPABASE_ORG_ID || '' }}
SUPABASE_PROJECT_REF: ${{ vars.SUPABASE_PROJECT_REF || '' }}
SUPABASE_PROJECT_URL: ${{ vars.SUPABASE_PROJECT_URL || '' }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD || '' }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY || '' }}
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY || '' }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET || '' }}
STRIPE_WEBHOOK_URL: ${{ vars.STRIPE_WEBHOOK_URL || '' }}
STRIPE_WEBHOOK_ID: ${{ vars.STRIPE_WEBHOOK_ID || '' }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID || '' }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY || '' }}
AWS_REGION: ${{ vars.AWS_REGION || '' }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS || '' }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID || '' }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET || '' }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID || '' }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID || '' }}
AZURE_ACR_NAME: ${{ vars.AZURE_ACR_NAME || '' }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID || '' }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET || '' }}
AUTH_GITHUB_CLIENT_ID: ${{ secrets.AUTH_GITHUB_CLIENT_ID || '' }}
AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.AUTH_GITHUB_CLIENT_SECRET || '' }}
AUTH_GITHUB_ORG_ID: ${{ vars.AUTH_GITHUB_ORG_ID || '' }}
AUTH_GITHUB_ORG_NAME: ${{ vars.AUTH_GITHUB_ORG_NAME || '' }}
AUTH_GITHUB_ORG_DESCRIPTION: ${{ vars.AUTH_GITHUB_ORG_DESCRIPTION || '' }}
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY || '' }}
HEROKU_APP_NAME: ${{ vars.HEROKU_APP_NAME || '' }}
HEROKU_APP_ID: ${{ vars.HEROKU_APP_ID || '' }}
HEROKU_APP_URL: ${{ vars.HEROKU_APP_URL || '' }}

on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main, develop, master]
push:
branches: []
issues:
types: [opened]
issue_comment:
types: [created]
schedule:
- cron: '*/30 * * * *'
workflow_run:
types: [completed]
workflows: [Build,Deploy,Tests,Release,E2E Tests,Infrastructure Deployment,Integration Tests,rust-ci]
workflow_dispatch:
inputs:
agent_uri:
description: 'Specific agent to run (optional - leave empty for auto-routing)'
required: false
debug:
description: 'Enable debug mode'
required: false
default: false
type: boolean

jobs:
a5c:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
security-events: write
actions: write
attestations: write
checks: write
deployments: write
discussions: write
id-token: write
models: read
packages: write
pages: write
statuses: write

steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Filter Self Workflow-run
id: filter-self
if: github.event_name == 'workflow_run' && (github.event.workflow_run.conclusion != 'failure' || github.event.workflow_run.head_branch != 'main')
run: |
echo "skip=true" >> "$GITHUB_OUTPUT"

- name: Run A5C
id: agents
if: steps.filter-self.outputs.skip != 'true'
uses: a5c-ai/action@main
with:
agent_uri: ${{ github.event.inputs.agent_uri || '' }}
github_token: ${{ secrets.A5C_AGENT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}

- name: Upload artifacts
id: upload-artifacts
uses: actions/upload-artifact@v4
with:
name: a5c-artifacts
path: |
/tmp/agent-output.md
/tmp/agent-output-*/**
12 changes: 12 additions & 0 deletions codex-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ codex completion zsh
codex completion fish
```

### Usage and Guardrail Resets

Check your current guardrail usage and next reset times via:

```
codex usage
# or
codex /usage
```

When usage data is unavailable, the command prints a clear, non-fatal message and exits successfully.

### Experimenting with the Codex Sandbox

To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI:
Expand Down
1 change: 1 addition & 0 deletions codex-rs/chatgpt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod apply_command;
mod chatgpt_client;
mod chatgpt_token;
pub mod get_task;
pub mod usage;
80 changes: 80 additions & 0 deletions codex-rs/chatgpt/src/usage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use anyhow::Context;
use codex_core::config::Config;
use serde::Deserialize;

use crate::chatgpt_client::chatgpt_get_request;

/// High-level summary of guardrail usage for display in CLI.
#[derive(Debug, Clone, Default)]
pub struct UsageSummary {
pub plan: Option<String>,
pub standard_used_minutes: Option<u64>,
pub standard_limit_minutes: Option<u64>,
pub reasoning_used_minutes: Option<u64>,
pub reasoning_limit_minutes: Option<u64>,
pub next_reset_at: Option<String>,
}

/// Flexible wire model so we can tolerate backend changes without breaking the CLI.
#[derive(Debug, Deserialize)]
struct RawUsage {
#[serde(default)]
plan: Option<String>,
#[serde(default)]
next_reset_at: Option<String>,
#[serde(default)]
reset_at: Option<String>,
#[serde(default)]
standard: Option<Bucket>,
#[serde(default)]
reasoning: Option<Bucket>,
}

#[derive(Debug, Deserialize)]
struct Bucket {
#[serde(default)]
used_minutes: Option<u64>,
#[serde(default)]
limit_minutes: Option<u64>,
#[serde(default)]
used: Option<u64>,
#[serde(default)]
limit: Option<u64>,
}

impl From<RawUsage> for UsageSummary {
fn from(raw: RawUsage) -> Self {
let plan = raw.plan;
let next_reset_at = raw.next_reset_at.or(raw.reset_at);
let (mut standard_used_minutes, mut standard_limit_minutes) = (None, None);
let (mut reasoning_used_minutes, mut reasoning_limit_minutes) = (None, None);

if let Some(b) = raw.standard {
standard_used_minutes = b.used_minutes.or(b.used);
standard_limit_minutes = b.limit_minutes.or(b.limit);
}
if let Some(b) = raw.reasoning {
reasoning_used_minutes = b.used_minutes.or(b.used);
reasoning_limit_minutes = b.limit_minutes.or(b.limit);
}

UsageSummary {
plan,
standard_used_minutes,
standard_limit_minutes,
reasoning_used_minutes,
reasoning_limit_minutes,
next_reset_at,
}
}
}

/// Fetch ChatGPT guardrail usage using the current auth and config.
pub async fn get_usage(config: &Config) -> anyhow::Result<UsageSummary> {
// This path is provided by the ChatGPT backend for Codex usage display.
// The structure is intentionally parsed via a flexible wire model.
let raw: RawUsage = chatgpt_get_request(config, "/wham/usage".to_string())
.await
.context("Failed to fetch usage from ChatGPT backend")?;
Ok(raw.into())
}
1 change: 1 addition & 0 deletions codex-rs/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod debug_sandbox;
mod exit_status;
pub mod login;
pub mod proto;
pub mod usage;

use clap::Parser;
use codex_common::CliConfigOverrides;
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ use codex_cli::login::run_login_with_api_key;
use codex_cli::login::run_login_with_chatgpt;
use codex_cli::login::run_logout;
use codex_cli::proto;
use codex_cli::usage::run_usage_command;
use codex_common::CliConfigOverrides;
use codex_exec::Cli as ExecCli;
use codex_tui::Cli as TuiCli;
use std::path::PathBuf;

use crate::proto::ProtoCli;
use codex_cli::usage::UsageCommand;

/// Codex CLI
///
Expand Down Expand Up @@ -76,6 +78,10 @@ enum Subcommand {
/// Internal: generate TypeScript protocol bindings.
#[clap(hide = true)]
GenerateTs(GenerateTsCommand),

/// Show current guardrail usage and reset times.
#[clap(visible_alias = "/usage")]
Usage(UsageCommand),
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -212,6 +218,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
Some(Subcommand::GenerateTs(gen_cli)) => {
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
}
Some(Subcommand::Usage(mut usage_cli)) => {
prepend_config_flags(&mut usage_cli.config_overrides, cli.config_overrides);
run_usage_command(usage_cli.config_overrides).await?;
}
}

Ok(())
Expand Down
100 changes: 100 additions & 0 deletions codex-rs/cli/src/usage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use codex_common::CliConfigOverrides;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_login::AuthMode;
use codex_login::CodexAuth;

use codex_chatgpt::usage::get_usage as get_chatgpt_usage;

#[derive(Debug, clap::Parser)]
pub struct UsageCommand {
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
}

pub async fn run_usage_command(cli_config_overrides: CliConfigOverrides) -> anyhow::Result<()> {
let config = load_config_or_exit(cli_config_overrides);

match CodexAuth::from_codex_home(&config.codex_home, config.preferred_auth_method) {
Ok(Some(auth)) => match auth.mode {
AuthMode::ApiKey => {
let plan = auth
.get_plan_type()
.unwrap_or_else(|| "unknown".to_string());
println!("Plan: {plan}");
println!(
"Using an API key. Guardrail usage does not apply; billing is per-token.\nSee https://platform.openai.com/account/usage for detailed usage."
);
Ok(())
}
AuthMode::ChatGPT => {
let plan = auth
.get_plan_type()
.unwrap_or_else(|| "unknown".to_string());
match get_chatgpt_usage(&config).await {
Ok(summary) => {
println!("Plan: {plan}");
if let Some(when) = summary.next_reset_at.as_deref() {
println!("Next reset: {when}");
}
if let (Some(u), Some(l)) = (
summary.standard_used_minutes,
summary.standard_limit_minutes,
) {
println!("Standard: {u} / {l} minutes used");
}
if let (Some(u), Some(l)) = (
summary.reasoning_used_minutes,
summary.reasoning_limit_minutes,
) {
println!("Reasoning: {u} / {l} minutes used");
}

// If no buckets printed, fall back to a generic message.
if summary.standard_used_minutes.is_none()
&& summary.reasoning_used_minutes.is_none()
{
println!("Usage data retrieved, but no bucket details available.");
}
Ok(())
}
Err(e) => {
println!(
"Plan: {plan}\nUnable to retrieve usage from ChatGPT backend.\nReason: {e}\nUsage information is currently unavailable."
);
Ok(())
}
}
}
},
Ok(None) => {
println!("Not logged in. Usage information requires authentication.\nRun: codex login");
Ok(())
}
Err(e) => {
println!(
"Unable to determine authentication status.\nReason: {e}\nUsage information is currently unavailable."
);
Ok(())
}
}
}

fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
let cli_overrides = match cli_config_overrides.parse_overrides() {
Ok(v) => v,
Err(e) => {
eprintln!("Error parsing -c overrides: {e}");
std::process::exit(1);
}
};

let config_overrides = ConfigOverrides::default();
match Config::load_with_cli_overrides(cli_overrides, config_overrides) {
Ok(config) => config,
Err(e) => {
eprintln!("Error loading configuration: {e}");
std::process::exit(1);
}
}
}
11 changes: 11 additions & 0 deletions codex-rs/cli/tests/usage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use codex_cli::usage::run_usage_command;
use codex_common::CliConfigOverrides;

#[tokio::test]
async fn usage_command_runs() {
// Should not error; prints a helpful message even if not logged in.
let overrides = CliConfigOverrides::default();
run_usage_command(overrides)
.await
.expect("usage should run");
}
Loading