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:
jif
2026-06-05 10:25:57 +02:00
committed by GitHub
parent 6a6a5f925e
commit 5f4d06ef18
34 changed files with 674 additions and 59 deletions
+8
View File
@@ -43,6 +43,9 @@ pub struct JsonSchema {
pub schema_type: Option<JsonSchemaType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Responses-only marker for reviewed encrypted tool parameters.
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypted: Option<bool>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<JsonValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -90,6 +93,11 @@ impl JsonSchema {
Self::typed(JsonSchemaPrimitiveType::String, description)
}
pub fn with_encrypted(mut self) -> Self {
self.encrypted = Some(true);
self
}
pub fn number(description: Option<String>) -> Self {
Self::typed(JsonSchemaPrimitiveType::Number, description)
}
+14
View File
@@ -24,6 +24,20 @@ fn parse_tool_input_schema_coerces_boolean_schemas() {
assert_eq!(schema, JsonSchema::string(/*description*/ None));
}
#[test]
fn json_schema_serializes_encrypted_marker() {
let schema = JsonSchema::string(Some("Secret value".to_string())).with_encrypted();
assert_eq!(
serde_json::to_value(schema).expect("serialize schema"),
serde_json::json!({
"type": "string",
"description": "Secret value",
"encrypted": true,
})
);
}
#[test]
fn parse_tool_input_schema_infers_object_shape_and_defaults_properties() {
// Example schema shape: