mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge remote-tracking branch 'origin/main' into guinness/realtime-terminal-output
# Conflicts: # codex-rs/core/src/session/turn.rs
This commit is contained in:
@@ -28,7 +28,7 @@ Accept any of the following:
|
||||
3. Inspect the `actions` list in the JSON response.
|
||||
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.
|
||||
6. If `process_review_comment` is present, inspect surfaced published review items and decide whether to address them.
|
||||
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`.
|
||||
@@ -92,9 +92,13 @@ The watcher surfaces review items from:
|
||||
- Inline review comments
|
||||
- Review submissions (COMMENT / APPROVED / CHANGES_REQUESTED)
|
||||
|
||||
Only act on published feedback. Ignore review submissions in GitHub's `PENDING` state and inline
|
||||
comments attached to those pending reviews. Do not mark pending review feedback as seen; it should
|
||||
be eligible to surface after the reviewer submits the review.
|
||||
|
||||
It intentionally surfaces Codex reviewer bot feedback (for example comments/reviews from `chatgpt-codex-connector[bot]`) in addition to human reviewer feedback. Most unrelated bot noise should still be ignored.
|
||||
For safety, the watcher only auto-surfaces trusted human review authors (for example repo OWNER/MEMBER/COLLABORATOR, plus the authenticated operator) and approved review bots such as Codex.
|
||||
On a fresh watcher state file, existing pending review feedback may be surfaced immediately (not only comments that arrive after monitoring starts). This is intentional so already-open review comments are not missed.
|
||||
On a fresh watcher state file, existing unaddressed published review feedback may be surfaced immediately (not only comments that arrive after monitoring starts). This is intentional so already-open review comments are not missed.
|
||||
|
||||
When you agree with a comment and it is actionable:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface:
|
||||
display_name: "PR Babysitter"
|
||||
short_description: "Watch PR review comments, CI, and merge conflicts"
|
||||
default_prompt: "Babysit the current PR: monitor reviewer comments, CI, and merge-conflict status (prefer the watcher’s --watch mode for live monitoring); surface new review feedback before acting on CI or mergeability work, fix valid issues, push updates, and rerun flaky failures up to 3 times. Do not post replies to human-authored review comments unless the user explicitly confirms the exact response. Do not patch unrelated flaky tests, CI infrastructure, dependency outages, runner issues, or other failures that are not caused by the branch. Keep exactly one watcher session active for the PR (do not leave duplicate --watch terminals running). If you pause monitoring to patch review/CI feedback, restart --watch yourself immediately after the push in the same turn. If a watcher is still running and no strict stop condition has been reached, the task is still in progress: keep consuming watcher output and sending progress updates instead of ending the turn. Do not treat a green + mergeable PR as a terminal stop while it is still open; continue polling autonomously after any push/rerun so newly posted review comments are surfaced until a strict terminal stop condition is reached or the user interrupts."
|
||||
default_prompt: "Babysit the current PR: monitor published reviewer comments, CI, and merge-conflict status (prefer the watcher’s --watch mode for live monitoring); ignore unpublished comments in pending GitHub reviews; surface new published review feedback before acting on CI or mergeability work, fix valid issues, push updates, and rerun flaky failures up to 3 times. Do not post replies to human-authored review comments unless the user explicitly confirms the exact response. Do not patch unrelated flaky tests, CI infrastructure, dependency outages, runner issues, or other failures that are not caused by the branch. Keep exactly one watcher session active for the PR (do not leave duplicate --watch terminals running). If you pause monitoring to patch review/CI feedback, restart --watch yourself immediately after the push in the same turn. If a watcher is still running and no strict stop condition has been reached, the task is still in progress: keep consuming watcher output and sending progress updates instead of ending the turn. Do not treat a green + mergeable PR as a terminal stop while it is still open; continue polling autonomously after any push/rerun so newly posted review comments are surfaced until a strict terminal stop condition is reached or the user interrupts."
|
||||
|
||||
@@ -44,6 +44,9 @@ Reruns only failed jobs (and dependencies) for a workflow run.
|
||||
- Review submissions:
|
||||
- `gh api repos/{owner}/{repo}/pulls/<pr_number>/reviews?per_page=100`
|
||||
|
||||
Use each inline comment's `pull_request_review_id` to find its parent review. Ignore parent reviews
|
||||
whose `state` is `PENDING`, along with their inline comments, until the review is submitted.
|
||||
|
||||
## JSON fields consumed by the watcher
|
||||
|
||||
### `gh pr view`
|
||||
|
||||
@@ -452,11 +452,14 @@ def normalize_issue_comments(items):
|
||||
return out
|
||||
|
||||
|
||||
def normalize_review_comments(items):
|
||||
def normalize_review_comments(items, review_states):
|
||||
out = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
review_id = str(item.get("pull_request_review_id") or "")
|
||||
if review_states.get(review_id) == "PENDING":
|
||||
continue
|
||||
line = item.get("line")
|
||||
if line is None:
|
||||
line = item.get("original_line")
|
||||
@@ -481,6 +484,8 @@ def normalize_reviews(items):
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if str(item.get("state") or "").upper() == "PENDING":
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"kind": "review",
|
||||
@@ -534,16 +539,33 @@ def fetch_new_review_items(pr, state, fresh_state, authenticated_login=None):
|
||||
review_payload = gh_api_list_paginated(endpoints["review"], repo=repo)
|
||||
|
||||
issue_items = normalize_issue_comments(issue_payload)
|
||||
review_comment_items = normalize_review_comments(review_comment_payload)
|
||||
review_states = {
|
||||
str(item.get("id")): str(item.get("state") or "").upper()
|
||||
for item in review_payload
|
||||
if isinstance(item, dict) and item.get("id") not in (None, "")
|
||||
}
|
||||
pending_review_ids = {
|
||||
review_id for review_id, review_state in review_states.items() if review_state == "PENDING"
|
||||
}
|
||||
pending_review_comment_ids = {
|
||||
str(item.get("id"))
|
||||
for item in review_comment_payload
|
||||
if isinstance(item, dict)
|
||||
and item.get("id") not in (None, "")
|
||||
and str(item.get("pull_request_review_id") or "") in pending_review_ids
|
||||
}
|
||||
review_comment_items = normalize_review_comments(review_comment_payload, review_states)
|
||||
review_items = normalize_reviews(review_payload)
|
||||
all_items = issue_items + review_comment_items + review_items
|
||||
|
||||
seen_issue = {str(x) for x in state.get("seen_issue_comment_ids") or []}
|
||||
seen_review_comment = {str(x) for x in state.get("seen_review_comment_ids") or []}
|
||||
seen_review = {str(x) for x in state.get("seen_review_ids") or []}
|
||||
seen_review_comment.difference_update(pending_review_comment_ids)
|
||||
seen_review.difference_update(pending_review_ids)
|
||||
|
||||
# On a brand-new state file, surface existing review activity instead of
|
||||
# silently treating it as seen. This avoids missing already-pending review
|
||||
# silently treating it as seen. This avoids missing already-published review
|
||||
# feedback when monitoring starts after comments were posted.
|
||||
|
||||
new_items = []
|
||||
|
||||
@@ -118,6 +118,74 @@ def test_recommend_actions_prioritizes_review_comments():
|
||||
]
|
||||
|
||||
|
||||
def test_pending_review_feedback_surfaces_only_after_publication(monkeypatch):
|
||||
state = {
|
||||
"seen_review_comment_ids": ["20"],
|
||||
"seen_review_ids": ["10"],
|
||||
}
|
||||
review = {
|
||||
"id": 10,
|
||||
"user": {"login": "octocat"},
|
||||
"author_association": "MEMBER",
|
||||
"state": "PENDING",
|
||||
"body": "Please rename this.",
|
||||
"created_at": "2026-06-08T10:00:00Z",
|
||||
"submitted_at": None,
|
||||
"html_url": "https://github.com/openai/codex/pull/123#pullrequestreview-10",
|
||||
}
|
||||
review_comment = {
|
||||
"id": 20,
|
||||
"pull_request_review_id": 10,
|
||||
"user": {"login": "octocat"},
|
||||
"author_association": "MEMBER",
|
||||
"body": "Please rename this.",
|
||||
"created_at": "2026-06-08T10:00:00Z",
|
||||
"path": "src/example.rs",
|
||||
"line": 7,
|
||||
"html_url": "https://github.com/openai/codex/pull/123#discussion_r20",
|
||||
}
|
||||
|
||||
def fake_list(endpoint, **kwargs):
|
||||
if endpoint.endswith("/issues/123/comments"):
|
||||
return []
|
||||
if endpoint.endswith("/pulls/123/comments"):
|
||||
return [review_comment]
|
||||
if endpoint.endswith("/pulls/123/reviews"):
|
||||
return [review]
|
||||
raise AssertionError(f"unexpected endpoint: {endpoint}")
|
||||
|
||||
monkeypatch.setattr(gh_pr_watch, "gh_api_list_paginated", fake_list)
|
||||
|
||||
assert (
|
||||
gh_pr_watch.fetch_new_review_items(
|
||||
sample_pr(),
|
||||
state,
|
||||
fresh_state=True,
|
||||
authenticated_login="octocat",
|
||||
)
|
||||
== []
|
||||
)
|
||||
assert state["seen_review_comment_ids"] == []
|
||||
assert state["seen_review_ids"] == []
|
||||
|
||||
review["state"] = "COMMENTED"
|
||||
review["submitted_at"] = "2026-06-08T10:05:00Z"
|
||||
|
||||
published_items = gh_pr_watch.fetch_new_review_items(
|
||||
sample_pr(),
|
||||
state,
|
||||
fresh_state=False,
|
||||
authenticated_login="octocat",
|
||||
)
|
||||
|
||||
assert {(item["kind"], item["id"]) for item in published_items} == {
|
||||
("review", "10"),
|
||||
("review_comment", "20"),
|
||||
}
|
||||
assert state["seen_review_comment_ids"] == ["20"]
|
||||
assert state["seen_review_ids"] == ["10"]
|
||||
|
||||
|
||||
def test_run_watch_keeps_polling_open_ready_to_merge_pr(monkeypatch):
|
||||
sleeps = []
|
||||
events = []
|
||||
|
||||
@@ -8,12 +8,20 @@ retries = 1
|
||||
[profile.default.junit]
|
||||
path = "junit.xml"
|
||||
|
||||
[profile.local]
|
||||
inherits = "default"
|
||||
|
||||
[test-groups.app_server_protocol_codegen]
|
||||
max-threads = 1
|
||||
|
||||
[test-groups.app_server_integration]
|
||||
max-threads = 1
|
||||
|
||||
# Higher concurrency causes integration test timeouts under resource contention
|
||||
# on common developer machines.
|
||||
[test-groups.app_server_integration_local]
|
||||
max-threads = 4
|
||||
|
||||
[test-groups.core_apply_patch_cli_integration]
|
||||
max-threads = 1
|
||||
|
||||
@@ -42,6 +50,12 @@ test-group = 'app_server_protocol_codegen'
|
||||
filter = 'package(codex-app-server) & kind(test)'
|
||||
test-group = 'app_server_integration'
|
||||
|
||||
[[profile.local.overrides]]
|
||||
# Use up to four app-server subprocesses locally. The global nextest pool still
|
||||
# limits this to the machine's logical CPU count.
|
||||
filter = 'package(codex-app-server) & kind(test)'
|
||||
test-group = 'app_server_integration_local'
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# These tests exercise full Codex turns and apply_patch execution, and they are
|
||||
# sensitive to Windows runner process-startup stalls when many cases launch at once.
|
||||
|
||||
Generated
+27
@@ -1982,6 +1982,7 @@ dependencies = [
|
||||
"codex-image-generation-extension",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-mcp-extension",
|
||||
"codex-memories-extension",
|
||||
"codex-memories-write",
|
||||
"codex-model-provider",
|
||||
@@ -1994,6 +1995,7 @@ dependencies = [
|
||||
"codex-rollout",
|
||||
"codex-sandboxing",
|
||||
"codex-shell-command",
|
||||
"codex-skills-extension",
|
||||
"codex-state",
|
||||
"codex-thread-store",
|
||||
"codex-tools",
|
||||
@@ -2919,9 +2921,12 @@ name = "codex-extension-api"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-config",
|
||||
"codex-context-fragments",
|
||||
"codex-protocol",
|
||||
"codex-tools",
|
||||
"pretty_assertions",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3244,6 +3249,23 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-mcp-extension"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-core-plugins",
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-mcp-server"
|
||||
version = "0.0.0"
|
||||
@@ -3320,6 +3342,8 @@ dependencies = [
|
||||
"codex-features",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
@@ -3796,8 +3820,11 @@ dependencies = [
|
||||
"async-trait",
|
||||
"codex-core",
|
||||
"codex-core-skills",
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-string",
|
||||
"pretty_assertions",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -49,6 +49,7 @@ members = [
|
||||
"ext/guardian",
|
||||
"ext/image-generation",
|
||||
"ext/memories",
|
||||
"ext/mcp",
|
||||
"ext/skills",
|
||||
"ext/web-search",
|
||||
"external-agent-migration",
|
||||
@@ -191,6 +192,7 @@ codex-web-search-extension = { path = "ext/web-search" }
|
||||
codex-memories-read = { path = "memories/read" }
|
||||
codex-memories-write = { path = "memories/write" }
|
||||
codex-mcp = { path = "codex-mcp" }
|
||||
codex-mcp-extension = { path = "ext/mcp" }
|
||||
codex-mcp-server = { path = "mcp-server" }
|
||||
codex-model-provider-info = { path = "model-provider-info" }
|
||||
codex-models-manager = { path = "models-manager" }
|
||||
@@ -212,6 +214,7 @@ codex-sandboxing = { path = "sandboxing" }
|
||||
codex-secrets = { path = "secrets" }
|
||||
codex-shell-command = { path = "shell-command" }
|
||||
codex-shell-escalation = { path = "shell-escalation" }
|
||||
codex-skills-extension = { path = "ext/skills" }
|
||||
codex-skills = { path = "skills" }
|
||||
codex-state = { path = "state" }
|
||||
codex-stdio-to-uds = { path = "stdio-to-uds" }
|
||||
|
||||
@@ -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;
|
||||
@@ -1373,7 +1374,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
},
|
||||
model: "gpt-5".to_string(),
|
||||
ephemeral: true,
|
||||
thread_source: Some(ThreadSource::User),
|
||||
thread_source: Some(ThreadSource::Feature("automation".to_string())),
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: None,
|
||||
parent_thread_id: None,
|
||||
@@ -1406,7 +1407,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
},
|
||||
"model": "gpt-5",
|
||||
"ephemeral": true,
|
||||
"thread_source": "user",
|
||||
"thread_source": "automation",
|
||||
"initialization_mode": "new",
|
||||
"subagent_source": null,
|
||||
"parent_thread_id": null,
|
||||
@@ -3794,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(),
|
||||
@@ -3841,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));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1249,7 +1263,7 @@ impl AnalyticsReducer {
|
||||
thread_metadata.session_id.clone(),
|
||||
connection_state.app_server_client.clone(),
|
||||
connection_state.runtime.clone(),
|
||||
thread_metadata.thread_source,
|
||||
thread_metadata.thread_source.clone(),
|
||||
thread_metadata.subagent_source.clone(),
|
||||
thread_metadata.parent_thread_id.clone(),
|
||||
),
|
||||
@@ -1354,7 +1368,7 @@ impl AnalyticsReducer {
|
||||
accepted_turn_id,
|
||||
app_server_client: connection_state.app_server_client.clone(),
|
||||
runtime: connection_state.runtime.clone(),
|
||||
thread_source: thread_metadata.thread_source,
|
||||
thread_source: thread_metadata.thread_source.clone(),
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
num_input_images: pending_request.num_input_images,
|
||||
@@ -1397,7 +1411,7 @@ impl AnalyticsReducer {
|
||||
review_id: pending_review.review_id,
|
||||
app_server_client: connection_state.app_server_client.clone(),
|
||||
runtime: connection_state.runtime.clone(),
|
||||
thread_source: thread_metadata.thread_source,
|
||||
thread_source: thread_metadata.thread_source.clone(),
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
subject_kind: pending_review.subject_kind,
|
||||
@@ -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 { .. }
|
||||
@@ -1964,7 +1979,7 @@ fn tool_item_base(
|
||||
item_id,
|
||||
app_server_client: context.connection_state.app_server_client.clone(),
|
||||
runtime: context.connection_state.runtime.clone(),
|
||||
thread_source: thread_metadata.thread_source,
|
||||
thread_source: thread_metadata.thread_source.clone(),
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
tool_name,
|
||||
@@ -2444,7 +2459,7 @@ fn codex_turn_event_params(
|
||||
runtime,
|
||||
submission_type,
|
||||
ephemeral,
|
||||
thread_source: thread_metadata.thread_source,
|
||||
thread_source: thread_metadata.thread_source.clone(),
|
||||
initialization_mode: thread_metadata.initialization_mode,
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
|
||||
@@ -70,13 +70,9 @@ pub use crate::remote::RemoteAppServerEndpoint;
|
||||
/// module exists so clients can remove a direct `codex-core` dependency
|
||||
/// while legacy startup/config paths are migrated to RPCs.
|
||||
pub mod legacy_core {
|
||||
pub use codex_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
pub use codex_core::LOCAL_AGENTS_MD_FILENAME;
|
||||
pub use codex_core::McpManager;
|
||||
pub use codex_core::check_execpolicy_for_warnings;
|
||||
pub use codex_core::format_exec_policy_error_with_source;
|
||||
pub use codex_core::grant_read_root_non_elevated;
|
||||
pub use codex_core::web_search_detail;
|
||||
|
||||
pub mod config {
|
||||
pub use codex_core::config::*;
|
||||
@@ -86,10 +82,6 @@ pub mod legacy_core {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod connectors {
|
||||
pub use codex_core::connectors::*;
|
||||
}
|
||||
|
||||
pub mod otel_init {
|
||||
pub use codex_core::otel_init::*;
|
||||
}
|
||||
@@ -98,22 +90,10 @@ pub mod legacy_core {
|
||||
pub use codex_core::personality_migration::*;
|
||||
}
|
||||
|
||||
pub mod review_format {
|
||||
pub use codex_core::review_format::*;
|
||||
}
|
||||
|
||||
pub mod review_prompts {
|
||||
pub use codex_core::review_prompts::*;
|
||||
}
|
||||
|
||||
pub mod test_support {
|
||||
pub use codex_core::test_support::*;
|
||||
}
|
||||
|
||||
pub mod util {
|
||||
pub use codex_core::util::*;
|
||||
}
|
||||
|
||||
pub mod windows_sandbox {
|
||||
pub use codex_core::windows_sandbox::*;
|
||||
}
|
||||
|
||||
+52
-5
@@ -180,6 +180,36 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CapabilityRootLocation": {
|
||||
"description": "Location used to resolve a selected capability root.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A path owned by an execution environment.",
|
||||
"properties": {
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"environment"
|
||||
],
|
||||
"title": "EnvironmentCapabilityRootLocationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"environmentId",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "EnvironmentCapabilityRootLocation",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ClientInfo": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -2947,6 +2977,28 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"SelectedCapabilityRoot": {
|
||||
"description": "A user-selected root that can expose one or more runtime capabilities.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Stable identifier supplied by the capability selection platform.",
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CapabilityRootLocation"
|
||||
}
|
||||
],
|
||||
"description": "Where the selected root can be resolved."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"location"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SendAddCreditsNudgeEmailParams": {
|
||||
"properties": {
|
||||
"creditType": {
|
||||
@@ -3795,11 +3847,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSourceKind": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -4575,11 +4615,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStartedNotification": {
|
||||
|
||||
+92
-5
@@ -6765,6 +6765,36 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CapabilityRootLocation": {
|
||||
"description": "Location used to resolve a selected capability root.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A path owned by an execution environment.",
|
||||
"properties": {
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"environment"
|
||||
],
|
||||
"title": "EnvironmentCapabilityRootLocationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"environmentId",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "EnvironmentCapabilityRootLocation",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.",
|
||||
"oneOf": [
|
||||
@@ -15061,6 +15091,28 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SelectedCapabilityRoot": {
|
||||
"description": "A user-selected root that can expose one or more runtime capabilities.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Stable identifier supplied by the capability selection platform.",
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/CapabilityRootLocation"
|
||||
}
|
||||
],
|
||||
"description": "Where the selected root can be resolved."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"location"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SendAddCreditsNudgeEmailParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -15580,6 +15632,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -16892,6 +16952,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": {
|
||||
@@ -18017,11 +18109,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSourceKind": {
|
||||
|
||||
+92
-5
@@ -1087,6 +1087,36 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CapabilityRootLocation": {
|
||||
"description": "Location used to resolve a selected capability root.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A path owned by an execution environment.",
|
||||
"properties": {
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"environment"
|
||||
],
|
||||
"title": "EnvironmentCapabilityRootLocationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"environmentId",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "EnvironmentCapabilityRootLocation",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ClientInfo": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -11563,6 +11593,28 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SelectedCapabilityRoot": {
|
||||
"description": "A user-selected root that can expose one or more runtime capabilities.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Stable identifier supplied by the capability selection platform.",
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CapabilityRootLocation"
|
||||
}
|
||||
],
|
||||
"description": "Where the selected root can be resolved."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"location"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SendAddCreditsNudgeEmailParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -13397,6 +13449,14 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentActivityKind": {
|
||||
"enum": [
|
||||
"started",
|
||||
"interacted",
|
||||
"interrupted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -14709,6 +14769,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": {
|
||||
@@ -15834,11 +15926,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSourceKind": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -73,11 +73,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
@@ -1790,11 +1830,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -1605,11 +1645,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
+40
-5
@@ -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": {
|
||||
@@ -1605,11 +1645,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -1605,11 +1645,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -1790,11 +1830,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -1605,11 +1645,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
@@ -64,6 +64,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"CapabilityRootLocation": {
|
||||
"description": "Location used to resolve a selected capability root.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A path owned by an execution environment.",
|
||||
"properties": {
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"environment"
|
||||
],
|
||||
"title": "EnvironmentCapabilityRootLocationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"environmentId",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "EnvironmentCapabilityRootLocation",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
@@ -106,12 +136,29 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
"SelectedCapabilityRoot": {
|
||||
"description": "A user-selected root that can expose one or more runtime capabilities.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Stable identifier supplied by the capability selection platform.",
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CapabilityRootLocation"
|
||||
}
|
||||
],
|
||||
"description": "Where the selected root can be resolved."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"location"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStartSource": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -1790,11 +1830,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
+40
-5
@@ -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": {
|
||||
@@ -1605,11 +1645,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -1605,11 +1645,6 @@
|
||||
]
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
"subagent",
|
||||
"memory_consolidation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadStatus": {
|
||||
|
||||
@@ -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,8 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Location used to resolve a selected capability root.
|
||||
*/
|
||||
export type CapabilityRootLocation = { "type": "environment", environmentId: string, path: string, };
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
import type { CapabilityRootLocation } from "./CapabilityRootLocation";
|
||||
|
||||
/**
|
||||
* A user-selected root that can expose one or more runtime capabilities.
|
||||
*/
|
||||
export type SelectedCapabilityRoot = {
|
||||
/**
|
||||
* Stable identifier supplied by the capability selection platform.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Where the selected root can be resolved.
|
||||
*/
|
||||
location: CapabilityRootLocation, };
|
||||
@@ -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, };
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadSource = "user" | "subagent" | "memory_consolidation";
|
||||
export type ThreadSource = string;
|
||||
|
||||
@@ -40,6 +40,7 @@ export type { ByteRange } from "./ByteRange";
|
||||
export type { CancelLoginAccountParams } from "./CancelLoginAccountParams";
|
||||
export type { CancelLoginAccountResponse } from "./CancelLoginAccountResponse";
|
||||
export type { CancelLoginAccountStatus } from "./CancelLoginAccountStatus";
|
||||
export type { CapabilityRootLocation } from "./CapabilityRootLocation";
|
||||
export type { ChatgptAuthTokensRefreshParams } from "./ChatgptAuthTokensRefreshParams";
|
||||
export type { ChatgptAuthTokensRefreshReason } from "./ChatgptAuthTokensRefreshReason";
|
||||
export type { ChatgptAuthTokensRefreshResponse } from "./ChatgptAuthTokensRefreshResponse";
|
||||
@@ -334,6 +335,7 @@ export type { ReviewTarget } from "./ReviewTarget";
|
||||
export type { SandboxMode } from "./SandboxMode";
|
||||
export type { SandboxPolicy } from "./SandboxPolicy";
|
||||
export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite";
|
||||
export type { SelectedCapabilityRoot } from "./SelectedCapabilityRoot";
|
||||
export type { SendAddCreditsNudgeEmailParams } from "./SendAddCreditsNudgeEmailParams";
|
||||
export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmailResponse";
|
||||
export type { ServerRequestResolvedNotification } from "./ServerRequestResolvedNotification";
|
||||
@@ -356,6 +358,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/")]
|
||||
|
||||
@@ -57,6 +57,30 @@ fn test_absolute_path() -> AbsolutePathBuf {
|
||||
absolute_path("readable")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_sources_round_trip_as_scalar_labels() {
|
||||
for (source, label) in [
|
||||
(ThreadSource::User, "user"),
|
||||
(ThreadSource::Subagent, "subagent"),
|
||||
(
|
||||
ThreadSource::Feature("automation".to_string()),
|
||||
"automation",
|
||||
),
|
||||
(ThreadSource::MemoryConsolidation, "memory_consolidation"),
|
||||
] {
|
||||
let value = serde_json::to_value(&source).expect("serialize thread source");
|
||||
|
||||
assert_eq!(value, json!(label));
|
||||
assert_eq!(
|
||||
serde_json::from_value::<ThreadSource>(value).expect("deserialize thread source"),
|
||||
source
|
||||
);
|
||||
|
||||
let core_source: codex_protocol::protocol::ThreadSource = source.clone().into();
|
||||
assert_eq!(ThreadSource::from(core_source), source);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -11,6 +11,8 @@ use super::TurnEnvironmentParams;
|
||||
use super::TurnItemsView;
|
||||
use super::shared::v2_enum_from_core;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
pub use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
pub use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
@@ -153,6 +155,10 @@ pub struct ThreadStartParams {
|
||||
#[experimental("thread/start.dynamicTools")]
|
||||
#[ts(optional = nullable)]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
/// Capability roots selected for this thread by the hosting platform.
|
||||
#[experimental("thread/start.selectedCapabilityRoots")]
|
||||
#[ts(optional = nullable)]
|
||||
pub selected_capability_roots: Option<Vec<SelectedCapabilityRoot>>,
|
||||
/// Test-only experimental field used to validate experimental gating and
|
||||
/// schema filtering behavior in a stable way.
|
||||
#[experimental("thread/start.mockExperimentalField")]
|
||||
|
||||
@@ -7,6 +7,8 @@ use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
|
||||
use codex_protocol::protocol::ThreadSource as CoreThreadSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::Schema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
@@ -61,20 +63,47 @@ impl From<SessionSource> for CoreSessionSource {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case", export_to = "v2/")]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
#[ts(type = "string")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ThreadSource {
|
||||
User,
|
||||
Subagent,
|
||||
Feature(String),
|
||||
MemoryConsolidation,
|
||||
}
|
||||
|
||||
impl JsonSchema for ThreadSource {
|
||||
fn schema_name() -> String {
|
||||
"ThreadSource".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
String::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ThreadSource {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
value.parse::<CoreThreadSource>().map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ThreadSource> for String {
|
||||
fn from(value: ThreadSource) -> Self {
|
||||
CoreThreadSource::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreThreadSource> for ThreadSource {
|
||||
fn from(value: CoreThreadSource) -> Self {
|
||||
match value {
|
||||
CoreThreadSource::User => ThreadSource::User,
|
||||
CoreThreadSource::Subagent => ThreadSource::Subagent,
|
||||
CoreThreadSource::Feature(feature) => ThreadSource::Feature(feature),
|
||||
CoreThreadSource::MemoryConsolidation => ThreadSource::MemoryConsolidation,
|
||||
}
|
||||
}
|
||||
@@ -85,6 +114,7 @@ impl From<ThreadSource> for CoreThreadSource {
|
||||
match value {
|
||||
ThreadSource::User => CoreThreadSource::User,
|
||||
ThreadSource::Subagent => CoreThreadSource::Subagent,
|
||||
ThreadSource::Feature(feature) => CoreThreadSource::Feature(feature),
|
||||
ThreadSource::MemoryConsolidation => CoreThreadSource::MemoryConsolidation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ codex-hooks = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-skills-extension = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
@@ -60,6 +61,7 @@ codex-memories-extension = { workspace = true }
|
||||
codex-web-search-extension = { workspace = true }
|
||||
codex-memories-write = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
codex-mcp-extension = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
@@ -130,7 +130,7 @@ Example with notification opt-out:
|
||||
|
||||
## API Overview
|
||||
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`.
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots. Skills found below those roots are listed and read through the owning environment; other plugin capabilities are not activated yet.
|
||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
|
||||
@@ -249,6 +249,18 @@ Start a fresh thread when you need a new Codex conversation.
|
||||
// "permissions": ":workspace"
|
||||
// Experimental runtime roots for :workspace_roots materialization:
|
||||
// "runtimeWorkspaceRoots": ["/Users/me/project", "/Users/me/openai"],
|
||||
// Experimental capability roots selected by the hosting platform:
|
||||
"selectedCapabilityRoots": [
|
||||
{
|
||||
"id": "github@openai",
|
||||
"location": {
|
||||
"type": "environment",
|
||||
"environmentId": "workspace",
|
||||
// Opaque to app-server; interpreted in the selected environment.
|
||||
"path": "/opt/cca/plugins/github"
|
||||
}
|
||||
}
|
||||
],
|
||||
// Do not send both "sandbox" and "permissions".
|
||||
"personality": "friendly",
|
||||
"serviceName": "my_app_server_client", // optional metrics tag (`service_name`)
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
@@ -3299,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();
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(crate) fn thread_extensions<S>(
|
||||
state_db: Option<StateDbHandle>,
|
||||
thread_manager: Weak<ThreadManager>,
|
||||
goal_service: Arc<GoalService>,
|
||||
executor_skill_provider: Arc<dyn codex_skills_extension::SkillProvider>,
|
||||
) -> Arc<ExtensionRegistry<Config>>
|
||||
where
|
||||
S: AgentSpawner<StartThreadOptions, Spawned = NewThread, Error = CodexErr> + 'static,
|
||||
@@ -49,8 +50,14 @@ where
|
||||
}
|
||||
codex_guardian::install(&mut builder, guardian_agent_spawner);
|
||||
codex_memories_extension::install(&mut builder, codex_otel::global());
|
||||
codex_mcp_extension::install(&mut builder);
|
||||
codex_web_search_extension::install(&mut builder, auth_manager.clone());
|
||||
codex_image_generation_extension::install(&mut builder, auth_manager);
|
||||
codex_skills_extension::install_with_providers(
|
||||
&mut builder,
|
||||
codex_skills_extension::SkillProviders::new()
|
||||
.with_executor_provider(executor_skill_provider),
|
||||
);
|
||||
Arc::new(builder.build())
|
||||
}
|
||||
|
||||
|
||||
@@ -67,10 +67,7 @@ async fn build_refresh_config(
|
||||
let config = config_manager
|
||||
.load_latest_config_for_thread(thread_config.as_ref())
|
||||
.await?;
|
||||
let mcp_servers = thread_manager
|
||||
.mcp_manager()
|
||||
.configured_servers(&config)
|
||||
.await;
|
||||
let mcp_servers = thread_manager.mcp_manager().runtime_servers(&config).await;
|
||||
Ok(McpServerRefreshConfig {
|
||||
mcp_servers: serde_json::to_value(mcp_servers).map_err(io::Error::other)?,
|
||||
mcp_oauth_credentials_store_mode: serde_json::to_value(
|
||||
@@ -181,12 +178,19 @@ mod tests {
|
||||
.await
|
||||
.expect("refresh tests require state db");
|
||||
let thread_store = thread_store_from_config(&good_config, Some(state_db.clone()));
|
||||
let environment_manager = Arc::new(EnvironmentManager::default_for_tests());
|
||||
let executor_skill_provider: Arc<dyn codex_skills_extension::SkillProvider> = Arc::new(
|
||||
codex_skills_extension::ExecutorSkillProvider::new_with_restriction_product(
|
||||
Arc::clone(&environment_manager),
|
||||
SessionSource::Exec.restriction_product(),
|
||||
),
|
||||
);
|
||||
let thread_manager = Arc::new_cyclic(|thread_manager| {
|
||||
ThreadManager::new(
|
||||
&good_config,
|
||||
auth_manager.clone(),
|
||||
SessionSource::Exec,
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
Arc::clone(&environment_manager),
|
||||
thread_extensions(
|
||||
guardian_agent_spawner(thread_manager.clone()),
|
||||
Arc::new(NoopExtensionEventSink),
|
||||
@@ -194,6 +198,7 @@ mod tests {
|
||||
Some(state_db.clone()),
|
||||
thread_manager.clone(),
|
||||
Arc::new(codex_goal_extension::GoalService::new()),
|
||||
Arc::clone(&executor_skill_provider),
|
||||
),
|
||||
/*analytics_events_client*/ None,
|
||||
Arc::clone(&thread_store),
|
||||
|
||||
@@ -307,6 +307,14 @@ impl MessageProcessor {
|
||||
// resumed, or forked threads to a different persistence backend/root.
|
||||
let thread_store = codex_core::thread_store_from_config(config.as_ref(), state_db.clone());
|
||||
let environment_manager_for_requests = Arc::clone(&environment_manager);
|
||||
let environment_manager_for_extensions = Arc::clone(&environment_manager);
|
||||
let restriction_product = session_source.restriction_product();
|
||||
let executor_skill_provider: Arc<dyn codex_skills_extension::SkillProvider> = Arc::new(
|
||||
codex_skills_extension::ExecutorSkillProvider::new_with_restriction_product(
|
||||
environment_manager_for_extensions,
|
||||
restriction_product,
|
||||
),
|
||||
);
|
||||
let goal_service = Arc::new(GoalService::new());
|
||||
let thread_manager = Arc::new_cyclic(|thread_manager| {
|
||||
ThreadManager::new(
|
||||
@@ -321,6 +329,7 @@ impl MessageProcessor {
|
||||
state_db.clone(),
|
||||
thread_manager.clone(),
|
||||
Arc::clone(&goal_service),
|
||||
Arc::clone(&executor_skill_provider),
|
||||
),
|
||||
Some(analytics_events_client.clone()),
|
||||
Arc::clone(&thread_store),
|
||||
|
||||
@@ -280,6 +280,7 @@ use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::CodexThreadSettingsOverrides;
|
||||
use codex_core::ForkSnapshot;
|
||||
use codex_core::McpManager;
|
||||
use codex_core::NewThread;
|
||||
#[cfg(test)]
|
||||
use codex_core::SessionMeta;
|
||||
|
||||
@@ -88,11 +88,19 @@ impl AppsRequestProcessor {
|
||||
let request = request_id.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let mcp_manager = self.thread_manager.mcp_manager();
|
||||
let shutdown_token = self.shutdown_token.child_token();
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = shutdown_token.cancelled() => {}
|
||||
_ = Self::apps_list_task(outgoing, request, params, config, environment_manager) => {}
|
||||
_ = Self::apps_list_task(
|
||||
outgoing,
|
||||
request,
|
||||
params,
|
||||
config,
|
||||
environment_manager,
|
||||
mcp_manager,
|
||||
) => {}
|
||||
}
|
||||
});
|
||||
Ok(None)
|
||||
@@ -108,11 +116,15 @@ impl AppsRequestProcessor {
|
||||
params: AppsListParams,
|
||||
config: Config,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) {
|
||||
let retry_params = params.clone();
|
||||
let retry_config = config.clone();
|
||||
let retry_environment_manager = Arc::clone(&environment_manager);
|
||||
let result = Self::apps_list_response(&outgoing, params, config, environment_manager).await;
|
||||
let retry_mcp_manager = Arc::clone(&mcp_manager);
|
||||
let result =
|
||||
Self::apps_list_response(&outgoing, params, config, environment_manager, mcp_manager)
|
||||
.await;
|
||||
let should_retry = result
|
||||
.as_ref()
|
||||
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
|
||||
@@ -128,6 +140,7 @@ impl AppsRequestProcessor {
|
||||
retry_params,
|
||||
retry_config,
|
||||
retry_environment_manager,
|
||||
retry_mcp_manager,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -141,6 +154,7 @@ impl AppsRequestProcessor {
|
||||
params: AppsListParams,
|
||||
config: Config,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {
|
||||
let AppsListParams {
|
||||
cursor,
|
||||
@@ -167,14 +181,14 @@ impl AppsRequestProcessor {
|
||||
let accessible_config = config.clone();
|
||||
let accessible_tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result =
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
&accessible_config,
|
||||
force_refetch,
|
||||
Arc::clone(&environment_manager),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("failed to load accessible apps: {err}"));
|
||||
let result = connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
&accessible_config,
|
||||
force_refetch,
|
||||
Arc::clone(&environment_manager),
|
||||
mcp_manager,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("failed to load accessible apps: {err}"));
|
||||
let _ = accessible_tx.send(AppListLoadResult::Accessible(result));
|
||||
});
|
||||
|
||||
|
||||
@@ -120,12 +120,16 @@ impl McpRequestProcessor {
|
||||
timeout_secs,
|
||||
} = params;
|
||||
|
||||
let configured_servers = self
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let effective_servers = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
.configured_servers(&config)
|
||||
.effective_servers(&config, auth.as_ref())
|
||||
.await;
|
||||
let Some(server) = configured_servers.get(&name) else {
|
||||
let Some(server) = effective_servers
|
||||
.get(&name)
|
||||
.and_then(codex_mcp::EffectiveMcpServer::configured_config)
|
||||
else {
|
||||
return Err(invalid_request(format!(
|
||||
"No MCP server named '{name}' found."
|
||||
)));
|
||||
@@ -210,8 +214,10 @@ impl McpRequestProcessor {
|
||||
}
|
||||
None => self.load_latest_config(/*fallback_cwd*/ None).await?,
|
||||
};
|
||||
let mcp_config = config
|
||||
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
|
||||
let mcp_config = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
.runtime_config(&config)
|
||||
.await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
@@ -361,8 +367,10 @@ impl McpRequestProcessor {
|
||||
}
|
||||
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
let mcp_config = config
|
||||
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
|
||||
let mcp_config = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
.runtime_config(&config)
|
||||
.await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
|
||||
@@ -9,8 +9,11 @@ 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;
|
||||
@@ -149,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(
|
||||
@@ -776,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(),
|
||||
@@ -798,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,
|
||||
@@ -898,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 {
|
||||
@@ -910,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
|
||||
@@ -1030,6 +1036,7 @@ impl PluginRequestProcessor {
|
||||
&config,
|
||||
&outcome.plugin.apps,
|
||||
Arc::clone(&environment_manager),
|
||||
self.thread_manager.mcp_manager(),
|
||||
)
|
||||
.await;
|
||||
let visible_skills = outcome
|
||||
@@ -1112,6 +1119,7 @@ impl PluginRequestProcessor {
|
||||
&config,
|
||||
&plugin_apps,
|
||||
Arc::clone(&environment_manager),
|
||||
self.thread_manager.mcp_manager(),
|
||||
)
|
||||
.await;
|
||||
remote_plugin_detail_to_info(remote_detail, app_summaries)
|
||||
@@ -1519,7 +1527,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,
|
||||
@@ -1538,7 +1546,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);
|
||||
|
||||
@@ -1548,15 +1556,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,
|
||||
@@ -1578,10 +1613,11 @@ impl PluginRequestProcessor {
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
|
||||
connectors::list_all_connectors_with_options(config, /*force_refetch*/ false),
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config,
|
||||
/*force_refetch*/ true,
|
||||
Arc::clone(&environment_manager)
|
||||
Arc::clone(&environment_manager),
|
||||
self.thread_manager.mcp_manager(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1848,6 +1884,7 @@ async fn load_plugin_app_summaries(
|
||||
config: &Config,
|
||||
plugin_apps: &[codex_plugin::AppConnectorId],
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) -> Vec<AppSummary> {
|
||||
if plugin_apps.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -1867,10 +1904,11 @@ async fn load_plugin_app_summaries(
|
||||
let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);
|
||||
|
||||
let accessible_connectors =
|
||||
match connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
match connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config,
|
||||
/*force_refetch*/ false,
|
||||
environment_manager,
|
||||
mcp_manager,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use crate::error_code::method_not_found;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
|
||||
@@ -327,6 +329,17 @@ pub(crate) struct ThreadRequestProcessor {
|
||||
pub(super) skills_watcher: Arc<SkillsWatcher>,
|
||||
}
|
||||
|
||||
/// Outcome of trying to satisfy a resume request from an already loaded thread.
|
||||
enum RunningThreadResumeResult {
|
||||
/// The request was delegated to the loaded thread.
|
||||
Handled,
|
||||
/// No loaded thread handled the request.
|
||||
///
|
||||
/// The optional stored thread contains the history-bearing probe that cold
|
||||
/// resume can reuse instead of reading the rollout again.
|
||||
NotRunning(Option<Box<StoredThread>>),
|
||||
}
|
||||
|
||||
impl ThreadRequestProcessor {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
@@ -822,6 +835,7 @@ impl ThreadRequestProcessor {
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
dynamic_tools,
|
||||
selected_capability_roots,
|
||||
mock_experimental_field: _mock_experimental_field,
|
||||
experimental_raw_events,
|
||||
personality,
|
||||
@@ -877,6 +891,7 @@ impl ThreadRequestProcessor {
|
||||
config,
|
||||
typesafe_overrides,
|
||||
dynamic_tools,
|
||||
selected_capability_roots.unwrap_or_default(),
|
||||
session_start_source,
|
||||
thread_source.map(Into::into),
|
||||
environment_selections,
|
||||
@@ -949,6 +964,7 @@ impl ThreadRequestProcessor {
|
||||
config_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
dynamic_tools: Option<Vec<ApiDynamicToolSpec>>,
|
||||
selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
||||
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
|
||||
thread_source: Option<codex_protocol::protocol::ThreadSource>,
|
||||
environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
@@ -1050,6 +1066,10 @@ impl ThreadRequestProcessor {
|
||||
.collect()
|
||||
};
|
||||
let core_dynamic_tool_count = core_dynamic_tools.len();
|
||||
let mut thread_extension_init = ExtensionDataInit::new();
|
||||
if !selected_capability_roots.is_empty() {
|
||||
thread_extension_init.insert(selected_capability_roots);
|
||||
}
|
||||
let create_thread_started_at = std::time::Instant::now();
|
||||
let NewThread {
|
||||
thread_id,
|
||||
@@ -1072,6 +1092,7 @@ impl ThreadRequestProcessor {
|
||||
metrics_service_name: service_name,
|
||||
parent_trace: request_trace,
|
||||
environments,
|
||||
thread_extension_init,
|
||||
})
|
||||
.instrument(tracing::info_span!(
|
||||
"app_server.thread_start.create_thread",
|
||||
@@ -2419,7 +2440,7 @@ impl ThreadRequestProcessor {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
match self
|
||||
let stored_thread_from_running_probe = match self
|
||||
.resume_running_thread(
|
||||
&request_id,
|
||||
¶ms,
|
||||
@@ -2428,13 +2449,13 @@ impl ThreadRequestProcessor {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(true) => return Ok(()),
|
||||
Ok(false) => {}
|
||||
Ok(RunningThreadResumeResult::Handled) => return Ok(()),
|
||||
Ok(RunningThreadResumeResult::NotRunning(stored_thread)) => stored_thread,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ThreadResumeParams {
|
||||
thread_id,
|
||||
@@ -2458,15 +2479,20 @@ impl ThreadRequestProcessor {
|
||||
} = params;
|
||||
let include_turns = !exclude_turns;
|
||||
|
||||
let (thread_history, resume_source_thread) = match if let Some(history) = history {
|
||||
let resume_result = if let Some(history) = history {
|
||||
self.resume_thread_from_history(history.as_slice())
|
||||
.await
|
||||
.map(|thread_history| (thread_history, None))
|
||||
} else if let Some(stored_thread) = stored_thread_from_running_probe {
|
||||
self.stored_thread_to_initial_history(&stored_thread)
|
||||
.await
|
||||
.map(|thread_history| (thread_history, Some(*stored_thread)))
|
||||
} else {
|
||||
self.resume_thread_from_rollout(&thread_id, path.as_ref())
|
||||
.await
|
||||
.map(|(thread_history, stored_thread)| (thread_history, Some(stored_thread)))
|
||||
} {
|
||||
};
|
||||
let (thread_history, resume_source_thread) = match resume_result {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -2707,7 +2733,7 @@ impl ThreadRequestProcessor {
|
||||
params: &ThreadResumeParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<bool, JSONRPCErrorError> {
|
||||
) -> Result<RunningThreadResumeResult, JSONRPCErrorError> {
|
||||
let running_thread = if params.history.is_some() {
|
||||
if let Ok(existing_thread_id) = ThreadId::from_string(¶ms.thread_id)
|
||||
&& self
|
||||
@@ -2743,7 +2769,11 @@ impl ThreadRequestProcessor {
|
||||
let existing_thread_id = source_thread.thread_id;
|
||||
match self.thread_manager.get_thread(existing_thread_id).await {
|
||||
Ok(existing_thread) => Some((existing_thread_id, existing_thread, source_thread)),
|
||||
Err(_) => None,
|
||||
Err(_) => {
|
||||
return Ok(RunningThreadResumeResult::NotRunning(Some(Box::new(
|
||||
source_thread,
|
||||
))));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2784,7 +2814,9 @@ impl ThreadRequestProcessor {
|
||||
ThreadShutdownResult::Complete => {
|
||||
self.thread_manager.remove_thread(&existing_thread_id).await;
|
||||
self.finalize_thread_teardown(existing_thread_id).await;
|
||||
return Ok(false);
|
||||
// Shutdown can flush newer rollout items, so reload the
|
||||
// stored thread before starting the replacement session.
|
||||
return Ok(RunningThreadResumeResult::NotRunning(None));
|
||||
}
|
||||
ThreadShutdownResult::SubmitFailed => {
|
||||
warn!("failed to submit Shutdown to thread {existing_thread_id}");
|
||||
@@ -2876,9 +2908,9 @@ impl ThreadRequestProcessor {
|
||||
"failed to enqueue running thread resume for thread {existing_thread_id}: thread listener command channel is closed"
|
||||
)));
|
||||
}
|
||||
return Ok(true);
|
||||
return Ok(RunningThreadResumeResult::Handled);
|
||||
}
|
||||
Ok(false)
|
||||
Ok(RunningThreadResumeResult::NotRunning(None))
|
||||
}
|
||||
|
||||
async fn resume_thread_from_history(
|
||||
@@ -4122,7 +4154,7 @@ fn summary_from_thread_metadata(metadata: &ThreadMetadata) -> ConversationSummar
|
||||
metadata.cwd.clone(),
|
||||
metadata.cli_version.clone(),
|
||||
metadata.source.clone(),
|
||||
metadata.thread_source,
|
||||
metadata.thread_source.clone(),
|
||||
metadata.agent_nickname.clone(),
|
||||
metadata.agent_role.clone(),
|
||||
metadata.git_sha.clone(),
|
||||
@@ -4211,7 +4243,7 @@ fn build_thread_from_snapshot(
|
||||
agent_nickname: config_snapshot.session_source.get_nickname(),
|
||||
agent_role: config_snapshot.session_source.get_agent_role(),
|
||||
source: config_snapshot.session_source.clone().into(),
|
||||
thread_source: config_snapshot.thread_source.map(Into::into),
|
||||
thread_source: config_snapshot.thread_source.clone().map(Into::into),
|
||||
git_info: None,
|
||||
name: None,
|
||||
turns: Vec::new(),
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use super::*;
|
||||
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
|
||||
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
|
||||
const DIRECT_INPUT_TO_MULTI_AGENT_V2_SUBAGENT_ERROR: &str =
|
||||
"direct app-server input is not allowed for multi-agent v2 sub-agents";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TurnRequestProcessor {
|
||||
@@ -236,6 +242,26 @@ impl TurnRequestProcessor {
|
||||
|
||||
Ok((thread_id, thread))
|
||||
}
|
||||
|
||||
async fn ensure_direct_input_allowed(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
thread: &CodexThread,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if thread.multi_agent_version() == Some(MultiAgentVersion::V2)
|
||||
&& matches!(
|
||||
thread.config_snapshot().await.session_source,
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
||||
)
|
||||
{
|
||||
let error = invalid_request(DIRECT_INPUT_TO_MULTI_AGENT_V2_SUBAGENT_ERROR);
|
||||
self.track_error_response(request_id, &error, /*error_type*/ None);
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_collaboration_mode(
|
||||
&self,
|
||||
mut collaboration_mode: CollaborationMode,
|
||||
@@ -370,6 +396,14 @@ impl TurnRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<TurnStartResponse, JSONRPCErrorError> {
|
||||
let (thread_id, thread) =
|
||||
self.load_thread(¶ms.thread_id)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(&request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
self.ensure_direct_input_allowed(&request_id, thread.as_ref())
|
||||
.await?;
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
self.track_error_response(
|
||||
&request_id,
|
||||
@@ -378,12 +412,6 @@ impl TurnRequestProcessor {
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
let (thread_id, thread) =
|
||||
self.load_thread(¶ms.thread_id)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(&request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
Self::set_app_server_client_info(
|
||||
thread.as_ref(),
|
||||
app_server_client_name,
|
||||
@@ -762,6 +790,8 @@ impl TurnRequestProcessor {
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
self.ensure_direct_input_allowed(request_id, thread.as_ref())
|
||||
.await?;
|
||||
|
||||
if params.expected_turn_id.is_empty() {
|
||||
return Err(invalid_request("expectedTurnId must not be empty"));
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::CapabilityRootLocation;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use core_test_support::responses;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const SKILL_NAME: &str = "demo-plugin:deploy";
|
||||
const SKILL_MARKER: &str = "EXECUTOR_SKILL_BODY_MARKER";
|
||||
const LOCAL_SKILL_MARKER: &str = "LOCAL_SKILL_BODY_MARKER";
|
||||
|
||||
#[tokio::test]
|
||||
async fn selected_executor_root_exposes_plugin_skill() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-selected"),
|
||||
responses::ev_assistant_message("msg-selected", "Done"),
|
||||
responses::ev_completed("resp-selected"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[skills]
|
||||
include_instructions = true
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#,
|
||||
server.uri()
|
||||
),
|
||||
)?;
|
||||
let local_skill_dir = codex_home.path().join("skills/local-deploy");
|
||||
std::fs::create_dir_all(&local_skill_dir)?;
|
||||
std::fs::write(
|
||||
local_skill_dir.join("SKILL.md"),
|
||||
format!(
|
||||
"---\nname: {SKILL_NAME}\ndescription: Colliding local skill.\n---\n\n# Local deploy\n\n{LOCAL_SKILL_MARKER}\n"
|
||||
),
|
||||
)?;
|
||||
let plugin_dir = TempDir::new()?;
|
||||
let manifest_dir = plugin_dir.path().join(".codex-plugin");
|
||||
let skill_dir = plugin_dir.path().join("skills/deploy");
|
||||
std::fs::create_dir_all(&manifest_dir)?;
|
||||
std::fs::create_dir_all(&skill_dir)?;
|
||||
std::fs::write(
|
||||
manifest_dir.join("plugin.json"),
|
||||
r#"{"name":"demo-plugin"}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
skill_dir.join("SKILL.md"),
|
||||
format!(
|
||||
"---\nname: deploy\ndescription: Deploy through the executor.\n---\n\n# Deploy\n\n{SKILL_MARKER}\n"
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(READ_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let request_id = app_server
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
selected_capability_roots: Some(vec![SelectedCapabilityRoot {
|
||||
id: "demo-plugin@1".to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
environment_id: "local".to_string(),
|
||||
path: plugin_dir.path().to_string_lossy().into_owned(),
|
||||
},
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
READ_TIMEOUT,
|
||||
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response(response)?;
|
||||
|
||||
let request_id = app_server
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![UserInput::Text {
|
||||
text: format!("Use ${SKILL_NAME}"),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
READ_TIMEOUT,
|
||||
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
READ_TIMEOUT,
|
||||
app_server.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let request = response_mock.single_request();
|
||||
assert!(
|
||||
request
|
||||
.message_input_texts("developer")
|
||||
.iter()
|
||||
.any(|text| text.contains(SKILL_NAME))
|
||||
);
|
||||
let skill_fragments = request
|
||||
.message_input_texts("user")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<skill>"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(1, skill_fragments.len());
|
||||
let skill_fragment = skill_fragments
|
||||
.first()
|
||||
.expect("executor skill instructions should be model-visible");
|
||||
assert!(skill_fragment.contains(&format!("<name>{SKILL_NAME}</name>")));
|
||||
assert!(skill_fragment.contains(SKILL_MARKER));
|
||||
assert!(!skill_fragment.contains(LOCAL_SKILL_MARKER));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,6 +12,7 @@ mod connection_handling_websocket;
|
||||
#[cfg(unix)]
|
||||
mod connection_handling_websocket_unix;
|
||||
mod dynamic_tools;
|
||||
mod executor_skills;
|
||||
mod experimental_api;
|
||||
mod experimental_feature_list;
|
||||
mod external_agent_config;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2371,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()?;
|
||||
|
||||
@@ -206,7 +206,7 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!(
|
||||
"/backend-api/plugins/{REMOTE_PLUGIN_ID}/uninstall"
|
||||
"/backend-api/ps/plugins/{REMOTE_PLUGIN_ID}/uninstall"
|
||||
)))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
@@ -249,7 +249,7 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("/plugins/{REMOTE_PLUGIN_ID}/uninstall"),
|
||||
&format!("/ps/plugins/{REMOTE_PLUGIN_ID}/uninstall"),
|
||||
/*expected_count*/ 1,
|
||||
)
|
||||
.await?;
|
||||
@@ -278,7 +278,7 @@ async fn plugin_uninstall_uses_detail_scope_for_cache_namespace() -> Result<()>
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!(
|
||||
"/backend-api/plugins/{REMOTE_PLUGIN_ID}/uninstall"
|
||||
"/backend-api/ps/plugins/{REMOTE_PLUGIN_ID}/uninstall"
|
||||
)))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
@@ -321,7 +321,7 @@ async fn plugin_uninstall_uses_detail_scope_for_cache_namespace() -> Result<()>
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("/plugins/{REMOTE_PLUGIN_ID}/uninstall"),
|
||||
&format!("/ps/plugins/{REMOTE_PLUGIN_ID}/uninstall"),
|
||||
/*expected_count*/ 1,
|
||||
)
|
||||
.await?;
|
||||
@@ -357,7 +357,7 @@ async fn plugin_uninstall_accepts_workspace_remote_plugin_id_shape() -> Result<(
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!(
|
||||
"/backend-api/plugins/{WORKSPACE_REMOTE_PLUGIN_ID}/uninstall"
|
||||
"/backend-api/ps/plugins/{WORKSPACE_REMOTE_PLUGIN_ID}/uninstall"
|
||||
)))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
@@ -395,7 +395,7 @@ async fn plugin_uninstall_accepts_workspace_remote_plugin_id_shape() -> Result<(
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("/plugins/{WORKSPACE_REMOTE_PLUGIN_ID}/uninstall"),
|
||||
&format!("/ps/plugins/{WORKSPACE_REMOTE_PLUGIN_ID}/uninstall"),
|
||||
/*expected_count*/ 1,
|
||||
)
|
||||
.await?;
|
||||
@@ -451,7 +451,7 @@ async fn plugin_uninstall_rejects_before_post_when_remote_detail_fetch_fails() -
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("/plugins/{REMOTE_PLUGIN_ID}/uninstall"),
|
||||
&format!("/ps/plugins/{REMOTE_PLUGIN_ID}/uninstall"),
|
||||
/*expected_count*/ 0,
|
||||
)
|
||||
.await?;
|
||||
@@ -487,7 +487,7 @@ async fn plugin_uninstall_rejects_remote_plugin_id_with_spaces_before_network_ca
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
"/plugins/sample plugin/uninstall",
|
||||
"/ps/plugins/sample plugin/uninstall",
|
||||
/*expected_count*/ 0,
|
||||
)
|
||||
.await?;
|
||||
@@ -522,7 +522,7 @@ async fn plugin_uninstall_rejects_invalid_remote_plugin_id_before_network_call()
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
"/plugins/linear/../../oops/uninstall",
|
||||
"/ps/plugins/linear/../../oops/uninstall",
|
||||
/*expected_count*/ 0,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -29,6 +29,7 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -37,6 +38,7 @@ use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
@@ -183,6 +185,122 @@ async fn thread_start_with_non_local_thread_store_does_not_create_local_persiste
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cold_thread_resume_reuses_non_local_history_probe() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
let store_id = Uuid::new_v4().to_string();
|
||||
create_config_toml_with_thread_store(codex_home.path(), &server.uri(), &store_id)?;
|
||||
|
||||
let loader_overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
let config = Arc::new(
|
||||
ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.loader_overrides(loader_overrides.clone())
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
let thread_store = InMemoryThreadStore::for_id(store_id.clone());
|
||||
let _in_memory_store = InMemoryThreadStoreId { store_id };
|
||||
|
||||
let mut client = start_in_process_client(config.clone(), loader_overrides.clone()).await?;
|
||||
let response = client
|
||||
.request(ClientRequest::ThreadStart {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: ThreadStartParams::default(),
|
||||
})
|
||||
.await?
|
||||
.expect("thread/start should succeed");
|
||||
let ThreadStartResponse { thread, .. } = serde_json::from_value(response)?;
|
||||
|
||||
client
|
||||
.request(ClientRequest::TurnStart {
|
||||
request_id: RequestId::Integer(2),
|
||||
params: TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
client_user_message_id: None,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Materialize the thread".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?
|
||||
.expect("turn/start should succeed");
|
||||
timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let Some(event) = client.next_event().await else {
|
||||
anyhow::bail!("in-process app-server stopped before turn/completed");
|
||||
};
|
||||
if let InProcessServerEvent::ServerNotification(ServerNotification::TurnCompleted(
|
||||
completed,
|
||||
)) = event
|
||||
&& completed.thread_id == thread.id
|
||||
{
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
client.shutdown().await?;
|
||||
|
||||
let client = start_in_process_client(config, loader_overrides).await?;
|
||||
let reads_before_resume = thread_store.calls().await.read_thread_with_history;
|
||||
// The in-memory store is pathless, so resume currently fails later while
|
||||
// assembling the response. The history-bearing probe must still be reused.
|
||||
let _resume_result = client
|
||||
.request(ClientRequest::ThreadResume {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: ThreadResumeParams {
|
||||
thread_id: thread.id.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
thread_store.calls().await.read_thread_with_history,
|
||||
reads_before_resume + 1
|
||||
);
|
||||
|
||||
client.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_in_process_client(
|
||||
config: Arc<Config>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
) -> std::io::Result<in_process::InProcessClientHandle> {
|
||||
in_process::start(InProcessStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config,
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides,
|
||||
strict_config: false,
|
||||
cloud_config_bundle: CloudConfigBundleLoader::default(),
|
||||
thread_config_loader: Arc::new(NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
enable_codex_api_key_env: false,
|
||||
initialize: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-app-server-tests".to_string(),
|
||||
title: None,
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
capabilities: None,
|
||||
},
|
||||
channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn assert_no_local_persistence_artifacts(codex_home: &Path) -> Result<()> {
|
||||
// These are the observable tripwires for accidental local persistence. If a
|
||||
// future code path constructs a local rollout/session store or opens the
|
||||
|
||||
@@ -793,6 +793,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
|
||||
thread_source: None,
|
||||
dynamic_tools: None,
|
||||
environments: None,
|
||||
selected_capability_roots: None,
|
||||
mock_experimental_field: None,
|
||||
experimental_raw_events: false,
|
||||
})
|
||||
|
||||
@@ -41,6 +41,7 @@ use codex_app_server_protocol::PatchChangeKind;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ServerRequestResolvedNotification;
|
||||
use codex_app_server_protocol::SubAgentActivityKind;
|
||||
use codex_app_server_protocol::TextElement;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadSource;
|
||||
@@ -53,6 +54,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_app_server_protocol::WarningNotification;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
@@ -3363,6 +3365,134 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_input_to_multi_agent_v2_subagent_is_rejected() -> Result<()> {
|
||||
const CHILD_PROMPT: &str = "child: do work";
|
||||
const PARENT_PROMPT: &str = "spawn a child and continue";
|
||||
const SPAWN_CALL_ID: &str = "spawn-call-direct-input-rejection";
|
||||
const ERROR_MESSAGE: &str =
|
||||
"direct app-server input is not allowed for multi-agent v2 sub-agents";
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let spawn_args = serde_json::to_string(&json!({
|
||||
"message": CHILD_PROMPT,
|
||||
"task_name": "worker",
|
||||
}))?;
|
||||
let _parent_turn = responses::mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, PARENT_PROMPT),
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-parent-1"),
|
||||
responses::ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
|
||||
responses::ev_completed("resp-parent-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::from([(Feature::MultiAgentV2, true)]),
|
||||
)?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("gpt-5.3-codex".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: PARENT_PROMPT.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let _: TurnStartResponse = to_response(turn_resp)?;
|
||||
|
||||
let child_thread_id = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let completed_notif = mcp
|
||||
.read_stream_until_notification_message("item/completed")
|
||||
.await?;
|
||||
let completed: ItemCompletedNotification =
|
||||
serde_json::from_value(completed_notif.params.expect("item/completed params"))?;
|
||||
if let ThreadItem::SubAgentActivity {
|
||||
id,
|
||||
kind: SubAgentActivityKind::Started,
|
||||
agent_thread_id,
|
||||
..
|
||||
} = completed.item
|
||||
&& id == SPAWN_CALL_ID
|
||||
{
|
||||
return Ok::<String, anyhow::Error>(agent_thread_id);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
|
||||
let direct_turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: child_thread_id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "direct app-server turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let direct_turn_error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(direct_turn_req)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(direct_turn_error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(direct_turn_error.error.message, ERROR_MESSAGE);
|
||||
|
||||
let direct_steer_req = mcp
|
||||
.send_turn_steer_request(TurnSteerParams {
|
||||
thread_id: child_thread_id,
|
||||
client_user_message_id: None,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "direct app-server steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: "any-active-turn".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let direct_steer_error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(direct_steer_req)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(direct_steer_error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(direct_steer_error.error.message, ERROR_MESSAGE);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_emits_spawn_agent_item_with_effective_role_model_metadata_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_connectors::merge::merge_plugin_connectors;
|
||||
use codex_core::config::Config;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status;
|
||||
pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools;
|
||||
|
||||
@@ -113,102 +113,6 @@ pub struct McpConnectionManager {
|
||||
}
|
||||
|
||||
impl McpConnectionManager {
|
||||
pub fn new_uninitialized(
|
||||
approval_policy: &Constrained<AskForApproval>,
|
||||
permission_profile: &Constrained<PermissionProfile>,
|
||||
prefix_mcp_tool_names: bool,
|
||||
) -> Self {
|
||||
Self::new_uninitialized_with_permission_profile(
|
||||
approval_policy,
|
||||
permission_profile.get(),
|
||||
prefix_mcp_tool_names,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_uninitialized_with_permission_profile(
|
||||
approval_policy: &Constrained<AskForApproval>,
|
||||
permission_profile: &PermissionProfile,
|
||||
prefix_mcp_tool_names: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
clients: HashMap::new(),
|
||||
server_metadata: HashMap::new(),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
host_owned_codex_apps_enabled: false,
|
||||
prefix_mcp_tool_names,
|
||||
elicitation_requests: ElicitationRequestManager::new(
|
||||
approval_policy.value(),
|
||||
permission_profile.clone(),
|
||||
/*reviewer*/ None,
|
||||
),
|
||||
startup_cancellation_token: CancellationToken::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_servers(&self) -> bool {
|
||||
!self.clients.is_empty()
|
||||
}
|
||||
|
||||
/// Drain all MCP clients from this manager and return a future that stops
|
||||
/// them and terminates their stdio server processes.
|
||||
pub fn begin_shutdown(&mut self) -> impl std::future::Future<Output = ()> + Send + 'static {
|
||||
self.startup_cancellation_token.cancel();
|
||||
let clients = std::mem::take(&mut self.clients);
|
||||
self.server_metadata.clear();
|
||||
async move {
|
||||
for client in clients.into_values() {
|
||||
client.shutdown().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop all MCP clients owned by this manager and terminate stdio server processes.
|
||||
pub async fn shutdown(&mut self) {
|
||||
self.begin_shutdown().await;
|
||||
}
|
||||
|
||||
pub fn server_origin(&self, server_name: &str) -> Option<&str> {
|
||||
self.server_metadata
|
||||
.get(server_name)
|
||||
.and_then(|metadata| metadata.origin.as_ref())
|
||||
.map(super::server::McpServerOrigin::as_str)
|
||||
}
|
||||
|
||||
pub fn server_pollutes_memory(&self, server_name: &str) -> bool {
|
||||
self.server_metadata
|
||||
.get(server_name)
|
||||
.is_none_or(|metadata| metadata.pollutes_memory)
|
||||
}
|
||||
|
||||
pub fn plugin_id_for_mcp_server_name(&self, server_name: &str) -> Option<&str> {
|
||||
self.tool_plugin_provenance
|
||||
.plugin_id_for_mcp_server_name(server_name)
|
||||
}
|
||||
|
||||
pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {
|
||||
self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
}
|
||||
|
||||
pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {
|
||||
if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() {
|
||||
*policy = approval_policy.value();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_permission_profile(&self, permission_profile: PermissionProfile) {
|
||||
if let Ok(mut profile) = self.elicitation_requests.permission_profile.lock() {
|
||||
*profile = permission_profile;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elicitations_auto_deny(&self) -> bool {
|
||||
self.elicitation_requests.auto_deny()
|
||||
}
|
||||
|
||||
pub fn set_elicitations_auto_deny(&self, auto_deny: bool) {
|
||||
self.elicitation_requests.set_auto_deny(auto_deny);
|
||||
}
|
||||
|
||||
#[allow(clippy::new_ret_no_self, clippy::too_many_arguments)]
|
||||
pub async fn new(
|
||||
mcp_servers: &HashMap<String, EffectiveMcpServer>,
|
||||
@@ -364,6 +268,90 @@ impl McpConnectionManager {
|
||||
(manager, cancel_token)
|
||||
}
|
||||
|
||||
pub fn new_uninitialized_with_permission_profile(
|
||||
approval_policy: &Constrained<AskForApproval>,
|
||||
permission_profile: &PermissionProfile,
|
||||
prefix_mcp_tool_names: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
clients: HashMap::new(),
|
||||
server_metadata: HashMap::new(),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
host_owned_codex_apps_enabled: false,
|
||||
prefix_mcp_tool_names,
|
||||
elicitation_requests: ElicitationRequestManager::new(
|
||||
approval_policy.value(),
|
||||
permission_profile.clone(),
|
||||
/*reviewer*/ None,
|
||||
),
|
||||
startup_cancellation_token: CancellationToken::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_servers(&self) -> bool {
|
||||
!self.clients.is_empty()
|
||||
}
|
||||
|
||||
/// Drain all MCP clients from this manager and return a future that stops
|
||||
/// them and terminates their stdio server processes.
|
||||
pub fn begin_shutdown(&mut self) -> impl std::future::Future<Output = ()> + Send + 'static {
|
||||
self.startup_cancellation_token.cancel();
|
||||
let clients = std::mem::take(&mut self.clients);
|
||||
self.server_metadata.clear();
|
||||
async move {
|
||||
for client in clients.into_values() {
|
||||
client.shutdown().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop all MCP clients owned by this manager and terminate stdio server processes.
|
||||
pub async fn shutdown(&mut self) {
|
||||
self.begin_shutdown().await;
|
||||
}
|
||||
|
||||
pub fn server_origin(&self, server_name: &str) -> Option<&str> {
|
||||
self.server_metadata
|
||||
.get(server_name)
|
||||
.and_then(|metadata| metadata.origin.as_ref())
|
||||
.map(super::server::McpServerOrigin::as_str)
|
||||
}
|
||||
|
||||
pub fn server_pollutes_memory(&self, server_name: &str) -> bool {
|
||||
self.server_metadata
|
||||
.get(server_name)
|
||||
.is_none_or(|metadata| metadata.pollutes_memory)
|
||||
}
|
||||
|
||||
pub fn plugin_id_for_mcp_server_name(&self, server_name: &str) -> Option<&str> {
|
||||
self.tool_plugin_provenance
|
||||
.plugin_id_for_mcp_server_name(server_name)
|
||||
}
|
||||
|
||||
pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {
|
||||
self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
}
|
||||
|
||||
pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {
|
||||
if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() {
|
||||
*policy = approval_policy.value();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_permission_profile(&self, permission_profile: PermissionProfile) {
|
||||
if let Ok(mut profile) = self.elicitation_requests.permission_profile.lock() {
|
||||
*profile = permission_profile;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elicitations_auto_deny(&self) -> bool {
|
||||
self.elicitation_requests.auto_deny()
|
||||
}
|
||||
|
||||
pub fn set_elicitations_auto_deny(&self, auto_deny: bool) {
|
||||
self.elicitation_requests.set_auto_deny(auto_deny);
|
||||
}
|
||||
|
||||
pub async fn resolve_elicitation(
|
||||
&self,
|
||||
server_name: String,
|
||||
@@ -452,31 +440,6 @@ impl McpConnectionManager {
|
||||
normalize_tools_for_model_with_prefix(tools, self.prefix_mcp_tool_names)
|
||||
}
|
||||
|
||||
/// Returns presentation metadata without waiting for uncached clients still initializing.
|
||||
/// Cached values will be used if available and the server is still starting up.
|
||||
pub async fn list_available_server_infos(&self) -> HashMap<String, McpServerInfo> {
|
||||
let mut server_infos = HashMap::new();
|
||||
for (server_name, client) in &self.clients {
|
||||
if !client.startup_complete.load(Ordering::Acquire) {
|
||||
if let Some(server_info) = client.cached_server_info.clone() {
|
||||
server_infos.insert(server_name.clone(), server_info);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match client.client().await {
|
||||
Ok(managed_client) => {
|
||||
server_infos.insert(server_name.clone(), managed_client.server_info);
|
||||
}
|
||||
Err(_) => {
|
||||
if let Some(server_info) = client.cached_server_info.clone() {
|
||||
server_infos.insert(server_name.clone(), server_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
server_infos
|
||||
}
|
||||
|
||||
/// Force-refresh codex apps tools by bypassing the in-process cache.
|
||||
///
|
||||
/// On success, the refreshed tools replace the cache contents and the
|
||||
@@ -532,21 +495,6 @@ impl McpConnectionManager {
|
||||
))
|
||||
}
|
||||
|
||||
fn with_server_metadata(&self, mut tool: ToolInfo) -> ToolInfo {
|
||||
let Some(metadata) = self.server_metadata.get(&tool.server_name) else {
|
||||
tool.supports_parallel_tool_calls = false;
|
||||
tool.server_origin = None;
|
||||
return tool;
|
||||
};
|
||||
|
||||
tool.supports_parallel_tool_calls = metadata.supports_parallel_tool_calls;
|
||||
tool.server_origin = metadata
|
||||
.origin
|
||||
.as_ref()
|
||||
.map(|origin| origin.as_str().to_string());
|
||||
tool
|
||||
}
|
||||
|
||||
/// Returns a single map that contains all resources. Each key is the
|
||||
/// server name and the value is a vector of resources.
|
||||
pub async fn list_all_resources(&self) -> HashMap<String, Vec<Resource>> {
|
||||
@@ -778,6 +726,46 @@ impl McpConnectionManager {
|
||||
.with_context(|| format!("resources/read failed for `{server}` ({uri})"))
|
||||
}
|
||||
|
||||
/// Returns presentation metadata without waiting for uncached clients still initializing.
|
||||
/// Cached values will be used if available and the server is still starting up.
|
||||
pub(crate) async fn list_available_server_infos(&self) -> HashMap<String, McpServerInfo> {
|
||||
let mut server_infos = HashMap::new();
|
||||
for (server_name, client) in &self.clients {
|
||||
if !client.startup_complete.load(Ordering::Acquire) {
|
||||
if let Some(server_info) = client.cached_server_info.clone() {
|
||||
server_infos.insert(server_name.clone(), server_info);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match client.client().await {
|
||||
Ok(managed_client) => {
|
||||
server_infos.insert(server_name.clone(), managed_client.server_info);
|
||||
}
|
||||
Err(_) => {
|
||||
if let Some(server_info) = client.cached_server_info.clone() {
|
||||
server_infos.insert(server_name.clone(), server_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
server_infos
|
||||
}
|
||||
|
||||
fn with_server_metadata(&self, mut tool: ToolInfo) -> ToolInfo {
|
||||
let Some(metadata) = self.server_metadata.get(&tool.server_name) else {
|
||||
tool.supports_parallel_tool_calls = false;
|
||||
tool.server_origin = None;
|
||||
return tool;
|
||||
};
|
||||
|
||||
tool.supports_parallel_tool_calls = metadata.supports_parallel_tool_calls;
|
||||
tool.server_origin = metadata
|
||||
.origin
|
||||
.as_ref()
|
||||
.map(|origin| origin.as_str().to_string());
|
||||
tool
|
||||
}
|
||||
|
||||
async fn client_by_name(&self, name: &str) -> Result<ManagedClient> {
|
||||
self.clients
|
||||
.get(name)
|
||||
@@ -786,6 +774,19 @@ impl McpConnectionManager {
|
||||
.await
|
||||
.context("failed to get client")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn new_uninitialized(
|
||||
approval_policy: &Constrained<AskForApproval>,
|
||||
permission_profile: &Constrained<PermissionProfile>,
|
||||
prefix_mcp_tool_names: bool,
|
||||
) -> Self {
|
||||
Self::new_uninitialized_with_permission_profile(
|
||||
approval_policy,
|
||||
permission_profile.get(),
|
||||
prefix_mcp_tool_names,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for McpConnectionManager {
|
||||
|
||||
@@ -24,6 +24,7 @@ pub use auth_elicitation::build_auth_elicitation_plan;
|
||||
pub use auth_elicitation::connector_auth_failure_from_tool_result;
|
||||
pub use codex_apps::CodexAppsToolsCacheKey;
|
||||
pub use codex_apps::codex_apps_tools_cache_key;
|
||||
pub use mcp::codex_apps_mcp_server_config;
|
||||
pub use mcp::configured_mcp_servers;
|
||||
pub use mcp::effective_mcp_servers;
|
||||
pub use mcp::effective_mcp_servers_from_configured;
|
||||
|
||||
@@ -131,6 +131,11 @@ pub struct McpConfig {
|
||||
/// ChatGPT auth is checked separately at runtime before the host-owned apps
|
||||
/// MCP server is added.
|
||||
pub apps_enabled: bool,
|
||||
/// Whether to synthesize the legacy host-owned Apps MCP server.
|
||||
///
|
||||
/// Hosts that install an MCP extension for this server disable the legacy
|
||||
/// loader and contribute the server through the normal runtime overlay.
|
||||
pub legacy_apps_mcp_loader_enabled: bool,
|
||||
/// Whether model-visible MCP tool namespaces should keep the legacy
|
||||
/// `mcp__` prefix.
|
||||
pub prefix_mcp_tool_names: bool,
|
||||
@@ -218,10 +223,20 @@ pub fn with_codex_apps_mcp(
|
||||
auth: Option<&CodexAuth>,
|
||||
config: &McpConfig,
|
||||
) -> HashMap<String, EffectiveMcpServer> {
|
||||
if !config.legacy_apps_mcp_loader_enabled {
|
||||
if !host_owned_codex_apps_enabled(config, auth) {
|
||||
servers.remove(CODEX_APPS_MCP_SERVER_NAME);
|
||||
}
|
||||
return servers;
|
||||
}
|
||||
if host_owned_codex_apps_enabled(config, auth) {
|
||||
servers.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
EffectiveMcpServer::configured(codex_apps_mcp_server_config(config)),
|
||||
EffectiveMcpServer::configured(codex_apps_mcp_server_config(
|
||||
&config.chatgpt_base_url,
|
||||
config.apps_mcp_path_override.as_deref(),
|
||||
config.apps_mcp_product_sku.as_deref(),
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
servers.remove(CODEX_APPS_MCP_SERVER_NAME);
|
||||
@@ -381,13 +396,6 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
snapshot
|
||||
}
|
||||
|
||||
pub(crate) fn codex_apps_mcp_url(config: &McpConfig) -> String {
|
||||
codex_apps_mcp_url_for_base_url(
|
||||
&config.chatgpt_base_url,
|
||||
config.apps_mcp_path_override.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The Responses API requires tool names to match `^[a-zA-Z0-9_-]+$`.
|
||||
/// MCP server/tool names are user-controlled, so sanitize the fully-qualified
|
||||
/// name we expose to the model by replacing any disallowed character with `_`.
|
||||
@@ -443,10 +451,14 @@ fn codex_apps_mcp_url_for_base_url(base_url: &str, apps_mcp_path_override: Optio
|
||||
format!("{base_url}/{path}")
|
||||
}
|
||||
|
||||
fn codex_apps_mcp_server_config(config: &McpConfig) -> McpServerConfig {
|
||||
let url = codex_apps_mcp_url(config);
|
||||
let http_headers = config.apps_mcp_product_sku.as_ref().map(|product_sku| {
|
||||
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.clone())])
|
||||
pub fn codex_apps_mcp_server_config(
|
||||
chatgpt_base_url: &str,
|
||||
apps_mcp_path_override: Option<&str>,
|
||||
apps_mcp_product_sku: Option<&str>,
|
||||
) -> McpServerConfig {
|
||||
let url = codex_apps_mcp_url_for_base_url(chatgpt_base_url, apps_mcp_path_override);
|
||||
let http_headers = apps_mcp_product_sku.map(|product_sku| {
|
||||
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())])
|
||||
});
|
||||
|
||||
McpServerConfig {
|
||||
|
||||
@@ -27,6 +27,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
apps_enabled: false,
|
||||
legacy_apps_mcp_loader_enabled: true,
|
||||
prefix_mcp_tool_names: true,
|
||||
client_elicitation_capability: ElicitationCapability::default(),
|
||||
configured_mcp_servers: HashMap::new(),
|
||||
@@ -206,16 +207,6 @@ fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_mcp_url_uses_legacy_codex_apps_path() {
|
||||
let config = test_mcp_config(PathBuf::from("/tmp"));
|
||||
|
||||
assert_eq!(
|
||||
codex_apps_mcp_url(&config),
|
||||
"https://chatgpt.com/backend-api/wham/apps"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_config_uses_legacy_codex_apps_path() {
|
||||
let mut config = test_mcp_config(PathBuf::from("/tmp"));
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -560,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(
|
||||
@@ -599,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(
|
||||
@@ -607,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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
@@ -845,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())
|
||||
@@ -1071,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.
|
||||
@@ -1079,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 {
|
||||
@@ -1095,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(
|
||||
@@ -1113,7 +1124,7 @@ pub async fn uninstall_remote_plugin(
|
||||
let plugin_name = plugin.name;
|
||||
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/plugins/{plugin_id}/uninstall");
|
||||
let url = format!("{base_url}/ps/plugins/{plugin_id}/uninstall");
|
||||
let client = build_reqwest_client();
|
||||
let request = authenticated_request(client.post(&url), auth)?;
|
||||
let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?;
|
||||
|
||||
@@ -169,8 +169,8 @@ where
|
||||
let mut file_systems_by_skill_path: HashMap<AbsolutePathBuf, Arc<dyn ExecutorFileSystem>> =
|
||||
HashMap::new();
|
||||
for root in roots {
|
||||
let root_path = canonicalize_for_skill_identity(&root.path);
|
||||
let fs = root.file_system;
|
||||
let root_path = canonicalize_for_skill_identity(fs.as_ref(), &root.path).await;
|
||||
let skills_before_root = outcome.skills.len();
|
||||
discover_skills_under_root(
|
||||
fs.as_ref(),
|
||||
@@ -471,8 +471,13 @@ fn dedupe_skill_roots_by_path(roots: &mut Vec<SkillRoot>) {
|
||||
roots.retain(|root| seen.insert(root.path.clone()));
|
||||
}
|
||||
|
||||
fn canonicalize_for_skill_identity(path: &AbsolutePathBuf) -> AbsolutePathBuf {
|
||||
path.canonicalize().unwrap_or_else(|_| path.clone())
|
||||
async fn canonicalize_for_skill_identity(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
path: &AbsolutePathBuf,
|
||||
) -> AbsolutePathBuf {
|
||||
fs.canonicalize(path, /*sandbox*/ None)
|
||||
.await
|
||||
.unwrap_or_else(|_| path.clone())
|
||||
}
|
||||
|
||||
async fn discover_skills_under_root(
|
||||
@@ -483,8 +488,11 @@ async fn discover_skills_under_root(
|
||||
plugin_root: Option<&AbsolutePathBuf>,
|
||||
outcome: &mut SkillLoadOutcome,
|
||||
) {
|
||||
let root = canonicalize_for_skill_identity(root);
|
||||
let plugin_root = plugin_root.map(canonicalize_for_skill_identity);
|
||||
let root = root.clone();
|
||||
let plugin_root = match plugin_root {
|
||||
Some(plugin_root) => Some(canonicalize_for_skill_identity(fs, plugin_root).await),
|
||||
None => None,
|
||||
};
|
||||
|
||||
match fs.get_metadata(&root, /*sandbox*/ None).await {
|
||||
Ok(metadata) if metadata.is_directory => {}
|
||||
@@ -557,7 +565,7 @@ async fn discover_skills_under_root(
|
||||
}
|
||||
match fs.read_directory(&path, /*sandbox*/ None).await {
|
||||
Ok(_) => {
|
||||
let resolved_dir = canonicalize_for_skill_identity(&path);
|
||||
let resolved_dir = canonicalize_for_skill_identity(fs, &path).await;
|
||||
enqueue_dir(
|
||||
&mut queue,
|
||||
&mut visited_dirs,
|
||||
@@ -582,7 +590,7 @@ async fn discover_skills_under_root(
|
||||
}
|
||||
|
||||
if metadata.is_directory {
|
||||
let resolved_dir = canonicalize_for_skill_identity(&path);
|
||||
let resolved_dir = canonicalize_for_skill_identity(fs, &path).await;
|
||||
enqueue_dir(
|
||||
&mut queue,
|
||||
&mut visited_dirs,
|
||||
@@ -672,7 +680,7 @@ async fn parse_skill_file(
|
||||
)?;
|
||||
}
|
||||
|
||||
let resolved_path = canonicalize_for_skill_identity(path);
|
||||
let resolved_path = canonicalize_for_skill_identity(fs, path).await;
|
||||
|
||||
Ok(SkillMetadata {
|
||||
name,
|
||||
|
||||
@@ -103,6 +103,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
parent_rollout_thread_trace: codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||
parent_trace: None,
|
||||
environment_selections: parent_ctx.environments.clone(),
|
||||
thread_extension_init: codex_extension_api::ExtensionDataInit::default(),
|
||||
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
|
||||
thread_store: Arc::clone(&parent_session.services.thread_store),
|
||||
attestation_provider: parent_session.services.attestation_provider.clone(),
|
||||
|
||||
@@ -1428,6 +1428,7 @@ impl Config {
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: self.features.use_legacy_landlock(),
|
||||
apps_enabled: self.features.enabled(Feature::Apps),
|
||||
legacy_apps_mcp_loader_enabled: true,
|
||||
prefix_mcp_tool_names: self.prefix_mcp_tool_names(),
|
||||
client_elicitation_capability: if self.features.enabled(Feature::AuthElicitation) {
|
||||
ElicitationCapability {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
@@ -41,8 +40,8 @@ use codex_mcp::ToolInfo;
|
||||
use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::codex_apps_tools_cache_key;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
use codex_mcp::host_owned_codex_apps_enabled;
|
||||
use codex_mcp::with_codex_apps_mcp;
|
||||
|
||||
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
||||
|
||||
@@ -220,6 +219,23 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
config: &Config,
|
||||
force_refetch: bool,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
||||
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
|
||||
let mcp_manager = Arc::new(McpManager::new(plugins_manager));
|
||||
list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config,
|
||||
force_refetch,
|
||||
environment_manager,
|
||||
mcp_manager,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config: &Config,
|
||||
force_refetch: bool,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
@@ -234,8 +250,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
});
|
||||
}
|
||||
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
||||
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
|
||||
let mcp_manager = McpManager::new(Arc::clone(&plugins_manager));
|
||||
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config).await;
|
||||
if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key)
|
||||
{
|
||||
@@ -250,8 +264,9 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
});
|
||||
}
|
||||
|
||||
let mcp_config = config.to_mcp_config(plugins_manager.as_ref()).await;
|
||||
let mcp_servers = with_codex_apps_mcp(HashMap::new(), auth.as_ref(), &mcp_config);
|
||||
let mcp_config = mcp_manager.runtime_config(config).await;
|
||||
let mut mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
|
||||
mcp_servers.retain(|name, _| name == CODEX_APPS_MCP_SERVER_NAME);
|
||||
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(&mcp_config, auth.as_ref());
|
||||
if mcp_servers.is_empty() {
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -4,8 +4,12 @@ use std::sync::Arc;
|
||||
use crate::config::Config;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::McpServerContribution;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::EffectiveMcpServer;
|
||||
use codex_mcp::McpConfig;
|
||||
use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::configured_mcp_servers;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
@@ -14,29 +18,91 @@ use codex_mcp::tool_plugin_provenance as collect_tool_plugin_provenance;
|
||||
#[derive(Clone)]
|
||||
pub struct McpManager {
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
extensions: Arc<ExtensionRegistry<Config>>,
|
||||
}
|
||||
|
||||
impl McpManager {
|
||||
pub fn new(plugins_manager: Arc<PluginsManager>) -> Self {
|
||||
Self { plugins_manager }
|
||||
Self {
|
||||
plugins_manager,
|
||||
extensions: codex_extension_api::empty_extension_registry(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a manager that resolves host-installed MCP contributions.
|
||||
pub fn new_with_extensions(
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
extensions: Arc<ExtensionRegistry<Config>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
plugins_manager,
|
||||
extensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the MCP config after applying runtime-only extension overlays.
|
||||
pub async fn runtime_config(&self, config: &Config) -> McpConfig {
|
||||
let mut mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
let contributions = self.contributions(config).await;
|
||||
if contributions
|
||||
.iter()
|
||||
.any(|contribution| contribution.name() == CODEX_APPS_MCP_SERVER_NAME)
|
||||
{
|
||||
mcp_config.legacy_apps_mcp_loader_enabled = false;
|
||||
}
|
||||
Self::apply_to_configured_servers(&contributions, &mut mcp_config.configured_mcp_servers);
|
||||
mcp_config
|
||||
}
|
||||
|
||||
/// Returns config- and plugin-backed servers without runtime contributions.
|
||||
pub async fn configured_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
||||
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
configured_mcp_servers(&mcp_config)
|
||||
}
|
||||
|
||||
/// Returns configured and host-contributed servers before auth gating.
|
||||
pub async fn runtime_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
||||
let mcp_config = self.runtime_config(config).await;
|
||||
configured_mcp_servers(&mcp_config)
|
||||
}
|
||||
|
||||
/// Returns runtime servers after auth gating and compatibility built-ins.
|
||||
pub async fn effective_servers(
|
||||
&self,
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<String, EffectiveMcpServer> {
|
||||
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
let mcp_config = self.runtime_config(config).await;
|
||||
effective_mcp_servers(&mcp_config, auth)
|
||||
}
|
||||
|
||||
/// Returns provenance for plugin-owned servers in the configured view.
|
||||
pub async fn tool_plugin_provenance(&self, config: &Config) -> ToolPluginProvenance {
|
||||
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
collect_tool_plugin_provenance(&mcp_config)
|
||||
}
|
||||
|
||||
async fn contributions(&self, config: &Config) -> Vec<McpServerContribution> {
|
||||
let mut contributions = Vec::new();
|
||||
for contributor in self.extensions.mcp_server_contributors() {
|
||||
contributions.extend(contributor.contribute(config).await);
|
||||
}
|
||||
contributions
|
||||
}
|
||||
|
||||
fn apply_to_configured_servers(
|
||||
contributions: &[McpServerContribution],
|
||||
servers: &mut HashMap<String, McpServerConfig>,
|
||||
) {
|
||||
for contribution in contributions {
|
||||
match contribution {
|
||||
McpServerContribution::Set { name, config } => {
|
||||
servers.insert(name.clone(), config.as_ref().clone());
|
||||
}
|
||||
McpServerContribution::Remove { name } => {
|
||||
servers.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(crate) async fn maybe_prompt_and_install_mcp_dependencies(
|
||||
let installed = sess
|
||||
.services
|
||||
.mcp_manager
|
||||
.configured_servers(config.as_ref())
|
||||
.runtime_servers(config.as_ref())
|
||||
.await;
|
||||
let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed);
|
||||
if missing.is_empty() {
|
||||
@@ -98,7 +98,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
}
|
||||
|
||||
let codex_home = config.codex_home.clone();
|
||||
let installed = sess.services.mcp_manager.configured_servers(config).await;
|
||||
let installed = sess.services.mcp_manager.runtime_servers(config).await;
|
||||
let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed);
|
||||
if missing.is_empty() {
|
||||
return;
|
||||
@@ -190,16 +190,22 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh from the config-backed merged MCP map (global + repo + managed)
|
||||
// and overlay the updated global servers so we don't drop repo-scoped
|
||||
// servers. Runtime additions such as built-ins are rebuilt by the refresh
|
||||
// path from the current config.
|
||||
let mut refresh_servers = sess.services.mcp_manager.configured_servers(config).await;
|
||||
let mut refresh_config = config.clone();
|
||||
let mut configured_servers = config.mcp_servers.get().clone();
|
||||
for (name, server_config) in &servers {
|
||||
refresh_servers
|
||||
configured_servers
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| server_config.clone());
|
||||
}
|
||||
if let Err(err) = refresh_config.mcp_servers.set(configured_servers) {
|
||||
warn!("failed to refresh MCP dependencies for mentioned skills: {err}");
|
||||
return;
|
||||
}
|
||||
let refresh_servers = sess
|
||||
.services
|
||||
.mcp_manager
|
||||
.runtime_servers(&refresh_config)
|
||||
.await;
|
||||
sess.refresh_mcp_servers_now(
|
||||
turn_context,
|
||||
refresh_servers,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::client::ModelClient;
|
||||
use crate::realtime_context::build_realtime_startup_context;
|
||||
use crate::realtime_context::truncate_realtime_text_to_token_budget;
|
||||
use crate::realtime_prompt::prepare_realtime_backend_prompt;
|
||||
use crate::session::session::Session;
|
||||
use anyhow::Context;
|
||||
@@ -64,6 +65,7 @@ const USER_TEXT_IN_QUEUE_CAPACITY: usize = 64;
|
||||
const ASSISTANT_OUTPUT_QUEUE_CAPACITY: usize = 64;
|
||||
const OUTPUT_EVENTS_QUEUE_CAPACITY: usize = 256;
|
||||
const REALTIME_STARTUP_CONTEXT_TOKEN_BUDGET: usize = 5_300;
|
||||
const REALTIME_ASSISTANT_OUTPUT_TOKEN_BUDGET: usize = 1_000;
|
||||
const DEFAULT_REALTIME_MODEL: &str = "gpt-realtime-1.5";
|
||||
pub(crate) const REALTIME_USER_TEXT_PREFIX: &str = "[USER] ";
|
||||
const DEFAULT_HANDOFF_ID: &str = "codex";
|
||||
@@ -202,7 +204,10 @@ impl RealtimeHandoffState {
|
||||
async fn assistant_output(&self, output_text: String) -> RealtimeAssistantOutput {
|
||||
RealtimeAssistantOutput {
|
||||
handoff_id: self.active_handoff.lock().await.clone(),
|
||||
output_text,
|
||||
output_text: truncate_realtime_text_to_token_budget(
|
||||
&output_text,
|
||||
REALTIME_ASSISTANT_OUTPUT_TOKEN_BUDGET,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use async_channel::bounded;
|
||||
use codex_config::config_toml::RealtimeWsVersion;
|
||||
use codex_protocol::protocol::RealtimeHandoffRequested;
|
||||
use codex_protocol::protocol::RealtimeTranscriptEntry;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
@@ -178,6 +179,22 @@ async fn assistant_output_without_handoff_has_no_active_id() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn assistant_output_is_capped_for_realtime_context() {
|
||||
let (tx, _rx) = bounded(1);
|
||||
let state = RealtimeHandoffState::new(tx);
|
||||
*state.active_handoff.lock().await = Some("handoff_1".to_string());
|
||||
let output = state
|
||||
.assistant_output(format!("start {} end", "middle ".repeat(2_000)))
|
||||
.await;
|
||||
|
||||
assert_eq!(output.handoff_id.as_deref(), Some("handoff_1"));
|
||||
assert!(approx_token_count(&output.output_text) <= 1_000);
|
||||
assert!(output.output_text.starts_with("start "));
|
||||
assert!(output.output_text.ends_with(" end"));
|
||||
assert!(output.output_text.contains("tokens truncated"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn finishing_turn_consumes_handoff() {
|
||||
let (tx, _rx) = bounded(1);
|
||||
|
||||
@@ -311,8 +311,10 @@ impl Session {
|
||||
) {
|
||||
let auth = self.services.auth_manager.auth().await;
|
||||
let config = self.get_config().await;
|
||||
let mcp_config = config
|
||||
.to_mcp_config(self.services.plugins_manager.as_ref())
|
||||
let mcp_config = self
|
||||
.services
|
||||
.mcp_manager
|
||||
.runtime_config(config.as_ref())
|
||||
.await;
|
||||
let tool_plugin_provenance = self
|
||||
.services
|
||||
|
||||
@@ -51,6 +51,7 @@ use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::FileSystemSandboxContext;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_extension_api::PromptSlot;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
@@ -416,6 +417,7 @@ pub(crate) struct CodexSpawnArgs {
|
||||
pub(crate) user_shell_override: Option<shell::Shell>,
|
||||
pub(crate) parent_trace: Option<W3cTraceContext>,
|
||||
pub(crate) environment_selections: ResolvedTurnEnvironments,
|
||||
pub(crate) thread_extension_init: ExtensionDataInit,
|
||||
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
pub(crate) thread_store: Arc<dyn ThreadStore>,
|
||||
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||
@@ -496,6 +498,7 @@ impl Codex {
|
||||
parent_rollout_thread_trace,
|
||||
parent_trace: _,
|
||||
environment_selections,
|
||||
thread_extension_init,
|
||||
analytics_events_client,
|
||||
thread_store,
|
||||
attestation_provider,
|
||||
@@ -640,6 +643,7 @@ impl Codex {
|
||||
plugins_manager,
|
||||
mcp_manager.clone(),
|
||||
extensions,
|
||||
thread_extension_init,
|
||||
agent_control,
|
||||
environment_manager,
|
||||
analytics_events_client,
|
||||
|
||||
@@ -91,7 +91,7 @@ pub(super) async fn spawn_review_thread(
|
||||
forked_from_thread_id,
|
||||
parent_turn_context.parent_thread_id,
|
||||
&session_source,
|
||||
parent_turn_context.thread_source,
|
||||
parent_turn_context.thread_source.clone(),
|
||||
review_turn_id.clone(),
|
||||
#[allow(deprecated)]
|
||||
parent_turn_context.cwd.clone(),
|
||||
@@ -121,7 +121,7 @@ pub(super) async fn spawn_review_thread(
|
||||
reasoning_summary,
|
||||
session_source,
|
||||
parent_thread_id: parent_turn_context.parent_thread_id,
|
||||
thread_source: parent_turn_context.thread_source,
|
||||
thread_source: parent_turn_context.thread_source.clone(),
|
||||
environments: parent_turn_context.environments.clone(),
|
||||
available_models,
|
||||
unified_exec_shell_mode,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::agents_md::LoadedAgentsMd;
|
||||
use crate::config::ConstraintError;
|
||||
use crate::skills::SkillError;
|
||||
use crate::state::ActiveTurn;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
@@ -192,7 +193,7 @@ impl SessionConfiguration {
|
||||
session_source: self.session_source.clone(),
|
||||
forked_from_thread_id: self.forked_from_thread_id,
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
thread_source: self.thread_source,
|
||||
thread_source: self.thread_source.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,6 +488,7 @@ impl Session {
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>,
|
||||
thread_extension_init: ExtensionDataInit,
|
||||
agent_control: AgentControl,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
@@ -547,7 +549,7 @@ impl Session {
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
source: session_source,
|
||||
thread_source: session_configuration.thread_source,
|
||||
thread_source: session_configuration.thread_source.clone(),
|
||||
base_instructions: BaseInstructions {
|
||||
text: session_configuration.base_instructions.clone(),
|
||||
},
|
||||
@@ -961,8 +963,10 @@ impl Session {
|
||||
);
|
||||
let session_extension_data =
|
||||
codex_extension_api::ExtensionData::new(session_id.to_string());
|
||||
let thread_extension_data =
|
||||
codex_extension_api::ExtensionData::new(thread_id.to_string());
|
||||
let thread_extension_data = codex_extension_api::ExtensionData::new_with_init(
|
||||
thread_id.to_string(),
|
||||
thread_extension_init,
|
||||
);
|
||||
for contributor in extensions.thread_lifecycle_contributors() {
|
||||
contributor.on_thread_start(codex_extension_api::ThreadStartInput {
|
||||
config: config.as_ref(),
|
||||
@@ -1085,7 +1089,7 @@ impl Session {
|
||||
thread_id,
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
thread_source: session_configuration.thread_source,
|
||||
thread_source: session_configuration.thread_source.clone(),
|
||||
thread_name: session_configuration.thread_name.clone(),
|
||||
model: session_configuration.collaboration_mode.model().to_string(),
|
||||
model_provider_id: config.model_provider_id.clone(),
|
||||
|
||||
@@ -4690,6 +4690,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
AgentControl::default(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
@@ -5030,6 +5031,7 @@ async fn make_session_with_config_and_rx(
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
AgentControl::default(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
@@ -5131,6 +5133,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
agent_control,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
|
||||
@@ -731,6 +731,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
environment_selections: ResolvedTurnEnvironments {
|
||||
turn_environments: Vec::new(),
|
||||
},
|
||||
thread_extension_init: codex_extension_api::ExtensionDataInit::default(),
|
||||
analytics_events_client: None,
|
||||
thread_store,
|
||||
attestation_provider: None,
|
||||
|
||||
@@ -239,7 +239,7 @@ impl TurnContext {
|
||||
reasoning_summary: self.reasoning_summary,
|
||||
session_source: self.session_source.clone(),
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
thread_source: self.thread_source,
|
||||
thread_source: self.thread_source.clone(),
|
||||
environments: self.environments.clone(),
|
||||
#[allow(deprecated)]
|
||||
cwd: self.cwd.clone(),
|
||||
@@ -514,7 +514,7 @@ impl Session {
|
||||
session_configuration.forked_from_thread_id,
|
||||
session_configuration.parent_thread_id,
|
||||
&session_configuration.session_source,
|
||||
session_configuration.thread_source,
|
||||
session_configuration.thread_source.clone(),
|
||||
sub_id.clone(),
|
||||
cwd.clone(),
|
||||
&session_configuration.permission_profile(),
|
||||
@@ -538,7 +538,7 @@ impl Session {
|
||||
reasoning_summary,
|
||||
session_source,
|
||||
parent_thread_id: session_configuration.parent_thread_id,
|
||||
thread_source: session_configuration.thread_source,
|
||||
thread_source: session_configuration.thread_source.clone(),
|
||||
environments,
|
||||
#[allow(deprecated)]
|
||||
cwd,
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
use codex_features::Feature;
|
||||
@@ -182,6 +183,7 @@ pub struct StartThreadOptions {
|
||||
pub metrics_service_name: Option<String>,
|
||||
pub parent_trace: Option<W3cTraceContext>,
|
||||
pub environments: Vec<TurnEnvironmentSelection>,
|
||||
pub thread_extension_init: ExtensionDataInit,
|
||||
}
|
||||
|
||||
pub(crate) struct ResumeThreadWithHistoryOptions {
|
||||
@@ -270,7 +272,10 @@ impl ThreadManager {
|
||||
codex_home.to_path_buf(),
|
||||
restriction_product,
|
||||
));
|
||||
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
|
||||
let mcp_manager = Arc::new(McpManager::new_with_extensions(
|
||||
Arc::clone(&plugins_manager),
|
||||
Arc::clone(&extensions),
|
||||
));
|
||||
let skills_manager = Arc::new(SkillsManager::new_with_restriction_product(
|
||||
codex_home,
|
||||
config.bundled_skills_enabled(),
|
||||
@@ -576,6 +581,7 @@ impl ThreadManager {
|
||||
metrics_service_name: None,
|
||||
parent_trace: None,
|
||||
environments,
|
||||
thread_extension_init: ExtensionDataInit::default(),
|
||||
}))
|
||||
.await
|
||||
}
|
||||
@@ -614,6 +620,7 @@ impl ThreadManager {
|
||||
/*inherited_exec_policy*/ None,
|
||||
options.parent_trace,
|
||||
options.environments,
|
||||
options.thread_extension_init,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -702,6 +709,7 @@ impl ThreadManager {
|
||||
/*inherited_exec_policy*/ None,
|
||||
parent_trace,
|
||||
environments,
|
||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -728,6 +736,7 @@ impl ThreadManager {
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
environments,
|
||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||
/*user_shell_override*/ Some(user_shell_override),
|
||||
))
|
||||
.await
|
||||
@@ -763,6 +772,7 @@ impl ThreadManager {
|
||||
/*inherited_exec_policy*/ None,
|
||||
/*parent_trace*/ None,
|
||||
environments,
|
||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||
/*user_shell_override*/ Some(user_shell_override),
|
||||
))
|
||||
.await
|
||||
@@ -931,6 +941,7 @@ impl ThreadManager {
|
||||
/*metrics_service_name*/ None,
|
||||
parent_trace,
|
||||
environments,
|
||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -1136,6 +1147,7 @@ impl ThreadManagerState {
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
environments,
|
||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -1172,6 +1184,7 @@ impl ThreadManagerState {
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
environments,
|
||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -1209,6 +1222,7 @@ impl ThreadManagerState {
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
environments,
|
||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -1229,6 +1243,7 @@ impl ThreadManagerState {
|
||||
metrics_service_name: Option<String>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
environments: Vec<TurnEnvironmentSelection>,
|
||||
thread_extension_init: ExtensionDataInit,
|
||||
user_shell_override: Option<crate::shell::Shell>,
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.spawn_thread_with_source(
|
||||
@@ -1246,6 +1261,7 @@ impl ThreadManagerState {
|
||||
/*inherited_exec_policy*/ None,
|
||||
parent_trace,
|
||||
environments,
|
||||
thread_extension_init,
|
||||
user_shell_override,
|
||||
))
|
||||
.await
|
||||
@@ -1268,6 +1284,7 @@ impl ThreadManagerState {
|
||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
environments: Vec<TurnEnvironmentSelection>,
|
||||
thread_extension_init: ExtensionDataInit,
|
||||
user_shell_override: Option<crate::shell::Shell>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let is_resumed_thread = matches!(&initial_history, InitialHistory::Resumed(_));
|
||||
@@ -1332,6 +1349,7 @@ impl ThreadManagerState {
|
||||
user_shell_override,
|
||||
parent_trace,
|
||||
environment_selections,
|
||||
thread_extension_init,
|
||||
analytics_events_client: self.analytics_events_client.clone(),
|
||||
thread_store: Arc::clone(&self.thread_store),
|
||||
attestation_provider: self.attestation_provider.clone(),
|
||||
|
||||
@@ -331,6 +331,7 @@ async fn start_thread_rejects_explicit_local_environment_when_default_provider_i
|
||||
environment_id: "local".to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}],
|
||||
thread_extension_init: Default::default(),
|
||||
})
|
||||
.await;
|
||||
let err = match result {
|
||||
@@ -368,6 +369,7 @@ async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
|
||||
metrics_service_name: None,
|
||||
parent_trace: None,
|
||||
environments: Vec::new(),
|
||||
thread_extension_init: Default::default(),
|
||||
})
|
||||
.await
|
||||
.expect("internal thread should start");
|
||||
@@ -384,6 +386,81 @@ async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
|
||||
assert!(manager.list_thread_ids().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_thread_seeds_extension_data_before_lifecycle_contributors_run() {
|
||||
struct InitialMarker(&'static str);
|
||||
|
||||
struct InitialDataRecorder {
|
||||
observed: Arc<std::sync::Mutex<Option<(String, String)>>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl codex_extension_api::ThreadLifecycleContributor<Config> for InitialDataRecorder {
|
||||
async fn on_thread_start(&self, input: codex_extension_api::ThreadStartInput<'_, Config>) {
|
||||
let marker = input
|
||||
.thread_store
|
||||
.get::<InitialMarker>()
|
||||
.expect("initial extension data should be available");
|
||||
*self
|
||||
.observed
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some((
|
||||
input.thread_store.level_id().to_string(),
|
||||
marker.0.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
let observed = Arc::new(std::sync::Mutex::new(None));
|
||||
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::new();
|
||||
extensions.thread_lifecycle_contributor(Arc::new(InitialDataRecorder {
|
||||
observed: Arc::clone(&observed),
|
||||
}));
|
||||
let manager = ThreadManager::new(
|
||||
&config,
|
||||
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
Arc::new(extensions.build()),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
TEST_INSTALLATION_ID.to_string(),
|
||||
/*attestation_provider*/ None,
|
||||
);
|
||||
let mut thread_extension_init = codex_extension_api::ExtensionDataInit::new();
|
||||
thread_extension_init.insert(InitialMarker("seeded"));
|
||||
|
||||
let thread = manager
|
||||
.start_thread_with_options(StartThreadOptions {
|
||||
config,
|
||||
initial_history: InitialHistory::New,
|
||||
session_source: None,
|
||||
thread_source: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
metrics_service_name: None,
|
||||
parent_trace: None,
|
||||
environments: Vec::new(),
|
||||
thread_extension_init,
|
||||
})
|
||||
.await
|
||||
.expect("start thread");
|
||||
|
||||
assert_eq!(
|
||||
observed
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone(),
|
||||
Some((thread.thread_id.to_string(), "seeded".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
@@ -423,6 +500,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
metrics_service_name: None,
|
||||
parent_trace: None,
|
||||
environments: environments.clone(),
|
||||
thread_extension_init: Default::default(),
|
||||
})
|
||||
.await
|
||||
.expect("start source thread");
|
||||
@@ -693,6 +771,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
|
||||
metrics_service_name: None,
|
||||
parent_trace: None,
|
||||
environments: Vec::new(),
|
||||
thread_extension_init: Default::default(),
|
||||
})
|
||||
.await
|
||||
.expect("start source thread");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -86,6 +86,7 @@ use codex_tools::shell_type_for_model_and_features;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use tracing::warn;
|
||||
|
||||
const MULTI_AGENT_V2_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";
|
||||
@@ -148,6 +149,7 @@ struct CoreToolPlanContext<'a> {
|
||||
wait_agent_timeouts: WaitAgentTimeoutOptions,
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub(crate) fn build_tool_router(
|
||||
turn_context: &TurnContext,
|
||||
params: ToolRouterParams<'_>,
|
||||
@@ -156,6 +158,7 @@ pub(crate) fn build_tool_router(
|
||||
ToolRouter::from_parts(registry, model_visible_specs)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn build_tool_specs_and_registry(
|
||||
turn_context: &TurnContext,
|
||||
params: ToolRouterParams<'_>,
|
||||
@@ -186,6 +189,7 @@ fn build_tool_specs_and_registry(
|
||||
build_model_visible_specs_and_registry(turn_context, planned_tools)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn build_model_visible_specs_and_registry(
|
||||
turn_context: &TurnContext,
|
||||
planned_tools: PlannedTools,
|
||||
@@ -557,6 +561,7 @@ fn code_mode_namespace_descriptions(
|
||||
namespace_descriptions
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn add_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
|
||||
add_shell_tools(context, planned_tools);
|
||||
add_mcp_resource_tools(context, planned_tools);
|
||||
@@ -837,6 +842,7 @@ fn add_extension_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut Pl
|
||||
);
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn append_tool_search_executor(
|
||||
context: &CoreToolPlanContext<'_>,
|
||||
planned_tools: &mut PlannedTools,
|
||||
|
||||
@@ -14,6 +14,7 @@ use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::exec::is_likely_sandbox_denied;
|
||||
use codex_exec_server::ExecProcess;
|
||||
use codex_exec_server::ProcessSignal as ExecServerProcessSignal;
|
||||
use codex_exec_server::ReadResponse as ExecReadResponse;
|
||||
use codex_exec_server::StartedExecProcess;
|
||||
use codex_exec_server::WriteStatus;
|
||||
@@ -23,6 +24,7 @@ use codex_protocol::protocol::TruncationPolicy;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_output_truncation::formatted_truncate_text;
|
||||
use codex_utils_pty::ExecCommandSession;
|
||||
use codex_utils_pty::ProcessSignal as PtyProcessSignal;
|
||||
use codex_utils_pty::SpawnedPty;
|
||||
|
||||
use super::UNIFIED_EXEC_OUTPUT_MAX_TOKENS;
|
||||
@@ -31,7 +33,6 @@ use super::head_tail_buffer::HeadTailBuffer;
|
||||
use super::process_state::ProcessState;
|
||||
|
||||
const EARLY_EXIT_GRACE_PERIOD: Duration = Duration::from_millis(150);
|
||||
|
||||
pub(crate) trait SpawnLifecycle: std::fmt::Debug + Send + Sync {
|
||||
/// Returns file descriptors that must stay open across the child `exec()`.
|
||||
///
|
||||
@@ -212,6 +213,18 @@ impl UnifiedExecProcess {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn interrupt(&self) -> Result<(), UnifiedExecError> {
|
||||
match &self.process_handle {
|
||||
ProcessHandle::Local(process_handle) => process_handle
|
||||
.signal(PtyProcessSignal::Interrupt)
|
||||
.map_err(|err| UnifiedExecError::process_failed(err.to_string())),
|
||||
ProcessHandle::ExecServer(process_handle) => process_handle
|
||||
.signal(ExecServerProcessSignal::Interrupt)
|
||||
.await
|
||||
.map_err(|err| UnifiedExecError::process_failed(err.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn fail_and_terminate(&self, message: String) {
|
||||
let state = self.state_rx.borrow().clone();
|
||||
if state.failure_message.is_none() {
|
||||
|
||||
@@ -72,6 +72,7 @@ const UNIFIED_EXEC_ENV: [(&str, &str); 10] = [
|
||||
const NETWORK_ACCESS_DENIED_MESSAGE: &str =
|
||||
"Network access was denied by the Codex sandbox network proxy.";
|
||||
const LATE_NETWORK_DENIAL_GRACE_PERIOD: Duration = Duration::from_millis(100);
|
||||
const INTERRUPT: &str = "\u{3}";
|
||||
|
||||
/// Test-only override for deterministic unified exec process IDs.
|
||||
///
|
||||
@@ -617,24 +618,29 @@ impl UnifiedExecProcessManager {
|
||||
|
||||
if !request.input.is_empty() {
|
||||
if !tty {
|
||||
return Err(UnifiedExecError::StdinClosed);
|
||||
}
|
||||
match process.write(request.input.as_bytes()).await {
|
||||
Ok(()) => {
|
||||
// Give the remote process a brief window to react so that we are
|
||||
// more likely to capture its output in the poll below.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
if request.input == INTERRUPT {
|
||||
process.interrupt().await?;
|
||||
} else {
|
||||
return Err(UnifiedExecError::StdinClosed);
|
||||
}
|
||||
Err(err) => {
|
||||
let status = self.refresh_process_state(process_id).await;
|
||||
if matches!(status, ProcessStatus::Exited { .. }) {
|
||||
status_after_write = Some(status);
|
||||
} else if matches!(err, UnifiedExecError::ProcessFailed { .. }) {
|
||||
process.terminate();
|
||||
self.release_process_id(process_id).await;
|
||||
return Err(err);
|
||||
} else {
|
||||
return Err(err);
|
||||
} else {
|
||||
match process.write(request.input.as_bytes()).await {
|
||||
Ok(()) => {
|
||||
// Give the remote process a brief window to react so that we are
|
||||
// more likely to capture its output in the poll below.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let status = self.refresh_process_state(process_id).await;
|
||||
if matches!(status, ProcessStatus::Exited { .. }) {
|
||||
status_after_write = Some(status);
|
||||
} else if matches!(err, UnifiedExecError::ProcessFailed { .. }) {
|
||||
process.terminate();
|
||||
self.release_process_id(process_id).await;
|
||||
return Err(err);
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_exec_server::ExecProcess;
|
||||
use codex_exec_server::ExecProcessEventReceiver;
|
||||
use codex_exec_server::ExecServerError;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_exec_server::ProcessSignal;
|
||||
use codex_exec_server::ReadResponse;
|
||||
use codex_exec_server::StartedExecProcess;
|
||||
use codex_exec_server::WriteResponse;
|
||||
@@ -63,6 +64,10 @@ impl ExecProcess for MockExecProcess {
|
||||
Ok(self.write_response.clone())
|
||||
}
|
||||
|
||||
async fn signal(&self, _signal: ProcessSignal) -> Result<(), ExecServerError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn terminate(&self) -> Result<(), ExecServerError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user