mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Merge main and remove unused migration code
This commit is contained in:
@@ -29,7 +29,7 @@ Accept any of the following:
|
||||
4. If `diagnose_ci_failure` is present, inspect failed run logs and classify the failure.
|
||||
5. If the failure is likely caused by the current branch, patch code locally, commit, and push. Do not patch random flaky tests, CI infrastructure, dependency outages, runner issues, or other failures that are unrelated to the branch.
|
||||
6. If `process_review_comment` is present, inspect surfaced review items and decide whether to address them.
|
||||
7. If a review item is actionable and correct, patch code locally, commit, push, and then mark the associated review thread/comment as resolved once the fix is on GitHub.
|
||||
7. If a review item is actionable and correct, patch code locally, commit, push, and then resolve the associated review thread only when allowed by the GitHub state mutation policy below.
|
||||
8. Do not post replies to human-authored review comments/threads unless the user explicitly confirms the exact response. If a human review item is non-actionable, already addressed, or not valid, surface the item and recommended response to the user instead of replying on GitHub.
|
||||
9. If the failure is likely flaky/unrelated and `retry_failed_checks` is present, rerun failed jobs with `--retry-failed-now`.
|
||||
10. If both actionable review feedback and `retry_failed_checks` are present, prioritize review feedback first; a new commit will retrigger CI, so avoid rerunning flaky checks on the old SHA unless you intentionally defer the review change.
|
||||
@@ -101,7 +101,7 @@ When you agree with a comment and it is actionable:
|
||||
1. Patch code locally.
|
||||
2. Commit with `codex: address PR review feedback (#<n>)`.
|
||||
3. Push to the PR head branch.
|
||||
4. After the push succeeds, mark the associated GitHub review thread/comment as resolved.
|
||||
4. After the push succeeds, resolve the associated GitHub review thread only when allowed by the GitHub state mutation policy below.
|
||||
5. Resume watching on the new SHA immediately (do not stop after reporting the push).
|
||||
6. If monitoring was running in `--watch` mode, restart `--watch` immediately after the push in the same turn; do not wait for the user to ask again.
|
||||
|
||||
@@ -109,6 +109,31 @@ Do not post replies to human-authored GitHub review comments/threads automatical
|
||||
If the watcher later surfaces your own approved reply because the authenticated operator is treated as a trusted review author, treat that self-authored item as already handled and do not reply again.
|
||||
If a code review comment/thread is already marked as resolved in GitHub, treat it as non-actionable and safely ignore it unless new unresolved follow-up feedback appears.
|
||||
|
||||
## GitHub State Mutation Policy
|
||||
|
||||
You can read any PR state you need for monitoring. Writes must comply with this policy.
|
||||
|
||||
You can push PRs to update the code under review or to force CI re-runs as described above.
|
||||
|
||||
You can resolve review comment threads from the human who requested babysitting or from the Codex
|
||||
review bot. When resolving, leave a comment prefixed with `[from Codex]: ` and explain what changes
|
||||
you made and which commit includes them. Don't touch review threads if other humans other than the
|
||||
user who requested babysitting have participated.
|
||||
|
||||
Before making any changes, fetch the PR state yourself instead of relying on the PR watcher script's
|
||||
output.
|
||||
|
||||
Unless explicitly asked, do not:
|
||||
|
||||
* comment on other humans' review threads, communicate with the user in chat instead
|
||||
* resolve review threads from humans other than the user
|
||||
* interact with humans other than the user
|
||||
* mark PRs as drafts or ready for review
|
||||
* close or reopen PRs
|
||||
|
||||
In general, never act on GitHub in ways that would make it hard to tell whether you or the user did
|
||||
something visible to other humans. When in doubt, ask the user for clarification in chat.
|
||||
|
||||
## Git Safety Rules
|
||||
|
||||
- Work only on the PR head branch.
|
||||
@@ -133,10 +158,10 @@ Use this loop in a live Codex session:
|
||||
3. First check whether the PR is now merged or otherwise closed; if so, report that terminal state and stop polling immediately.
|
||||
4. Check CI summary, new review items, and mergeability/conflict status.
|
||||
5. Diagnose CI failures and classify branch-related vs flaky/unrelated. If the overall run is still pending but `failed_jobs` already includes a failed job, fetch that job's logs and diagnose immediately instead of waiting for the whole workflow run to finish. Patch only when the failure is branch-related.
|
||||
6. For each surfaced review item from another author, patch/commit/push and then resolve it if it is actionable. If it is non-actionable, already addressed, or requires a written answer, surface it to the user with a suggested response instead of posting automatically. If a later snapshot surfaces your own approved reply, treat it as informational and continue without responding again.
|
||||
6. For each surfaced review item from another author, patch/commit/push if it is actionable, then resolve it only when allowed by the GitHub state mutation policy above. If it is non-actionable, already addressed, or requires a written answer, surface it to the user with a suggested response instead of posting automatically. If a later snapshot surfaces your own approved reply, treat it as informational and continue without responding again.
|
||||
7. Process actionable review comments before flaky reruns when both are present; if a review fix requires a commit, push it and skip rerunning failed checks on the old SHA.
|
||||
8. Retry failed checks only when `retry_failed_checks` is present and you are not about to replace the current SHA with a review/CI fix commit. Do not make code changes for unrelated flakes or infrastructure failures just to get CI green.
|
||||
9. If you pushed a commit, resolved a review thread, or triggered a rerun, report the action briefly and continue polling (do not stop). If a human review comment needs a written GitHub response, stop and ask for confirmation before posting.
|
||||
9. If you pushed a commit, resolved an eligible review thread, or triggered a rerun, report the action briefly and continue polling (do not stop). If a human review comment needs a written GitHub response, stop and ask for confirmation before posting.
|
||||
10. After a review-fix push, proactively restart continuous monitoring (`--watch`) in the same turn unless a strict stop condition has already been reached.
|
||||
11. If everything is passing, mergeable, not blocked on required review approval, and there are no unaddressed review items, report that the PR is currently ready to merge but keep the watcher running so new review comments are surfaced quickly while the PR remains open.
|
||||
12. If blocked on a user-help-required issue (infra outage, exhausted flaky retries, unclear reviewer request, permissions), report the blocker and stop.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/codex-rs/core/ @openai/codex-core-agent-team
|
||||
/codex-rs/ext/extension-api/ @openai/codex-core-agent-team
|
||||
/codex-rs/prompts/ @openai/codex-core-agent-team
|
||||
/codex-rs/utils/path-uri/ @openai/codex-core-agent-team
|
||||
|
||||
# Keep macOS AKV signing changes reviewed by Codex maintainers.
|
||||
/.github/actions/setup-akv-pkcs11-codesigning/ @openai/codex-core-agent-team
|
||||
|
||||
Generated
+16
@@ -3106,6 +3106,7 @@ dependencies = [
|
||||
"codex-protocol",
|
||||
"codex-tools",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-image",
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
@@ -4151,6 +4152,21 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-path-uri"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"ts-rs",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-plugins"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -83,6 +83,7 @@ members = [
|
||||
"tools",
|
||||
"v8-poc",
|
||||
"utils/absolute-path",
|
||||
"utils/path-uri",
|
||||
"utils/cargo-bin",
|
||||
"git-utils",
|
||||
"utils/cache",
|
||||
|
||||
@@ -108,6 +108,7 @@ use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ServerResponse;
|
||||
use codex_app_server_protocol::SessionSource as AppServerSessionSource;
|
||||
use codex_app_server_protocol::SubAgentActivityKind;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
@@ -3318,7 +3319,6 @@ fn turn_event_serializes_expected_shape() {
|
||||
status: Some(TurnStatus::Completed),
|
||||
turn_error: None,
|
||||
codex_error_kind: None,
|
||||
codex_error_subreason: None,
|
||||
codex_error_http_status_code: None,
|
||||
steer_count: Some(0),
|
||||
total_tool_call_count: None,
|
||||
@@ -3391,7 +3391,6 @@ fn turn_event_serializes_expected_shape() {
|
||||
"status": "completed",
|
||||
"turn_error": null,
|
||||
"codex_error_kind": null,
|
||||
"codex_error_subreason": null,
|
||||
"codex_error_http_status_code": null,
|
||||
"steer_count": 0,
|
||||
"total_tool_call_count": null,
|
||||
@@ -3796,6 +3795,12 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
reasoning_effort: None,
|
||||
agents_states: Default::default(),
|
||||
},
|
||||
ThreadItem::SubAgentActivity {
|
||||
id: "sub-agent-activity-1".to_string(),
|
||||
kind: SubAgentActivityKind::Interacted,
|
||||
agent_thread_id: "thread-child".to_string(),
|
||||
agent_path: "/root/child".to_string(),
|
||||
},
|
||||
ThreadItem::WebSearch {
|
||||
id: "web-1".to_string(),
|
||||
query: "codex".to_string(),
|
||||
@@ -3843,14 +3848,14 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
.find(|event| matches!(event, TrackEventRequest::TurnEvent(_)))
|
||||
.expect("turn event should be emitted");
|
||||
let payload = serde_json::to_value(turn_event).expect("serialize turn event");
|
||||
assert_eq!(payload["event_params"]["total_tool_call_count"], json!(7));
|
||||
assert_eq!(payload["event_params"]["total_tool_call_count"], json!(8));
|
||||
assert_eq!(payload["event_params"]["shell_command_count"], json!(1));
|
||||
assert_eq!(payload["event_params"]["file_change_count"], json!(1));
|
||||
assert_eq!(payload["event_params"]["mcp_tool_call_count"], json!(1));
|
||||
assert_eq!(payload["event_params"]["dynamic_tool_call_count"], json!(1));
|
||||
assert_eq!(
|
||||
payload["event_params"]["subagent_tool_call_count"],
|
||||
json!(1)
|
||||
json!(2)
|
||||
);
|
||||
assert_eq!(payload["event_params"]["web_search_count"], json!(1));
|
||||
assert_eq!(payload["event_params"]["image_generation_count"], json!(1));
|
||||
@@ -4098,10 +4103,6 @@ async fn turn_lifecycle_emits_failed_turn_event() {
|
||||
payload["event_params"]["codex_error_kind"],
|
||||
json!("invalid_request")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["codex_error_subreason"],
|
||||
json!("unknown turn environment id `env-2`")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["codex_error_http_status_code"],
|
||||
json!(null)
|
||||
|
||||
@@ -803,7 +803,6 @@ pub(crate) struct CodexTurnEventParams {
|
||||
pub(crate) status: Option<TurnStatus>,
|
||||
pub(crate) turn_error: Option<CodexErrorInfo>,
|
||||
pub(crate) codex_error_kind: Option<CodexErrKind>,
|
||||
pub(crate) codex_error_subreason: Option<String>,
|
||||
pub(crate) codex_error_http_status_code: Option<u16>,
|
||||
pub(crate) steer_count: Option<usize>,
|
||||
pub(crate) total_tool_call_count: Option<usize>,
|
||||
|
||||
@@ -30,9 +30,6 @@ use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const INVALID_REQUEST_SUBREASON_MAX_BYTES: usize = 512;
|
||||
const INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX: &str = "...";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AcceptedLineFingerprint {
|
||||
pub path_hash: String,
|
||||
@@ -182,7 +179,6 @@ pub(crate) enum CodexErrKind {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TurnCodexError {
|
||||
pub(crate) kind: CodexErrKind,
|
||||
pub(crate) subreason: Option<String>,
|
||||
pub(crate) http_status_code: Option<u16>,
|
||||
}
|
||||
|
||||
@@ -190,26 +186,6 @@ impl TurnCodexError {
|
||||
fn from_codex_err(error: &CodexErr) -> Self {
|
||||
Self {
|
||||
kind: error.into(),
|
||||
subreason: match error {
|
||||
CodexErr::InvalidRequest(message) => {
|
||||
// InvalidRequest can contain raw provider response bodies, so bound the
|
||||
// analytics copy without changing the source CodexErr.
|
||||
let subreason = if message.len() <= INVALID_REQUEST_SUBREASON_MAX_BYTES {
|
||||
message.clone()
|
||||
} else {
|
||||
let truncated_len = message.floor_char_boundary(
|
||||
INVALID_REQUEST_SUBREASON_MAX_BYTES
|
||||
.saturating_sub(INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX.len()),
|
||||
);
|
||||
format!(
|
||||
"{}{INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX}",
|
||||
&message[..truncated_len]
|
||||
)
|
||||
};
|
||||
Some(subreason)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
http_status_code: error.http_status_code_value(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +360,9 @@ impl TurnToolCounts {
|
||||
ThreadItem::FileChange { .. } => self.file_change += 1,
|
||||
ThreadItem::McpToolCall { .. } => self.mcp_tool_call += 1,
|
||||
ThreadItem::DynamicToolCall { .. } => self.dynamic_tool_call += 1,
|
||||
ThreadItem::CollabAgentToolCall { .. } => self.subagent_tool_call += 1,
|
||||
ThreadItem::CollabAgentToolCall { .. } | ThreadItem::SubAgentActivity { .. } => {
|
||||
self.subagent_tool_call += 1;
|
||||
}
|
||||
ThreadItem::WebSearch { .. } => self.web_search += 1,
|
||||
ThreadItem::ImageGeneration { .. } => self.image_generation += 1,
|
||||
ThreadItem::UserMessage { .. }
|
||||
@@ -1091,6 +1093,18 @@ impl AnalyticsReducer {
|
||||
);
|
||||
}
|
||||
ServerNotification::ItemCompleted(notification) => {
|
||||
if matches!(notification.item, ThreadItem::SubAgentActivity { .. }) {
|
||||
let Some(turn_state) = self.turns.get_mut(¬ification.turn_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %notification.thread_id,
|
||||
turn_id = %notification.turn_id,
|
||||
"dropping sub-agent activity tool count update: missing turn state"
|
||||
);
|
||||
return;
|
||||
};
|
||||
turn_state.tool_counts.record(¬ification.item);
|
||||
return;
|
||||
}
|
||||
let Some(item_id) = tracked_tool_item_id(¬ification.item) else {
|
||||
return;
|
||||
};
|
||||
@@ -1562,6 +1576,7 @@ fn tracked_tool_item_id(item: &ThreadItem) -> Option<&str> {
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
| ThreadItem::Plan { .. }
|
||||
| ThreadItem::Reasoning { .. }
|
||||
| ThreadItem::SubAgentActivity { .. }
|
||||
| ThreadItem::ImageView { .. }
|
||||
| ThreadItem::EnteredReviewMode { .. }
|
||||
| ThreadItem::ExitedReviewMode { .. }
|
||||
@@ -2470,7 +2485,6 @@ fn codex_turn_event_params(
|
||||
status: completed.status,
|
||||
turn_error: completed.turn_error,
|
||||
codex_error_kind: codex_error.map(|error| error.kind),
|
||||
codex_error_subreason: codex_error.and_then(|error| error.subreason.clone()),
|
||||
codex_error_http_status_code: codex_error.and_then(|error| error.http_status_code),
|
||||
steer_count: Some(turn_state.steer_count),
|
||||
total_tool_call_count: Some(turn_state.tool_counts.total),
|
||||
|
||||
@@ -3177,6 +3177,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -4107,6 +4115,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
+40
@@ -15580,6 +15580,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -16892,6 +16900,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/v2/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
+40
@@ -13397,6 +13397,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -14709,6 +14717,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -466,6 +466,14 @@
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
@@ -989,6 +997,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -466,6 +466,14 @@
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
@@ -989,6 +997,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -610,6 +610,14 @@
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
@@ -1133,6 +1141,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -861,6 +861,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1617,6 +1625,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -676,6 +676,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1432,6 +1440,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
+40
@@ -676,6 +676,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1432,6 +1440,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -676,6 +676,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1432,6 +1440,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -861,6 +861,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1617,6 +1625,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -676,6 +676,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1432,6 +1440,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -861,6 +861,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1617,6 +1625,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -676,6 +676,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1432,6 +1440,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -676,6 +676,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1432,6 +1440,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -610,6 +610,14 @@
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
@@ -1133,6 +1141,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -610,6 +610,14 @@
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
@@ -1133,6 +1141,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -610,6 +610,14 @@
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
@@ -1133,6 +1141,38 @@
|
||||
"title": "CollabAgentToolCallThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"agentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"agentThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"$ref": "#/definitions/SubAgentActivityKind"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"subAgentActivity"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentPath",
|
||||
"agentThreadId",
|
||||
"id",
|
||||
"kind",
|
||||
"type"
|
||||
],
|
||||
"title": "SubAgentActivityThreadItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SubAgentActivityKind = "started" | "interacted" | "interrupted";
|
||||
@@ -20,6 +20,7 @@ import type { McpToolCallResult } from "./McpToolCallResult";
|
||||
import type { McpToolCallStatus } from "./McpToolCallStatus";
|
||||
import type { MemoryCitation } from "./MemoryCitation";
|
||||
import type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
import type { SubAgentActivityKind } from "./SubAgentActivityKind";
|
||||
import type { UserInput } from "./UserInput";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
@@ -98,4 +99,4 @@ reasoningEffort: ReasoningEffort | null,
|
||||
/**
|
||||
* Last known status of the target agents, when available.
|
||||
*/
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
|
||||
@@ -356,6 +356,7 @@ export type { SkillsListParams } from "./SkillsListParams";
|
||||
export type { SkillsListResponse } from "./SkillsListResponse";
|
||||
export type { SortDirection } from "./SortDirection";
|
||||
export type { SpendControlLimitSnapshot } from "./SpendControlLimitSnapshot";
|
||||
export type { SubAgentActivityKind } from "./SubAgentActivityKind";
|
||||
export type { SubagentMigration } from "./SubagentMigration";
|
||||
export type { TerminalInteractionNotification } from "./TerminalInteractionNotification";
|
||||
export type { TextElement } from "./TextElement";
|
||||
|
||||
@@ -178,6 +178,20 @@ pub fn item_event_to_server_notification(
|
||||
completed_at_ms: end_event.completed_at_ms,
|
||||
})
|
||||
}
|
||||
EventMsg::SubAgentActivity(activity) => {
|
||||
let item = ThreadItem::SubAgentActivity {
|
||||
id: activity.event_id,
|
||||
kind: activity.kind.into(),
|
||||
agent_thread_id: activity.agent_thread_id.to_string(),
|
||||
agent_path: String::from(activity.agent_path),
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
completed_at_ms: activity.occurred_at_ms,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabWaitingBegin(begin_event) => {
|
||||
let receiver_thread_ids = begin_event
|
||||
.receiver_thread_ids
|
||||
|
||||
@@ -203,6 +203,7 @@ impl ThreadHistoryBuilder {
|
||||
EventMsg::CollabAgentInteractionEnd(payload) => {
|
||||
self.handle_collab_agent_interaction_end(payload)
|
||||
}
|
||||
EventMsg::SubAgentActivity(payload) => self.handle_sub_agent_activity(payload),
|
||||
EventMsg::CollabWaitingBegin(payload) => self.handle_collab_waiting_begin(payload),
|
||||
EventMsg::CollabWaitingEnd(payload) => self.handle_collab_waiting_end(payload),
|
||||
EventMsg::CollabCloseBegin(payload) => self.handle_collab_close_begin(payload),
|
||||
@@ -702,6 +703,18 @@ impl ThreadHistoryBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_sub_agent_activity(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::SubAgentActivityEvent,
|
||||
) {
|
||||
self.upsert_item_in_current_turn(ThreadItem::SubAgentActivity {
|
||||
id: payload.event_id.clone(),
|
||||
kind: payload.kind.into(),
|
||||
agent_thread_id: payload.agent_thread_id.to_string(),
|
||||
agent_path: String::from(payload.agent_path.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_collab_waiting_begin(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::CollabWaitingBeginEvent,
|
||||
|
||||
@@ -29,6 +29,7 @@ use codex_protocol::protocol::GuardianRiskLevel as CoreGuardianRiskLevel;
|
||||
use codex_protocol::protocol::GuardianUserAuthorization as CoreGuardianUserAuthorization;
|
||||
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
|
||||
use codex_protocol::protocol::ReviewDecision as CoreReviewDecision;
|
||||
use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -336,6 +337,14 @@ pub enum ThreadItem {
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
SubAgentActivity {
|
||||
id: String,
|
||||
kind: SubAgentActivityKind,
|
||||
agent_thread_id: String,
|
||||
agent_path: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
WebSearch {
|
||||
id: String,
|
||||
query: String,
|
||||
@@ -387,6 +396,7 @@ impl ThreadItem {
|
||||
| ThreadItem::McpToolCall { id, .. }
|
||||
| ThreadItem::DynamicToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::SubAgentActivity { id, .. }
|
||||
| ThreadItem::WebSearch { id, .. }
|
||||
| ThreadItem::ImageView { id, .. }
|
||||
| ThreadItem::ImageGeneration { id, .. }
|
||||
@@ -1004,6 +1014,25 @@ pub enum CollabAgentToolCallStatus {
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum SubAgentActivityKind {
|
||||
Started,
|
||||
Interacted,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
impl From<CoreSubAgentActivityKind> for SubAgentActivityKind {
|
||||
fn from(value: CoreSubAgentActivityKind) -> Self {
|
||||
match value {
|
||||
CoreSubAgentActivityKind::Started => SubAgentActivityKind::Started,
|
||||
CoreSubAgentActivityKind::Interacted => SubAgentActivityKind::Interacted,
|
||||
CoreSubAgentActivityKind::Interrupted => SubAgentActivityKind::Interrupted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -76,7 +76,7 @@ pub struct TurnStartParams {
|
||||
#[experimental("turn/start.additionalContext")]
|
||||
#[ts(optional = nullable)]
|
||||
pub additional_context: Option<HashMap<String, AdditionalContextEntry>>,
|
||||
/// Optional turn-scoped environments.
|
||||
/// Optional environments for this turn and subsequent turns.
|
||||
///
|
||||
/// Omitted uses the thread sticky environments. Empty disables
|
||||
/// environment access for this turn. Non-empty selects the first
|
||||
|
||||
@@ -100,6 +100,7 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RealtimeEvent;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::protocol::SubAgentActivityKind;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnCompleteEvent;
|
||||
@@ -767,7 +768,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
let requested_permissions = request.permissions.clone();
|
||||
let request_cwd = match request.cwd.clone() {
|
||||
Some(cwd) => cwd,
|
||||
None => conversation.config_snapshot().await.cwd,
|
||||
None => conversation.config_snapshot().await.cwd().clone(),
|
||||
};
|
||||
let params = PermissionsRequestApprovalParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
@@ -861,6 +862,24 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::SubAgentActivity(activity) => {
|
||||
if activity.kind == SubAgentActivityKind::Interrupted
|
||||
&& thread_manager
|
||||
.get_thread(activity.agent_thread_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
thread_watch_manager
|
||||
.remove_thread(&activity.agent_thread_id.to_string())
|
||||
.await;
|
||||
}
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::SubAgentActivity(activity),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::CollabCloseEnd(end_event) => {
|
||||
if thread_manager
|
||||
.get_thread(end_event.receiver_thread_id)
|
||||
@@ -1161,7 +1180,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
return;
|
||||
}
|
||||
};
|
||||
let fallback_cwd = conversation.config_snapshot().await.cwd;
|
||||
let fallback_cwd = conversation.config_snapshot().await.cwd().clone();
|
||||
let stored_thread = match conversation
|
||||
.read_thread(
|
||||
/*include_archived*/ true, /*include_history*/ true,
|
||||
@@ -2104,6 +2123,7 @@ mod tests {
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
@@ -2125,6 +2145,7 @@ mod tests {
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentActivityEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
@@ -2181,6 +2202,7 @@ mod tests {
|
||||
];
|
||||
let stored_thread = StoredThread {
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
rollout_path: None,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -3298,6 +3320,94 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn interrupted_subagent_activity_removes_missing_thread_watch() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = load_default_config_for_test(&codex_home).await;
|
||||
let thread_manager = Arc::new(
|
||||
codex_core::test_support::thread_manager_with_models_provider_and_home(
|
||||
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
),
|
||||
);
|
||||
let codex_core::NewThread {
|
||||
thread_id: conversation_id,
|
||||
thread: conversation,
|
||||
..
|
||||
} = thread_manager.start_thread(config).await?;
|
||||
let child_thread_id = ThreadId::new();
|
||||
let child_thread_id_string = child_thread_id.to_string();
|
||||
let thread_watch_manager = ThreadWatchManager::new();
|
||||
thread_watch_manager
|
||||
.note_turn_started(&child_thread_id_string)
|
||||
.await;
|
||||
assert_eq!(thread_watch_manager.running_turn_count().await, 1);
|
||||
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(
|
||||
tx,
|
||||
codex_analytics::AnalyticsEventsClient::disabled(),
|
||||
));
|
||||
let outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing,
|
||||
vec![ConnectionId(1)],
|
||||
conversation_id,
|
||||
);
|
||||
|
||||
apply_bespoke_event_handling(
|
||||
Event {
|
||||
id: "turn-1".to_string(),
|
||||
msg: EventMsg::SubAgentActivity(SubAgentActivityEvent {
|
||||
event_id: "activity-1".to_string(),
|
||||
occurred_at_ms: 42,
|
||||
agent_thread_id: child_thread_id,
|
||||
agent_path: AgentPath::try_from("/root/worker")
|
||||
.expect("agent path should parse"),
|
||||
kind: SubAgentActivityKind::Interrupted,
|
||||
}),
|
||||
},
|
||||
conversation_id,
|
||||
conversation,
|
||||
thread_manager,
|
||||
outgoing,
|
||||
new_thread_state(),
|
||||
thread_watch_manager.clone(),
|
||||
Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)),
|
||||
"test-provider".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
thread_watch_manager
|
||||
.loaded_status_for_thread(&child_thread_id_string)
|
||||
.await,
|
||||
ThreadStatus::NotLoaded
|
||||
);
|
||||
assert_eq!(thread_watch_manager.running_turn_count().await, 0);
|
||||
let message = recv_broadcast_message(&mut rx).await?;
|
||||
let OutgoingMessage::AppServerNotification(ServerNotification::ItemCompleted(payload)) =
|
||||
message
|
||||
else {
|
||||
bail!("unexpected message: {message:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
payload,
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::SubAgentActivity {
|
||||
id: "activity-1".to_string(),
|
||||
kind: codex_app_server_protocol::SubAgentActivityKind::Interrupted,
|
||||
agent_thread_id: child_thread_id_string,
|
||||
agent_path: "/root/worker".to_string(),
|
||||
},
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 42,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_turn_complete_emits_completed_without_error() -> Result<()> {
|
||||
let conversation_id = ThreadId::new();
|
||||
|
||||
@@ -330,6 +330,7 @@ use codex_core_plugins::remote::RemotePluginShareContext as RemoteCatalogPluginS
|
||||
use codex_core_plugins::remote::RemotePluginShareSummary as RemoteCatalogPluginShareSummary;
|
||||
use codex_core_plugins::remote::RemotePluginSummary as RemoteCatalogPluginSummary;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
@@ -400,6 +401,7 @@ use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
|
||||
@@ -53,7 +53,7 @@ impl AppsRequestProcessor {
|
||||
None
|
||||
};
|
||||
let fallback_cwd = match thread.as_ref() {
|
||||
Some(thread) => Some(thread.config_snapshot().await.cwd.to_path_buf()),
|
||||
Some(thread) => Some(thread.config_snapshot().await.cwd().to_path_buf()),
|
||||
None => None,
|
||||
};
|
||||
let mut config = self.load_latest_config(fallback_cwd).await?;
|
||||
|
||||
@@ -177,6 +177,7 @@ impl ExternalAgentSessionImporter {
|
||||
let now = Utc::now();
|
||||
let create_params = CreateThreadParams {
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: source.clone(),
|
||||
|
||||
@@ -7,9 +7,13 @@ use codex_app_server_protocol::PluginSharePrincipalRole;
|
||||
use codex_app_server_protocol::PluginShareTargetRole;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::PluginListBackgroundTaskOptions;
|
||||
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::RemoteAppTemplateUnavailableReason;
|
||||
use codex_core_plugins::remote::RemotePluginScope;
|
||||
use codex_core_plugins::remote::is_valid_remote_plugin_id;
|
||||
use codex_core_plugins::remote::validate_remote_plugin_id;
|
||||
use codex_mcp::McpOAuthLoginSupport;
|
||||
@@ -148,15 +152,18 @@ fn convert_configured_marketplace_plugin_to_plugin_summary(
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_installed_plugin_visible_scopes(config: &Config) -> Vec<RemotePluginScope> {
|
||||
let mut scopes = Vec::new();
|
||||
fn remote_installed_plugin_visible_marketplaces(config: &Config) -> Vec<&'static str> {
|
||||
let mut marketplaces = Vec::new();
|
||||
if config.features.enabled(Feature::RemotePlugin) {
|
||||
scopes.push(RemotePluginScope::Global);
|
||||
marketplaces.push(REMOTE_GLOBAL_MARKETPLACE_NAME);
|
||||
}
|
||||
marketplaces.push(REMOTE_WORKSPACE_MARKETPLACE_NAME);
|
||||
if config.features.enabled(Feature::PluginSharing) {
|
||||
scopes.push(RemotePluginScope::Workspace);
|
||||
marketplaces.push(REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME);
|
||||
marketplaces.push(REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME);
|
||||
marketplaces.push(REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME);
|
||||
}
|
||||
scopes
|
||||
marketplaces
|
||||
}
|
||||
|
||||
fn filter_openai_curated_installed_conflicts(
|
||||
@@ -543,21 +550,30 @@ impl PluginRequestProcessor {
|
||||
return Ok(empty_response());
|
||||
}
|
||||
let plugins_input = config.plugins_config_input();
|
||||
if include_local || marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe) {
|
||||
plugins_manager.maybe_start_plugin_list_background_tasks_for_config(
|
||||
&plugins_input,
|
||||
auth.clone(),
|
||||
&roots,
|
||||
Some(self.effective_plugins_changed_callback()),
|
||||
let include_shared_with_me =
|
||||
marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe);
|
||||
let include_global_remote =
|
||||
!explicit_marketplace_kinds && config.features.enabled(Feature::RemotePlugin);
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
let refresh_global_remote_catalog_cache = include_global_remote
|
||||
&& codex_core_plugins::remote::has_cached_global_remote_plugin_catalog(
|
||||
config.codex_home.as_path(),
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
);
|
||||
}
|
||||
let (mut data, marketplace_load_errors) = if include_local {
|
||||
let config_for_marketplace_listing = plugins_input.clone();
|
||||
let plugins_manager_for_marketplace_listing = plugins_manager.clone();
|
||||
let roots_for_marketplace_listing = roots.clone();
|
||||
let shared_plugin_ids_by_local_path = load_shared_plugin_ids_by_local_path(&config)?;
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
let outcome = plugins_manager_for_marketplace_listing
|
||||
.list_marketplaces_for_config(&config_for_marketplace_listing, &roots)?;
|
||||
.list_marketplaces_for_config(
|
||||
&config_for_marketplace_listing,
|
||||
&roots_for_marketplace_listing,
|
||||
)?;
|
||||
Ok::<
|
||||
(
|
||||
Vec<PluginMarketplaceEntry>,
|
||||
@@ -617,9 +633,6 @@ impl PluginRequestProcessor {
|
||||
// TODO(remote plugins): Remove this once remote plugins are ready and vertical plugins are
|
||||
// served directly from the normal remote catalog.
|
||||
if include_vertical && !config.features.enabled(Feature::RemotePlugin) {
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
match codex_core_plugins::remote::fetch_openai_curated_remote_collection_marketplace(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
@@ -644,21 +657,16 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
|
||||
let mut remote_sources = Vec::new();
|
||||
if !explicit_marketplace_kinds && config.features.enabled(Feature::RemotePlugin) {
|
||||
if include_global_remote {
|
||||
remote_sources.push(RemoteMarketplaceSource::Global);
|
||||
}
|
||||
if marketplace_kinds.contains(&PluginListMarketplaceKind::WorkspaceDirectory) {
|
||||
remote_sources.push(RemoteMarketplaceSource::WorkspaceDirectory);
|
||||
}
|
||||
if marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe)
|
||||
&& config.features.enabled(Feature::PluginSharing)
|
||||
{
|
||||
if include_shared_with_me && config.features.enabled(Feature::PluginSharing) {
|
||||
remote_sources.push(RemoteMarketplaceSource::SharedWithMe);
|
||||
}
|
||||
if !remote_sources.is_empty() {
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
match codex_core_plugins::remote::fetch_remote_marketplaces(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
@@ -702,6 +710,17 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
}
|
||||
}
|
||||
if include_local || include_shared_with_me || include_global_remote {
|
||||
plugins_manager.maybe_start_plugin_list_background_tasks_for_config(
|
||||
&plugins_input,
|
||||
auth.clone(),
|
||||
&roots,
|
||||
PluginListBackgroundTaskOptions {
|
||||
refresh_global_remote_catalog_cache,
|
||||
},
|
||||
Some(self.effective_plugins_changed_callback()),
|
||||
);
|
||||
}
|
||||
|
||||
let featured_plugin_ids = if data
|
||||
.iter()
|
||||
@@ -763,8 +782,8 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let remote_installed_plugin_visible_scopes =
|
||||
remote_installed_plugin_visible_scopes(&config);
|
||||
let remote_installed_plugin_visible_marketplaces =
|
||||
remote_installed_plugin_visible_marketplaces(&config);
|
||||
plugins_manager.maybe_start_remote_installed_plugin_bundle_sync(
|
||||
&plugins_input,
|
||||
auth.clone(),
|
||||
@@ -785,7 +804,7 @@ impl PluginRequestProcessor {
|
||||
self.load_remote_installed_plugins(
|
||||
plugins_manager,
|
||||
&plugins_input,
|
||||
&remote_installed_plugin_visible_scopes,
|
||||
&remote_installed_plugin_visible_marketplaces,
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await,
|
||||
@@ -885,11 +904,11 @@ impl PluginRequestProcessor {
|
||||
&self,
|
||||
plugins_manager: Arc<codex_core_plugins::PluginsManager>,
|
||||
plugins_input: &codex_core_plugins::PluginsConfigInput,
|
||||
visible_scopes: &[RemotePluginScope],
|
||||
visible_marketplaces: &[&str],
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Vec<PluginMarketplaceEntry> {
|
||||
let remote_marketplaces = if let Some(remote_marketplaces) =
|
||||
plugins_manager.build_remote_installed_plugin_marketplaces_from_cache(visible_scopes)
|
||||
let remote_marketplaces = if let Some(remote_marketplaces) = plugins_manager
|
||||
.build_remote_installed_plugin_marketplaces_from_cache(visible_marketplaces)
|
||||
{
|
||||
Ok(remote_marketplaces)
|
||||
} else {
|
||||
@@ -897,7 +916,7 @@ impl PluginRequestProcessor {
|
||||
.build_and_cache_remote_installed_plugin_marketplaces(
|
||||
plugins_input,
|
||||
auth,
|
||||
visible_scopes,
|
||||
visible_marketplaces,
|
||||
Some(self.effective_plugins_changed_callback()),
|
||||
)
|
||||
.await
|
||||
@@ -1506,7 +1525,7 @@ impl PluginRequestProcessor {
|
||||
// Cache first so a backend install cannot succeed when local materialization fails.
|
||||
// If this backend call fails, the cache entry is harmless because remote installed state
|
||||
// is still backend-gated.
|
||||
codex_core_plugins::remote::install_remote_plugin(
|
||||
let install_result = codex_core_plugins::remote::install_remote_plugin(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&actual_remote_marketplace_name,
|
||||
@@ -1525,7 +1544,7 @@ impl PluginRequestProcessor {
|
||||
|
||||
let mut plugin_metadata =
|
||||
plugin_telemetry_metadata_from_root(&result.plugin_id, &result.installed_path).await;
|
||||
plugin_metadata.remote_plugin_id = Some(remote_plugin_id);
|
||||
plugin_metadata.remote_plugin_id = Some(remote_plugin_id.clone());
|
||||
self.analytics_events_client
|
||||
.track_plugin_installed(plugin_metadata);
|
||||
|
||||
@@ -1535,15 +1554,42 @@ impl PluginRequestProcessor {
|
||||
.await;
|
||||
}
|
||||
|
||||
let plugin_apps = load_plugin_apps(result.installed_path.as_path()).await;
|
||||
let apps_needing_auth = self
|
||||
.plugin_apps_needing_auth_for_install(
|
||||
&config,
|
||||
auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth),
|
||||
&result.plugin_id.as_key(),
|
||||
&plugin_apps,
|
||||
)
|
||||
.await;
|
||||
let is_chatgpt_auth = auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth);
|
||||
let apps_needing_auth =
|
||||
if let Some(app_ids_needing_auth) = install_result.app_ids_needing_auth {
|
||||
if app_ids_needing_auth.is_empty()
|
||||
|| !config.features.apps_enabled_for_auth(is_chatgpt_auth)
|
||||
{
|
||||
Vec::new()
|
||||
} else {
|
||||
let plugin_apps = app_ids_needing_auth
|
||||
.into_iter()
|
||||
.map(codex_plugin::AppConnectorId)
|
||||
.collect::<Vec<_>>();
|
||||
let all_connectors = connectors::list_cached_all_connectors(&config)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps)
|
||||
.into_iter()
|
||||
.map(|connector| AppSummary {
|
||||
id: connector.id,
|
||||
name: connector.name,
|
||||
description: connector.description,
|
||||
install_url: connector.install_url,
|
||||
needs_auth: true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
} else {
|
||||
let plugin_apps = load_plugin_apps(result.installed_path.as_path()).await;
|
||||
self.plugin_apps_needing_auth_for_install(
|
||||
&config,
|
||||
is_chatgpt_auth,
|
||||
&result.plugin_id.as_key(),
|
||||
&plugin_apps,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
Ok(PluginInstallResponse {
|
||||
auth_policy: remote_detail.summary.auth_policy,
|
||||
|
||||
@@ -627,6 +627,8 @@ pub(super) async fn handle_pending_thread_resume_request(
|
||||
}
|
||||
}
|
||||
|
||||
let config_snapshot = pending.config_snapshot;
|
||||
let cwd = config_snapshot.cwd().clone();
|
||||
let ThreadConfigSnapshot {
|
||||
model,
|
||||
model_provider_id,
|
||||
@@ -635,11 +637,10 @@ pub(super) async fn handle_pending_thread_resume_request(
|
||||
approvals_reviewer,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
cwd,
|
||||
workspace_roots,
|
||||
reasoning_effort,
|
||||
..
|
||||
} = pending.config_snapshot;
|
||||
} = config_snapshot;
|
||||
let instruction_sources = pending.instruction_sources;
|
||||
let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
|
||||
let active_permission_profile =
|
||||
|
||||
@@ -47,11 +47,11 @@ fn collect_resume_override_mismatches(
|
||||
}
|
||||
if let Some(requested_cwd) = request.cwd.as_deref() {
|
||||
let requested_cwd_path = std::path::PathBuf::from(requested_cwd);
|
||||
if requested_cwd_path != config_snapshot.cwd.as_path() {
|
||||
if requested_cwd_path != config_snapshot.cwd().as_path() {
|
||||
mismatch_details.push(format!(
|
||||
"cwd requested={} active={}",
|
||||
requested_cwd_path.display(),
|
||||
config_snapshot.cwd.display()
|
||||
config_snapshot.cwd().display()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1154,8 +1154,9 @@ impl ThreadRequestProcessor {
|
||||
|
||||
let sandbox = thread_response_sandbox_policy(
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd.as_path(),
|
||||
config_snapshot.cwd().as_path(),
|
||||
);
|
||||
let cwd = config_snapshot.cwd().clone();
|
||||
let active_permission_profile =
|
||||
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
|
||||
|
||||
@@ -1164,7 +1165,7 @@ impl ThreadRequestProcessor {
|
||||
model: config_snapshot.model,
|
||||
model_provider: config_snapshot.model_provider_id,
|
||||
service_tier: config_snapshot.service_tier,
|
||||
cwd: config_snapshot.cwd,
|
||||
cwd,
|
||||
runtime_workspace_roots: config_snapshot.workspace_roots,
|
||||
instruction_sources,
|
||||
approval_policy: config_snapshot.approval_policy.into(),
|
||||
@@ -2601,7 +2602,7 @@ impl ThreadRequestProcessor {
|
||||
let config_snapshot = codex_thread.config_snapshot().await;
|
||||
let sandbox = thread_response_sandbox_policy(
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd.as_path(),
|
||||
config_snapshot.cwd().as_path(),
|
||||
);
|
||||
let active_permission_profile = thread_response_active_permission_profile(
|
||||
config_snapshot.active_permission_profile,
|
||||
@@ -3335,7 +3336,7 @@ impl ThreadRequestProcessor {
|
||||
let config_snapshot = forked_thread.config_snapshot().await;
|
||||
let sandbox = thread_response_sandbox_policy(
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd.as_path(),
|
||||
config_snapshot.cwd().as_path(),
|
||||
);
|
||||
let active_permission_profile =
|
||||
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
|
||||
@@ -4205,7 +4206,7 @@ fn build_thread_from_snapshot(
|
||||
updated_at: now,
|
||||
status: ThreadStatus::NotLoaded,
|
||||
path,
|
||||
cwd: config_snapshot.cwd.clone(),
|
||||
cwd: config_snapshot.cwd().clone(),
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
agent_nickname: config_snapshot.session_source.get_nickname(),
|
||||
agent_role: config_snapshot.session_source.get_agent_role(),
|
||||
|
||||
@@ -77,6 +77,7 @@ mod thread_processor_behavior_tests {
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_state::ThreadMetadataBuilder;
|
||||
use codex_thread_store::StoredThread;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
@@ -393,6 +394,7 @@ mod thread_processor_behavior_tests {
|
||||
ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread");
|
||||
let stored_thread = StoredThread {
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
rollout_path: Some(PathBuf::from("/tmp/thread.jsonl")),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -685,7 +687,7 @@ mod thread_processor_behavior_tests {
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
|
||||
active_permission_profile: None,
|
||||
cwd,
|
||||
environments: TurnEnvironmentSelections::new(cwd, Vec::new()),
|
||||
workspace_roots: Vec::new(),
|
||||
profile_workspace_roots: Vec::new(),
|
||||
ephemeral: false,
|
||||
|
||||
@@ -190,12 +190,12 @@ pub(crate) fn thread_settings_from_config_snapshot(
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
) -> ThreadSettings {
|
||||
ThreadSettings {
|
||||
cwd: config_snapshot.cwd.clone(),
|
||||
cwd: config_snapshot.cwd().clone(),
|
||||
approval_policy: config_snapshot.approval_policy.into(),
|
||||
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
|
||||
sandbox_policy: thread_response_sandbox_policy(
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd.as_path(),
|
||||
config_snapshot.cwd().as_path(),
|
||||
),
|
||||
active_permission_profile: thread_response_active_permission_profile(
|
||||
config_snapshot.active_permission_profile.clone(),
|
||||
@@ -213,24 +213,36 @@ pub(crate) fn thread_settings_from_config_snapshot(
|
||||
pub(crate) fn thread_settings_from_core_snapshot(
|
||||
snapshot: codex_protocol::protocol::ThreadSettingsSnapshot,
|
||||
) -> ThreadSettings {
|
||||
let codex_protocol::protocol::ThreadSettingsSnapshot {
|
||||
model,
|
||||
model_provider_id,
|
||||
service_tier,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
cwd,
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
personality,
|
||||
collaboration_mode,
|
||||
} = snapshot;
|
||||
let sandbox_policy = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
|
||||
ThreadSettings {
|
||||
sandbox_policy: thread_response_sandbox_policy(
|
||||
&snapshot.permission_profile,
|
||||
snapshot.cwd.as_path(),
|
||||
),
|
||||
cwd: snapshot.cwd,
|
||||
approval_policy: snapshot.approval_policy.into(),
|
||||
approvals_reviewer: snapshot.approvals_reviewer.into(),
|
||||
sandbox_policy,
|
||||
cwd,
|
||||
approval_policy: approval_policy.into(),
|
||||
approvals_reviewer: approvals_reviewer.into(),
|
||||
active_permission_profile: thread_response_active_permission_profile(
|
||||
snapshot.active_permission_profile,
|
||||
active_permission_profile,
|
||||
),
|
||||
model: snapshot.model,
|
||||
model_provider: snapshot.model_provider_id,
|
||||
service_tier: snapshot.service_tier,
|
||||
effort: snapshot.reasoning_effort,
|
||||
summary: snapshot.reasoning_summary,
|
||||
collaboration_mode: snapshot.collaboration_mode,
|
||||
personality: snapshot.personality,
|
||||
model,
|
||||
model_provider: model_provider_id,
|
||||
service_tier,
|
||||
effort: reasoning_effort,
|
||||
summary: reasoning_summary,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ fn map_additional_context(
|
||||
|
||||
struct ThreadSettingsBuildParams {
|
||||
method: &'static str,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
environments: Option<TurnEnvironmentSelections>,
|
||||
runtime_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
|
||||
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
|
||||
@@ -406,12 +406,14 @@ impl TurnRequestProcessor {
|
||||
let additional_context = map_additional_context(params.additional_context);
|
||||
let turn_has_input = !mapped_items.is_empty();
|
||||
let cwd = resolve_request_cwd(params.cwd)?;
|
||||
let environments =
|
||||
Self::build_environment_override(thread.as_ref(), cwd, environment_selections).await;
|
||||
let thread_settings = self
|
||||
.build_thread_settings_overrides(
|
||||
thread.as_ref(),
|
||||
ThreadSettingsBuildParams {
|
||||
method: "turn/start",
|
||||
cwd,
|
||||
environments,
|
||||
runtime_workspace_roots: params.runtime_workspace_roots,
|
||||
approval_policy: params.approval_policy,
|
||||
approvals_reviewer: params.approvals_reviewer,
|
||||
@@ -430,7 +432,6 @@ impl TurnRequestProcessor {
|
||||
// Start the turn by submitting the user input. Return its submission id as turn_id.
|
||||
let turn_op = Op::UserInput {
|
||||
items: mapped_items,
|
||||
environments: environment_selections,
|
||||
final_output_json_schema: params.output_schema,
|
||||
responsesapi_client_metadata: params.responsesapi_client_metadata,
|
||||
additional_context,
|
||||
@@ -478,6 +479,31 @@ impl TurnRequestProcessor {
|
||||
Ok(TurnStartResponse { turn })
|
||||
}
|
||||
|
||||
async fn build_environment_override(
|
||||
thread: &CodexThread,
|
||||
cwd: Option<AbsolutePathBuf>,
|
||||
environment_selections: Option<Vec<TurnEnvironmentSelection>>,
|
||||
) -> Option<TurnEnvironmentSelections> {
|
||||
if cwd.is_none() && environment_selections.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snapshot = thread.config_snapshot().await;
|
||||
let environment_selections =
|
||||
environment_selections.unwrap_or_else(|| snapshot.environment_selections().to_vec());
|
||||
let legacy_fallback_cwd = cwd.unwrap_or_else(|| {
|
||||
environment_selections
|
||||
.iter()
|
||||
.find(|selection| selection.environment_id == LOCAL_ENVIRONMENT_ID)
|
||||
.map(|selection| selection.cwd.clone())
|
||||
.unwrap_or_else(|| snapshot.cwd().clone())
|
||||
});
|
||||
Some(TurnEnvironmentSelections::new(
|
||||
legacy_fallback_cwd,
|
||||
environment_selections,
|
||||
))
|
||||
}
|
||||
|
||||
async fn build_thread_settings_overrides(
|
||||
&self,
|
||||
thread: &CodexThread,
|
||||
@@ -485,7 +511,7 @@ impl TurnRequestProcessor {
|
||||
) -> Result<codex_protocol::protocol::ThreadSettingsOverrides, JSONRPCErrorError> {
|
||||
let ThreadSettingsBuildParams {
|
||||
method,
|
||||
cwd,
|
||||
environments,
|
||||
runtime_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
@@ -508,6 +534,7 @@ impl TurnRequestProcessor {
|
||||
let collaboration_mode =
|
||||
collaboration_mode.map(|mode| self.normalize_collaboration_mode(mode));
|
||||
let runtime_workspace_roots_request = runtime_workspace_roots;
|
||||
let has_environment_override = environments.is_some();
|
||||
// `thread/settings/update` only acknowledges that the update was queued.
|
||||
// Clients that send dependent partial updates should wait for
|
||||
// `thread/settings/updated` or combine the fields in one request.
|
||||
@@ -517,7 +544,7 @@ impl TurnRequestProcessor {
|
||||
None
|
||||
};
|
||||
|
||||
let has_any_overrides = cwd.is_some()
|
||||
let has_any_overrides = has_environment_override
|
||||
|| runtime_workspace_roots_request.is_some()
|
||||
|| approval_policy.is_some()
|
||||
|| approvals_reviewer.is_some()
|
||||
@@ -545,7 +572,9 @@ impl TurnRequestProcessor {
|
||||
)));
|
||||
};
|
||||
let overrides = ConfigOverrides {
|
||||
cwd: cwd.as_ref().map(AbsolutePathBuf::to_path_buf),
|
||||
cwd: environments
|
||||
.as_ref()
|
||||
.map(|environments| environments.legacy_fallback_cwd.to_path_buf()),
|
||||
workspace_roots: Some(
|
||||
runtime_workspace_roots
|
||||
.clone()
|
||||
@@ -561,7 +590,7 @@ impl TurnRequestProcessor {
|
||||
.load_for_cwd(
|
||||
/*request_overrides*/ None,
|
||||
overrides,
|
||||
Some(snapshot.cwd.to_path_buf()),
|
||||
Some(snapshot.cwd().to_path_buf()),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
@@ -588,7 +617,7 @@ impl TurnRequestProcessor {
|
||||
if has_any_overrides {
|
||||
thread
|
||||
.preview_thread_settings_overrides(CodexThreadSettingsOverrides {
|
||||
cwd: cwd.clone(),
|
||||
environments: environments.clone(),
|
||||
workspace_roots: runtime_workspace_roots.clone(),
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
@@ -611,7 +640,7 @@ impl TurnRequestProcessor {
|
||||
}
|
||||
|
||||
Ok(codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots: runtime_workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
@@ -636,12 +665,18 @@ impl TurnRequestProcessor {
|
||||
) -> Result<ThreadSettingsUpdateResponse, JSONRPCErrorError> {
|
||||
let (_, thread) = self.load_thread(¶ms.thread_id).await?;
|
||||
let cwd = resolve_request_cwd(params.cwd)?;
|
||||
let environments = Self::build_environment_override(
|
||||
thread.as_ref(),
|
||||
cwd,
|
||||
/*environment_selections*/ None,
|
||||
)
|
||||
.await;
|
||||
let thread_settings = self
|
||||
.build_thread_settings_overrides(
|
||||
thread.as_ref(),
|
||||
ThreadSettingsBuildParams {
|
||||
method: "thread/settings/update",
|
||||
cwd,
|
||||
environments,
|
||||
runtime_workspace_roots: None,
|
||||
approval_policy: params.approval_policy,
|
||||
approvals_reviewer: params.approvals_reviewer,
|
||||
|
||||
@@ -122,6 +122,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Cli,
|
||||
|
||||
@@ -29,6 +29,11 @@ use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
const RESULT: &str = "cG5n";
|
||||
const TINY_PNG_BYTES: &[u8] = &[
|
||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0,
|
||||
0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1,
|
||||
122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ImagegenTestMode {
|
||||
@@ -59,7 +64,6 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
|
||||
"image_gen",
|
||||
"imagegen",
|
||||
&json!({
|
||||
"action": "generate",
|
||||
"prompt": "paint a blue whale",
|
||||
})
|
||||
.to_string(),
|
||||
@@ -142,6 +146,67 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_image_edit_uses_attached_model_visible_image() -> Result<()> {
|
||||
let edit_request = run_image_edit_test(|codex_home| {
|
||||
let image_path = codex_home.join("attached.png");
|
||||
std::fs::write(&image_path, TINY_PNG_BYTES)?;
|
||||
Ok((
|
||||
json!({
|
||||
"prompt": "add a red hat",
|
||||
"referenced_image_paths": [image_path.display().to_string()],
|
||||
}),
|
||||
vec![
|
||||
V2UserInput::Text {
|
||||
text: "Edit the attached image".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
V2UserInput::LocalImage {
|
||||
path: image_path,
|
||||
detail: None,
|
||||
},
|
||||
],
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(edit_request["prompt"], "add a red hat");
|
||||
assert!(
|
||||
edit_request["images"][0]["image_url"]
|
||||
.as_str()
|
||||
.is_some_and(|image_url| image_url.starts_with("data:image/png;base64,"))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_image_edit_uses_recent_pathless_image() -> Result<()> {
|
||||
let image_url = "https://example.com/reference.png";
|
||||
let edit_request = run_image_edit_test(|_| {
|
||||
Ok((
|
||||
json!({
|
||||
"prompt": "add a red hat",
|
||||
"num_last_images_to_include": 1,
|
||||
}),
|
||||
vec![
|
||||
V2UserInput::Text {
|
||||
text: "Edit the attached image".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
V2UserInput::Image {
|
||||
url: image_url.to_string(),
|
||||
detail: None,
|
||||
},
|
||||
],
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(edit_request["prompt"], "add a red hat");
|
||||
assert_eq!(edit_request["images"][0]["image_url"], image_url);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_image_generation_is_exposed_in_code_mode_only() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -202,7 +267,6 @@ async fn standalone_image_generation_is_callable_from_code_mode_only() -> Result
|
||||
"exec",
|
||||
r#"
|
||||
const result = await tools.image_gen__imagegen({
|
||||
action: "generate",
|
||||
prompt: "paint a blue whale",
|
||||
});
|
||||
generatedImage(result);
|
||||
@@ -263,6 +327,81 @@ generatedImage(result);
|
||||
}
|
||||
|
||||
async fn start_image_generation_turn(mcp: &mut TestAppServer) -> Result<()> {
|
||||
start_turn(
|
||||
mcp,
|
||||
vec![V2UserInput::Text {
|
||||
text: "Generate an image".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn run_image_edit_test(
|
||||
input: impl FnOnce(&Path) -> Result<(serde_json::Value, Vec<V2UserInput>)>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let call_id = "image-edit-1";
|
||||
let server = responses::start_mock_server().await;
|
||||
mount_image_edit_response(&server).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let (arguments, input) = input(codex_home.path())?;
|
||||
let response_mock = responses::mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_function_call_with_namespace(
|
||||
call_id,
|
||||
"image_gen",
|
||||
"imagegen",
|
||||
&arguments.to_string(),
|
||||
),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
create_config_toml(codex_home.path(), &server.uri(), ImagegenTestMode::Direct)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
start_turn(&mut mcp, input).await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
wait_for_image_generation_completed(&mut mcp),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(response_mock.requests().len(), 2);
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.context("failed to fetch received requests")?;
|
||||
Ok(requests
|
||||
.iter()
|
||||
.find(|request| request.url.path() == "/api/codex/images/edits")
|
||||
.context("image edit request should be sent")?
|
||||
.body_json::<serde_json::Value>()?)
|
||||
}
|
||||
|
||||
async fn start_turn(mcp: &mut TestAppServer, input: Vec<V2UserInput>) -> Result<()> {
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
@@ -277,10 +416,7 @@ async fn start_image_generation_turn(mcp: &mut TestAppServer) -> Result<()> {
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
client_user_message_id: None,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Generate an image".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
input,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -324,6 +460,18 @@ async fn mount_image_response(server: &MockServer) {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mount_image_edit_response(server: &MockServer) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/codex/images/edits"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"created": 1,
|
||||
"data": [{"b64_json": RESULT}],
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn create_config_toml(
|
||||
codex_home: &Path,
|
||||
server_uri: &str,
|
||||
|
||||
@@ -288,6 +288,59 @@ async fn plugin_install_writes_remote_plugin_to_cloud_and_cache() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_uses_remote_apps_needing_auth_response() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
let bundle_url = mount_remote_plugin_bundle(
|
||||
&server,
|
||||
/*status_code*/ 200,
|
||||
remote_plugin_bundle_tar_gz_bytes("linear")?,
|
||||
)
|
||||
.await;
|
||||
configure_remote_plugin_with_apps_test(codex_home.path(), &server)?;
|
||||
mount_remote_plugin_detail(&server, REMOTE_PLUGIN_ID, "1.2.3", Some(&bundle_url)).await;
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
mount_remote_plugin_install_with_apps_needing_auth(&server, REMOTE_PLUGIN_ID, &["alpha"]).await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginInstallResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
PluginInstallResponse {
|
||||
auth_policy: PluginAuthPolicy::OnUse,
|
||||
apps_needing_auth: vec![AppSummary {
|
||||
id: "alpha".to_string(),
|
||||
name: "alpha".to_string(),
|
||||
description: None,
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
needs_auth: true,
|
||||
}],
|
||||
}
|
||||
);
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"GET",
|
||||
"/backend-api/connectors/directory/list",
|
||||
/*expected_count*/ 0,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_rejects_missing_remote_bundle_url() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1398,6 +1451,34 @@ fn configure_remote_plugin_test(codex_home: &std::path::Path, server: &MockServe
|
||||
)
|
||||
}
|
||||
|
||||
fn configure_remote_plugin_with_apps_test(
|
||||
codex_home: &std::path::Path,
|
||||
server: &MockServer,
|
||||
) -> Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
chatgpt_base_url = "{}/backend-api/"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
connectors = true
|
||||
"#,
|
||||
server.uri()
|
||||
),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home,
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
}
|
||||
|
||||
async fn mount_remote_plugin_bundle(
|
||||
server: &MockServer,
|
||||
status_code: u16,
|
||||
@@ -1552,6 +1633,27 @@ async fn mount_remote_plugin_install(server: &MockServer, remote_plugin_id: &str
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mount_remote_plugin_install_with_apps_needing_auth(
|
||||
server: &MockServer,
|
||||
remote_plugin_id: &str,
|
||||
app_ids_needing_auth: &[&str],
|
||||
) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!(
|
||||
"/backend-api/ps/plugins/{remote_plugin_id}/install"
|
||||
)))
|
||||
.and(query_param("includeAppsNeedingAuth", "true"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"id": remote_plugin_id,
|
||||
"enabled": true,
|
||||
"app_ids_needing_auth": app_ids_needing_auth,
|
||||
})))
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CacheManifestExists {
|
||||
manifest_path: std::path::PathBuf,
|
||||
|
||||
@@ -1863,6 +1863,102 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_uses_cached_global_remote_catalog_and_refreshes_it() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_catalog_config(
|
||||
codex_home.path(),
|
||||
&format!("{}/backend-api/", server.uri()),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let cached_remote_plugin_id = "plugins~Plugin_00000000000000000000000000000000";
|
||||
let refreshed_remote_plugin_id = "plugins~Plugin_11111111111111111111111111111111";
|
||||
let cached_body =
|
||||
remote_plugin_list_body(cached_remote_plugin_id, "linear", "Linear", "Plan work");
|
||||
let refreshed_body = remote_plugin_list_body(
|
||||
refreshed_remote_plugin_id,
|
||||
"notion",
|
||||
"Notion",
|
||||
"Capture notes",
|
||||
);
|
||||
mount_remote_plugin_list(&server, "GLOBAL", &cached_body).await;
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())
|
||||
.await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: None,
|
||||
marketplace_kinds: None,
|
||||
})
|
||||
.await?;
|
||||
let response: PluginListResponse = to_response(
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??,
|
||||
)?;
|
||||
let remote_marketplace = response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == "openai-curated-remote")
|
||||
.expect("expected warmed remote marketplace");
|
||||
assert_eq!(
|
||||
remote_marketplace.plugins[0].id,
|
||||
"linear@openai-curated-remote"
|
||||
);
|
||||
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 1).await?;
|
||||
wait_for_cached_remote_catalog_plugin_ids(codex_home.path(), &[cached_remote_plugin_id])
|
||||
.await?;
|
||||
|
||||
server.reset().await;
|
||||
mount_remote_plugin_list(&server, "GLOBAL", &refreshed_body).await;
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())
|
||||
.await;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: None,
|
||||
marketplace_kinds: None,
|
||||
})
|
||||
.await?;
|
||||
let response: PluginListResponse = to_response(
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??,
|
||||
)?;
|
||||
let remote_marketplace = response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == "openai-curated-remote")
|
||||
.expect("expected cached remote marketplace");
|
||||
assert_eq!(
|
||||
remote_marketplace.plugins[0].id,
|
||||
"linear@openai-curated-remote"
|
||||
);
|
||||
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 1).await?;
|
||||
wait_for_cached_remote_catalog_plugin_ids(codex_home.path(), &[refreshed_remote_plugin_id])
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_includes_openai_curated_remote_collection_when_requested() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -2275,6 +2371,92 @@ plugin_sharing = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_installed_includes_workspace_directory_without_plugin_sharing() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"chatgpt_base_url = "{}/backend-api/"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
remote_plugin = false
|
||||
plugin_sharing = false
|
||||
"#,
|
||||
server.uri()
|
||||
),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
let mut workspace_installed_body: serde_json::Value =
|
||||
serde_json::from_str(&workspace_remote_plugin_page_body(
|
||||
"plugins~Plugin_11111111111111111111111111111111",
|
||||
"workspace-linear",
|
||||
"Workspace Linear",
|
||||
"LISTED",
|
||||
/*enabled*/ Some(true),
|
||||
))?;
|
||||
let shared_installed_body: serde_json::Value =
|
||||
serde_json::from_str(&workspace_remote_plugin_page_body(
|
||||
"plugins~Plugin_22222222222222222222222222222222",
|
||||
"shared-linear",
|
||||
"Shared Linear",
|
||||
"PRIVATE",
|
||||
/*enabled*/ Some(true),
|
||||
))?;
|
||||
workspace_installed_body["plugins"]
|
||||
.as_array_mut()
|
||||
.expect("installed plugins should be an array")
|
||||
.push(shared_installed_body["plugins"][0].clone());
|
||||
let workspace_installed_body = serde_json::to_string(&workspace_installed_body)?;
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_installed_request(PluginInstalledParams {
|
||||
cwds: None,
|
||||
install_suggestion_plugin_names: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginInstalledResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(response.marketplaces.len(), 1);
|
||||
let marketplace = &response.marketplaces[0];
|
||||
assert_eq!(marketplace.name, "workspace-directory");
|
||||
assert_eq!(
|
||||
marketplace
|
||||
.plugins
|
||||
.iter()
|
||||
.map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(
|
||||
"workspace-linear@workspace-directory".to_string(),
|
||||
true,
|
||||
true
|
||||
)]
|
||||
);
|
||||
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
|
||||
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_installed_starts_remote_installed_bundle_sync() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -3025,6 +3207,52 @@ async fn wait_for_remote_installed_scope_request(server: &MockServer, scope: &st
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_cached_remote_catalog_plugin_ids(
|
||||
codex_home: &std::path::Path,
|
||||
expected_plugin_ids: &[&str],
|
||||
) -> Result<()> {
|
||||
let mut expected_plugin_ids = expected_plugin_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
expected_plugin_ids.sort();
|
||||
timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
let plugin_ids = cached_remote_catalog_plugin_ids(codex_home)?;
|
||||
if plugin_ids == expected_plugin_ids {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cached_remote_catalog_plugin_ids(codex_home: &std::path::Path) -> Result<Vec<String>> {
|
||||
let cache_dir = codex_home.join("cache/remote_plugin_catalog");
|
||||
if !cache_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut plugin_ids = Vec::new();
|
||||
for entry in std::fs::read_dir(cache_dir)? {
|
||||
let path = entry?.path();
|
||||
let cached_catalog: serde_json::Value = serde_json::from_slice(&std::fs::read(path)?)?;
|
||||
let Some(plugins) = cached_catalog["plugins"].as_array() else {
|
||||
continue;
|
||||
};
|
||||
plugin_ids.extend(
|
||||
plugins
|
||||
.iter()
|
||||
.filter_map(|plugin| plugin["id"].as_str())
|
||||
.map(str::to_string),
|
||||
);
|
||||
}
|
||||
plugin_ids.sort();
|
||||
Ok(plugin_ids)
|
||||
}
|
||||
|
||||
async fn wait_for_path_exists(path: &std::path::Path) -> Result<()> {
|
||||
timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
@@ -3063,6 +3291,43 @@ async fn mount_remote_plugin_list(server: &MockServer, scope: &str, body: &str)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn remote_plugin_list_body(
|
||||
remote_plugin_id: &str,
|
||||
plugin_name: &str,
|
||||
display_name: &str,
|
||||
short_description: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
"plugins": [
|
||||
{{
|
||||
"id": "{remote_plugin_id}",
|
||||
"name": "{plugin_name}",
|
||||
"scope": "GLOBAL",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"status": "ENABLED",
|
||||
"release": {{
|
||||
"version": "1.2.3",
|
||||
"display_name": "{display_name}",
|
||||
"description": "{display_name}",
|
||||
"app_ids": [],
|
||||
"interface": {{
|
||||
"short_description": "{short_description}",
|
||||
"capabilities": ["Read"]
|
||||
}},
|
||||
"skills": []
|
||||
}}
|
||||
}}
|
||||
],
|
||||
"pagination": {{
|
||||
"limit": 50,
|
||||
"next_page_token": null
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
async fn mount_openai_curated_remote_collection_plugin_list(server: &MockServer, body: &str) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/list"))
|
||||
|
||||
@@ -1356,6 +1356,7 @@ async fn seed_pathless_store_thread(
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: ProtocolSessionSource::Cli,
|
||||
|
||||
@@ -209,6 +209,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Cli,
|
||||
|
||||
@@ -3,6 +3,9 @@ load("//:defs.bzl", "MACOS_WEBRTC_RUSTC_LINK_FLAGS", "codex_rust_crate", "multip
|
||||
codex_rust_crate(
|
||||
name = "cli",
|
||||
crate_name = "codex_cli",
|
||||
extra_binaries = [
|
||||
"//codex-rs/bwrap:bwrap",
|
||||
],
|
||||
rustc_flags_extra = MACOS_WEBRTC_RUSTC_LINK_FLAGS,
|
||||
)
|
||||
|
||||
|
||||
@@ -258,6 +258,9 @@ async fn run_command_under_sandbox(
|
||||
let network = network_proxy
|
||||
.as_ref()
|
||||
.map(codex_core::config::StartedNetworkProxy::proxy);
|
||||
// Proxy containment depends on whether a proxy is active, not whether its
|
||||
// policy came from managed requirements.
|
||||
let enforce_managed_network = network.is_some();
|
||||
let managed_mitm_ca_trust_bundle_path = match network.as_ref() {
|
||||
Some(network) => network.managed_mitm_ca_trust_bundle_path(),
|
||||
None => None,
|
||||
@@ -278,7 +281,7 @@ async fn run_command_under_sandbox(
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
|
||||
enforce_managed_network: false,
|
||||
enforce_managed_network,
|
||||
network: network.as_ref(),
|
||||
extra_allow_unix_sockets: allow_unix_sockets,
|
||||
});
|
||||
@@ -311,7 +314,7 @@ async fn run_command_under_sandbox(
|
||||
&runtime_permission_profile,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
use_legacy_landlock,
|
||||
allow_network_for_proxy(managed_network_requirements_enabled),
|
||||
allow_network_for_proxy(enforce_managed_network),
|
||||
);
|
||||
spawn_debug_sandbox_child(
|
||||
codex_linux_sandbox_exe,
|
||||
|
||||
@@ -682,7 +682,7 @@ fn structured_json_details(details: &[String]) -> (BTreeMap<String, JsonDetailVa
|
||||
notes.push(redacted);
|
||||
continue;
|
||||
}
|
||||
let value = value.to_string();
|
||||
let value = json_detail_value(key, value);
|
||||
match structured.get_mut(key) {
|
||||
Some(existing) => existing.push(value),
|
||||
None => {
|
||||
@@ -693,6 +693,21 @@ fn structured_json_details(details: &[String]) -> (BTreeMap<String, JsonDetailVa
|
||||
(structured, notes)
|
||||
}
|
||||
|
||||
fn json_detail_value(key: &str, value: &str) -> String {
|
||||
if matches!(
|
||||
key,
|
||||
"VISUAL" | "EDITOR" | "PAGER" | "GIT_PAGER" | "GH_PAGER" | "LESS"
|
||||
) && !value.eq_ignore_ascii_case("not set")
|
||||
{
|
||||
// Editor and pager configuration can contain arbitrary arguments or
|
||||
// inline environment assignments. Keep full values local to human output
|
||||
// because the JSON report may be attached to feedback.
|
||||
"set".to_string()
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn run_sync_check(
|
||||
label: &'static str,
|
||||
progress: Arc<dyn DoctorProgress>,
|
||||
@@ -3226,6 +3241,18 @@ mod tests {
|
||||
overall_status: CheckStatus::Warning,
|
||||
codex_version: "0.0.0".to_string(),
|
||||
checks: vec![
|
||||
DoctorCheck::new(
|
||||
"system.environment",
|
||||
"system",
|
||||
CheckStatus::Ok,
|
||||
"OS language en-US",
|
||||
)
|
||||
.detail("VISUAL: code --wait")
|
||||
.detail("EDITOR: env AWS_ACCESS_KEY_ID=AKIAEXAMPLE vim")
|
||||
.detail("PAGER: env PRIVATE_PAGER_VALUE=pager-secret less")
|
||||
.detail("GIT_PAGER: delta")
|
||||
.detail("GH_PAGER: less")
|
||||
.detail("LESS: -FRX"),
|
||||
DoctorCheck::new(
|
||||
"mcp.config",
|
||||
"mcp",
|
||||
@@ -3260,8 +3287,35 @@ mod tests {
|
||||
assert!(!redacted.contains("user:pass"));
|
||||
assert!(!redacted.contains("x=abc"));
|
||||
assert!(!redacted.contains("sk-live-secret"));
|
||||
assert!(!redacted.contains("AKIAEXAMPLE"));
|
||||
assert!(!redacted.contains("pager-secret"));
|
||||
assert!(!redacted.contains("code --wait"));
|
||||
assert!(redacted.contains("https://example.com/mcp"));
|
||||
assert_eq!(json["checks"].is_object(), true);
|
||||
assert_eq!(
|
||||
json["checks"]["system.environment"]["details"]["VISUAL"],
|
||||
"set"
|
||||
);
|
||||
assert_eq!(
|
||||
json["checks"]["system.environment"]["details"]["EDITOR"],
|
||||
"set"
|
||||
);
|
||||
assert_eq!(
|
||||
json["checks"]["system.environment"]["details"]["PAGER"],
|
||||
"set"
|
||||
);
|
||||
assert_eq!(
|
||||
json["checks"]["system.environment"]["details"]["GIT_PAGER"],
|
||||
"set"
|
||||
);
|
||||
assert_eq!(
|
||||
json["checks"]["system.environment"]["details"]["GH_PAGER"],
|
||||
"set"
|
||||
);
|
||||
assert_eq!(
|
||||
json["checks"]["system.environment"]["details"]["LESS"],
|
||||
"set"
|
||||
);
|
||||
assert_eq!(json["checks"]["mcp.config"]["id"], "mcp.config");
|
||||
assert_eq!(
|
||||
json["checks"]["mcp.config"]["details"]["OPENAI_API_KEY"],
|
||||
|
||||
@@ -1133,7 +1133,13 @@ mod tests {
|
||||
"OS language en-US",
|
||||
)
|
||||
.detail("os: macOS 15.0")
|
||||
.detail("os language: en-US"),
|
||||
.detail("os language: en-US")
|
||||
.detail("VISUAL: code --wait")
|
||||
.detail("EDITOR: vim")
|
||||
.detail("PAGER: less -R")
|
||||
.detail("GIT_PAGER: delta")
|
||||
.detail("GH_PAGER: less")
|
||||
.detail("LESS: -FRX"),
|
||||
DoctorCheck::new(
|
||||
"runtime.provenance",
|
||||
"runtime",
|
||||
@@ -1246,6 +1252,12 @@ Environment
|
||||
✓ system en-US
|
||||
os macOS 15.0
|
||||
OS language en-US
|
||||
VISUAL code --wait
|
||||
EDITOR vim
|
||||
PAGER less -R
|
||||
GIT_PAGER delta
|
||||
GH_PAGER less
|
||||
LESS -FRX
|
||||
✓ runtime running local build on darwin-arm64
|
||||
✓ install consistent
|
||||
managed by npm: no · bun: no · package root —
|
||||
|
||||
@@ -62,6 +62,12 @@ fn system_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail> {
|
||||
push_row_if_present(&mut out, parsed, "LC_ALL", "LC_ALL");
|
||||
push_row_if_present(&mut out, parsed, "LC_CTYPE", "LC_CTYPE");
|
||||
push_row_if_present(&mut out, parsed, "LANG", "LANG");
|
||||
push_row_if_present(&mut out, parsed, "VISUAL", "VISUAL");
|
||||
push_row_if_present(&mut out, parsed, "EDITOR", "EDITOR");
|
||||
push_row_if_present(&mut out, parsed, "PAGER", "PAGER");
|
||||
push_row_if_present(&mut out, parsed, "GIT_PAGER", "GIT_PAGER");
|
||||
push_row_if_present(&mut out, parsed, "GH_PAGER", "GH_PAGER");
|
||||
push_row_if_present(&mut out, parsed, "LESS", "LESS");
|
||||
push_remaining(
|
||||
&mut out,
|
||||
parsed,
|
||||
@@ -73,6 +79,12 @@ fn system_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail> {
|
||||
"LC_ALL",
|
||||
"LC_CTYPE",
|
||||
"LANG",
|
||||
"VISUAL",
|
||||
"EDITOR",
|
||||
"PAGER",
|
||||
"GIT_PAGER",
|
||||
"GH_PAGER",
|
||||
"LESS",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
|
||||
+6
@@ -13,6 +13,12 @@ Environment
|
||||
✓ system en-US
|
||||
os macOS 15.0
|
||||
OS language en-US
|
||||
VISUAL code --wait
|
||||
EDITOR vim
|
||||
PAGER less -R
|
||||
GIT_PAGER delta
|
||||
GH_PAGER less
|
||||
LESS -FRX
|
||||
✓ runtime running local build on darwin-arm64
|
||||
✓ install consistent
|
||||
managed by npm: no · bun: no · package root —
|
||||
|
||||
@@ -4,6 +4,9 @@ use std::env;
|
||||
use super::DoctorCheck;
|
||||
use super::LOCALE_ENV_VARS;
|
||||
|
||||
const EDITOR_ENV_VARS: &[&str] = &["VISUAL", "EDITOR"];
|
||||
const PAGER_ENV_VARS: &[&str] = &["PAGER", "GIT_PAGER", "GH_PAGER", "LESS"];
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
struct SystemCheckInputs {
|
||||
os: String,
|
||||
@@ -11,6 +14,8 @@ struct SystemCheckInputs {
|
||||
os_version: String,
|
||||
os_language: Option<String>,
|
||||
locale_env: BTreeMap<String, String>,
|
||||
editor_env: BTreeMap<String, String>,
|
||||
pager_env: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl SystemCheckInputs {
|
||||
@@ -24,12 +29,30 @@ impl SystemCheckInputs {
|
||||
.map(|value| ((*name).to_string(), value))
|
||||
})
|
||||
.collect();
|
||||
let editor_env = EDITOR_ENV_VARS
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let value = env::var_os(name)
|
||||
.map(|value| value.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "not set".to_string());
|
||||
((*name).to_string(), value)
|
||||
})
|
||||
.collect();
|
||||
let pager_env = PAGER_ENV_VARS
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
env::var_os(name)
|
||||
.map(|value| ((*name).to_string(), value.to_string_lossy().into_owned()))
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
os: info.to_string(),
|
||||
os_type: info.os_type().to_string(),
|
||||
os_version: info.version().to_string(),
|
||||
os_language: sys_locale::get_locale(),
|
||||
locale_env,
|
||||
editor_env,
|
||||
pager_env,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +77,16 @@ fn system_check_from_inputs(inputs: SystemCheckInputs) -> DoctorCheck {
|
||||
details.push(format!("{name}: {value}"));
|
||||
}
|
||||
}
|
||||
for name in EDITOR_ENV_VARS {
|
||||
if let Some(value) = inputs.editor_env.get(*name) {
|
||||
details.push(format!("{name}: {value}"));
|
||||
}
|
||||
}
|
||||
for name in PAGER_ENV_VARS {
|
||||
if let Some(value) = inputs.pager_env.get(*name) {
|
||||
details.push(format!("{name}: {value}"));
|
||||
}
|
||||
}
|
||||
|
||||
let summary = inputs
|
||||
.os_language
|
||||
@@ -76,20 +109,46 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn system_check_reports_os_language_and_locale_env() {
|
||||
fn system_check_reports_os_language_locale_editor_and_pager_env() {
|
||||
let mut locale_env = BTreeMap::new();
|
||||
locale_env.insert("LANG".to_string(), "en_US.UTF-8".to_string());
|
||||
let editor_env = BTreeMap::from([
|
||||
("EDITOR".to_string(), "vim".to_string()),
|
||||
("VISUAL".to_string(), "code --wait".to_string()),
|
||||
]);
|
||||
let pager_env = BTreeMap::from([
|
||||
("GH_PAGER".to_string(), "less".to_string()),
|
||||
("GIT_PAGER".to_string(), "delta".to_string()),
|
||||
("LESS".to_string(), "-FRX".to_string()),
|
||||
("PAGER".to_string(), "less -R".to_string()),
|
||||
]);
|
||||
let check = system_check_from_inputs(SystemCheckInputs {
|
||||
os: "macOS 15.0".to_string(),
|
||||
os_type: "macos".to_string(),
|
||||
os_version: "15.0".to_string(),
|
||||
os_language: Some("en-US".to_string()),
|
||||
locale_env,
|
||||
editor_env,
|
||||
pager_env,
|
||||
});
|
||||
|
||||
assert_eq!(check.summary, "OS language en-US");
|
||||
assert!(check.details.contains(&"os language: en-US".to_string()));
|
||||
assert!(check.details.contains(&"LANG: en_US.UTF-8".to_string()));
|
||||
assert_eq!(
|
||||
check.details,
|
||||
vec![
|
||||
"os: macOS 15.0",
|
||||
"os type: macos",
|
||||
"os version: 15.0",
|
||||
"os language: en-US",
|
||||
"LANG: en_US.UTF-8",
|
||||
"VISUAL: code --wait",
|
||||
"EDITOR: vim",
|
||||
"PAGER: less -R",
|
||||
"GIT_PAGER: delta",
|
||||
"GH_PAGER: less",
|
||||
"LESS: -FRX",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -100,13 +159,24 @@ mod tests {
|
||||
os_version: "unknown".to_string(),
|
||||
os_language: None,
|
||||
locale_env: BTreeMap::new(),
|
||||
editor_env: BTreeMap::from([
|
||||
("EDITOR".to_string(), "not set".to_string()),
|
||||
("VISUAL".to_string(), "not set".to_string()),
|
||||
]),
|
||||
pager_env: BTreeMap::new(),
|
||||
});
|
||||
|
||||
assert_eq!(check.summary, "OS language unavailable");
|
||||
assert!(
|
||||
check
|
||||
.details
|
||||
.contains(&"os language: unavailable".to_string())
|
||||
assert_eq!(
|
||||
check.details,
|
||||
vec![
|
||||
"os: Linux",
|
||||
"os type: linux",
|
||||
"os version: unknown",
|
||||
"os language: unavailable",
|
||||
"VISUAL: not set",
|
||||
"EDITOR: not set",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ pub use login::run_logout;
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SeatbeltCommand {
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
#[arg(long = "permissions-profile", short = 'P', value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
|
||||
@@ -74,7 +74,7 @@ fn parse_allow_unix_socket_path(raw: &str) -> Result<AbsolutePathBuf, String> {
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct LandlockCommand {
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
#[arg(long = "permissions-profile", short = 'P', value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
|
||||
@@ -109,7 +109,7 @@ pub struct LandlockCommand {
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct WindowsCommand {
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
#[arg(long = "permissions-profile", short = 'P', value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
|
||||
|
||||
@@ -2880,6 +2880,21 @@ mod tests {
|
||||
assert_eq!(command.command, vec!["echo"]);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
||||
#[test]
|
||||
fn sandbox_parses_permissions_profile_short_alias() {
|
||||
let cli =
|
||||
MultitoolCli::try_parse_from(["codex", "sandbox", "-P", ":workspace", "--", "echo"])
|
||||
.expect("parse");
|
||||
|
||||
let Some(Subcommand::Sandbox(command)) = cli.subcommand else {
|
||||
panic!("expected sandbox command");
|
||||
};
|
||||
|
||||
assert_eq!(command.permissions_profile.as_deref(), Some(":workspace"));
|
||||
assert_eq!(command.command, vec!["echo"]);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
||||
#[test]
|
||||
fn sandbox_parses_config_profile() {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::net::TcpListener;
|
||||
|
||||
use anyhow::Result;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable";
|
||||
|
||||
#[test]
|
||||
fn sandbox_with_network_proxy_blocks_direct_loopback_access() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let listener = TcpListener::bind("127.0.0.2:0")?;
|
||||
let port = listener.local_addr()?.port();
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"
|
||||
default_permissions = "network-test"
|
||||
|
||||
[features]
|
||||
network_proxy = true
|
||||
use_legacy_landlock = true
|
||||
|
||||
[permissions.network-test]
|
||||
extends = ":workspace"
|
||||
|
||||
[permissions.network-test.network]
|
||||
enabled = true
|
||||
mode = "full"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let url = format!("http://127.0.0.2:{port}/");
|
||||
let output = std::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
|
||||
.env("CODEX_HOME", codex_home.path())
|
||||
.args([
|
||||
"sandbox",
|
||||
"--permissions-profile",
|
||||
"network-test",
|
||||
"--",
|
||||
"curl",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--connect-timeout",
|
||||
"1",
|
||||
"--max-time",
|
||||
"2",
|
||||
url.as_str(),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if stderr.contains(BWRAP_UNAVAILABLE_ERR) {
|
||||
eprintln!("skipping network proxy sandbox test: bubblewrap is unavailable");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(7),
|
||||
"expected direct loopback access to be blocked; status={:?}; stdout={}; stderr={}",
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
stderr,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -35,6 +35,7 @@ pub use codex_core::ThreadManager;
|
||||
pub use codex_core::ThreadShutdownReport;
|
||||
pub use codex_core::config::Config;
|
||||
pub use codex_core::config::Constrained;
|
||||
pub use codex_core::config::ExtraConfig;
|
||||
pub use codex_core::config::GhostSnapshotConfig;
|
||||
pub use codex_core::config::MultiAgentV2Config;
|
||||
pub use codex_core::config::Permissions;
|
||||
|
||||
@@ -14,7 +14,6 @@ use crate::PluginsConfigInput;
|
||||
use crate::PluginsManager;
|
||||
use crate::marketplace::MarketplacePluginInstallPolicy;
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use crate::remote::RemotePluginScope;
|
||||
|
||||
const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[
|
||||
"github@openai-curated",
|
||||
@@ -100,7 +99,9 @@ impl PluginsManager {
|
||||
.collect::<HashSet<_>>();
|
||||
installed_app_connector_ids.extend(input.loaded_plugin_app_connector_ids.iter().cloned());
|
||||
let remote_installed_marketplaces = if input.plugins.remote_plugin_enabled {
|
||||
self.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global])
|
||||
self.build_remote_installed_plugin_marketplaces_from_cache(&[
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ pub use manager::PluginDetailsUnavailableReason;
|
||||
pub use manager::PluginInstallError;
|
||||
pub use manager::PluginInstallOutcome;
|
||||
pub use manager::PluginInstallRequest;
|
||||
pub use manager::PluginListBackgroundTaskOptions;
|
||||
pub use manager::PluginReadOutcome;
|
||||
pub use manager::PluginReadRequest;
|
||||
pub use manager::PluginUninstallError;
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::manifest::load_plugin_manifest;
|
||||
use crate::marketplace::MarketplacePluginSource;
|
||||
use crate::marketplace::list_marketplaces;
|
||||
use crate::marketplace::load_marketplace;
|
||||
use crate::marketplace::load_raw_marketplace_plugin_names;
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use crate::remote::RemoteInstalledPlugin;
|
||||
use crate::store::PluginStore;
|
||||
@@ -307,6 +308,10 @@ pub fn refresh_curated_plugin_cache(
|
||||
.join(".agents/plugins/marketplace.json"),
|
||||
)
|
||||
.map_err(|_| "local curated marketplace is not available".to_string())?;
|
||||
let marketplace_plugin_names = load_raw_marketplace_plugin_names(&curated_marketplace_path)
|
||||
.map_err(|err| {
|
||||
format!("failed to load curated marketplace plugin names for cache refresh: {err}")
|
||||
})?;
|
||||
let curated_marketplace = load_marketplace(&curated_marketplace_path)
|
||||
.map_err(|err| format!("failed to load curated marketplace for cache refresh: {err}"))?;
|
||||
|
||||
@@ -321,36 +326,40 @@ pub fn refresh_curated_plugin_cache(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let source_path = match plugin.source {
|
||||
MarketplacePluginSource::Local { path } => path,
|
||||
MarketplacePluginSource::Git { .. } => {
|
||||
warn!(
|
||||
plugin = plugin_name,
|
||||
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
|
||||
"skipping remote curated plugin source during cache refresh"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
plugin_sources.insert(plugin_name, source_path);
|
||||
if let MarketplacePluginSource::Local { path } = plugin.source {
|
||||
plugin_sources.insert(plugin_name, path);
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache_refreshed = false;
|
||||
for plugin_id in configured_curated_plugin_ids {
|
||||
if store.active_plugin_version(plugin_id).as_deref() == Some(cache_plugin_version.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(source_path) = plugin_sources.get(&plugin_id.plugin_name).cloned() else {
|
||||
if !marketplace_plugin_names.contains(&plugin_id.plugin_name) {
|
||||
warn!(
|
||||
plugin = plugin_id.plugin_name,
|
||||
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
|
||||
"configured curated plugin no longer exists in curated marketplace during cache refresh"
|
||||
);
|
||||
if store.plugin_base_root(plugin_id).as_path().exists() {
|
||||
store.uninstall(plugin_id).map_err(|err| {
|
||||
format!(
|
||||
"failed to remove stale curated plugin cache for {}: {err}",
|
||||
plugin_id.as_key()
|
||||
)
|
||||
})?;
|
||||
cache_refreshed = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(source_path) = plugin_sources.get(&plugin_id.plugin_name).cloned() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if store.active_plugin_version(plugin_id).as_deref() == Some(cache_plugin_version.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
store
|
||||
.install_with_version(source_path, plugin_id.clone(), cache_plugin_version.clone())
|
||||
.map_err(|err| {
|
||||
|
||||
@@ -38,7 +38,6 @@ use crate::marketplace_upgrade::configured_git_marketplace_names;
|
||||
use crate::marketplace_upgrade::upgrade_configured_git_marketplaces;
|
||||
use crate::remote::RemoteInstalledPlugin;
|
||||
use crate::remote::RemotePluginCatalogError;
|
||||
use crate::remote::RemotePluginScope;
|
||||
use crate::remote::RemotePluginServiceConfig;
|
||||
use crate::remote_legacy::RemotePluginFetchError;
|
||||
use crate::remote_legacy::RemotePluginMutationError;
|
||||
@@ -146,6 +145,22 @@ struct RemoteInstalledPluginsCacheRefreshState {
|
||||
in_flight: bool,
|
||||
}
|
||||
|
||||
struct GlobalRemoteCatalogCacheRefreshRequest {
|
||||
service_config: RemotePluginServiceConfig,
|
||||
auth: Option<CodexAuth>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GlobalRemoteCatalogCacheRefreshState {
|
||||
requested: Option<GlobalRemoteCatalogCacheRefreshRequest>,
|
||||
in_flight: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct PluginListBackgroundTaskOptions {
|
||||
pub refresh_global_remote_catalog_cache: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct NonCuratedCacheRefreshRequest {
|
||||
roots: Vec<AbsolutePathBuf>,
|
||||
@@ -302,6 +317,7 @@ pub struct PluginsManager {
|
||||
enabled_outcome_load_semaphore: Semaphore,
|
||||
remote_installed_plugins_cache: RwLock<Option<Vec<RemoteInstalledPlugin>>>,
|
||||
remote_installed_plugins_cache_refresh_state: RwLock<RemoteInstalledPluginsCacheRefreshState>,
|
||||
global_remote_catalog_cache_refresh_state: RwLock<GlobalRemoteCatalogCacheRefreshState>,
|
||||
restriction_product: Option<Product>,
|
||||
analytics_events_client: RwLock<Option<AnalyticsEventsClient>>,
|
||||
}
|
||||
@@ -355,6 +371,9 @@ impl PluginsManager {
|
||||
remote_installed_plugins_cache_refresh_state: RwLock::new(
|
||||
RemoteInstalledPluginsCacheRefreshState::default(),
|
||||
),
|
||||
global_remote_catalog_cache_refresh_state: RwLock::new(
|
||||
GlobalRemoteCatalogCacheRefreshState::default(),
|
||||
),
|
||||
restriction_product,
|
||||
analytics_events_client: RwLock::new(None),
|
||||
}
|
||||
@@ -540,14 +559,19 @@ impl PluginsManager {
|
||||
|
||||
pub fn build_remote_installed_plugin_marketplaces_from_cache(
|
||||
&self,
|
||||
visible_scopes: &[RemotePluginScope],
|
||||
visible_marketplaces: &[&str],
|
||||
) -> Option<Vec<crate::remote::RemoteMarketplace>> {
|
||||
let cache = match self.remote_installed_plugins_cache.read() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
let plugins = cache.as_ref()?;
|
||||
Some(crate::remote::group_remote_installed_plugins_by_marketplaces(plugins, visible_scopes))
|
||||
Some(
|
||||
crate::remote::group_remote_installed_plugins_by_marketplaces(
|
||||
plugins,
|
||||
visible_marketplaces,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn cached_global_remote_discoverable_plugins_for_config(
|
||||
@@ -579,7 +603,7 @@ impl PluginsManager {
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
auth: Option<&CodexAuth>,
|
||||
visible_scopes: &[RemotePluginScope],
|
||||
visible_marketplaces: &[&str],
|
||||
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
|
||||
) -> Result<Vec<crate::remote::RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let plugins = crate::remote::fetch_remote_installed_plugins(
|
||||
@@ -587,8 +611,10 @@ impl PluginsManager {
|
||||
auth,
|
||||
)
|
||||
.await?;
|
||||
let marketplaces =
|
||||
crate::remote::group_remote_installed_plugins_by_marketplaces(&plugins, visible_scopes);
|
||||
let marketplaces = crate::remote::group_remote_installed_plugins_by_marketplaces(
|
||||
&plugins,
|
||||
visible_marketplaces,
|
||||
);
|
||||
let changed = self.write_remote_installed_plugins_cache(plugins);
|
||||
if changed && let Some(on_effective_plugins_changed) = on_effective_plugins_changed {
|
||||
on_effective_plugins_changed();
|
||||
@@ -702,14 +728,33 @@ impl PluginsManager {
|
||||
);
|
||||
}
|
||||
|
||||
fn maybe_start_global_remote_catalog_cache_refresh(
|
||||
self: &Arc<Self>,
|
||||
config: &PluginsConfigInput,
|
||||
auth: Option<CodexAuth>,
|
||||
) {
|
||||
if !config.plugins_enabled || !config.remote_plugin_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.schedule_global_remote_catalog_cache_refresh(GlobalRemoteCatalogCacheRefreshRequest {
|
||||
service_config: remote_plugin_service_config(config),
|
||||
auth,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn maybe_start_plugin_list_background_tasks_for_config(
|
||||
self: &Arc<Self>,
|
||||
config: &PluginsConfigInput,
|
||||
auth: Option<CodexAuth>,
|
||||
roots: &[AbsolutePathBuf],
|
||||
options: PluginListBackgroundTaskOptions,
|
||||
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
|
||||
) {
|
||||
self.maybe_start_non_curated_plugin_cache_refresh(roots);
|
||||
if options.refresh_global_remote_catalog_cache {
|
||||
self.maybe_start_global_remote_catalog_cache_refresh(config, auth.clone());
|
||||
}
|
||||
self.maybe_start_remote_installed_plugins_cache_refresh(
|
||||
config,
|
||||
auth.clone(),
|
||||
@@ -1447,6 +1492,33 @@ impl PluginsManager {
|
||||
});
|
||||
}
|
||||
|
||||
fn schedule_global_remote_catalog_cache_refresh(
|
||||
self: &Arc<Self>,
|
||||
request: GlobalRemoteCatalogCacheRefreshRequest,
|
||||
) {
|
||||
let should_spawn = {
|
||||
let mut state = match self.global_remote_catalog_cache_refresh_state.write() {
|
||||
Ok(state) => state,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
state.requested = Some(request);
|
||||
if state.in_flight {
|
||||
false
|
||||
} else {
|
||||
state.in_flight = true;
|
||||
true
|
||||
}
|
||||
};
|
||||
if !should_spawn {
|
||||
return;
|
||||
}
|
||||
|
||||
let manager = Arc::clone(self);
|
||||
tokio::spawn(async move {
|
||||
manager.run_global_remote_catalog_cache_refresh_loop().await;
|
||||
});
|
||||
}
|
||||
|
||||
fn schedule_non_curated_plugin_cache_refresh(
|
||||
self: &Arc<Self>,
|
||||
roots: &[AbsolutePathBuf],
|
||||
@@ -1613,6 +1685,44 @@ impl PluginsManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_global_remote_catalog_cache_refresh_loop(self: Arc<Self>) {
|
||||
loop {
|
||||
let request = {
|
||||
let mut state = match self.global_remote_catalog_cache_refresh_state.write() {
|
||||
Ok(state) => state,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
match state.requested.take() {
|
||||
Some(request) => request,
|
||||
None => {
|
||||
state.in_flight = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match crate::remote::fetch_and_cache_global_remote_plugin_catalog(
|
||||
self.codex_home.as_path(),
|
||||
&request.service_config,
|
||||
request.auth.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(
|
||||
RemotePluginCatalogError::AuthRequired
|
||||
| RemotePluginCatalogError::UnsupportedAuthMode,
|
||||
) => {}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
error = %err,
|
||||
"failed to refresh cached global remote plugin catalog"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_non_curated_plugin_cache_refresh_loop(self: Arc<Self>) {
|
||||
loop {
|
||||
let request = {
|
||||
|
||||
@@ -6,8 +6,10 @@ use crate::loader::load_plugins_from_layer_stack;
|
||||
use crate::loader::refresh_non_curated_plugin_cache;
|
||||
use crate::loader::refresh_non_curated_plugin_cache_force_reinstall;
|
||||
use crate::marketplace::MarketplacePluginInstallPolicy;
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use crate::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME;
|
||||
use crate::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
|
||||
use crate::remote::RemoteInstalledPlugin;
|
||||
use crate::remote::RemotePluginScope;
|
||||
use crate::startup_sync::curated_plugins_repo_path;
|
||||
use crate::test_support::TEST_CURATED_PLUGIN_CACHE_VERSION;
|
||||
use crate::test_support::TEST_CURATED_PLUGIN_SHA;
|
||||
@@ -137,8 +139,15 @@ fn remote_installed_linear_plugin() -> RemoteInstalledPlugin {
|
||||
}
|
||||
|
||||
fn remote_installed_plugin(name: &str) -> RemoteInstalledPlugin {
|
||||
remote_installed_plugin_in_marketplace(name, REMOTE_GLOBAL_MARKETPLACE_NAME)
|
||||
}
|
||||
|
||||
fn remote_installed_plugin_in_marketplace(
|
||||
name: &str,
|
||||
marketplace_name: &str,
|
||||
) -> RemoteInstalledPlugin {
|
||||
RemoteInstalledPlugin {
|
||||
marketplace_name: "openai-curated-remote".to_string(),
|
||||
marketplace_name: marketplace_name.to_string(),
|
||||
id: format!("plugins~Plugin_{name}"),
|
||||
name: name.to_string(),
|
||||
enabled: true,
|
||||
@@ -484,7 +493,7 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad
|
||||
manager.write_remote_installed_plugins_cache(vec![plugin]);
|
||||
|
||||
let marketplaces = manager
|
||||
.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global])
|
||||
.build_remote_installed_plugin_marketplaces_from_cache(&[REMOTE_GLOBAL_MARKETPLACE_NAME])
|
||||
.expect("remote installed cache should be present");
|
||||
assert_eq!(marketplaces.len(), 1);
|
||||
assert_eq!(marketplaces[0].name, "openai-curated-remote");
|
||||
@@ -521,12 +530,45 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad
|
||||
);
|
||||
assert_eq!(
|
||||
manager
|
||||
.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Workspace])
|
||||
.build_remote_installed_plugin_marketplaces_from_cache(&[
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME
|
||||
])
|
||||
.expect("remote installed cache should be present"),
|
||||
Vec::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_remote_installed_plugin_marketplaces_from_cache_filters_by_marketplace_name() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
manager.write_remote_installed_plugins_cache(vec![
|
||||
remote_installed_plugin_in_marketplace(
|
||||
"workspace-linear",
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME,
|
||||
),
|
||||
remote_installed_plugin_in_marketplace(
|
||||
"shared-linear",
|
||||
REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME,
|
||||
),
|
||||
]);
|
||||
|
||||
let marketplaces = manager
|
||||
.build_remote_installed_plugin_marketplaces_from_cache(&[REMOTE_WORKSPACE_MARKETPLACE_NAME])
|
||||
.expect("remote installed cache should be present");
|
||||
|
||||
assert_eq!(marketplaces.len(), 1);
|
||||
assert_eq!(marketplaces[0].name, REMOTE_WORKSPACE_MARKETPLACE_NAME);
|
||||
assert_eq!(
|
||||
marketplaces[0]
|
||||
.plugins
|
||||
.iter()
|
||||
.map(|plugin| plugin.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["workspace-linear@workspace-directory"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_plugins_resolves_disabled_skill_names_against_loaded_plugin_skills() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -3039,6 +3081,33 @@ fn refresh_curated_plugin_cache_reinstalls_missing_configured_plugin_with_curren
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_curated_plugin_cache_removes_cache_for_plugin_removed_from_marketplace() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let curated_root = curated_plugins_repo_path(tmp.path());
|
||||
write_openai_curated_marketplace(&curated_root, &[]);
|
||||
let plugin_id = PluginId::new(
|
||||
"google-sheets".to_string(),
|
||||
OPENAI_CURATED_MARKETPLACE_NAME.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
let plugin_cache_root = tmp
|
||||
.path()
|
||||
.join("plugins/cache/openai-curated/google-sheets");
|
||||
write_plugin(
|
||||
&tmp.path().join("plugins/cache/openai-curated"),
|
||||
&format!("google-sheets/{TEST_CURATED_PLUGIN_CACHE_VERSION}"),
|
||||
"google-sheets",
|
||||
);
|
||||
|
||||
assert!(
|
||||
refresh_curated_plugin_cache(tmp.path(), TEST_CURATED_PLUGIN_SHA, &[plugin_id])
|
||||
.expect("cache refresh should remove stale configured plugin")
|
||||
);
|
||||
|
||||
assert!(!plugin_cache_root.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curated_plugin_ids_from_config_keys_reads_latest_codex_home_user_config() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_protocol::protocol::Product;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Component;
|
||||
@@ -326,6 +327,16 @@ pub fn load_marketplace(path: &AbsolutePathBuf) -> Result<Marketplace, Marketpla
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn load_raw_marketplace_plugin_names(
|
||||
path: &AbsolutePathBuf,
|
||||
) -> Result<HashSet<String>, MarketplaceError> {
|
||||
Ok(load_raw_marketplace_manifest(path)?
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| plugin.name)
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn list_marketplaces_with_home(
|
||||
additional_roots: &[AbsolutePathBuf],
|
||||
|
||||
@@ -550,6 +550,12 @@ struct RemotePluginInstalledResponse {
|
||||
struct RemotePluginMutationResponse {
|
||||
id: String,
|
||||
enabled: bool,
|
||||
app_ids_needing_auth: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemotePluginInstallResult {
|
||||
pub app_ids_needing_auth: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn fetch_remote_marketplaces(
|
||||
@@ -576,6 +582,25 @@ pub async fn fetch_remote_marketplaces(
|
||||
match source {
|
||||
RemoteMarketplaceSource::Global => {
|
||||
let scope = RemotePluginScope::Global;
|
||||
if let Some(codex_home) = global_catalog_cache_path
|
||||
&& let Some(directory_plugins) =
|
||||
catalog_cache::load_cached_global_directory_plugins(
|
||||
codex_home, config, auth,
|
||||
)
|
||||
{
|
||||
let installed_plugins =
|
||||
fetch_installed_plugins_for_scope(config, auth, scope).await?;
|
||||
if let Some(marketplace) = build_remote_marketplace(
|
||||
scope.marketplace_name(),
|
||||
scope.marketplace_display_name(),
|
||||
directory_plugins,
|
||||
installed_plugins,
|
||||
/*include_installed_only*/ true,
|
||||
)? {
|
||||
marketplaces.push(marketplace);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (directory_plugins, installed_plugins) = tokio::try_join!(
|
||||
fetch_directory_plugins_for_scope(config, auth, scope),
|
||||
fetch_installed_plugins_for_scope(config, auth, scope),
|
||||
@@ -692,6 +717,17 @@ pub async fn fetch_and_cache_global_remote_plugin_catalog(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_cached_global_remote_plugin_catalog(
|
||||
codex_home: &Path,
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> bool {
|
||||
let Ok(auth) = ensure_chatgpt_auth(auth) else {
|
||||
return false;
|
||||
};
|
||||
catalog_cache::load_cached_global_directory_plugins(codex_home, config, auth).is_some()
|
||||
}
|
||||
|
||||
pub fn cached_global_remote_discoverable_plugins(
|
||||
codex_home: &Path,
|
||||
config: &RemotePluginServiceConfig,
|
||||
@@ -815,14 +851,12 @@ pub(crate) async fn fetch_remote_installed_plugins(
|
||||
|
||||
pub fn group_remote_installed_plugins_by_marketplaces(
|
||||
plugins: &[RemoteInstalledPlugin],
|
||||
visible_scopes: &[RemotePluginScope],
|
||||
visible_marketplaces: &[&str],
|
||||
) -> Vec<RemoteMarketplace> {
|
||||
let mut plugins_by_marketplace = BTreeMap::<String, Vec<RemotePluginSummary>>::new();
|
||||
|
||||
for plugin in plugins {
|
||||
if !RemotePluginScope::from_marketplace_name(&plugin.marketplace_name)
|
||||
.is_some_and(|scope| visible_scopes.contains(&scope))
|
||||
{
|
||||
if !visible_marketplaces.contains(&plugin.marketplace_name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let Ok(plugin_id) = PluginId::new(plugin.name.clone(), plugin.marketplace_name.clone())
|
||||
@@ -1041,7 +1075,7 @@ pub async fn install_remote_plugin(
|
||||
auth: Option<&CodexAuth>,
|
||||
_marketplace_name: &str,
|
||||
plugin_id: &str,
|
||||
) -> Result<(), RemotePluginCatalogError> {
|
||||
) -> Result<RemotePluginInstallResult, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
// Remote plugin IDs uniquely identify remote plugins, so the caller-provided
|
||||
// marketplace name is not validated before sending the install mutation.
|
||||
@@ -1049,7 +1083,12 @@ pub async fn install_remote_plugin(
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/ps/plugins/{plugin_id}/install");
|
||||
let client = build_reqwest_client();
|
||||
let request = authenticated_request(client.post(&url), auth)?;
|
||||
let request = authenticated_request(
|
||||
client
|
||||
.post(&url)
|
||||
.query(&[("includeAppsNeedingAuth", "true")]),
|
||||
auth,
|
||||
)?;
|
||||
let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?;
|
||||
if response.id != plugin_id {
|
||||
return Err(RemotePluginCatalogError::UnexpectedPluginId {
|
||||
@@ -1065,7 +1104,9 @@ pub async fn install_remote_plugin(
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(RemotePluginInstallResult {
|
||||
app_ids_needing_auth: response.app_ids_needing_auth,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn uninstall_remote_plugin(
|
||||
|
||||
@@ -28,16 +28,16 @@ pub const SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS: &str = r###"- Discovery: The li
|
||||
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
|
||||
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
|
||||
- How to use a skill (progressive disclosure):
|
||||
1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
|
||||
1) After deciding to use a skill, the main agent must open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF.
|
||||
2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
|
||||
3) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
|
||||
3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
|
||||
4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
|
||||
5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
|
||||
- Coordination and sequencing:
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets.
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."###;
|
||||
@@ -45,16 +45,16 @@ pub const SKILLS_HOW_TO_USE_WITH_ALIASES: &str = r###"- Discovery: The list abov
|
||||
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
|
||||
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
|
||||
- How to use a skill (progressive disclosure):
|
||||
1) After deciding to use a skill, expand the listed short `path` with the matching alias from `### Skill roots`, then open its `SKILL.md`. Read only enough to follow the workflow.
|
||||
1) After deciding to use a skill, the main agent must expand the listed short `path` with the matching alias from `### Skill roots`, then open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF.
|
||||
2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the directory containing that expanded `SKILL.md` first, and only consider other paths if needed.
|
||||
3) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
|
||||
3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
|
||||
4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
|
||||
5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
|
||||
- Coordination and sequencing:
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
|
||||
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets.
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."###;
|
||||
@@ -982,6 +982,30 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_usage_instructions_require_complete_main_agent_reads() {
|
||||
for instructions in [
|
||||
SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS,
|
||||
SKILLS_HOW_TO_USE_WITH_ALIASES,
|
||||
] {
|
||||
assert!(instructions.contains("read its `SKILL.md` completely"));
|
||||
assert!(instructions.contains("continue until EOF"));
|
||||
assert!(instructions.contains(
|
||||
"The main agent must read each required instruction or reference file itself"
|
||||
));
|
||||
assert!(instructions.contains(
|
||||
"Do not delegate reading, summarizing, or interpreting skill instructions"
|
||||
));
|
||||
assert!(instructions.contains(
|
||||
"Subagents may still perform task work when the selected skill allows it"
|
||||
));
|
||||
assert!(instructions.contains(
|
||||
"Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file"
|
||||
));
|
||||
assert!(!instructions.contains("Read only enough to follow the workflow"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_budget_uses_two_percent_of_full_context_window() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -458,7 +458,6 @@ async fn send_input_submits_user_message() {
|
||||
let expected = (
|
||||
thread_id,
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello from tests".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -797,7 +796,6 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
|
||||
let expected = (
|
||||
thread_id,
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "spawned".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1017,7 +1015,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
let expected = (
|
||||
child_thread_id,
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "child task".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
|
||||
@@ -196,7 +196,6 @@ pub(crate) async fn run_codex_thread_one_shot(
|
||||
|
||||
// Send the initial input to kick off the one-shot turn.
|
||||
io.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: input,
|
||||
final_output_json_schema,
|
||||
responsesapi_client_metadata: None,
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_thread_store::StoredThread;
|
||||
@@ -59,7 +60,7 @@ pub struct ThreadConfigSnapshot {
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
pub permission_profile: PermissionProfile,
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub environments: TurnEnvironmentSelections,
|
||||
pub workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub profile_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub ephemeral: bool,
|
||||
@@ -114,10 +115,18 @@ impl TryStartTurnIfIdleError {
|
||||
}
|
||||
|
||||
impl ThreadConfigSnapshot {
|
||||
pub fn cwd(&self) -> &AbsolutePathBuf {
|
||||
&self.environments.legacy_fallback_cwd
|
||||
}
|
||||
|
||||
pub fn environment_selections(&self) -> &[TurnEnvironmentSelection] {
|
||||
&self.environments.environments
|
||||
}
|
||||
|
||||
pub fn sandbox_policy(&self) -> SandboxPolicy {
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&self.permission_profile,
|
||||
self.cwd.as_path(),
|
||||
self.cwd().as_path(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -125,7 +134,7 @@ impl ThreadConfigSnapshot {
|
||||
/// Thread settings overrides that app-server validates before starting a turn.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CodexThreadSettingsOverrides {
|
||||
pub cwd: Option<AbsolutePathBuf>,
|
||||
pub environments: Option<TurnEnvironmentSelections>,
|
||||
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
@@ -330,7 +339,7 @@ impl CodexThread {
|
||||
overrides: CodexThreadSettingsOverrides,
|
||||
) -> SessionSettingsUpdate {
|
||||
let CodexThreadSettingsOverrides {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
@@ -357,7 +366,7 @@ impl CodexThread {
|
||||
};
|
||||
|
||||
SessionSettingsUpdate {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
|
||||
@@ -8,8 +8,6 @@ use crate::compact::InitialContextInjection;
|
||||
use crate::compact::compaction_status_from_result;
|
||||
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
|
||||
use crate::context_manager::ContextManager;
|
||||
use crate::context_manager::TotalTokenUsageBreakdown;
|
||||
use crate::context_manager::estimate_response_item_model_visible_bytes;
|
||||
use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
use crate::hook_runtime::run_post_compact_hooks;
|
||||
@@ -35,9 +33,7 @@ use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_rollout_trace::CompactionCheckpointTracePayload;
|
||||
use futures::TryFutureExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
|
||||
const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str =
|
||||
@@ -199,9 +195,8 @@ async fn run_remote_compact_task_inner_impl(
|
||||
}
|
||||
if estimated_deleted_tokens > 0 {
|
||||
let max_local_deleted_tokens = sess
|
||||
.get_total_token_usage_breakdown()
|
||||
.await
|
||||
.estimated_tokens_of_items_added_since_last_successful_api_response;
|
||||
.estimated_tokens_after_last_model_generated_item()
|
||||
.await;
|
||||
analytics_details.active_context_tokens_before = analytics_details
|
||||
.active_context_tokens_before
|
||||
.map(|active_context_tokens_before| {
|
||||
@@ -252,18 +247,6 @@ async fn run_remote_compact_task_inner_impl(
|
||||
&compaction_trace,
|
||||
turn_metadata_header.as_deref(),
|
||||
)
|
||||
.or_else(|err| async {
|
||||
let total_usage_breakdown = sess.get_total_token_usage_breakdown().await;
|
||||
let compact_request_log_data =
|
||||
build_compact_request_log_data(&prompt.input, &prompt.base_instructions.text);
|
||||
log_remote_compact_failure(
|
||||
turn_context,
|
||||
&compact_request_log_data,
|
||||
total_usage_breakdown,
|
||||
&err,
|
||||
);
|
||||
Err(err)
|
||||
})
|
||||
.await?;
|
||||
new_history = process_compacted_history(
|
||||
sess.as_ref(),
|
||||
@@ -363,47 +346,6 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CompactRequestLogData {
|
||||
failing_compaction_request_model_visible_bytes: i64,
|
||||
}
|
||||
|
||||
pub(crate) fn build_compact_request_log_data(
|
||||
input: &[ResponseItem],
|
||||
instructions: &str,
|
||||
) -> CompactRequestLogData {
|
||||
let failing_compaction_request_model_visible_bytes = input
|
||||
.iter()
|
||||
.map(estimate_response_item_model_visible_bytes)
|
||||
.fold(
|
||||
i64::try_from(instructions.len()).unwrap_or(i64::MAX),
|
||||
i64::saturating_add,
|
||||
);
|
||||
|
||||
CompactRequestLogData {
|
||||
failing_compaction_request_model_visible_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn log_remote_compact_failure(
|
||||
turn_context: &TurnContext,
|
||||
log_data: &CompactRequestLogData,
|
||||
total_usage_breakdown: TotalTokenUsageBreakdown,
|
||||
err: &CodexErr,
|
||||
) {
|
||||
error!(
|
||||
turn_id = %turn_context.sub_id,
|
||||
last_api_response_total_tokens = total_usage_breakdown.last_api_response_total_tokens,
|
||||
all_history_items_model_visible_bytes = total_usage_breakdown.all_history_items_model_visible_bytes,
|
||||
estimated_tokens_of_items_added_since_last_successful_api_response = total_usage_breakdown.estimated_tokens_of_items_added_since_last_successful_api_response,
|
||||
estimated_bytes_of_items_added_since_last_successful_api_response = total_usage_breakdown.estimated_bytes_of_items_added_since_last_successful_api_response,
|
||||
model_context_window_tokens = ?turn_context.model_context_window(),
|
||||
failing_compaction_request_model_visible_bytes = log_data.failing_compaction_request_model_visible_bytes,
|
||||
compact_error = %err,
|
||||
"remote compaction failed"
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn trim_function_call_history_to_fit_context_window(
|
||||
history: &mut ContextManager,
|
||||
turn_context: &TurnContext,
|
||||
|
||||
@@ -8,8 +8,6 @@ use crate::compact::CompactionAnalyticsAttempt;
|
||||
use crate::compact::CompactionAnalyticsDetails;
|
||||
use crate::compact::InitialContextInjection;
|
||||
use crate::compact::compaction_status_from_result;
|
||||
use crate::compact_remote::build_compact_request_log_data;
|
||||
use crate::compact_remote::log_remote_compact_failure;
|
||||
use crate::compact_remote::process_compacted_history;
|
||||
use crate::compact_remote::should_keep_compacted_history_item;
|
||||
use crate::compact_remote::trim_function_call_history_to_fit_context_window;
|
||||
@@ -212,9 +210,8 @@ async fn run_remote_compact_task_inner_impl(
|
||||
}
|
||||
if estimated_deleted_tokens > 0 {
|
||||
let max_local_deleted_tokens = sess
|
||||
.get_total_token_usage_breakdown()
|
||||
.await
|
||||
.estimated_tokens_of_items_added_since_last_successful_api_response;
|
||||
.estimated_tokens_after_last_model_generated_item()
|
||||
.await;
|
||||
analytics_details.active_context_tokens_before = analytics_details
|
||||
.active_context_tokens_before
|
||||
.map(|active_context_tokens_before| {
|
||||
@@ -354,12 +351,9 @@ async fn run_remote_compaction_request_v2(
|
||||
|
||||
match result {
|
||||
Ok(compaction_output) => return Ok(compaction_output),
|
||||
Err(err) if !err.is_retryable() => {
|
||||
log_remote_compaction_request_failure(sess, turn_context, prompt, &err).await;
|
||||
return Err(err);
|
||||
}
|
||||
Err(err) if !err.is_retryable() => return Err(err),
|
||||
Err(err) => {
|
||||
if let Err(err) = handle_retryable_response_stream_error(
|
||||
handle_retryable_response_stream_error(
|
||||
&mut retries,
|
||||
max_retries,
|
||||
err,
|
||||
@@ -368,33 +362,12 @@ async fn run_remote_compaction_request_v2(
|
||||
turn_context,
|
||||
ResponsesStreamRequest::RemoteCompactionV2,
|
||||
)
|
||||
.await
|
||||
{
|
||||
log_remote_compaction_request_failure(sess, turn_context, prompt, &err).await;
|
||||
return Err(err);
|
||||
}
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn log_remote_compaction_request_failure(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
prompt: &Prompt,
|
||||
err: &CodexErr,
|
||||
) {
|
||||
let total_usage_breakdown = sess.get_total_token_usage_breakdown().await;
|
||||
let compact_request_log_data =
|
||||
build_compact_request_log_data(&prompt.input, &prompt.base_instructions.text);
|
||||
log_remote_compact_failure(
|
||||
turn_context,
|
||||
&compact_request_log_data,
|
||||
total_usage_breakdown,
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
||||
async fn collect_compaction_output(
|
||||
mut stream: ResponseStream,
|
||||
) -> CodexResult<RemoteCompactionV2Output> {
|
||||
|
||||
@@ -103,6 +103,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
pub use codex_thread_store::ExtraConfig;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
@@ -202,7 +203,7 @@ All agents in the team, including the agents that you can assign tasks to, are e
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
|
||||
Use multi-agent capabilities only when there is a real reason to split the work; handle trivial or simple tasks directly.
|
||||
Default to doing the work yourself. Spawn sub-agents only for concrete, bounded subtasks that can run independently alongside useful local work and are likely to materially shorten completion time. Do not delegate simple tasks, small edits, routine searches, or work you can complete quickly yourself.
|
||||
|
||||
You will receive messages in the analysis channel in the form:
|
||||
```
|
||||
@@ -219,7 +220,7 @@ You can spawn sub-agents to handle subtasks, and those sub-agents can spawn thei
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
Use multi-agent capabilities only when there is a real reason to split the work; handle trivial or simple tasks directly.
|
||||
Default to doing the work yourself. Spawn sub-agents only for concrete, bounded subtasks that can run independently alongside useful local work and are likely to materially shorten completion time. Do not delegate simple tasks, small edits, routine searches, or work you can complete quickly yourself.
|
||||
|
||||
When you provide a response in the final channel, that content is immediately delivered back to your parent agent.
|
||||
|
||||
@@ -870,6 +871,9 @@ pub struct Config {
|
||||
/// When true, session is not persisted on disk. Default to `false`
|
||||
pub ephemeral: bool,
|
||||
|
||||
/// Optional extra configuration fields for the thread.
|
||||
pub extra_config: Option<ExtraConfig>,
|
||||
|
||||
/// Whether enabled hooks should run without requiring persisted hook trust for this session.
|
||||
///
|
||||
/// This is a runtime-only knob populated from invocation overrides, not from config files.
|
||||
@@ -3525,6 +3529,7 @@ impl Config {
|
||||
config_layer_stack,
|
||||
history,
|
||||
ephemeral: ephemeral.unwrap_or_default(),
|
||||
extra_config: None,
|
||||
bypass_hook_trust,
|
||||
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
|
||||
codex_self_exe,
|
||||
|
||||
@@ -50,14 +50,6 @@ pub(crate) struct ContextManager {
|
||||
reference_context_item: Option<TurnContextItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub(crate) struct TotalTokenUsageBreakdown {
|
||||
pub last_api_response_total_tokens: i64,
|
||||
pub all_history_items_model_visible_bytes: i64,
|
||||
pub estimated_tokens_of_items_added_since_last_successful_api_response: i64,
|
||||
pub estimated_bytes_of_items_added_since_last_successful_api_response: i64,
|
||||
}
|
||||
|
||||
impl ContextManager {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
@@ -321,32 +313,11 @@ impl ContextManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_total_token_usage_breakdown(&self) -> TotalTokenUsageBreakdown {
|
||||
let last_usage = self
|
||||
.token_info
|
||||
.as_ref()
|
||||
.map(|info| info.last_token_usage.clone())
|
||||
.unwrap_or_default();
|
||||
let items_after_last_model_generated = self.items_after_last_model_generated_item();
|
||||
|
||||
TotalTokenUsageBreakdown {
|
||||
last_api_response_total_tokens: last_usage.total_tokens,
|
||||
all_history_items_model_visible_bytes: self
|
||||
.items
|
||||
.iter()
|
||||
.map(estimate_response_item_model_visible_bytes)
|
||||
.fold(0i64, i64::saturating_add),
|
||||
estimated_tokens_of_items_added_since_last_successful_api_response:
|
||||
items_after_last_model_generated
|
||||
.iter()
|
||||
.map(estimate_item_token_count)
|
||||
.fold(0i64, i64::saturating_add),
|
||||
estimated_bytes_of_items_added_since_last_successful_api_response:
|
||||
items_after_last_model_generated
|
||||
.iter()
|
||||
.map(estimate_response_item_model_visible_bytes)
|
||||
.fold(0i64, i64::saturating_add),
|
||||
}
|
||||
pub(crate) fn estimated_tokens_after_last_model_generated_item(&self) -> i64 {
|
||||
self.items_after_last_model_generated_item()
|
||||
.iter()
|
||||
.map(estimate_item_token_count)
|
||||
.fold(0i64, i64::saturating_add)
|
||||
}
|
||||
|
||||
/// This function enforces a couple of invariants on the in-memory history:
|
||||
@@ -532,7 +503,7 @@ static ORIGINAL_IMAGE_ESTIMATE_CACHE: LazyLock<BlockingLruCache<[u8; 20], Option
|
||||
)
|
||||
});
|
||||
|
||||
pub(crate) fn estimate_response_item_model_visible_bytes(item: &ResponseItem) -> i64 {
|
||||
fn estimate_response_item_model_visible_bytes(item: &ResponseItem) -> i64 {
|
||||
match item {
|
||||
ResponseItem::Reasoning {
|
||||
encrypted_content: Some(content),
|
||||
|
||||
@@ -3,7 +3,5 @@ mod normalize;
|
||||
pub(crate) mod updates;
|
||||
|
||||
pub(crate) use history::ContextManager;
|
||||
pub(crate) use history::TotalTokenUsageBreakdown;
|
||||
pub(crate) use history::estimate_response_item_model_visible_bytes;
|
||||
pub(crate) use history::is_user_turn_boundary;
|
||||
pub(crate) use history::truncate_function_output_payload;
|
||||
|
||||
@@ -340,46 +340,6 @@ async fn test_get_has_changes_with_untracked_change_returns_true() {
|
||||
assert_eq!(get_has_changes(&repo_path).await, Some(true));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_get_has_changes_ignores_repo_fsmonitor_config() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let repo_path = create_test_git_repo(&temp_dir).await;
|
||||
let helper_path = repo_path.join("fsmonitor-helper.sh");
|
||||
let marker_path = repo_path.join("fsmonitor-ran");
|
||||
|
||||
fs::write(
|
||||
&helper_path,
|
||||
format!(
|
||||
"#!/bin/sh\nprintf ran > \"{}\"\n",
|
||||
marker_path.to_string_lossy()
|
||||
),
|
||||
)
|
||||
.expect("write fsmonitor helper");
|
||||
let mut permissions = fs::metadata(&helper_path)
|
||||
.expect("read fsmonitor helper metadata")
|
||||
.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&helper_path, permissions).expect("mark fsmonitor helper executable");
|
||||
|
||||
Command::new("git")
|
||||
.args([
|
||||
"config",
|
||||
"core.fsmonitor",
|
||||
helper_path.to_string_lossy().as_ref(),
|
||||
])
|
||||
.current_dir(&repo_path)
|
||||
.output()
|
||||
.await
|
||||
.expect("configure fsmonitor helper");
|
||||
|
||||
assert_eq!(get_has_changes(&repo_path).await, Some(true));
|
||||
assert!(
|
||||
!marker_path.exists(),
|
||||
"metadata collection should not invoke repository fsmonitor helpers"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_get_has_changes_ignores_configured_hooks_path() {
|
||||
|
||||
@@ -721,19 +721,27 @@ async fn run_review_on_session(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let guardian_permission_profile = PermissionProfile::read_only();
|
||||
let parent_turn_environments = params.parent_turn.environments.to_selections();
|
||||
let parent_turn_legacy_fallback_cwd = params
|
||||
.parent_turn
|
||||
.environments
|
||||
.primary()
|
||||
.map(|environment| environment.cwd.clone())
|
||||
.unwrap_or_else(|| params.parent_turn.config.cwd.clone());
|
||||
|
||||
let submit_result = run_before_review_deadline(
|
||||
deadline,
|
||||
params.external_cancel.as_ref(),
|
||||
Box::pin(review_session.codex.submit(Op::UserInput {
|
||||
items: prompt_items.items,
|
||||
environments: None,
|
||||
final_output_json_schema: Some(params.schema.clone()),
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
#[allow(deprecated)]
|
||||
cwd: Some(params.parent_turn.cwd.clone()),
|
||||
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
|
||||
parent_turn_legacy_fallback_cwd,
|
||||
parent_turn_environments,
|
||||
)),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
sandbox_policy: None,
|
||||
permission_profile: Some(guardian_permission_profile),
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::plugins::test_support::write_plugins_feature_config;
|
||||
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::PluginInstallRequest;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core_plugins::remote::RemotePluginScope;
|
||||
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::RemotePluginServiceConfig;
|
||||
use codex_core_plugins::remote::fetch_and_cache_global_remote_plugin_catalog;
|
||||
use codex_core_plugins::startup_sync::curated_plugins_repo_path;
|
||||
@@ -362,7 +362,7 @@ remote_plugin = true
|
||||
.build_and_cache_remote_installed_plugin_marketplaces(
|
||||
&config.plugins_config_input(),
|
||||
Some(&auth),
|
||||
&[RemotePluginScope::Global],
|
||||
&[REMOTE_GLOBAL_MARKETPLACE_NAME],
|
||||
/*on_effective_plugins_changed*/ None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -30,6 +30,7 @@ use tempfile::TempDir;
|
||||
fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThread {
|
||||
StoredThread {
|
||||
thread_id: ThreadId::new(),
|
||||
extra_config: None,
|
||||
rollout_path: Some(PathBuf::from("/tmp/rollout.jsonl")),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -122,7 +122,7 @@ async fn thread_settings_update(
|
||||
thread_settings: ThreadSettingsOverrides,
|
||||
) -> SessionSettingsUpdate {
|
||||
let ThreadSettingsOverrides {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
@@ -151,7 +151,7 @@ async fn thread_settings_update(
|
||||
}
|
||||
};
|
||||
SessionSettingsUpdate {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
@@ -173,6 +173,7 @@ async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
|
||||
let state = sess.state.lock().await;
|
||||
state.session_configuration.thread_config_snapshot()
|
||||
};
|
||||
let cwd = snapshot.cwd().clone();
|
||||
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
|
||||
thread_settings: ThreadSettingsSnapshot {
|
||||
model: snapshot.model,
|
||||
@@ -182,7 +183,7 @@ async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
|
||||
approvals_reviewer: snapshot.approvals_reviewer,
|
||||
permission_profile: snapshot.permission_profile,
|
||||
active_permission_profile: snapshot.active_permission_profile,
|
||||
cwd: snapshot.cwd,
|
||||
cwd,
|
||||
reasoning_effort: snapshot.reasoning_effort,
|
||||
reasoning_summary: snapshot.reasoning_summary,
|
||||
personality: snapshot.personality,
|
||||
@@ -200,7 +201,6 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
) {
|
||||
let Op::UserInput {
|
||||
items,
|
||||
environments,
|
||||
final_output_json_schema,
|
||||
responsesapi_client_metadata,
|
||||
additional_context,
|
||||
@@ -216,7 +216,6 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
SessionSettingsUpdate::default()
|
||||
};
|
||||
updates.final_output_json_schema = Some(final_output_json_schema);
|
||||
updates.environments = environments;
|
||||
|
||||
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id.clone(), updates).await else {
|
||||
// new_turn_with_sub_id already emits the error event.
|
||||
|
||||
@@ -116,6 +116,7 @@ use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
@@ -181,7 +182,6 @@ use crate::config::PermissionProfileState;
|
||||
use crate::config::StartedNetworkProxy;
|
||||
use crate::config::resolve_web_search_mode_for_turn;
|
||||
use crate::context_manager::ContextManager;
|
||||
use crate::context_manager::TotalTokenUsageBreakdown;
|
||||
use crate::thread_rollout_truncation::initial_history_has_prior_user_turns;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerSource;
|
||||
@@ -602,11 +602,13 @@ impl Codex {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: session_permission_profile_state_from_config(&config)?,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(
|
||||
config.cwd.clone(),
|
||||
environment_selections.to_selections(),
|
||||
),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: environment_selections.to_selections(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name,
|
||||
app_server_client_name: None,
|
||||
@@ -814,7 +816,10 @@ impl Codex {
|
||||
|
||||
pub(crate) async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
|
||||
let state = self.session.state.lock().await;
|
||||
state.session_configuration.environments.clone()
|
||||
state
|
||||
.session_configuration
|
||||
.environment_selections()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn state_db(&self) -> Option<state_db::StateDbHandle> {
|
||||
@@ -1114,7 +1119,6 @@ impl Session {
|
||||
self,
|
||||
self.next_internal_sub_id(),
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text,
|
||||
text_elements: Vec::new(),
|
||||
@@ -1140,9 +1144,11 @@ impl Session {
|
||||
state.auto_compact_window_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_total_token_usage_breakdown(&self) -> TotalTokenUsageBreakdown {
|
||||
pub(crate) async fn estimated_tokens_after_last_model_generated_item(&self) -> i64 {
|
||||
let state = self.state.lock().await;
|
||||
state.history.get_total_token_usage_breakdown()
|
||||
state
|
||||
.history
|
||||
.estimated_tokens_after_last_model_generated_item()
|
||||
}
|
||||
|
||||
pub(crate) async fn total_token_usage(&self) -> Option<TokenUsage> {
|
||||
@@ -1414,12 +1420,12 @@ impl Session {
|
||||
.then(|| Self::build_effective_session_config(&state.session_configuration));
|
||||
let new_config =
|
||||
notify_config_contributors.then(|| Self::build_effective_session_config(&updated));
|
||||
let previous_cwd = state.session_configuration.cwd.clone();
|
||||
let previous_cwd = state.session_configuration.cwd().clone();
|
||||
let previous_permission_profile = state.session_configuration.permission_profile();
|
||||
let updated_permission_profile = updated.permission_profile();
|
||||
let permission_profile_changed =
|
||||
previous_permission_profile != updated_permission_profile;
|
||||
let next_cwd = updated.cwd.clone();
|
||||
let next_cwd = updated.cwd().clone();
|
||||
let codex_home = updated.codex_home.clone();
|
||||
let session_source = updated.session_source.clone();
|
||||
state.session_configuration = updated;
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use std::sync::OnceLock;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
@@ -75,11 +76,9 @@ pub(crate) struct SessionConfiguration {
|
||||
pub(super) permission_profile_state: PermissionProfileState,
|
||||
pub(super) windows_sandbox_level: WindowsSandboxLevel,
|
||||
|
||||
/// Absolute working directory that should be treated as the *root* of the
|
||||
/// session. All relative paths supplied by the model as well as the
|
||||
/// execution sandbox are resolved against this directory **instead** of
|
||||
/// the process-wide current working directory.
|
||||
pub(super) cwd: AbsolutePathBuf,
|
||||
/// Sticky thread-level environment selections plus the legacy cwd used
|
||||
/// when a turn does not select an environment.
|
||||
pub(super) environments: TurnEnvironmentSelections,
|
||||
/// Thread-scoped runtime workspace roots for materializing symbolic
|
||||
/// workspace permissions at session runtime.
|
||||
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
|
||||
@@ -87,8 +86,6 @@ pub(crate) struct SessionConfiguration {
|
||||
pub(super) codex_home: AbsolutePathBuf,
|
||||
/// Optional user-facing name for the thread, updated during the session.
|
||||
pub(super) thread_name: Option<String>,
|
||||
/// Sticky environments for turns that do not provide a turn-local override.
|
||||
pub(super) environments: Vec<TurnEnvironmentSelection>,
|
||||
|
||||
// TODO(pakrym): Remove config from here
|
||||
pub(super) original_config_do_not_use: Arc<Config>,
|
||||
@@ -110,6 +107,14 @@ pub(crate) struct SessionConfiguration {
|
||||
}
|
||||
|
||||
impl SessionConfiguration {
|
||||
pub(super) fn cwd(&self) -> &AbsolutePathBuf {
|
||||
&self.environments.legacy_fallback_cwd
|
||||
}
|
||||
|
||||
pub(super) fn environment_selections(&self) -> &[TurnEnvironmentSelection] {
|
||||
&self.environments.environments
|
||||
}
|
||||
|
||||
pub(crate) fn codex_home(&self) -> &AbsolutePathBuf {
|
||||
&self.codex_home
|
||||
}
|
||||
@@ -153,7 +158,7 @@ impl SessionConfiguration {
|
||||
let permission_profile = self.permission_profile();
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&self.cwd,
|
||||
self.cwd(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -176,7 +181,7 @@ impl SessionConfiguration {
|
||||
approvals_reviewer: self.approvals_reviewer,
|
||||
permission_profile: self.permission_profile(),
|
||||
active_permission_profile: self.active_permission_profile(),
|
||||
cwd: self.cwd.clone(),
|
||||
environments: self.environments.clone(),
|
||||
workspace_roots: self.workspace_roots.clone(),
|
||||
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
|
||||
ephemeral: self.original_config_do_not_use.ephemeral,
|
||||
@@ -199,11 +204,11 @@ impl SessionConfiguration {
|
||||
let legacy_file_system_projection =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||||
¤t_sandbox_policy,
|
||||
&self.cwd,
|
||||
self.cwd(),
|
||||
¤t_file_system_sandbox_policy,
|
||||
);
|
||||
let file_system_policy_matches_legacy = current_file_system_sandbox_policy
|
||||
.is_semantically_equivalent_to(&legacy_file_system_projection, &self.cwd);
|
||||
.is_semantically_equivalent_to(&legacy_file_system_projection, self.cwd());
|
||||
let file_system_policy_has_rebindable_project_root_write =
|
||||
current_file_system_sandbox_policy
|
||||
.entries
|
||||
@@ -249,18 +254,21 @@ impl SessionConfiguration {
|
||||
next_configuration.windows_sandbox_level = windows_sandbox_level;
|
||||
}
|
||||
|
||||
let absolute_cwd = updates.cwd.clone().unwrap_or_else(|| self.cwd.clone());
|
||||
|
||||
let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path();
|
||||
next_configuration.cwd = absolute_cwd;
|
||||
let current_cwd = self.cwd().clone();
|
||||
let next_environments = updates
|
||||
.environments
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.environments.clone());
|
||||
let cwd_changed = next_environments.legacy_fallback_cwd.as_path() != current_cwd.as_path();
|
||||
next_configuration.environments = next_environments;
|
||||
if let Some(workspace_roots) = updates.workspace_roots.clone() {
|
||||
next_configuration.workspace_roots = workspace_roots;
|
||||
} else if cwd_changed && self.workspace_roots.contains(&self.cwd) {
|
||||
} else if cwd_changed && self.workspace_roots.contains(¤t_cwd) {
|
||||
let mut retargeted_workspace_roots =
|
||||
Vec::with_capacity(next_configuration.workspace_roots.len());
|
||||
for root in &self.workspace_roots {
|
||||
let root = if root == &self.cwd {
|
||||
next_configuration.cwd.clone()
|
||||
let root = if root == ¤t_cwd {
|
||||
next_configuration.cwd().clone()
|
||||
} else {
|
||||
root.clone()
|
||||
};
|
||||
@@ -317,7 +325,7 @@ impl SessionConfiguration {
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||||
&sandbox_policy,
|
||||
&next_configuration.cwd,
|
||||
next_configuration.cwd(),
|
||||
¤t_file_system_sandbox_policy,
|
||||
);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
@@ -340,7 +348,7 @@ impl SessionConfiguration {
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||||
¤t_sandbox_policy,
|
||||
&next_configuration.cwd,
|
||||
next_configuration.cwd(),
|
||||
¤t_file_system_sandbox_policy,
|
||||
);
|
||||
next_configuration
|
||||
@@ -401,7 +409,7 @@ impl SessionConfiguration {
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) cwd: Option<AbsolutePathBuf>,
|
||||
pub(crate) environments: Option<TurnEnvironmentSelections>,
|
||||
pub(crate) workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub(crate) profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub(crate) approval_policy: Option<AskForApproval>,
|
||||
@@ -414,10 +422,6 @@ pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
|
||||
pub(crate) service_tier: Option<Option<String>>,
|
||||
pub(crate) final_output_json_schema: Option<Option<Value>>,
|
||||
/// Turn-local environment override. `None` inherits the sticky thread
|
||||
/// environments stored on `SessionConfiguration`; `Some([])` explicitly
|
||||
/// disables environments for this turn.
|
||||
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
pub(crate) personality: Option<Personality>,
|
||||
pub(crate) app_server_client_name: Option<String>,
|
||||
pub(crate) app_server_client_version: Option<String>,
|
||||
@@ -539,6 +543,7 @@ impl Session {
|
||||
Arc::clone(&thread_store),
|
||||
CreateThreadParams {
|
||||
thread_id,
|
||||
extra_config: config.extra_config.clone(),
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
source: session_source,
|
||||
@@ -634,7 +639,7 @@ impl Session {
|
||||
Arc::clone(&environment_manager),
|
||||
Arc::clone(&plugins_manager),
|
||||
Arc::clone(&skills_manager),
|
||||
session_configuration.environments.clone(),
|
||||
session_configuration.environment_selections().to_vec(),
|
||||
)
|
||||
.instrument(info_span!(
|
||||
"session_init.plugin_skill_warmup",
|
||||
@@ -686,7 +691,7 @@ impl Session {
|
||||
nickname: session_configuration.session_source.get_nickname(),
|
||||
agent_role: session_configuration.session_source.get_agent_role(),
|
||||
session_source: session_configuration.session_source.clone(),
|
||||
cwd: session_configuration.cwd.to_path_buf(),
|
||||
cwd: session_configuration.cwd().to_path_buf(),
|
||||
rollout_path: rollout_path.clone(),
|
||||
model: session_configuration.collaboration_mode.model().to_string(),
|
||||
provider_name: config.model_provider_id.clone(),
|
||||
@@ -790,7 +795,7 @@ impl Session {
|
||||
/*inc*/ 1,
|
||||
&[(
|
||||
"is_git",
|
||||
if get_git_repo_root(&session_configuration.cwd).is_some() {
|
||||
if get_git_repo_root(session_configuration.cwd()).is_some() {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
@@ -809,7 +814,7 @@ impl Session {
|
||||
config.permissions.approval_policy.value(),
|
||||
config
|
||||
.permissions
|
||||
.legacy_sandbox_policy(session_configuration.cwd.as_path()),
|
||||
.legacy_sandbox_policy(session_configuration.cwd().as_path()),
|
||||
mcp_servers.keys().map(String::as_str).collect(),
|
||||
);
|
||||
|
||||
@@ -844,7 +849,7 @@ impl Session {
|
||||
ShellSnapshot::start_snapshotting(
|
||||
config.codex_home.clone(),
|
||||
thread_id,
|
||||
session_configuration.cwd.clone(),
|
||||
session_configuration.cwd().clone(),
|
||||
&mut default_shell,
|
||||
session_telemetry.clone(),
|
||||
state_db_ctx.clone(),
|
||||
@@ -1089,7 +1094,7 @@ impl Session {
|
||||
approvals_reviewer: session_configuration.approvals_reviewer,
|
||||
permission_profile: session_configuration.permission_profile(),
|
||||
active_permission_profile: session_configuration.active_permission_profile(),
|
||||
cwd: session_configuration.cwd.clone(),
|
||||
cwd: session_configuration.cwd().clone(),
|
||||
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
|
||||
initial_messages,
|
||||
network_proxy: session_network_proxy.filter(|_| {
|
||||
@@ -1135,7 +1140,7 @@ impl Session {
|
||||
}
|
||||
let turn_environment = crate::environment_selection::resolve_environment_selections(
|
||||
sess.services.environment_manager.as_ref(),
|
||||
&session_configuration.environments,
|
||||
session_configuration.environment_selections(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
CodexErr::InvalidRequest(err.to_string().replace(
|
||||
@@ -1152,7 +1157,7 @@ impl Session {
|
||||
),
|
||||
None => McpRuntimeContext::new(
|
||||
Arc::clone(&sess.services.environment_manager),
|
||||
session_configuration.cwd.to_path_buf(),
|
||||
session_configuration.cwd().to_path_buf(),
|
||||
),
|
||||
};
|
||||
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
|
||||
|
||||
+137
-127
@@ -22,6 +22,7 @@ use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -53,6 +54,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::NonSteerableTurnKind;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use tracing::Span;
|
||||
@@ -142,6 +144,7 @@ use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::local;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_path_buf;
|
||||
use core_test_support::tracing::install_test_tracing;
|
||||
@@ -2392,7 +2395,8 @@ async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow:
|
||||
);
|
||||
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
session_configuration.cwd = config.cwd.clone();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new());
|
||||
session_configuration.workspace_roots = config.workspace_roots.clone();
|
||||
session_configuration.permission_profile_state = session_permission_profile_state;
|
||||
|
||||
@@ -2404,8 +2408,8 @@ async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow:
|
||||
..Default::default()
|
||||
})?;
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2437,7 +2441,6 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
|
||||
initial
|
||||
.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "fork seed".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2483,7 +2486,6 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
|
||||
forked
|
||||
.thread
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "after fork".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -3157,11 +3159,10 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3265,11 +3266,10 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3526,6 +3526,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
|
||||
Arc::clone(&session.services.thread_store),
|
||||
CreateThreadParams {
|
||||
thread_id: session.thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Exec,
|
||||
@@ -3797,11 +3798,10 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3920,7 +3920,8 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
|
||||
let project_root = project_root.abs();
|
||||
let docs_dir = docs_dir.abs();
|
||||
|
||||
session_configuration.cwd = original_cwd.abs();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(original_cwd.abs(), Vec::new());
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
@@ -3954,7 +3955,7 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(project_root),
|
||||
environments: Some(TurnEnvironmentSelections::new(project_root, Vec::new())),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
@@ -3969,7 +3970,8 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
|
||||
async fn session_configuration_apply_permission_profile_preserves_existing_deny_read_entries() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let cwd = tempfile::tempdir().expect("create temp dir");
|
||||
session_configuration.cwd = cwd.path().abs();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(cwd.path().abs(), Vec::new());
|
||||
|
||||
let workspace_policy = SandboxPolicy::new_workspace_write_policy();
|
||||
let deny_entry = FileSystemSandboxEntry {
|
||||
@@ -3981,7 +3983,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
|
||||
let mut existing_file_system_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&workspace_policy,
|
||||
session_configuration.cwd.as_path(),
|
||||
session_configuration.cwd().as_path(),
|
||||
);
|
||||
existing_file_system_policy.glob_scan_max_depth = Some(2);
|
||||
existing_file_system_policy.entries.push(deny_entry.clone());
|
||||
@@ -3997,7 +3999,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
|
||||
|
||||
let requested_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&workspace_policy,
|
||||
session_configuration.cwd.as_path(),
|
||||
session_configuration.cwd().as_path(),
|
||||
);
|
||||
let permission_profile = codex_protocol::models::PermissionProfile::from_runtime_permissions(
|
||||
&requested_file_system_policy,
|
||||
@@ -4024,7 +4026,8 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
|
||||
async fn session_configuration_apply_permission_profile_accepts_direct_write_roots() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let cwd = tempfile::tempdir().expect("create cwd");
|
||||
session_configuration.cwd = cwd.path().abs();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(cwd.path().abs(), Vec::new());
|
||||
let external_write_dir = tempfile::tempdir().expect("create external write root");
|
||||
let external_write_path = AbsolutePathBuf::from_absolute_path(
|
||||
codex_utils_absolute_path::canonicalize_preserving_symlinks(external_write_dir.path())
|
||||
@@ -4100,8 +4103,8 @@ async fn session_configuration_apply_rebinds_symbolic_profile_to_updated_workspa
|
||||
.expect("permission profile update should succeed");
|
||||
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
|
||||
assert_eq!(
|
||||
updated.active_permission_profile(),
|
||||
Some(ActivePermissionProfile::new("dev"))
|
||||
@@ -4118,7 +4121,8 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
|
||||
let old_root = old_root.path().abs();
|
||||
let new_root = new_root.path().abs();
|
||||
let extra_root = extra_root.path().abs();
|
||||
session_configuration.cwd = old_root.clone();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(old_root.clone(), Vec::new());
|
||||
session_configuration.workspace_roots = vec![old_root.clone(), extra_root.clone()];
|
||||
|
||||
let file_system_sandbox_policy =
|
||||
@@ -4138,7 +4142,7 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(new_root.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(new_root.clone(), Vec::new())),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
@@ -4148,9 +4152,9 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
|
||||
vec![new_root.clone(), extra_root.clone()]
|
||||
);
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd().as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4353,8 +4357,9 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let original_cwd = workspace.path().join("repo-a").abs();
|
||||
let project_root = workspace.path().join("repo-b").abs();
|
||||
session_configuration.cwd = original_cwd.clone();
|
||||
session_configuration.workspace_roots = vec![session_configuration.cwd.clone()];
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new());
|
||||
session_configuration.workspace_roots = vec![session_configuration.cwd().clone()];
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
@@ -4363,7 +4368,7 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&sandbox_policy,
|
||||
&session_configuration.cwd,
|
||||
session_configuration.cwd(),
|
||||
);
|
||||
session_configuration
|
||||
.set_permission_profile_for_tests(
|
||||
@@ -4377,7 +4382,10 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(project_root.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
project_root.clone(),
|
||||
Vec::new(),
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
@@ -4386,13 +4394,13 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
|
||||
assert!(
|
||||
updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(project_root.as_path(), updated.cwd.as_path()),
|
||||
.can_write_path_with_cwd(project_root.as_path(), updated.cwd().as_path()),
|
||||
"cwd-only update should keep the new cwd writable"
|
||||
);
|
||||
assert!(
|
||||
!updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd.as_path()),
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd().as_path()),
|
||||
"cwd-only update should not keep the old implicit cwd writable"
|
||||
);
|
||||
}
|
||||
@@ -4408,7 +4416,8 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
|
||||
let original_cwd = original_cwd.abs();
|
||||
let next_cwd = next_cwd.abs();
|
||||
|
||||
session_configuration.cwd = original_cwd.clone();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new());
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
@@ -4435,7 +4444,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(next_cwd.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(next_cwd.clone(), Vec::new())),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
@@ -4447,13 +4456,13 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
|
||||
assert!(
|
||||
updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd.as_path()),
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd().as_path()),
|
||||
"absolute grant to the old cwd must remain writable"
|
||||
);
|
||||
assert!(
|
||||
!updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(next_cwd.as_path(), updated.cwd.as_path()),
|
||||
.can_write_path_with_cwd(next_cwd.as_path(), updated.cwd().as_path()),
|
||||
"cwd-only update must not reinterpret an absolute old-cwd grant as :workspace_roots"
|
||||
);
|
||||
}
|
||||
@@ -4463,11 +4472,21 @@ async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
#[allow(deprecated)]
|
||||
let updated_cwd = turn_context.cwd.join("project");
|
||||
let current_environments = {
|
||||
let state = session.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.environment_selections()
|
||||
.to_vec()
|
||||
};
|
||||
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
|
||||
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
cwd: Some(updated_cwd.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
updated_cwd.clone(),
|
||||
current_environments,
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
@@ -4475,7 +4494,7 @@ async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() {
|
||||
|
||||
let session_cwd = {
|
||||
let state = session.state.lock().await;
|
||||
state.session_configuration.cwd.clone()
|
||||
state.session_configuration.cwd().clone()
|
||||
};
|
||||
let config = session.get_config().await;
|
||||
let next_turn = session.new_default_turn().await;
|
||||
@@ -4495,55 +4514,65 @@ async fn relative_cwd_update_without_environments_resolves_under_session_cwd() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let original_cwd = {
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.environments = Vec::new();
|
||||
state.session_configuration.cwd.clone()
|
||||
state.session_configuration.environments.environments = Vec::new();
|
||||
state.session_configuration.cwd().clone()
|
||||
};
|
||||
let updated_cwd = original_cwd.join("project");
|
||||
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
|
||||
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
cwd: Some(updated_cwd.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
updated_cwd.clone(),
|
||||
Vec::new(),
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("cwd update should succeed");
|
||||
|
||||
let state = session.state.lock().await;
|
||||
assert_eq!(state.session_configuration.cwd, updated_cwd);
|
||||
assert!(state.session_configuration.environments.is_empty());
|
||||
assert_eq!(state.session_configuration.cwd(), &updated_cwd);
|
||||
assert!(
|
||||
state
|
||||
.session_configuration
|
||||
.environment_selections()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cwd_update_does_not_rewrite_sticky_environment_cwd() {
|
||||
async fn cwd_update_rewrites_sticky_environment_cwd() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let (original_cwd, environment_cwd) = {
|
||||
let (original_cwd, environment_cwd, environments) = {
|
||||
let mut state = session.state.lock().await;
|
||||
let original_cwd = state.session_configuration.cwd.clone();
|
||||
let original_cwd = state.session_configuration.cwd().clone();
|
||||
let environment_cwd = original_cwd.join("environment");
|
||||
state.session_configuration.environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: environment_cwd.clone(),
|
||||
}];
|
||||
(original_cwd, environment_cwd)
|
||||
let environments = vec![local(environment_cwd.clone())];
|
||||
state.session_configuration.environments.environments = environments.clone();
|
||||
(original_cwd, environment_cwd, environments)
|
||||
};
|
||||
let updated_cwd = original_cwd.join("project");
|
||||
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
|
||||
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
cwd: Some(updated_cwd.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
updated_cwd.clone(),
|
||||
environments,
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("cwd update should succeed");
|
||||
|
||||
let state = session.state.lock().await;
|
||||
assert_eq!(state.session_configuration.cwd, updated_cwd);
|
||||
assert_eq!(state.session_configuration.cwd(), &updated_cwd);
|
||||
assert_eq!(
|
||||
state.session_configuration.environments[0].cwd,
|
||||
environment_cwd
|
||||
state.session_configuration.environment_selections()[0].cwd,
|
||||
updated_cwd
|
||||
);
|
||||
assert_ne!(environment_cwd, updated_cwd);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4551,7 +4580,7 @@ async fn absolute_cwd_update_with_turn_environment_is_allowed() {
|
||||
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
|
||||
let absolute_cwd = {
|
||||
let state = session.state.lock().await;
|
||||
state.session_configuration.cwd.join("absolute-turn")
|
||||
state.session_configuration.cwd().join("absolute-turn")
|
||||
};
|
||||
std::fs::create_dir_all(absolute_cwd.as_path()).expect("create absolute turn dir");
|
||||
|
||||
@@ -4559,11 +4588,10 @@ async fn absolute_cwd_update_with_turn_environment_is_allowed() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
cwd: Some(absolute_cwd.clone()),
|
||||
environments: Some(vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: absolute_cwd.clone(),
|
||||
}]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
absolute_cwd.clone(),
|
||||
vec![local(absolute_cwd.clone())],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -4622,11 +4650,10 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -4712,10 +4739,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let default_environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}];
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
@@ -4733,11 +4757,10 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -4751,7 +4774,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
user_shell_override: None,
|
||||
};
|
||||
let per_turn_config =
|
||||
Session::build_per_turn_config(&session_configuration, session_configuration.cwd.clone());
|
||||
Session::build_per_turn_config(&session_configuration, session_configuration.cwd().clone());
|
||||
let model_info = construct_model_info_offline_for_tests(
|
||||
session_configuration.collaboration_mode.model(),
|
||||
&per_turn_config.to_models_manager_config(),
|
||||
@@ -4863,7 +4886,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
.skills_for_config(&skills_input, Some(Arc::clone(&skill_fs)))
|
||||
.await,
|
||||
);
|
||||
let turn_environments = turn_environments_for_tests(&environment, &session_configuration.cwd);
|
||||
let turn_environments = turn_environments_for_tests(&environment, session_configuration.cwd());
|
||||
let turn_context = Session::make_turn_context(
|
||||
thread_id,
|
||||
SessionId::from(thread_id),
|
||||
@@ -4880,7 +4903,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
&models_manager,
|
||||
/*network*/ None,
|
||||
turn_environments,
|
||||
session_configuration.cwd.clone(),
|
||||
session_configuration.cwd().clone(),
|
||||
"turn_id".to_string(),
|
||||
skills_outcome,
|
||||
);
|
||||
@@ -4948,10 +4971,7 @@ async fn make_session_with_config_and_rx(
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let default_environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}];
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
@@ -4969,11 +4989,10 @@ async fn make_session_with_config_and_rx(
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -5053,10 +5072,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let default_environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}];
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
@@ -5074,11 +5090,10 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -5937,7 +5952,6 @@ fn submission_dispatch_span_uses_debug_for_realtime_audio() {
|
||||
fn op_kind_for_input_and_context_ops() {
|
||||
assert_eq!(
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
@@ -5969,12 +5983,11 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(config.cwd.clone()),
|
||||
environments: Some(local_selections(config.cwd.clone())),
|
||||
approval_policy: Some(config.permissions.approval_policy.value()),
|
||||
approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::AutoReview),
|
||||
sandbox_policy: Some(config.legacy_sandbox_policy()),
|
||||
@@ -6013,10 +6026,10 @@ async fn turn_environments_set_primary_environment() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
environments: Some(vec![TurnEnvironmentSelection {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: selected_cwd.clone(),
|
||||
}]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
selected_cwd.clone(),
|
||||
vec![local(selected_cwd.clone())],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -6041,7 +6054,7 @@ async fn turn_environments_set_primary_environment() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
|
||||
async fn default_turn_does_not_overlay_legacy_fallback_cwd_onto_stored_thread_environments() {
|
||||
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
|
||||
let session_cwd = session.get_config().await.cwd.clone();
|
||||
let selected_cwd =
|
||||
@@ -6049,10 +6062,7 @@ async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
|
||||
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: selected_cwd.clone(),
|
||||
}];
|
||||
state.session_configuration.environments.environments = vec![local(selected_cwd.clone())];
|
||||
}
|
||||
|
||||
let turn_context = session.new_default_turn().await;
|
||||
@@ -6069,8 +6079,8 @@ async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
|
||||
));
|
||||
#[allow(deprecated)]
|
||||
let turn_cwd = turn_context.cwd.clone();
|
||||
assert_eq!(turn_cwd, session_cwd);
|
||||
assert_eq!(turn_context.config.cwd, session_cwd);
|
||||
assert_eq!(turn_cwd, selected_cwd);
|
||||
assert_eq!(turn_context.config.cwd, selected_cwd);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -6080,7 +6090,7 @@ async fn default_turn_honors_empty_stored_thread_environments() {
|
||||
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.environments = Vec::new();
|
||||
state.session_configuration.environments.environments = Vec::new();
|
||||
}
|
||||
|
||||
let turn_context = session.new_default_turn().await;
|
||||
@@ -6143,7 +6153,10 @@ async fn empty_turn_environments_clear_primary_environment() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
environments: Some(vec![]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
session.get_config().await.cwd.clone(),
|
||||
vec![],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -6170,10 +6183,13 @@ async fn unknown_turn_environment_returns_error() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
environments: Some(vec![TurnEnvironmentSelection {
|
||||
environment_id: "missing".to_string(),
|
||||
cwd: original_configuration.cwd.clone(),
|
||||
}]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
original_configuration.cwd().clone(),
|
||||
vec![TurnEnvironmentSelection {
|
||||
environment_id: "missing".to_string(),
|
||||
cwd: original_configuration.cwd().clone(),
|
||||
}],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -6186,10 +6202,10 @@ async fn unknown_turn_environment_returns_error() {
|
||||
};
|
||||
assert!(matches!(err, CodexErr::InvalidRequest(_)));
|
||||
assert!(err.to_string().contains("missing"));
|
||||
assert_eq!(current_configuration.cwd, original_configuration.cwd);
|
||||
assert_eq!(current_configuration.cwd(), original_configuration.cwd());
|
||||
assert_eq!(
|
||||
current_configuration.environments,
|
||||
original_configuration.environments
|
||||
current_configuration.environment_selections(),
|
||||
original_configuration.environment_selections()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6205,16 +6221,13 @@ async fn duplicate_turn_environment_returns_error_without_mutating_session() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
environments: Some(vec![
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: original_configuration.cwd.clone(),
|
||||
},
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: original_configuration.cwd.join("second"),
|
||||
},
|
||||
]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
original_configuration.cwd().clone(),
|
||||
vec![
|
||||
local(original_configuration.cwd().clone()),
|
||||
local(original_configuration.cwd().join("second")),
|
||||
],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -6227,10 +6240,10 @@ async fn duplicate_turn_environment_returns_error_without_mutating_session() {
|
||||
};
|
||||
assert!(matches!(err, CodexErr::InvalidRequest(_)));
|
||||
assert!(err.to_string().contains("duplicate"));
|
||||
assert_eq!(current_configuration.cwd, original_configuration.cwd);
|
||||
assert_eq!(current_configuration.cwd(), original_configuration.cwd());
|
||||
assert_eq!(
|
||||
current_configuration.environments,
|
||||
original_configuration.environments
|
||||
current_configuration.environment_selections(),
|
||||
original_configuration.environment_selections()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6348,6 +6361,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
|
||||
Arc::clone(&thread_store),
|
||||
CreateThreadParams {
|
||||
thread_id: session.thread_id,
|
||||
extra_config: None,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
source: SessionSource::Exec,
|
||||
@@ -6789,10 +6803,7 @@ where
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let default_environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}];
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
@@ -6810,11 +6821,10 @@ where
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -6828,7 +6838,7 @@ where
|
||||
user_shell_override: None,
|
||||
};
|
||||
let per_turn_config =
|
||||
Session::build_per_turn_config(&session_configuration, session_configuration.cwd.clone());
|
||||
Session::build_per_turn_config(&session_configuration, session_configuration.cwd().clone());
|
||||
let model_info = construct_model_info_offline_for_tests(
|
||||
session_configuration.collaboration_mode.model(),
|
||||
&per_turn_config.to_models_manager_config(),
|
||||
@@ -6940,7 +6950,7 @@ where
|
||||
.skills_for_config(&skills_input, Some(Arc::clone(&skill_fs)))
|
||||
.await,
|
||||
);
|
||||
let turn_environments = turn_environments_for_tests(&environment, &session_configuration.cwd);
|
||||
let turn_environments = turn_environments_for_tests(&environment, session_configuration.cwd());
|
||||
let turn_context = Arc::new(Session::make_turn_context(
|
||||
thread_id,
|
||||
SessionId::from(thread_id),
|
||||
@@ -6957,7 +6967,7 @@ where
|
||||
&models_manager,
|
||||
/*network*/ None,
|
||||
turn_environments,
|
||||
session_configuration.cwd.clone(),
|
||||
session_configuration.cwd().clone(),
|
||||
"turn_id".to_string(),
|
||||
skills_outcome,
|
||||
));
|
||||
|
||||
@@ -1431,7 +1431,8 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
|
||||
| EventMsg::CollabCloseBegin(_)
|
||||
| EventMsg::CollabCloseEnd(_)
|
||||
| EventMsg::CollabResumeBegin(_)
|
||||
| EventMsg::CollabResumeEnd(_) => None,
|
||||
| EventMsg::CollabResumeEnd(_)
|
||||
| EventMsg::SubAgentActivity(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -441,7 +441,7 @@ impl Session {
|
||||
session_configuration: &SessionConfiguration,
|
||||
) -> Config {
|
||||
let mut config =
|
||||
Self::build_per_turn_config(session_configuration, session_configuration.cwd.clone());
|
||||
Self::build_per_turn_config(session_configuration, session_configuration.cwd().clone());
|
||||
config.model = Some(session_configuration.collaboration_mode.model().to_string());
|
||||
config.permissions.approval_policy = session_configuration.approval_policy.clone();
|
||||
config.workspace_roots = session_configuration.workspace_roots.clone();
|
||||
@@ -587,19 +587,9 @@ impl Session {
|
||||
let mut state = self.state.lock().await;
|
||||
match state.session_configuration.clone().apply(&updates) {
|
||||
Ok(next) => {
|
||||
let mut effective_environments = updates
|
||||
.environments
|
||||
.clone()
|
||||
.unwrap_or_else(|| next.environments.clone());
|
||||
if updates.environments.is_none() {
|
||||
Self::overlay_runtime_cwd_on_primary_environment(
|
||||
&mut effective_environments,
|
||||
&next.cwd,
|
||||
);
|
||||
}
|
||||
let turn_environments =
|
||||
self.resolve_turn_environments(&effective_environments)?;
|
||||
let previous_cwd = state.session_configuration.cwd.clone();
|
||||
self.resolve_turn_environments(next.environment_selections())?;
|
||||
let previous_cwd = state.session_configuration.cwd().clone();
|
||||
let previous_permission_profile =
|
||||
state.session_configuration.permission_profile();
|
||||
let next_permission_profile = next.permission_profile();
|
||||
@@ -656,7 +646,7 @@ impl Session {
|
||||
self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref());
|
||||
self.maybe_refresh_shell_snapshot_for_cwd(
|
||||
&previous_cwd,
|
||||
&session_configuration.cwd,
|
||||
session_configuration.cwd(),
|
||||
&codex_home,
|
||||
&session_source,
|
||||
);
|
||||
@@ -731,7 +721,7 @@ impl Session {
|
||||
let cwd = primary_turn_environment
|
||||
.as_ref()
|
||||
.map(|turn_environment| turn_environment.cwd.clone())
|
||||
.unwrap_or_else(|| session_configuration.cwd.clone());
|
||||
.unwrap_or_else(|| session_configuration.cwd().clone());
|
||||
let per_turn_config = Self::build_per_turn_config(&session_configuration, cwd.clone());
|
||||
{
|
||||
let mcp_connection_manager = self.services.mcp_connection_manager.read().await;
|
||||
@@ -871,29 +861,14 @@ impl Session {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.clone()
|
||||
};
|
||||
let mut effective_environments = session_configuration.environments.clone();
|
||||
Self::overlay_runtime_cwd_on_primary_environment(
|
||||
&mut effective_environments,
|
||||
&session_configuration.cwd,
|
||||
);
|
||||
let turn_environments = match self.resolve_turn_environments(&effective_environments) {
|
||||
Ok(turn_environments) => turn_environments,
|
||||
Err(err) => {
|
||||
warn!("failed to resolve stored session environments: {err}");
|
||||
ResolvedTurnEnvironments::default()
|
||||
}
|
||||
};
|
||||
let turn_environments =
|
||||
match self.resolve_turn_environments(session_configuration.environment_selections()) {
|
||||
Ok(turn_environments) => turn_environments,
|
||||
Err(err) => {
|
||||
warn!("failed to resolve stored session environments: {err}");
|
||||
ResolvedTurnEnvironments::default()
|
||||
}
|
||||
};
|
||||
(session_configuration, turn_environments)
|
||||
}
|
||||
|
||||
fn overlay_runtime_cwd_on_primary_environment(
|
||||
environments: &mut [TurnEnvironmentSelection],
|
||||
runtime_cwd: &AbsolutePathBuf,
|
||||
) {
|
||||
if let Some(turn_environment) = environments.first_mut()
|
||||
&& turn_environment.cwd != *runtime_cwd
|
||||
{
|
||||
turn_environment.cwd = runtime_cwd.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::responses::mount_models_once;
|
||||
@@ -343,99 +342,6 @@ async fn start_thread_rejects_explicit_local_environment_when_default_provider_i
|
||||
assert!(manager.list_thread_ids().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_thread_uses_all_default_environments_from_codex_home() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
std::fs::write(
|
||||
config.codex_home.join("environments.toml"),
|
||||
r#"
|
||||
default = "dev"
|
||||
|
||||
[[environments]]
|
||||
id = "dev"
|
||||
program = "ssh"
|
||||
args = ["dev", "cd /tmp && true"]
|
||||
"#,
|
||||
)
|
||||
.expect("write environments.toml");
|
||||
|
||||
let runtime_paths = codex_exec_server::ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe path"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths");
|
||||
let environment_manager = Arc::new(
|
||||
codex_exec_server::EnvironmentManager::from_codex_home(
|
||||
config.codex_home.clone(),
|
||||
Some(runtime_paths),
|
||||
)
|
||||
.await
|
||||
.expect("environment manager"),
|
||||
);
|
||||
assert_eq!(
|
||||
environment_manager.default_environment_ids(),
|
||||
vec!["dev".to_string(), "local".to_string()]
|
||||
);
|
||||
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
environment_manager,
|
||||
);
|
||||
|
||||
let thread = manager
|
||||
.start_thread(config)
|
||||
.await
|
||||
.expect("thread should start");
|
||||
|
||||
let prompt_items = crate::prompt_debug::build_prompt_input_from_session(
|
||||
thread.thread.codex.session.as_ref(),
|
||||
Vec::<UserInput>::new(),
|
||||
)
|
||||
.await
|
||||
.expect("prompt input");
|
||||
let environment_context = prompt_items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ResponseItem::Message { content, .. } => Some(content),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.find_map(|content| match content {
|
||||
ContentItem::InputText { text } if text.contains("<environment_context>") => {
|
||||
Some(text.as_str())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("environment context prompt item");
|
||||
assert!(environment_context.contains("<environments>"));
|
||||
let cwd = thread.session_configured.cwd.display().to_string();
|
||||
let dev_entry = format!(
|
||||
r#"<environment id="dev">
|
||||
<cwd>{cwd}</cwd>
|
||||
<shell>"#
|
||||
);
|
||||
let local_entry = format!(
|
||||
r#"<environment id="local">
|
||||
<cwd>{cwd}</cwd>
|
||||
<shell>"#
|
||||
);
|
||||
let dev_position = environment_context
|
||||
.find(&dev_entry)
|
||||
.expect("dev environment entry");
|
||||
let local_position = environment_context
|
||||
.find(&local_entry)
|
||||
.expect("local environment entry");
|
||||
assert!(dev_position < local_position);
|
||||
assert!(!environment_context.contains("\n <cwd>"));
|
||||
assert!(!environment_context.contains("\n <shell>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
|
||||
@@ -741,6 +741,7 @@ fn spawn_agent_tool_description_v2(
|
||||
You are then able to refer to this agent as `task_3` or `/root/task1/task_3` interchangeably. However an agent `/root/task2/task_3` would only be able to communicate with this agent via its canonical name `/root/task1/task_3`.
|
||||
The spawned agent will have the same tools as you and the ability to spawn its own subagents.
|
||||
{inherited_model_guidance}
|
||||
Only call this tool for a concrete, bounded subtask that can run independently alongside useful local work; otherwise continue locally.
|
||||
It will be able to send you and other running agents messages, and its final answer will be provided to you when it finishes.
|
||||
The new agent's canonical task name will be provided to it along with the message.
|
||||
{concurrency_guidance}"#
|
||||
|
||||
@@ -2676,7 +2676,6 @@ async fn send_input_accepts_structured_items() {
|
||||
.expect("send_input should succeed");
|
||||
|
||||
let expected = Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![
|
||||
UserInput::Mention {
|
||||
name: "drive".to_string(),
|
||||
|
||||
@@ -14,15 +14,11 @@ use crate::tools::registry::ToolExecutor;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::CollabAgentInteractionBeginEvent;
|
||||
use codex_protocol::protocol::CollabAgentInteractionEndEvent;
|
||||
use codex_protocol::protocol::CollabAgentSpawnBeginEvent;
|
||||
use codex_protocol::protocol::CollabAgentSpawnEndEvent;
|
||||
use codex_protocol::protocol::CollabCloseBeginEvent;
|
||||
use codex_protocol::protocol::CollabCloseEndEvent;
|
||||
use codex_protocol::protocol::CollabWaitingBeginEvent;
|
||||
use codex_protocol::protocol::CollabWaitingEndEvent;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::SubAgentActivityEvent;
|
||||
use codex_protocol::protocol::SubAgentActivityKind;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_tools::ToolName;
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -59,18 +59,9 @@ async fn handle_interrupt_agent(
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabCloseBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
sender_thread_id: session.thread_id,
|
||||
receiver_thread_id: agent_id,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
let receiver_agent_path = receiver_agent.agent_path.clone().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel("target agent is missing an agent_path".to_string())
|
||||
})?;
|
||||
let status = session.services.agent_control.get_status(agent_id).await;
|
||||
let result = match session
|
||||
.services
|
||||
@@ -81,22 +72,20 @@ async fn handle_interrupt_agent(
|
||||
Ok(_) | Err(CodexErr::ThreadNotFound(_)) | Err(CodexErr::InternalAgentDied) => Ok(()),
|
||||
Err(err) => Err(collab_agent_error(agent_id, err)),
|
||||
};
|
||||
result?;
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabCloseEndEvent {
|
||||
call_id,
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
sender_thread_id: session.thread_id,
|
||||
receiver_thread_id: agent_id,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname,
|
||||
receiver_agent_role: receiver_agent.agent_role,
|
||||
status: status.clone(),
|
||||
SubAgentActivityEvent {
|
||||
event_id: call_id,
|
||||
occurred_at_ms: now_unix_timestamp_ms(),
|
||||
agent_thread_id: agent_id,
|
||||
agent_path: receiver_agent_path,
|
||||
kind: SubAgentActivityKind::Interrupted,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
result?;
|
||||
|
||||
Ok(InterruptAgentResult {
|
||||
previous_status: status,
|
||||
|
||||
@@ -69,7 +69,6 @@ pub(crate) async fn handle_message_string_tool(
|
||||
call_id,
|
||||
..
|
||||
} = invocation;
|
||||
let prompt = String::new();
|
||||
let receiver_thread_id = resolve_agent_target(&session, &turn, &target).await?;
|
||||
let receiver_agent = session
|
||||
.services
|
||||
@@ -96,52 +95,32 @@ pub(crate) async fn handle_message_string_tool(
|
||||
.ensure_v2_agent_loaded(resume_config, receiver_thread_id)
|
||||
.await
|
||||
.map_err(|err| collab_agent_error(receiver_thread_id, err))?;
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabAgentInteractionBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
sender_thread_id: session.thread_id,
|
||||
receiver_thread_id,
|
||||
prompt: prompt.clone(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
let author = turn
|
||||
.session_source
|
||||
.get_agent_path()
|
||||
.unwrap_or_else(AgentPath::root);
|
||||
let communication = communication_from_tool_message(author, receiver_agent_path, message);
|
||||
let communication =
|
||||
communication_from_tool_message(author, receiver_agent_path.clone(), message);
|
||||
let result = session
|
||||
.services
|
||||
.agent_control
|
||||
.send_inter_agent_communication(receiver_thread_id, mode.apply(communication))
|
||||
.await
|
||||
.map_err(|err| collab_agent_error(receiver_thread_id, err));
|
||||
let status = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_status(receiver_thread_id)
|
||||
.await;
|
||||
result?;
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabAgentInteractionEndEvent {
|
||||
call_id,
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
sender_thread_id: session.thread_id,
|
||||
receiver_thread_id,
|
||||
receiver_agent_nickname: receiver_agent.agent_nickname,
|
||||
receiver_agent_role: receiver_agent.agent_role,
|
||||
prompt,
|
||||
status,
|
||||
SubAgentActivityEvent {
|
||||
event_id: call_id,
|
||||
occurred_at_ms: now_unix_timestamp_ms(),
|
||||
agent_thread_id: receiver_thread_id,
|
||||
agent_path: receiver_agent_path,
|
||||
kind: SubAgentActivityKind::Interacted,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
result?;
|
||||
|
||||
Ok(FunctionToolOutput::from_text(String::new(), Some(true)))
|
||||
}
|
||||
|
||||
@@ -61,24 +61,8 @@ async fn handle_spawn_agent(
|
||||
|
||||
let message = args.message.clone();
|
||||
let initial_operation = parse_collab_input(Some(args.message), /*items*/ None)?;
|
||||
let prompt = String::new();
|
||||
|
||||
let session_source = turn.session_source.clone();
|
||||
let child_depth = next_thread_spawn_depth(&session_source);
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabAgentSpawnBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
sender_thread_id: session.thread_id,
|
||||
prompt: prompt.clone(),
|
||||
model: args.model.clone().unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.clone().unwrap_or_default(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
let mut config =
|
||||
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
|
||||
if let Some(service_tier) = args.service_tier.as_ref() {
|
||||
@@ -119,11 +103,16 @@ async fn handle_spawn_agent(
|
||||
role_name,
|
||||
Some(args.task_name.clone()),
|
||||
)?;
|
||||
let result = Box::pin(
|
||||
let new_agent_path = spawn_source.get_agent_path().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"spawned agent is missing a canonical task name".to_string(),
|
||||
)
|
||||
})?;
|
||||
let spawned_agent = Box::pin(
|
||||
session.services.agent_control.spawn_agent_with_metadata(
|
||||
config,
|
||||
match (spawn_source.get_agent_path(), initial_operation) {
|
||||
(Some(recipient), Op::UserInput { items, .. })
|
||||
match initial_operation {
|
||||
Op::UserInput { items, .. }
|
||||
if items
|
||||
.iter()
|
||||
.all(|item| matches!(item, UserInput::Text { .. })) =>
|
||||
@@ -132,10 +121,11 @@ async fn handle_spawn_agent(
|
||||
.session_source
|
||||
.get_agent_path()
|
||||
.unwrap_or_else(AgentPath::root);
|
||||
let communication = communication_from_tool_message(author, recipient, message);
|
||||
let communication =
|
||||
communication_from_tool_message(author, new_agent_path.clone(), message);
|
||||
Op::InterAgentCommunication { communication }
|
||||
}
|
||||
(_, initial_operation) => initial_operation,
|
||||
initial_operation => initial_operation,
|
||||
},
|
||||
Some(spawn_source),
|
||||
SpawnAgentOptions {
|
||||
@@ -147,78 +137,37 @@ async fn handle_spawn_agent(
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(collab_spawn_error);
|
||||
let (new_thread_id, new_agent_metadata, status) = match &result {
|
||||
Ok(spawned_agent) => (
|
||||
Some(spawned_agent.thread_id),
|
||||
Some(spawned_agent.metadata.clone()),
|
||||
spawned_agent.status.clone(),
|
||||
),
|
||||
Err(_) => (None, None, AgentStatus::NotFound),
|
||||
};
|
||||
let agent_snapshot = match new_thread_id {
|
||||
Some(thread_id) => {
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_config_snapshot(thread_id)
|
||||
.await
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let (new_agent_path, new_agent_nickname, new_agent_role) =
|
||||
match (&agent_snapshot, new_agent_metadata) {
|
||||
(Some(snapshot), _) => (
|
||||
snapshot.session_source.get_agent_path().map(String::from),
|
||||
snapshot.session_source.get_nickname(),
|
||||
snapshot.session_source.get_agent_role(),
|
||||
),
|
||||
(None, Some(metadata)) => (
|
||||
metadata.agent_path.map(String::from),
|
||||
metadata.agent_nickname,
|
||||
metadata.agent_role,
|
||||
),
|
||||
(None, None) => (None, None, None),
|
||||
};
|
||||
let effective_model = agent_snapshot
|
||||
.map_err(collab_spawn_error)?;
|
||||
let new_thread_id = spawned_agent.thread_id;
|
||||
let agent_snapshot = session
|
||||
.services
|
||||
.agent_control
|
||||
.get_agent_config_snapshot(new_thread_id)
|
||||
.await;
|
||||
let nickname = agent_snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| snapshot.model.clone())
|
||||
.unwrap_or_else(|| args.model.clone().unwrap_or_default());
|
||||
let effective_reasoning_effort = agent_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.reasoning_effort.clone())
|
||||
.unwrap_or(args.reasoning_effort.unwrap_or_default());
|
||||
let nickname = new_agent_nickname.clone();
|
||||
.and_then(|snapshot| snapshot.session_source.get_nickname())
|
||||
.or(spawned_agent.metadata.agent_nickname);
|
||||
session
|
||||
.send_event(
|
||||
&turn,
|
||||
CollabAgentSpawnEndEvent {
|
||||
call_id,
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
sender_thread_id: session.thread_id,
|
||||
new_thread_id,
|
||||
new_agent_nickname,
|
||||
new_agent_role,
|
||||
prompt,
|
||||
model: effective_model,
|
||||
reasoning_effort: effective_reasoning_effort,
|
||||
status,
|
||||
SubAgentActivityEvent {
|
||||
event_id: call_id,
|
||||
occurred_at_ms: now_unix_timestamp_ms(),
|
||||
agent_thread_id: new_thread_id,
|
||||
agent_path: new_agent_path.clone(),
|
||||
kind: SubAgentActivityKind::Started,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
let _ = result?;
|
||||
let role_tag = role_name.unwrap_or(DEFAULT_ROLE_NAME);
|
||||
turn.session_telemetry.counter(
|
||||
"codex.multi_agent.spawn",
|
||||
/*inc*/ 1,
|
||||
&[("role", role_tag)],
|
||||
);
|
||||
let task_name = new_agent_path.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"spawned agent is missing a canonical task name".to_string(),
|
||||
)
|
||||
})?;
|
||||
let task_name = String::from(new_agent_path);
|
||||
|
||||
let hide_agent_metadata = turn.config.multi_agent_v2.hide_spawn_agent_metadata;
|
||||
if hide_agent_metadata {
|
||||
|
||||
@@ -40,6 +40,7 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use futures::future::BoxFuture;
|
||||
@@ -70,6 +71,17 @@ const REMOTE_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_TEST_REMOTE_EXEC_SERVER_URL"
|
||||
static REMOTE_TEST_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
const SUBMIT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub fn local(cwd: AbsolutePathBuf) -> TurnEnvironmentSelection {
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_selections(cwd: AbsolutePathBuf) -> TurnEnvironmentSelections {
|
||||
TurnEnvironmentSelections::new(cwd.clone(), vec![local(cwd)])
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestEnv {
|
||||
environment: codex_exec_server::Environment,
|
||||
@@ -789,18 +801,20 @@ impl TestCodex {
|
||||
let (sandbox_policy, permission_profile) =
|
||||
turn_permission_fields(permission_profile, self.config.cwd.as_path());
|
||||
let session_model = self.session_configured.model.clone();
|
||||
let turn_environment_selections = environments.map(|environments| {
|
||||
TurnEnvironmentSelections::new(self.config.cwd.clone(), environments)
|
||||
});
|
||||
self.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: prompt.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(self.config.cwd.clone()),
|
||||
environments: turn_environment_selections,
|
||||
approval_policy: Some(approval_policy),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
permission_profile,
|
||||
|
||||
@@ -46,7 +46,6 @@ async fn interrupt_long_running_tool_emits_turn_aborted() {
|
||||
// Kick off a turn that triggers the function call.
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "start sleep".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -104,7 +103,6 @@ async fn interrupt_tool_records_history_entries() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "start history recording".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -126,7 +124,6 @@ async fn interrupt_tool_records_history_entries() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "follow up".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -210,7 +207,6 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "start interrupt marker".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -232,7 +228,6 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "follow up".into(),
|
||||
text_elements: Vec::new(),
|
||||
|
||||
@@ -37,7 +37,6 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "inspect the active tab".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -136,7 +135,6 @@ async fn external_context_like_user_text_remains_a_user_message_item() -> Result
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![user_input.clone()],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
@@ -182,7 +180,6 @@ async fn additional_context_trust_controls_message_role() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "inspect context".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -263,7 +260,6 @@ async fn additional_context_is_deduplicated_between_turns_while_retained() -> Re
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "first turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -281,7 +277,6 @@ async fn additional_context_is_deduplicated_between_turns_while_retained() -> Re
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "second turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -343,7 +338,6 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "first turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -376,7 +370,6 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "second turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -409,7 +402,6 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "third turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -506,7 +498,6 @@ async fn additional_context_values_are_truncated_before_model_input() -> Result<
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "summarize context".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
|
||||
@@ -7,6 +7,7 @@ use core_test_support::responses::ev_apply_patch_custom_tool_call;
|
||||
use core_test_support::responses::ev_apply_patch_shell_command_call_via_heredoc;
|
||||
use core_test_support::responses::ev_shell_command_call;
|
||||
use core_test_support::test_codex::ApplyPatchModelOutput;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -52,6 +53,7 @@ use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_remote;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::TestCodexHarness;
|
||||
use core_test_support::test_codex::local;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::wait_for_event;
|
||||
@@ -100,12 +102,11 @@ async fn submit_without_wait_with_turn_permissions(
|
||||
text: prompt.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(harness.cwd_abs()),
|
||||
environments: Some(local_selections(harness.cwd_abs())),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
permission_profile,
|
||||
@@ -1631,27 +1632,27 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
|
||||
|
||||
let (sandbox_policy, permission_profile) =
|
||||
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
|
||||
let environments = vec![
|
||||
local(shared_cwd.clone()),
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: shared_cwd.clone(),
|
||||
},
|
||||
];
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "apply matching patches to local and remote environments".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: Some(vec![
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: shared_cwd.clone(),
|
||||
},
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: shared_cwd.clone(),
|
||||
},
|
||||
]),
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.clone()),
|
||||
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
|
||||
test.config.cwd.clone(),
|
||||
environments,
|
||||
)),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
permission_profile,
|
||||
|
||||
@@ -39,6 +39,7 @@ use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::wait_for_event;
|
||||
@@ -658,12 +659,11 @@ async fn submit_turn(
|
||||
text: prompt.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.clone()),
|
||||
environments: Some(local_selections(test.config.cwd.clone())),
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
@@ -2619,12 +2619,11 @@ async fn env_zsh_script_spawned_by_python_can_request_escalation_under_zsh_fork(
|
||||
text: "run nested env zsh script through python".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.clone()),
|
||||
environments: Some(local_selections(test.config.cwd.clone())),
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
@@ -2764,12 +2763,11 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
|
||||
text: "run allowed touch under zsh fork".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.clone()),
|
||||
environments: Some(local_selections(test.config.cwd.clone())),
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
@@ -3356,12 +3354,11 @@ allow_local_binding = true
|
||||
text: "deny-read network retry".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(test.config.cwd.clone()),
|
||||
environments: Some(local_selections(test.config.cwd.clone())),
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
sandbox_policy: Some(turn_sandbox_policy),
|
||||
|
||||
@@ -34,6 +34,7 @@ use core_test_support::responses::sse;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_sandbox;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::wait_for_event;
|
||||
@@ -160,12 +161,11 @@ async fn remote_model_override_uses_catalog_model_for_strict_auto_review() -> Re
|
||||
text: "run the Guardian model override check".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(cwd_path),
|
||||
environments: Some(local_selections(cwd_path)),
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
permission_profile,
|
||||
|
||||
@@ -66,6 +66,7 @@ use core_test_support::responses::sse;
|
||||
use core_test_support::responses::sse_failed;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use dunce::canonicalize as normalize_path;
|
||||
@@ -386,7 +387,6 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
|
||||
// 2) Submit new input; the request body must include the prior items, then initial context, then new user input.
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -755,7 +755,6 @@ async fn includes_session_id_thread_id_and_model_headers_in_request() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -968,7 +967,6 @@ async fn includes_base_instructions_override_in_request() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1026,7 +1024,6 @@ async fn chatgpt_auth_sends_correct_request() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1151,7 +1148,6 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1191,7 +1187,6 @@ async fn includes_user_instructions_message_in_request() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1280,7 +1275,6 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1344,7 +1338,6 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1404,7 +1397,6 @@ async fn omits_apps_guidance_when_configured_off() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1447,7 +1439,6 @@ async fn omits_environment_context_when_configured_off() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1505,7 +1496,6 @@ async fn skills_append_to_developer_message() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1589,7 +1579,6 @@ async fn skills_use_aliases_in_developer_message_under_budget_pressure() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1651,7 +1640,6 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1694,7 +1682,6 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1738,7 +1725,6 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1794,12 +1780,11 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(config.cwd.clone()),
|
||||
environments: Some(local_selections(config.cwd.clone())),
|
||||
approval_policy: Some(config.permissions.approval_policy.value()),
|
||||
sandbox_policy: Some(config.legacy_sandbox_policy()),
|
||||
summary: Some(
|
||||
@@ -1847,7 +1832,6 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1904,7 +1888,6 @@ async fn responses_lite_sets_all_turns_context_and_disables_parallel_tool_calls(
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1972,12 +1955,11 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default()
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(config.cwd.clone()),
|
||||
environments: Some(local_selections(config.cwd.clone())),
|
||||
approval_policy: Some(config.permissions.approval_policy.value()),
|
||||
sandbox_policy: Some(config.legacy_sandbox_policy()),
|
||||
summary: Some(ReasoningSummary::Concise),
|
||||
@@ -2029,7 +2011,6 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2089,7 +2070,6 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2129,7 +2109,6 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2178,7 +2157,6 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2226,7 +2204,6 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2279,7 +2256,6 @@ async fn includes_developer_instructions_message_in_request() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2577,7 +2553,6 @@ async fn token_count_includes_rate_limits_snapshot() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2719,7 +2694,6 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
|
||||
|
||||
let submission_id = codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2797,7 +2771,6 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "seed turn".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2813,7 +2786,6 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "trigger context window".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2899,7 +2871,6 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res
|
||||
.await?;
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "trigger incomplete".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -3011,7 +2982,6 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -3101,7 +3071,6 @@ async fn env_var_overrides_loaded_auth() {
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -3159,7 +3128,6 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
// Turn 1: user sends U1; wait for completion.
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "U1".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -3176,7 +3144,6 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
// Turn 2: user sends U2; wait for completion.
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "U2".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -3193,7 +3160,6 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
// Turn 3: user sends U3; wait for completion.
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "U3".into(),
|
||||
text_elements: Vec::new(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user