ZedのAIエージェントは、PATHをどこから持ってくる?

AIエージェントパネルで作業していて、ちょっとハマったので調べてみた。

Zedではターミナルが使える。ターミナルのデフォルトシェルはzshなので、~/.zprofileなどを読み込んでいる。当然、そこで設定されたPATHにあるツールは使える。

AIエージェントでも同様にツールが使える。ところが、AIエージェントの動作を見ていると、ちょくちょくshを使っていることが見て取れる。なんでzshがあるのにshを使ってて、しかも~/.zprofileで設定したPATHのツールが使えてるんだ???

調べてみた。

環境変数はログインシェルから採取する

pub fn print_env() {
    let env_vars: HashMap<String, String> = std::env::vars().collect();
    let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
        eprintln!("Error serializing environment variables: {}", err);
        std::process::exit(1);
    });
    println!("{}", json);
}

/// Capture all environment variables from the login shell in the given directory.
pub async fn capture(
    shell_path: impl AsRef<Path>,
    args: &[String],
    directory: impl AsRef<Path>,
) -> Result<collections::HashMap<String, String>> {
    #[cfg(windows)]
    return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
    #[cfg(unix)]
    return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
}

shell に対して -l -i -c ... を付けて起動

        }
    }
    // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
    command_string.push_str(&format!("cd '{}';", directory.display()));
    if let Some(prefix) = shell_kind.command_prefix() {
        command_string.push(prefix);
    }
    command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
    command.args(["-i", "-c", &command_string]);

ログインシェルを、インタラクティブで起動して、printenvの結果をJSONにしてる。

  • -l = login shell
  • -i = interactive shell
  • cd してから zed --printenv
  • zed --printenv はそのシェル環境を JSON 出力

で、これがどう使われるかというと、ProjectEnvironment::local_directory_environment で、そのディレクトリ用環境を読み込むと。

    /// Returns the project environment, if possible.
    /// If the project was opened from the CLI, then the inherited CLI environment is returned.
    /// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in
    /// that directory, to get environment variables as if the user has `cd`'d there.
    pub fn local_directory_environment(
        &mut self,
        shell: &Shell,
        abs_path: Arc<Path>,
        cx: &mut App,
    ) -> Shared<Task<Option<HashMap<String, String>>>> {
        if let Some(cli_environment) = self.get_cli_environment() {
            log::debug!("using project environment variables from CLI");
            return Task::ready(Some(cli_environment)).shared();
        }

        self.local_environments
            .entry((shell.clone(), abs_path.clone()))
            .or_insert_with(|| {
                let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
                let shell = shell.clone();

AIエージェントのshって何?

/bin/sh決め打ち

/// Get the default system shell, preferring bash on Windows.
pub fn get_default_system_shell_preferring_bash() -> String {
    if cfg!(windows) {
        get_windows_bash().unwrap_or_else(|| get_windows_system_shell())
    } else {
        "/bin/sh".to_string()
    }
}

AIエージェントのターミナルを生成する時には、envを作って、get_default_system_shell_preferring_bash()を呼ぶ。

    let mut env = if let Some(dir) = &cwd {
        project
            .update(cx, |project, cx| {
                project.environment().update(cx, |env, cx| {
                    env.directory_environment(dir.clone().into(), cx)
                })
            })
            .await
            .unwrap_or_default()
    } else {
        Default::default()
    };

    // Disable pagers so agent/terminal commands don't hang behind interactive UIs
    env.insert("PAGER".into(), "".into());
    // Override user core.pager (e.g. delta) which Git prefers over PAGER
    env.insert("GIT_PAGER".into(), "cat".into());
    env.extend(env_vars);

    // Use remote shell or default system shell, as appropriate
    let shell = project
        .update(cx, |project, cx| {
            project
                .remote_client()
                .and_then(|r| r.read(cx).default_system_shell())
                .map(Shell::Program)
        })
        .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
    let is_windows = project.read_with(cx, |project, cx| project.path_style(cx).is_windows());
    let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
        .redirect_stdin_to_dev_null()
        .build(Some(command.clone()), &args);

まとめ

/bin/shを名乗ってるのに、.zprofileを読むんじゃない!(訳わからなかった、の意)

そして、危険なことに気がついた。ZedのAIエージェントはttyを持ってる。まずい、envrcctl経由でKeychainアクセスできるわ…。

追記:stdinは/dev/nullにリダイレクトされているので、sys.stdin.isatty()はfalseに判定される。たぶん、大丈夫、なはず…。

    let shell = project
        .update(cx, |project, cx| {
            project
                .remote_client()
                .and_then(|r| r.read(cx).default_system_shell())
                .map(Shell::Program)
        })
        .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
    let is_windows = project.read_with(cx, |project, cx| project.path_style(cx).is_windows());
    let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
        .redirect_stdin_to_dev_null()
        .build(Some(command.clone()), &args);
def _is_interactive() -> bool:
    return sys.stdin.isatty() and sys.stdout.isatty()

追記:大丈夫でした。やはり、sys.stdin.isatty()はfalseになる。

タイトルとURLをコピーしました