mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
maybe at handler
This commit is contained in:
@@ -34,7 +34,7 @@ pub(crate) fn build_track_events_context(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SkillInvocation {
|
||||
pub(crate) skill_name: String,
|
||||
pub(crate) skill_scope: SkillScope,
|
||||
@@ -42,7 +42,7 @@ pub(crate) struct SkillInvocation {
|
||||
pub(crate) invocation_type: InvocationType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum InvocationType {
|
||||
Explicit,
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::fmt::Debug;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
|
||||
use crate::AuthManager;
|
||||
@@ -16,7 +17,6 @@ use crate::agent::agent_status_from_event;
|
||||
use crate::analytics_client::AnalyticsEventsClient;
|
||||
use crate::analytics_client::AppInvocation;
|
||||
use crate::analytics_client::InvocationType;
|
||||
use crate::analytics_client::TrackEventsContext;
|
||||
use crate::analytics_client::build_track_events_context;
|
||||
use crate::apps::render_apps_section;
|
||||
use crate::commit_attribution::commit_message_trailer_instruction;
|
||||
@@ -35,7 +35,6 @@ use crate::parse_command::parse_command;
|
||||
use crate::parse_turn_item;
|
||||
use crate::rollout::session_index;
|
||||
use crate::skills::ImplicitInvocationContext;
|
||||
use crate::skills::build_implicit_invocation_context;
|
||||
use crate::stream_events_utils::HandleOutputCtx;
|
||||
use crate::stream_events_utils::handle_non_tool_response_item;
|
||||
use crate::stream_events_utils::handle_output_item_done;
|
||||
@@ -568,6 +567,7 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
|
||||
pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
|
||||
pub(crate) implicit_invocation_seen_skill_ids: Arc<Mutex<HashSet<String>>>,
|
||||
pub(crate) implicit_invocation_context: Arc<OnceLock<Option<Arc<ImplicitInvocationContext>>>>,
|
||||
}
|
||||
impl TurnContext {
|
||||
pub(crate) fn model_context_window(&self) -> Option<i64> {
|
||||
@@ -650,6 +650,7 @@ impl TurnContext {
|
||||
dynamic_tools: self.dynamic_tools.clone(),
|
||||
turn_metadata_state: self.turn_metadata_state.clone(),
|
||||
implicit_invocation_seen_skill_ids: self.implicit_invocation_seen_skill_ids.clone(),
|
||||
implicit_invocation_context: self.implicit_invocation_context.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -993,6 +994,7 @@ impl Session {
|
||||
dynamic_tools: session_configuration.dynamic_tools.clone(),
|
||||
turn_metadata_state,
|
||||
implicit_invocation_seen_skill_ids: Arc::new(Mutex::new(HashSet::new())),
|
||||
implicit_invocation_context: Arc::new(OnceLock::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4150,6 +4152,7 @@ async fn spawn_review_thread(
|
||||
truncation_policy: model_info.truncation_policy.into(),
|
||||
turn_metadata_state,
|
||||
implicit_invocation_seen_skill_ids: Arc::new(Mutex::new(HashSet::new())),
|
||||
implicit_invocation_context: Arc::new(OnceLock::new()),
|
||||
};
|
||||
|
||||
// Seed the child task with the review prompt as the initial user message.
|
||||
@@ -4273,6 +4276,11 @@ pub(crate) async fn run_turn(
|
||||
.skills_for_cwd(&turn_context.cwd, false)
|
||||
.await,
|
||||
);
|
||||
let _ = turn_context.implicit_invocation_context.set(
|
||||
skills_outcome
|
||||
.as_ref()
|
||||
.and_then(|outcome| outcome.implicit_invocation_context.clone()),
|
||||
);
|
||||
|
||||
let available_connectors = if turn_context.config.features.enabled(Feature::Apps) {
|
||||
let mcp_tools = match sess
|
||||
@@ -4332,11 +4340,6 @@ pub(crate) async fn run_turn(
|
||||
thread_id,
|
||||
turn_context.sub_id.clone(),
|
||||
);
|
||||
let implicit_invocation_context = build_implicit_invocation_context(
|
||||
skills_outcome.as_ref().map_or_else(Vec::new, |outcome| {
|
||||
outcome.allowed_skills_for_implicit_invocation()
|
||||
}),
|
||||
);
|
||||
let SkillInjections {
|
||||
items: skill_items,
|
||||
warnings: skill_warnings,
|
||||
@@ -4459,8 +4462,6 @@ pub(crate) async fn run_turn(
|
||||
&explicitly_enabled_connectors,
|
||||
skills_outcome.as_ref(),
|
||||
&mut server_model_warning_emitted_for_turn,
|
||||
implicit_invocation_context.as_ref(),
|
||||
&tracking,
|
||||
cancellation_token.child_token(),
|
||||
)
|
||||
.await
|
||||
@@ -4835,8 +4836,6 @@ async fn run_sampling_request(
|
||||
explicitly_enabled_connectors: &HashSet<String>,
|
||||
skills_outcome: Option<&SkillLoadOutcome>,
|
||||
server_model_warning_emitted_for_turn: &mut bool,
|
||||
implicit_invocation_context: Option<&ImplicitInvocationContext>,
|
||||
tracking: &TrackEventsContext,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> CodexResult<SamplingRequestResult> {
|
||||
let router = built_tools(
|
||||
@@ -4863,7 +4862,6 @@ async fn run_sampling_request(
|
||||
personality: turn_context.personality,
|
||||
output_schema: turn_context.final_output_json_schema.clone(),
|
||||
};
|
||||
|
||||
let mut retries = 0;
|
||||
loop {
|
||||
let err = match try_run_sampling_request(
|
||||
@@ -4875,8 +4873,6 @@ async fn run_sampling_request(
|
||||
Arc::clone(&turn_diff_tracker),
|
||||
server_model_warning_emitted_for_turn,
|
||||
&prompt,
|
||||
implicit_invocation_context,
|
||||
tracking,
|
||||
cancellation_token.child_token(),
|
||||
)
|
||||
.await
|
||||
@@ -5446,8 +5442,6 @@ async fn try_run_sampling_request(
|
||||
turn_diff_tracker: SharedTurnDiffTracker,
|
||||
server_model_warning_emitted_for_turn: &mut bool,
|
||||
prompt: &Prompt,
|
||||
implicit_invocation_context: Option<&ImplicitInvocationContext>,
|
||||
tracking: &TrackEventsContext,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> CodexResult<SamplingRequestResult> {
|
||||
let collaboration_mode = sess.current_collaboration_mode().await;
|
||||
@@ -5561,8 +5555,6 @@ async fn try_run_sampling_request(
|
||||
turn_context: turn_context.clone(),
|
||||
tool_runtime: tool_runtime.clone(),
|
||||
cancellation_token: cancellation_token.child_token(),
|
||||
implicit_invocation_context,
|
||||
tracking,
|
||||
};
|
||||
|
||||
let output_result = handle_output_item_done(&mut ctx, item, previously_active_item)
|
||||
|
||||
@@ -2,41 +2,29 @@ use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::analytics_client::InvocationType;
|
||||
use crate::analytics_client::SkillInvocation;
|
||||
use crate::analytics_client::build_track_events_context;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::skills::SkillMetadata;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ImplicitSkillCandidate {
|
||||
pub(crate) invocation: SkillInvocation,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct ImplicitSkillDetector {
|
||||
pub(crate) by_scripts_dir: HashMap<PathBuf, ImplicitSkillCandidate>,
|
||||
pub(crate) by_skill_doc_path: HashMap<PathBuf, ImplicitSkillCandidate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ImplicitInvocationContext {
|
||||
pub(crate) detector: ImplicitSkillDetector,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ShellCommandDetectionArgs {
|
||||
command: String,
|
||||
workdir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExecCommandDetectionArgs {
|
||||
cmd: String,
|
||||
workdir: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn build_implicit_invocation_context(
|
||||
skills: Vec<SkillMetadata>,
|
||||
) -> Option<ImplicitInvocationContext> {
|
||||
@@ -68,21 +56,15 @@ pub(crate) fn build_implicit_invocation_context(
|
||||
Some(ImplicitInvocationContext { detector })
|
||||
}
|
||||
|
||||
pub(crate) fn detect_implicit_skill_invocation(
|
||||
fn detect_implicit_skill_invocation_for_command(
|
||||
detector: &ImplicitSkillDetector,
|
||||
turn_context: &TurnContext,
|
||||
item: &ResponseItem,
|
||||
command: &str,
|
||||
workdir: Option<&str>,
|
||||
) -> Option<ImplicitSkillCandidate> {
|
||||
let ResponseItem::FunctionCall {
|
||||
name, arguments, ..
|
||||
} = item
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let (command, workdir) = parse_implicit_detection_command(name, arguments)?;
|
||||
let workdir = turn_context.resolve_path(workdir);
|
||||
let workdir = turn_context.resolve_path(workdir.map(str::to_owned));
|
||||
let workdir = normalize_path(workdir.as_path());
|
||||
let tokens = tokenize_command(command.as_str());
|
||||
let tokens = tokenize_command(command);
|
||||
|
||||
if let Some(candidate) = detect_skill_script_run(detector, tokens.as_slice(), workdir.as_path())
|
||||
{
|
||||
@@ -96,19 +78,63 @@ pub(crate) fn detect_implicit_skill_invocation(
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_implicit_detection_command(
|
||||
tool_name: &str,
|
||||
arguments: &str,
|
||||
) -> Option<(String, Option<String>)> {
|
||||
match tool_name {
|
||||
"shell_command" => serde_json::from_str::<ShellCommandDetectionArgs>(arguments)
|
||||
.ok()
|
||||
.map(|args| (args.command, args.workdir)),
|
||||
"exec_command" => serde_json::from_str::<ExecCommandDetectionArgs>(arguments)
|
||||
.ok()
|
||||
.map(|args| (args.cmd, args.workdir)),
|
||||
_ => None,
|
||||
pub(crate) async fn maybe_emit_implicit_skill_invocation(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
command: &str,
|
||||
workdir: Option<&str>,
|
||||
) {
|
||||
let Some(implicit) = turn_context
|
||||
.implicit_invocation_context
|
||||
.get()
|
||||
.and_then(|value| value.as_deref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(candidate) = detect_implicit_skill_invocation_for_command(
|
||||
&implicit.detector,
|
||||
turn_context,
|
||||
command,
|
||||
workdir,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let skill_scope = match candidate.invocation.skill_scope {
|
||||
codex_protocol::protocol::SkillScope::User => "user",
|
||||
codex_protocol::protocol::SkillScope::Repo => "repo",
|
||||
codex_protocol::protocol::SkillScope::System => "system",
|
||||
codex_protocol::protocol::SkillScope::Admin => "admin",
|
||||
};
|
||||
let skill_path = candidate.invocation.skill_path.to_string_lossy();
|
||||
let skill_name = candidate.invocation.skill_name.as_str();
|
||||
let seen_key = format!("{skill_scope}:{skill_path}:{skill_name}");
|
||||
let inserted = {
|
||||
let mut seen_skills = turn_context.implicit_invocation_seen_skill_ids.lock().await;
|
||||
seen_skills.insert(seen_key)
|
||||
};
|
||||
if !inserted {
|
||||
return;
|
||||
}
|
||||
|
||||
turn_context.otel_manager.counter(
|
||||
"codex.skill.injected",
|
||||
1,
|
||||
&[
|
||||
("status", "ok"),
|
||||
("skill", skill_name),
|
||||
("invoke_type", "implicit"),
|
||||
],
|
||||
);
|
||||
sess.services
|
||||
.analytics_events_client
|
||||
.track_skill_invocations(
|
||||
build_track_events_context(
|
||||
turn_context.model_info.slug.clone(),
|
||||
sess.conversation_id.to_string(),
|
||||
turn_context.sub_id.clone(),
|
||||
),
|
||||
vec![candidate.invocation],
|
||||
);
|
||||
}
|
||||
|
||||
fn tokenize_command(command: &str) -> Vec<String> {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
@@ -16,6 +17,7 @@ use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
use crate::config_loader::load_config_layers_state;
|
||||
use crate::skills::SkillLoadOutcome;
|
||||
use crate::skills::build_implicit_invocation_context;
|
||||
use crate::skills::loader::SkillRoot;
|
||||
use crate::skills::loader::load_skills_from_roots;
|
||||
use crate::skills::loader::skill_roots_from_layer_stack_with_agents;
|
||||
@@ -50,6 +52,9 @@ impl SkillsManager {
|
||||
skill_roots_from_layer_stack_with_agents(&config.config_layer_stack, &config.cwd);
|
||||
let mut outcome = load_skills_from_roots(roots);
|
||||
outcome.disabled_paths = disabled_paths_from_stack(&config.config_layer_stack);
|
||||
outcome.implicit_invocation_context =
|
||||
build_implicit_invocation_context(outcome.allowed_skills_for_implicit_invocation())
|
||||
.map(Arc::new);
|
||||
let mut cache = match self.cache_by_cwd.write() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
@@ -125,6 +130,9 @@ impl SkillsManager {
|
||||
);
|
||||
let mut outcome = load_skills_from_roots(roots);
|
||||
outcome.disabled_paths = disabled_paths_from_stack(&config_layer_stack);
|
||||
outcome.implicit_invocation_context =
|
||||
build_implicit_invocation_context(outcome.allowed_skills_for_implicit_invocation())
|
||||
.map(Arc::new);
|
||||
let mut cache = match self.cache_by_cwd.write() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
|
||||
@@ -16,7 +16,7 @@ pub(crate) use injection::build_skill_injections;
|
||||
pub(crate) use injection::collect_explicit_skill_mentions;
|
||||
pub(crate) use invocation_utils::ImplicitInvocationContext;
|
||||
pub(crate) use invocation_utils::build_implicit_invocation_context;
|
||||
pub(crate) use invocation_utils::detect_implicit_skill_invocation;
|
||||
pub(crate) use invocation_utils::maybe_emit_implicit_skill_invocation;
|
||||
pub use loader::load_skills;
|
||||
pub use manager::SkillsManager;
|
||||
pub use model::SkillError;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::Permissions;
|
||||
use crate::skills::invocation_utils::ImplicitInvocationContext;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -68,6 +70,7 @@ pub struct SkillLoadOutcome {
|
||||
pub skills: Vec<SkillMetadata>,
|
||||
pub errors: Vec<SkillError>,
|
||||
pub disabled_paths: HashSet<PathBuf>,
|
||||
pub(crate) implicit_invocation_context: Option<Arc<ImplicitInvocationContext>>,
|
||||
}
|
||||
|
||||
impl SkillLoadOutcome {
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::analytics_client::TrackEventsContext;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::error::CodexErr;
|
||||
@@ -13,8 +12,6 @@ use crate::error::Result;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::parse_turn_item;
|
||||
use crate::proposed_plan_parser::strip_proposed_plan_blocks;
|
||||
use crate::skills::ImplicitInvocationContext;
|
||||
use crate::skills::detect_implicit_skill_invocation;
|
||||
use crate::tools::parallel::ToolCallRuntime;
|
||||
use crate::tools::router::ToolRouter;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
@@ -38,18 +35,16 @@ pub(crate) struct OutputItemResult {
|
||||
pub tool_future: Option<InFlightFuture<'static>>,
|
||||
}
|
||||
|
||||
pub(crate) struct HandleOutputCtx<'a> {
|
||||
pub(crate) struct HandleOutputCtx {
|
||||
pub sess: Arc<Session>,
|
||||
pub turn_context: Arc<TurnContext>,
|
||||
pub tool_runtime: ToolCallRuntime,
|
||||
pub cancellation_token: CancellationToken,
|
||||
pub implicit_invocation_context: Option<&'a ImplicitInvocationContext>,
|
||||
pub tracking: &'a TrackEventsContext,
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub(crate) async fn handle_output_item_done(
|
||||
ctx: &mut HandleOutputCtx<'_>,
|
||||
ctx: &mut HandleOutputCtx,
|
||||
item: ResponseItem,
|
||||
previously_active_item: Option<TurnItem>,
|
||||
) -> Result<OutputItemResult> {
|
||||
@@ -67,8 +62,6 @@ pub(crate) async fn handle_output_item_done(
|
||||
payload_preview
|
||||
);
|
||||
|
||||
maybe_emit_implicit_skill_invocation(ctx, &item).await;
|
||||
|
||||
ctx.sess
|
||||
.record_conversation_items(&ctx.turn_context, std::slice::from_ref(&item))
|
||||
.await;
|
||||
@@ -165,51 +158,6 @@ pub(crate) async fn handle_output_item_done(
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
async fn maybe_emit_implicit_skill_invocation(ctx: &mut HandleOutputCtx<'_>, item: &ResponseItem) {
|
||||
let Some(implicit) = ctx.implicit_invocation_context else {
|
||||
return;
|
||||
};
|
||||
let Some(candidate) =
|
||||
detect_implicit_skill_invocation(&implicit.detector, ctx.turn_context.as_ref(), item)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let skill_scope = match candidate.invocation.skill_scope {
|
||||
codex_protocol::protocol::SkillScope::User => "user",
|
||||
codex_protocol::protocol::SkillScope::Repo => "repo",
|
||||
codex_protocol::protocol::SkillScope::System => "system",
|
||||
codex_protocol::protocol::SkillScope::Admin => "admin",
|
||||
};
|
||||
let skill_path = candidate.invocation.skill_path.to_string_lossy();
|
||||
let skill_name = candidate.invocation.skill_name.as_str();
|
||||
let seen_key = format!("{skill_scope}:{skill_path}:{skill_name}");
|
||||
let inserted = {
|
||||
let mut seen_skills = ctx
|
||||
.turn_context
|
||||
.implicit_invocation_seen_skill_ids
|
||||
.lock()
|
||||
.await;
|
||||
seen_skills.insert(seen_key)
|
||||
};
|
||||
if !inserted {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.turn_context.otel_manager.counter(
|
||||
"codex.skill.injected",
|
||||
1,
|
||||
&[
|
||||
("status", "ok"),
|
||||
("skill", skill_name),
|
||||
("invoke_type", "implicit"),
|
||||
],
|
||||
);
|
||||
ctx.sess
|
||||
.services
|
||||
.analytics_events_client
|
||||
.track_skill_invocations(ctx.tracking.clone(), vec![candidate.invocation]);
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_non_tool_response_item(
|
||||
item: &ResponseItem,
|
||||
plan_mode: bool,
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::is_safe_command::is_known_safe_command;
|
||||
use crate::protocol::ExecCommandSource;
|
||||
use crate::shell::Shell;
|
||||
use crate::skills::maybe_emit_implicit_skill_invocation;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
@@ -209,6 +210,13 @@ impl ToolHandler for ShellCommandHandler {
|
||||
};
|
||||
|
||||
let params: ShellCommandToolCallParams = parse_arguments(&arguments)?;
|
||||
maybe_emit_implicit_skill_invocation(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
¶ms.command,
|
||||
params.workdir.as_deref(),
|
||||
)
|
||||
.await;
|
||||
let prefix_rule = params.prefix_rule.clone();
|
||||
let exec_params = Self::to_exec_params(
|
||||
¶ms,
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::protocol::TerminalInteractionEvent;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::get_shell_by_model_provided_path;
|
||||
use crate::skills::maybe_emit_implicit_skill_invocation;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
@@ -128,6 +129,13 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
let response = match tool_name.as_str() {
|
||||
"exec_command" => {
|
||||
let args: ExecCommandArgs = parse_arguments(&arguments)?;
|
||||
maybe_emit_implicit_skill_invocation(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
&args.cmd,
|
||||
args.workdir.as_deref(),
|
||||
)
|
||||
.await;
|
||||
let process_id = manager.allocate_process_id().await;
|
||||
let command = get_command(&args, session.user_shell());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user