mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Encrypt multi-agent v2 message payloads (#26210)
## Why
Multi-agent v2 currently routes agent instructions through normal tool
arguments and inter-agent context. That means the parent model can emit
plaintext task text, Codex can persist it in history/rollouts, and the
recipient can receive it as ordinary assistant-message JSON.
This changes the v2 path so agent instructions stay encrypted between
model calls: Responses encrypts the `message` argument returned by the
model, Codex forwards only that ciphertext, and Responses decrypts it
internally for the recipient model.
## What changed
- Mark the v2 `message` parameter as encrypted for `spawn_agent`,
`send_message`, and `followup_task`.
- Treat multi-agent v2 tool `message` values as ciphertext
unconditionally.
- Store v2 inter-agent task text in
`InterAgentCommunication.encrypted_content` with empty plaintext
`content`.
- Convert encrypted inter-agent communications into the Responses
`agent_message` input item before sending the child request.
- Preserve `agent_message` items across history, rollout, compaction,
telemetry, and app-server schema paths.
- Leave multi-agent v1 unchanged.
## Message shape
The model still calls the v2 tools with a `message` argument, but that
value is now ciphertext:
```json
{
"name": "spawn_agent",
"arguments": {
"task_name": "worker",
"message": "<ciphertext>"
}
}
```
Codex stores the task as encrypted inter-agent communication:
```json
{
"author": "/root",
"recipient": "/root/worker",
"content": "",
"encrypted_content": "<ciphertext>",
"trigger_turn": true
}
```
When Codex builds the recipient request, it forwards the ciphertext
using the new Responses input item:
```json
{
"type": "agent_message",
"author": "/root",
"recipient": "/root/worker",
"content": [
{
"type": "encrypted_content",
"encrypted_content": "<ciphertext>"
}
]
}
```
Responses decrypts that item internally for the recipient model.
## Context impact
- Parent context no longer carries plaintext v2 agent task instructions
from these tool arguments.
- Codex rollout/history stores ciphertext for v2 agent instructions.
- Recipient requests receive an `agent_message` item instead of
assistant commentary JSON for encrypted task delivery.
- Plaintext completion/status notifications are still plaintext because
they are Codex-generated status messages, not encrypted model tool
arguments.
## Validation
- `just test -p codex-tools`
- `just test -p codex-protocol`
- `just test -p codex-rollout`
- `just test -p codex-rollout-trace`
- `just test -p codex-otel`
- `just write-app-server-schema`
This commit is contained in:
@@ -715,6 +715,12 @@ pub enum ContentItem {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AgentMessageInputContent {
|
||||
EncryptedContent { encrypted_content: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ImageDetail {
|
||||
@@ -758,6 +764,11 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
phase: Option<MessagePhase>,
|
||||
},
|
||||
AgentMessage {
|
||||
author: String,
|
||||
recipient: String,
|
||||
content: Vec<AgentMessageInputContent>,
|
||||
},
|
||||
Reasoning {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
|
||||
@@ -33,6 +33,7 @@ use crate::mcp::CallToolResult;
|
||||
use crate::mcp::RequestId;
|
||||
use crate::memory_citation::MemoryCitation;
|
||||
use crate::models::ActivePermissionProfile;
|
||||
use crate::models::AgentMessageInputContent;
|
||||
use crate::models::BaseInstructions;
|
||||
use crate::models::ContentItem;
|
||||
use crate::models::ImageDetail;
|
||||
@@ -690,6 +691,9 @@ pub struct InterAgentCommunication {
|
||||
#[serde(default)]
|
||||
pub other_recipients: Vec<AgentPath>,
|
||||
pub content: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub encrypted_content: Option<String>,
|
||||
pub trigger_turn: bool,
|
||||
}
|
||||
|
||||
@@ -706,6 +710,24 @@ impl InterAgentCommunication {
|
||||
recipient,
|
||||
other_recipients,
|
||||
content,
|
||||
encrypted_content: None,
|
||||
trigger_turn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_encrypted(
|
||||
author: AgentPath,
|
||||
recipient: AgentPath,
|
||||
other_recipients: Vec<AgentPath>,
|
||||
encrypted_content: String,
|
||||
trigger_turn: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
author,
|
||||
recipient,
|
||||
other_recipients,
|
||||
content: String::new(),
|
||||
encrypted_content: Some(encrypted_content),
|
||||
trigger_turn,
|
||||
}
|
||||
}
|
||||
@@ -720,6 +742,19 @@ impl InterAgentCommunication {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_model_input_item(&self) -> ResponseItem {
|
||||
match &self.encrypted_content {
|
||||
Some(encrypted_content) => ResponseItem::AgentMessage {
|
||||
author: self.author.to_string(),
|
||||
recipient: self.recipient.to_string(),
|
||||
content: vec![AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: encrypted_content.clone(),
|
||||
}],
|
||||
},
|
||||
None => self.to_response_input_item().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_message_content(content: &[ContentItem]) -> bool {
|
||||
Self::from_message_content(content).is_some()
|
||||
}
|
||||
@@ -4063,6 +4098,7 @@ mod tests {
|
||||
recipient: AgentPath::root().join("reviewer").expect("recipient path"),
|
||||
other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],
|
||||
content: "review the diff".to_string(),
|
||||
encrypted_content: None,
|
||||
trigger_turn: true,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user