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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ The redacted template for a project config lives at `decodex.example.toml`.
When a project enables `[codex.accounts]`, the shared ChatGPT account pool is
`~/.codex/decodex/accounts.jsonl`; it is global Decodex state, not a project-local
file, and project configs do not own an account-pool path override. Set
`[codex.accounts].fixed_account` to pin runs to one account; the operator dashboard can
write the same project config field from the Accounts UI.
`[codex.accounts].fixed_account` in `~/.codex/decodex/config.toml` to pin all new
account-pool runs to one account. When that global selector is absent, Decodex balances
new runs across the pool. The operator dashboard Accounts UI writes and clears the same
global selector; project configs do not pin specific accounts.

`decodex diagnose --json` writes the local agent evidence index under
`~/.codex/decodex/agent-evidence/<service-id>/` and prints the same handoff index for
Expand Down
4 changes: 3 additions & 1 deletion apps/decodex/src/agent/codex_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ pub(crate) struct CodexAccountPool {
}
impl CodexAccountPool {
pub(crate) fn from_config(config: &ProjectCodexAccountsConfig) -> crate::prelude::Result<Self> {
let fixed_account = runtime::global_fixed_account_selector()?;

Self::new_with_fixed_account(
runtime::accounts_path()?,
config.usage_endpoint().unwrap_or(DEFAULT_USAGE_ENDPOINT),
config.refresh_endpoint().unwrap_or(DEFAULT_REFRESH_ENDPOINT),
config.fixed_account(),
fixed_account.as_deref(),
)
}

Expand Down
36 changes: 24 additions & 12 deletions apps/decodex/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ impl Default for ProjectCodexConfig {
pub struct ProjectCodexAccountsConfig {
usage_endpoint: Option<String>,
refresh_endpoint: Option<String>,
fixed_account: Option<String>,
}
impl ProjectCodexAccountsConfig {
/// Override for ChatGPT usage probes. Defaults to the Codex `/wham/usage` endpoint.
Expand All @@ -228,11 +227,6 @@ impl ProjectCodexAccountsConfig {
self.refresh_endpoint.as_deref()
}

/// Fixed account selector for runs. Matches an account email, id, or displayed fingerprint.
pub fn fixed_account(&self) -> Option<&str> {
self.fixed_account.as_deref()
}

fn validate(&self) -> Result<()> {
validate_optional_nonempty_string(
"codex.accounts.usage_endpoint",
Expand All @@ -242,10 +236,6 @@ impl ProjectCodexAccountsConfig {
"codex.accounts.refresh_endpoint",
self.refresh_endpoint.as_deref(),
)?;
validate_optional_nonempty_string(
"codex.accounts.fixed_account",
self.fixed_account.as_deref(),
)?;

Ok(())
}
Expand Down Expand Up @@ -1099,15 +1089,37 @@ mod tests {
[codex.accounts]
usage_endpoint = "http://127.0.0.1:1234/wham/usage"
refresh_endpoint = "http://127.0.0.1:1234/oauth/token"
fixed_account = "primary@example.com"
"#,
);
let config = ServiceConfig::from_path(&config_path).expect("accounts should parse");
let accounts = config.codex().accounts().expect("accounts should be configured");

assert_eq!(accounts.usage_endpoint(), Some("http://127.0.0.1:1234/wham/usage"));
assert_eq!(accounts.refresh_endpoint(), Some("http://127.0.0.1:1234/oauth/token"));
assert_eq!(accounts.fixed_account(), Some("primary@example.com"));
}

#[test]
fn rejects_project_scoped_codex_fixed_account() {
let temp_dir = TempDir::new().expect("temp dir should exist");
let config_path = write_config_file(
temp_dir.path(),
r#"
service_id = "pubfi"

[tracker]
api_key_env_var = "HOME"

[github]
token_env_var = "HOME"

[codex.accounts]
fixed_account = "primary@example.com"
"#,
);
let error = ServiceConfig::from_path(&config_path)
.expect_err("project-scoped account selection should fail");

assert!(error.to_string().contains("fixed_account"));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion apps/decodex/src/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use time::{OffsetDateTime, format_description::well_known::Rfc3339};

use crate::{agent, default_branch_sync, git_credentials, state};
#[rustfmt::skip]
use crate::{agent::{ACTIVE_RUN_IDLE_TIMEOUT, AppServerCapabilityPreflightFailure, AppServerDynamicToolFailure, AppServerHomePreflightFailure, AppServerProcessEnv, AppServerRunRequest, AppServerRunResult, AppServerTransportFailure, AppServerTurnFailure, ISSUE_DELIVERY_CLOSEOUT_COMPLETE_TOOL_NAME, ISSUE_LABEL_ADD_TOOL_NAME, ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME, ISSUE_REVIEW_CHECKPOINT_TOOL_NAME, ISSUE_REVIEW_HANDOFF_TOOL_NAME, ISSUE_REVIEW_REPAIR_COMPLETE_TOOL_NAME, ISSUE_TERMINAL_FINALIZE_TOOL_NAME, ISSUE_TRANSITION_TOOL_NAME, DecodexRunContext, DecodexToolBridge, ReviewExecutionMode, ReviewHandoffContext, ReviewHandoffWritebackFailed, ReviewPolicyStopReason, ReviewPolicyStopRequested, RunCompletionDisposition, TrackerToolBridge, TurnContinuationGuard}, config::{InternalReviewMode, ProjectCodexAccountsConfig, ServiceConfig}, git_credentials::GitCredentialSource, github, prelude::{Result, eyre}, state::{ChildAgentActivityBucket, ChildAgentActivitySummary, CodexAccountActivitySummary, ProjectRegistration, ProjectRunStatus, ProtocolActivitySummary, RUN_OPERATION_AGENT_RUN, RUN_OPERATION_APP_SERVER_PREFLIGHT, RUN_OPERATION_GIT_CREDENTIALS, RUN_OPERATION_IDLE, RUN_OPERATION_RECONCILIATION, RUN_OPERATION_REPO_GATE, RUN_OPERATION_REVIEW_WRITEBACK, RUN_OPERATION_WAITING_EXTERNAL, ReviewHandoffMarker, ReviewOrchestrationMarker, RunActivityMarker, RunAttempt, StateStore, WorktreeMapping}, tracker::{IssueTracker, TrackerComment, TrackerIssue, linear::LinearClient, records}, workflow::{WorkflowDocument, WorkflowExecution}, worktree::{WorktreeManager, WorktreeSpec}};
use crate::{agent::{ACTIVE_RUN_IDLE_TIMEOUT, AppServerCapabilityPreflightFailure, AppServerDynamicToolFailure, AppServerHomePreflightFailure, AppServerProcessEnv, AppServerRunRequest, AppServerRunResult, AppServerTransportFailure, AppServerTurnFailure, ISSUE_DELIVERY_CLOSEOUT_COMPLETE_TOOL_NAME, ISSUE_LABEL_ADD_TOOL_NAME, ISSUE_PROGRESS_CHECKPOINT_TOOL_NAME, ISSUE_REVIEW_CHECKPOINT_TOOL_NAME, ISSUE_REVIEW_HANDOFF_TOOL_NAME, ISSUE_REVIEW_REPAIR_COMPLETE_TOOL_NAME, ISSUE_TERMINAL_FINALIZE_TOOL_NAME, ISSUE_TRANSITION_TOOL_NAME, DecodexRunContext, DecodexToolBridge, ReviewExecutionMode, ReviewHandoffContext, ReviewHandoffWritebackFailed, ReviewPolicyStopReason, ReviewPolicyStopRequested, RunCompletionDisposition, TrackerToolBridge, TurnContinuationGuard}, config::{InternalReviewMode, ServiceConfig}, git_credentials::GitCredentialSource, github, prelude::{Result, eyre}, state::{ChildAgentActivityBucket, ChildAgentActivitySummary, CodexAccountActivitySummary, ProjectRegistration, ProjectRunStatus, ProtocolActivitySummary, RUN_OPERATION_AGENT_RUN, RUN_OPERATION_APP_SERVER_PREFLIGHT, RUN_OPERATION_GIT_CREDENTIALS, RUN_OPERATION_IDLE, RUN_OPERATION_RECONCILIATION, RUN_OPERATION_REPO_GATE, RUN_OPERATION_REVIEW_WRITEBACK, RUN_OPERATION_WAITING_EXTERNAL, ReviewHandoffMarker, ReviewOrchestrationMarker, RunActivityMarker, RunAttempt, StateStore, WorktreeMapping}, tracker::{IssueTracker, TrackerComment, TrackerIssue, linear::LinearClient, records}, workflow::{WorkflowDocument, WorkflowExecution}, worktree::{WorktreeManager, WorktreeSpec}};

include!("orchestrator/types.rs");

Expand Down
1 change: 1 addition & 0 deletions apps/decodex/src/orchestrator/entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ where
aggregate_control_plane_snapshot(registered_project_count, project_snapshots);

snapshot.projects = project_statuses;
snapshot.account_control = global_codex_account_control_status();

for warning in snapshot_warnings {
add_operator_snapshot_warning(&mut snapshot, warning);
Expand Down
Loading