Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ceauto",
"version": "0.16.0",
"version": "0.17.0",
"description": "Autonomous CEO Agent — multi-agent orchestration, episodic memory, executive decision frameworks",
"author": "CEAuto",
"mcpServer": {
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## [0.17.0] - 2026-07-20
### Added
- **`codex` executor** — run a task through the OpenAI Codex CLI non-interactively
(`codex exec`, prompt on stdin), the sibling of `claude-code`. Default **OFF**
(`executors.codex.enabled`), config-bound command/args/cwd, and behind the same
budget / approval / veto / `comms/STOP` gates as every other runtime. Sandbox
policy is passed through `args` (e.g. `--sandbox workspace-write`); no bypass flag
is ever injected.
- **Run with no API key.** Both CLI runtimes (`claude -p`, `codex exec`) bill your
Claude / ChatGPT **subscription** instead of per-token API credits, so an
autonomous loop can do real work on a plan you already pay for. They cost $0 in the
ledger — keyed on the **provider** (a constant inside each executor), never on the
binary name, so renaming a binary can't make a real API-backed model free. Their
**tokens are still metered** against the token caps.
- Both CLI executors now **strip `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` from the child
environment**. The daemon usually has those set for the `llm` executor, and an
inherited key could silently flip the CLI to per-token API billing — spend the $0
ledger price would then hide from the global USD cap. Scrubbing makes
"subscription-billed" true by construction rather than assumed. `passApiKeys: true`
opts back in deliberately (then set real prices for the model it reports).

### Verified
- Standalone operation end-to-end with **no MCP client and no API key**: the daemon
(`node bin/ceauto-daemon.js --once`) drained a seeded task to `done` in an isolated
`CEAUTO_WORKSPACE`, and the offline demo completed the full boot → delegate → cycle
→ metrics walkthrough.
- Live subscription-backed run: the runner completed a real task through `codex exec`
(ChatGPT login, no `OPENAI_API_KEY`), recording `provider=codex` with `usd=0`.

## [0.16.0] - 2026-07-16
### Added
- **Pursue goals autonomously (`lib/strategist.js`).** When the heartbeat's ready
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ per-task veto (`comms/vetos.md`) and from a budget pause.
- **Executors** (`executors.*`): each agent's work runs through one runtime —
`llm` (default), `mcp-tool` (call any MCP server as an agent), `shell`
(allowlisted), `webhook`/`http-webhook` (POST to an allowlisted host, redirects
refused), `claude-code` (headless `claude -p`, **default-off**), `composite`
refused), `claude-code` (headless `claude -p`, **default-off**), `codex`
(headless `codex exec`, **default-off**) — both CLI runtimes bill your Claude /
ChatGPT **subscription** instead of per-token API credits, so the loop can run
real coding work with no API key — `composite`
(an ordered `chain`/`map` of other executors, max 10 steps, no nesting). Budget,
approval, and veto wrap every runtime identically.
- **Sources** (`sources.*`, all default-off): file-watch (chokidar), an inbound
Expand Down
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CEAuto

![version](https://img.shields.io/badge/version-0.16.0-blue)
![version](https://img.shields.io/badge/version-0.17.0-blue)
![node](https://img.shields.io/badge/node-18%2B-339933?logo=node.js&logoColor=white)
![MCP](https://img.shields.io/badge/MCP-compatible-blueviolet)
![platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
Expand Down Expand Up @@ -424,11 +424,20 @@ decomposes the task into a subtask DAG instead of doing it directly.
### How work runs

Every task goes through one **executor**: `llm` (default), `mcp-tool` (call any MCP
server as an agent), `shell`, `webhook`, `claude-code`, or `composite` (an ordered
chain or map of other executors). Budget, approval, and veto gates wrap all of them
the same way. **Reactive sources** (file-watch, an inbound webhook, `@role`
mentions) feed the queue from outside the heartbeat. Sources and the higher-risk
executors are off by default.
server as an agent), `shell`, `webhook`, `claude-code` (headless `claude -p`), `codex`
(headless `codex exec`), or `composite` (an ordered chain or map of other executors).
Budget, approval, and veto gates wrap all of them the same way. **Reactive sources**
(file-watch, an inbound webhook, `@role` mentions) feed the queue from outside the
heartbeat. Sources and the higher-risk executors are off by default.

**No API key required.** The `claude-code` and `codex` executors drive your installed
CLI, which bills your Claude / ChatGPT **subscription** rather than per-token API
credits — so an autonomous loop can do real work on a plan you already pay for. They
cost $0 in the ledger (keyed on the provider, so a renamed binary can't buy a free
ride) while their tokens are still metered against the token caps. The executors also
strip `ANTHROPIC_API_KEY`/`OPENAI_API_KEY` from the child environment, so an inherited
key can't silently flip the CLI to per-token API billing — set `passApiKeys: true` if
you deliberately want an API-backed CLI.

**Governance.** Strategic decisions, sensitive actions, and budget overages gate on
human approval. Drop in `config/policy.yaml` for rule-based gating and
Expand Down Expand Up @@ -511,7 +520,7 @@ CEAuto/
├── memory/context.md # your company/project context (fill this in)
├── strategy/goals.md # current strategic goals (fill this in)
├── workflows/ # YAML workflow definitions
├── test/ # unit + integration + e2e (232 tests, mock provider)
├── test/ # unit + integration + e2e (240 tests, mock provider)
├── install.sh # installer (Linux/macOS/Git Bash)
└── install.bat # installer (Windows)
```
Expand Down
5 changes: 5 additions & 0 deletions config/providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ providers:
# Model pricing in USD per 1K tokens. `default` is used for unknown models.
pricing:
default: { input: 0.003, output: 0.015 }
# NOTE: the subscription-billed CLI runtimes (`claude -p`, `codex exec`) are NOT
# priced here. They are $0 by PROVIDER in lib/budget.js — keying on the binary name
# would let a rename silently make a real API-backed model free. Their tokens are
# still metered. If you deliberately run either CLI API-backed (`passApiKeys: true`),
# add a price entry for the model name it reports.
claude-sonnet-4-6: { input: 0.003, output: 0.015 }
claude-opus-4-8: { input: 0.015, output: 0.075 }
claude-haiku-4-5-20251001: { input: 0.001, output: 0.005 }
Expand Down
15 changes: 14 additions & 1 deletion config/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## General
name: "Autonomous CEO Agent"
version: "0.16.0"
version: "0.17.0"
framework: "MCP-compatible"

## Autonomy
Expand Down Expand Up @@ -68,6 +68,19 @@ executors:
enabled: false # default OFF — claude-code can edit files; enable deliberately
command: claude # the Claude Code CLI binary
# outputFormat: json # parse { result, usage } from `claude -p --output-format json`
# Codex CLI (`codex exec`, prompt on stdin) — the OpenAI-plan sibling of claude_code.
# Both bill your SUBSCRIPTION rather than per-token API credits, so the loop can run
# real coding work with no API key. Default OFF (it can run commands / edit files).
codex:
enabled: false # default OFF — enable deliberately
command: codex # the Codex CLI binary
# args: [exec, --skip-git-repo-check, --color, never] # the default
# args: [exec, --skip-git-repo-check, --color, never, --sandbox, workspace-write] # let it write
# cwd: F:/path/to/a/sandbox # give it a scratch working root, not your main repo
# passApiKeys: false # keys are stripped from the child env so billing stays on your
# # PLAN; set true only if you deliberately run the CLI API-backed
# CAVEAT: your own ~/.codex/config.toml can widen sandbox/approvals globally — the
# "no bypass flag injected" guarantee only covers the argv CEAuto builds.

## Reactive sources (Pillar 4, G1) — feed the queue from outside the cron tick.
## Master switch + per-source switch, all default-off (new inbound surface).
Expand Down
11 changes: 8 additions & 3 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ Shipped: `llm`, `mcp-tool`, `shell` (Phase A); **`webhook`/`http-webhook` and
`claude-code` (G1)** — both gated (host allowlist / default-off) and sharing a
hardened child-process runner (`lib/executors/spawn-capture.js`); **`composite`**
(`lib/executors/composite.js`) — an ordered chain/map of other executors, bounded
to 10 steps with no nesting; the runner scales the budget gate by step count.
**All runtimes done.**
to 10 steps with no nesting; the runner scales the budget gate by step count; and
**`codex`** (v0.17.0) — the OpenAI-plan sibling of `claude-code`. Both CLI runtimes
bill the operator's **subscription** rather than per-token API credits (priced $0 by
provider, API keys stripped from the child env), so the loop can run real coding work
with no API key at all. **All runtimes done** — this closes the "agent runtimes" gap
in the table above outright.

```
// lib/executors/index.js
Expand All @@ -61,7 +65,8 @@ Built-in executors (each its own `lib/executors/*.js`):
- `shell` — run a sandboxed command, capture stdout/exit; usage = wall/CPU proxy.
- `http-webhook` — POST the task envelope to a URL, await a structured reply
(lets an external worker/n8n/Zapier be an agent).
- `claude-code` — spawn a Claude Code subagent for coding tasks.
- `claude-code` — spawn a headless Claude Code subagent (`claude -p`) for coding tasks.
- `codex` — spawn a headless OpenAI Codex subagent (`codex exec`) for coding tasks.
- `composite` — ordered fan to several executors (map/reduce).

Agent definition (`agents.yaml` / `org.yaml`) picks `executor` + `params`. The
Expand Down
24 changes: 17 additions & 7 deletions lib/budget.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,23 @@ function resetConfig() {
CONFIG = null;
}

// Runtimes that don't spend our LLM API dollars — a local command (shell), an
// external worker (webhook), or another MCP server (mcp). Pricing their proxy
// token counts at pricing.default would accrue phantom USD against the global USD
// cap, so their spend is $0. NOTE: claude-code and composite are deliberately
// EXCLUDED — both can drive real LLM spend and must stay under the USD cap
// (composite is priced conservatively via pricing.default).
const NON_LLM_PROVIDERS = new Set(['shell', 'webhook', 'mcp']);
// Runtimes that cost $0 of LLM API spend. Pricing their proxy token counts at
// pricing.default would accrue phantom USD against the global USD cap.
//
// no LLM at all: a local command (shell), an external worker (webhook), or
// another MCP server (mcp).
//
// subscription-billed CLIs: `claude -p` and `codex exec` bill the operator's
// Claude / ChatGPT PLAN, not per-token API credits. This is keyed on the PROVIDER
// (a hardcoded constant in each executor), never on the model/binary name, which
// is operator-config and could be renamed into or out of a price. The executors
// also strip ANTHROPIC_API_KEY/OPENAI_API_KEY from the child env, so the CLI
// CANNOT fall back to API billing — that is what makes $0 true rather than
// assumed. Opting in via `passApiKeys: true` means you should set real prices.
//
// `composite` stays EXCLUDED — it can chain a real `llm` step, so it is priced
// conservatively via pricing.default. Tokens are still metered for everything here.
const NON_LLM_PROVIDERS = new Set(['shell', 'webhook', 'mcp', 'claude-code', 'codex']);

function priceFor(model, inputTokens, outputTokens) {
const { pricing } = getConfig();
Expand Down
9 changes: 9 additions & 0 deletions lib/executors/claude-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
const { runChild, basename } = require('./spawn-capture');
const { estimateTokens } = require('../llm-adapter');

// Stripped from the child env so `claude -p` bills the SUBSCRIPTION, not the API.
// The daemon usually has these set for the `llm` executor; an inherited key would
// silently flip billing to per-token API spend that the $0 ledger price hides from
// the USD cap. `passApiKeys: true` opts back in (then set real prices).
const PROVIDER_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'];

async function claudeCode(ctx, deps = {}) {
const cfg = deps.claudeCode || {};
if (!cfg.enabled) {
Expand All @@ -29,12 +35,15 @@ async function claudeCode(ctx, deps = {}) {

const prompt = [ctx.agentSpec, ctx.context, ctx.task].filter(Boolean).join('\n\n');

const passApiKeys = params.passApiKeys ?? cfg.passApiKeys ?? false;

const { stdout } = await runChild({
command,
args,
input: prompt,
cwd,
timeoutMs,
unsetEnv: passApiKeys ? [] : PROVIDER_KEYS,
label: `claude-code executor: "${basename(command)}"`,
});

Expand Down
96 changes: 96 additions & 0 deletions lib/executors/codex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* codex executor — run a task by spawning the OpenAI Codex CLI non-interactively
* (`codex exec`, prompt on stdin). The sibling of the claude-code executor.
*
* Why both: `claude -p` and `codex exec` bill the operator's SUBSCRIPTION (Claude
* plan / ChatGPT plan) rather than per-token API credits, so an autonomous loop can
* run real coding work without an API key at all. Pick whichever plan you have.
*
* High blast radius (Codex can run commands and edit files), so it is DEFAULT-OFF:
* it only runs when config enables it (deps.codex.enabled), and like every executor
* it still sits behind the runner's budget / approval / veto / STOP gates.
*
* `codex exec` reads the prompt from stdin when no PROMPT argument is given, which
* is exactly how claude-code feeds it — same shape, same hardened child runner.
*
* params: { command?='codex', args?=['exec', ...], cwd?, timeoutMs?, outputFormat? }
* Sandboxing is Codex's own concern: pass `--sandbox workspace-write` (or
* read-only / danger-full-access) through `args` to choose the policy. We do NOT
* inject a bypass flag — opt into wider access deliberately.
* outputFormat:'json' parses a { result|text, usage } object from stdout. Kept for
* parity with claude-code; note no current `codex exec` flag emits that shape
* (`--json` is JSONL events), so today it harmlessly falls back to raw text.
*/

const { runChild, basename } = require('./spawn-capture');
const { estimateTokens } = require('../llm-adapter');

// --skip-git-repo-check: the workspace may not be a git repo.
// --color never: keep captured stdout clean of ANSI escapes.
const DEFAULT_ARGS = ['exec', '--skip-git-repo-check', '--color', 'never'];

// Provider API keys are stripped from the child's environment by default. The
// daemon usually has ANTHROPIC_API_KEY / OPENAI_API_KEY set for the `llm` executor,
// and an inherited key can silently flip the CLI from SUBSCRIPTION billing to
// per-token API billing — spend the $0 ledger price would then hide from the USD
// cap. Scrubbing them makes "subscription-billed" true by construction.
// Set `passApiKeys: true` only if you deliberately want an API-backed CLI (and then
// give it real per-1K prices in providers.yaml).
const PROVIDER_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'];

async function codex(ctx, deps = {}) {
const cfg = deps.codex || {};
if (!cfg.enabled) {
throw new Error('codex executor is disabled — set executors.codex.enabled in config/settings.yaml');
}

const params = ctx.params || {};
const command = params.command || cfg.command || 'codex';
const args = params.args || cfg.args || DEFAULT_ARGS;
const cwd = params.cwd || cfg.cwd;
const timeoutMs = params.timeoutMs || cfg.timeoutMs || 180000;
const outputFormat = params.outputFormat || cfg.outputFormat || null;

const prompt = [ctx.agentSpec, ctx.context, ctx.task].filter(Boolean).join('\n\n');

const passApiKeys = params.passApiKeys ?? cfg.passApiKeys ?? false;

const { stdout } = await runChild({
command,
args,
input: prompt,
cwd,
timeoutMs,
unsetEnv: passApiKeys ? [] : PROVIDER_KEYS,
label: `codex executor: "${basename(command)}"`,
});

let text = stdout;
let usage = null;
if (outputFormat === 'json') {
try {
const parsed = JSON.parse(stdout);
text = parsed.result ?? parsed.text ?? stdout;
if (parsed.usage) {
usage = {
input_tokens: parsed.usage.input_tokens || 0,
output_tokens: parsed.usage.output_tokens || 0,
};
}
} catch {
// not JSON after all — fall back to the raw text + estimate
}
}

return {
text,
usage: {
input_tokens: usage?.input_tokens ?? estimateTokens(prompt),
output_tokens: usage?.output_tokens ?? estimateTokens(text),
model: basename(command),
provider: 'codex',
},
};
}

module.exports = codex;
2 changes: 2 additions & 0 deletions lib/executors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const shell = require('./shell');
const mcpTool = require('./mcp-tool');
const webhook = require('./webhook');
const claudeCode = require('./claude-code');
const codex = require('./codex');
const composite = require('./composite');

const REGISTRY = {
Expand All @@ -23,6 +24,7 @@ const REGISTRY = {
webhook,
'http-webhook': webhook,
'claude-code': claudeCode,
codex,
composite,
};

Expand Down
12 changes: 10 additions & 2 deletions lib/executors/spawn-capture.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,21 @@ function basename(cmd) {
return path.basename(String(cmd || ''));
}

function runChild({ command, args = [], input, cwd, env, timeoutMs = 60000, maxOutput = MAX_OUTPUT_DEFAULT, label } = {}) {
function runChild({ command, args = [], input, cwd, env, unsetEnv = [], timeoutMs = 60000, maxOutput = MAX_OUTPUT_DEFAULT, label } = {}) {
const name = label || basename(command);
// `unsetEnv` REMOVES inherited vars (the spread can only add). Used to strip
// provider API keys before spawning a subscription-billed CLI.
const buildEnv = () => {
if (!env && !unsetEnv.length) return process.env;
const merged = { ...process.env, ...(env || {}) };
for (const key of unsetEnv) delete merged[key];
return merged;
};
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
shell: false,
cwd: cwd || undefined,
env: env ? { ...process.env, ...env } : process.env,
env: buildEnv(),
});
let out = '';
let err = '';
Expand Down
3 changes: 2 additions & 1 deletion lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ async function runTask(taskId, deps = {}) {
const webhookAllowlist = (execCfg.webhook && execCfg.webhook.allowlist) || [];
const mcpAllowlist = (execCfg['mcp-tool'] && execCfg['mcp-tool'].allowlist) || [];
const claudeCode = execCfg.claude_code || {};
const execDeps = { dispatch, shellAllowlist, webhookAllowlist, mcpAllowlist, claudeCode };
const codex = execCfg.codex || {};
const execDeps = { dispatch, shellAllowlist, webhookAllowlist, mcpAllowlist, claudeCode, codex };

// Learned dispatch policy (Pillar 6), opt-in: route this agent to the cheapest
// model that has historically cleared the bar. null (off / not enough signal)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ceauto",
"version": "0.16.0",
"version": "0.17.0",
"description": "Autonomous CEO Agent — LLM-agnostic MCP server with multi-agent orchestration",
"main": "server.js",
"type": "commonjs",
Expand Down
2 changes: 1 addition & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ async function main() {
memory.init(path.join(WORKSPACE, 'db', 'memory.sqlite'));

const server = new Server(
{ name: 'ceauto', version: '0.16.0' },
{ name: 'ceauto', version: '0.17.0' },
{ capabilities: { tools: {} } }
);

Expand Down
Loading
Loading