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 .github/workflows/codespell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v5
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@9ba2c57125d4908eade4308f32c4ff814c184633 # v1
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
- name: Codespell
uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2.1
with:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ jobs:
- name: Install sccache
uses: taiki-e/install-action@6f9c7cc51aa54b13cbcbd12f8bbf69d8ba405b4b # v2
with:
tool: sccache
version: 0.7.5
tool: sccache@0.7.5

- name: Configure sccache backend
shell: bash
Expand Down
46 changes: 0 additions & 46 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ rand = "0.9"
ratatui = "0.29.0"
ratatui-macros = "0.6.0"
regex-lite = "0.1.7"
reqwest = "0.12"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
rmcp = { version = "0.8.4", default-features = false }
schemars = "0.8.22"
seccompiler = "0.5.0"
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/app-server/tests/suite/user_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ async fn get_user_agent_returns_current_codex_user_agent() -> Result<()> {
.await??;

let os_info = os_info::get();
let originator = codex_core::default_client::originator().value.as_str();
let user_agent = format!(
"codex_cli_rs/0.0.0 ({} {}; {}) {} (codex-app-server-tests; 0.1.0)",
"{originator}/0.0.0 ({} {}; {}) {} (codex-app-server-tests; 0.1.0)",
os_info.os_type(),
os_info.version(),
os_info.architecture().unwrap_or("unknown"),
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ mcp-types = { workspace = true }
os_info = { workspace = true }
rand = { workspace = true }
regex-lite = { workspace = true }
reqwest = { workspace = true, features = ["json", "stream"] }
reqwest = { workspace = true, features = ["json", "stream", "rustls-tls"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha1 = { workspace = true }
Expand Down
20 changes: 14 additions & 6 deletions codex-rs/core/src/default_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ pub fn create_client() -> CodexHttpClient {
let build_client = || {
let mut builder = reqwest::Client::builder()
.use_rustls_tls()
// Set UA via dedicated helper to avoid header validation pitfalls
.user_agent(ua.clone())
.default_headers(default_headers.clone());
if is_sandboxed() {
Expand Down Expand Up @@ -302,7 +303,8 @@ mod tests {
#[test]
fn test_get_codex_user_agent() {
let user_agent = get_codex_user_agent();
assert!(user_agent.starts_with("codex_cli_rs/"));
let prefix = format!("{}/", originator().value.as_str());
assert!(user_agent.starts_with(&prefix));
}

#[tokio::test]
Expand Down Expand Up @@ -343,7 +345,10 @@ mod tests {
let originator_header = headers
.get("originator")
.expect("originator header missing");
assert_eq!(originator_header.to_str().unwrap(), "codex_cli_rs");
assert_eq!(
originator_header.to_str().unwrap(),
originator().value.as_str(),
);

// User-Agent matches the computed Codex UA for that originator
let expected_ua = get_codex_user_agent();
Expand Down Expand Up @@ -379,11 +384,14 @@ mod tests {
#[cfg(target_os = "macos")]
fn test_macos() {
use regex_lite::Regex;
use regex_lite::escape;
let user_agent = get_codex_user_agent();
let re = Regex::new(
r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$",
)
.unwrap();
let originator = escape(originator().value.as_str());
let pattern = format!(
r"^{}/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$",
originator,
);
let re = Regex::new(&pattern).unwrap();
assert!(re.is_match(&user_agent));
}
}
11 changes: 9 additions & 2 deletions codex-rs/core/tests/suite/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use codex_core::ResponseItem;
use codex_core::WireApi;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::built_in_model_providers;
use codex_core::default_client::originator;
use codex_core::error::CodexErr;
use codex_core::model_family::find_family_for_model;
use codex_core::protocol::EventMsg;
Expand Down Expand Up @@ -368,7 +369,10 @@ async fn includes_conversation_id_and_model_headers_in_request() {
request_conversation_id.to_str().unwrap(),
conversation_id.to_string()
);
assert_eq!(request_originator.to_str().unwrap(), "codex_cli_rs");
assert_eq!(
request_originator.to_str().unwrap(),
originator().value.as_str(),
);
assert_eq!(
request_authorization.to_str().unwrap(),
"Bearer Test API Key"
Expand Down Expand Up @@ -485,7 +489,10 @@ async fn chatgpt_auth_sends_correct_request() {
request_conversation_id.to_str().unwrap(),
conversation_id.to_string()
);
assert_eq!(request_originator.to_str().unwrap(), "codex_cli_rs");
assert_eq!(
request_originator.to_str().unwrap(),
originator().value.as_str(),
);
assert_eq!(
request_authorization.to_str().unwrap(),
"Bearer Access Token"
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/mcp-server/tests/common/mcp_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ impl McpProcess {

let initialized = self.read_jsonrpc_message().await?;
let os_info = os_info::get();
let originator = codex_core::default_client::originator().value.as_str();
let user_agent = format!(
"codex_cli_rs/0.0.0 ({} {}; {}) {} (elicitation test; 0.0.0)",
"{originator}/0.0.0 ({} {}; {}) {} (elicitation test; 0.0.0)",
os_info.os_type(),
os_info.version(),
os_info.architecture().unwrap_or("unknown"),
Expand Down
94 changes: 63 additions & 31 deletions sdk/native/examples/agents/agents-guardrails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,43 @@ import * as os from 'node:os';
import * as path from 'node:path';
import { promises as fs } from 'node:fs';
import { z } from 'zod';
import { Agent, run } from '@openai/agents';
import { CodexProvider } from '../../src/index';

type GuardrailInput = string | unknown[];

function normalizeInput(input: GuardrailInput): string {
if (typeof input === 'string') {
return input;
}

try {
return input
.map(item => {
if (typeof item === 'string') {
return item;
}
return JSON.stringify(item);
})
.join('\n');
} catch {
return JSON.stringify(input);
}
}

function guardrailSuccess(outputInfo?: Record<string, unknown>) {
return {
tripwireTriggered: false,
outputInfo,
};
}

function guardrailFailure(reason: string) {
return {
tripwireTriggered: true,
outputInfo: { reason },
};
}
import * as Agents from '@openai/agents';
import { setDefaultModelProvider } from '@openai/agents-core';
import { CodexProvider } from '../../src/index';
Expand Down Expand Up @@ -83,26 +120,24 @@ async function main() {
});

// Create a guardrail that validates input structure
const inputValidationGuardrail = guardrail({
const inputValidationGuardrail = {
name: 'input-validation',
validate: async (input: string) => {
execute: async ({ input }: { input: GuardrailInput }) => {
const normalized = normalizeInput(input);
try {
// Try to parse as JSON first
const parsed = JSON.parse(input);
const parsed = JSON.parse(normalized);
validInputSchema.parse(parsed);
return { valid: true };
return guardrailSuccess({ mode: 'json' });
} catch (error) {
// If not JSON, check if it's a plain string that meets requirements
if (typeof input === 'string' && input.length >= 10 && input.length <= 1000) {
return { valid: true };
if (normalized.length >= 10 && normalized.length <= 1000) {
return guardrailSuccess({ mode: 'text', length: normalized.length });
}
return {
valid: false,
reason: 'Input must be between 10 and 1000 characters, or a valid JSON object with type, content, and optional priority fields',
};
return guardrailFailure(
'Input must be between 10 and 1000 characters, or a valid JSON object with type, content, and optional priority fields'
);
}
},
});
};

const agentWithValidation = new Agent({
name: 'ValidatedAgent',
Expand Down Expand Up @@ -143,22 +178,19 @@ async function main() {
// List of blocked terms (in a real app, this would be more sophisticated)
const blockedTerms = ['spam', 'phishing', 'malware', 'hack'];

const contentFilterGuardrail = guardrail({
const contentFilterGuardrail = {
name: 'content-filter',
validate: async (input: string) => {
const lowerInput = input.toLowerCase();
execute: async ({ input }: { input: GuardrailInput }) => {
const lowerInput = normalizeInput(input).toLowerCase();
const foundTerms = blockedTerms.filter(term => lowerInput.includes(term));

if (foundTerms.length > 0) {
return {
valid: false,
reason: `Input contains blocked terms: ${foundTerms.join(', ')}`,
};
return guardrailFailure(`Input contains blocked terms: ${foundTerms.join(', ')}`);
}

return { valid: true };
return guardrailSuccess();
},
});
};

const filteredAgent = new Agent({
name: 'FilteredAgent',
Expand Down Expand Up @@ -202,20 +234,20 @@ async function main() {
/drop\s+table/i,
];

const securityGuardrail = guardrail({
const securityGuardrail = {
name: 'security-check',
validate: async (input: string) => {
execute: async ({ input }: { input: GuardrailInput }) => {
const normalized = normalizeInput(input);
for (const pattern of dangerousPatterns) {
if (pattern.test(input)) {
return {
valid: false,
reason: 'Input contains potentially dangerous commands that could cause data loss',
};
if (pattern.test(normalized)) {
return guardrailFailure(
'Input contains potentially dangerous commands that could cause data loss'
);
}
}
return { valid: true };
return guardrailSuccess();
},
});
};

const secureAgent = new Agent({
name: 'SecureAgent',
Expand Down
Loading
Loading