merge: 00001KVTNAY20 worker runtime spawn
This commit is contained in:
commit
97555bb5a1
|
|
@ -0,0 +1,37 @@
|
||||||
|
# 実装報告: 00001KVTNAY20
|
||||||
|
|
||||||
|
## 変更概要
|
||||||
|
|
||||||
|
- `client::spawn` に `PodProcessLaunchConfig` と `PodProcessLaunchOptions` を導入し、低レベルの Pod プロセス起動設定から Ticket role marker を分離した。
|
||||||
|
- Ticket role 起動は `TicketRoleLaunchPlan::spawn_options()` 経由で hidden CLI marker を渡す形にし、`TicketRoleLaunchResult` に Run 受理証跡 (`TicketRoleLaunchAcceptanceEvidence`) を追加した。
|
||||||
|
- Workspace server の `LocalRuntimeBridge` を `WorkspaceWorkerRuntime` trait の実装として整理し、hosts/workers 一覧、worker lookup、spawn/stop typed request/result、将来の proxy/stream 接続点を型として追加した。
|
||||||
|
- Workspace 側の spawn request shape は policy intent ベースにし、browser/API caller から raw `workspace_root` / `cwd` / executable path / raw profile selector を受け取らない形にした。
|
||||||
|
- Dashboard/TUI 側の直接 spawn 呼び出しを新しい low-level config/options 分離に追従した。
|
||||||
|
|
||||||
|
## 変更ファイル
|
||||||
|
|
||||||
|
- `crates/client/src/lib.rs`
|
||||||
|
- `crates/client/src/spawn.rs`
|
||||||
|
- `crates/client/src/ticket_role.rs`
|
||||||
|
- `crates/tui/src/dashboard/mod.rs`
|
||||||
|
- `crates/tui/src/spawn.rs`
|
||||||
|
- `crates/workspace-server/src/hosts.rs`
|
||||||
|
- `crates/workspace-server/src/server.rs`
|
||||||
|
|
||||||
|
## 検証結果
|
||||||
|
|
||||||
|
- `cargo test -p yoi-workspace-server`: 成功
|
||||||
|
- `cargo check -p yoi`: 成功
|
||||||
|
- `cd web/workspace && deno task check && deno task build`: 成功
|
||||||
|
- `cargo test -p client`: 成功(追加確認)
|
||||||
|
- `git diff --check`: 成功
|
||||||
|
|
||||||
|
## コミット
|
||||||
|
|
||||||
|
- 実装コミット: `217a4828d73ab553b5406cc7e22e43b1ec7be48e`
|
||||||
|
|
||||||
|
## 残リスク / 非ゴールとして残したもの
|
||||||
|
|
||||||
|
- `WorkspaceWorkerRuntime::spawn_worker` / `stop_worker` は typed boundary と request/result を用意した段階で、実際の Worker operation UI 完成、stream proxy、remote Host protocol、認可/権限、registry locking までは実装していない。
|
||||||
|
- low-level launcher は trusted in-process resolver からの追加 CLI args を受け取れるが、Ticket role などのドメイン概念は `PodProcessLaunchConfig` からは除外している。
|
||||||
|
- TS 型生成やフロントエンド API surface の追加は本 Ticket の非ゴールとして変更していない。
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
title: 'Abstract Workspace Worker runtime spawn operations'
|
title: 'Abstract Workspace Worker runtime spawn operations'
|
||||||
state: 'inprogress'
|
state: 'inprogress'
|
||||||
created_at: '2026-06-23T16:34:39Z'
|
created_at: '2026-06-23T16:34:39Z'
|
||||||
updated_at: '2026-06-23T19:33:48Z'
|
updated_at: '2026-06-24T10:26:04Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-06-23T19:25:09Z'
|
queued_at: '2026-06-23T19:25:09Z'
|
||||||
|
|
|
||||||
|
|
@ -166,4 +166,49 @@ Next action:
|
||||||
- Coder Pod/profile/provider startup issue の解消後に同じ worktree/branch で multi-agent workflow を再開する。
|
- Coder Pod/profile/provider startup issue の解消後に同じ worktree/branch で multi-agent workflow を再開する。
|
||||||
- あるいは人間が明示的に Orchestrator direct implementation を許可する場合のみ、Orchestrator がこの child worktree で実装へ進む。
|
- あるいは人間が明示的に Orchestrator direct implementation を許可する場合のみ、Orchestrator がこの child worktree で実装へ進む。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-24T10:26:04Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
# 実装報告: 00001KVTNAY20
|
||||||
|
|
||||||
|
## 変更概要
|
||||||
|
|
||||||
|
- `client::spawn` に `PodProcessLaunchConfig` と `PodProcessLaunchOptions` を導入し、低レベルの Pod プロセス起動設定から Ticket role marker を分離した。
|
||||||
|
- Ticket role 起動は `TicketRoleLaunchPlan::spawn_options()` 経由で hidden CLI marker を渡す形にし、`TicketRoleLaunchResult` に Run 受理証跡 (`TicketRoleLaunchAcceptanceEvidence`) を追加した。
|
||||||
|
- Workspace server の `LocalRuntimeBridge` を `WorkspaceWorkerRuntime` trait の実装として整理し、hosts/workers 一覧、worker lookup、spawn/stop typed request/result、将来の proxy/stream 接続点を型として追加した。
|
||||||
|
- Workspace 側の spawn request shape は policy intent ベースにし、browser/API caller から raw `workspace_root` / `cwd` / executable path / raw profile selector を受け取らない形にした。
|
||||||
|
- Dashboard/TUI 側の直接 spawn 呼び出しを新しい low-level config/options 分離に追従した。
|
||||||
|
|
||||||
|
## 変更ファイル
|
||||||
|
|
||||||
|
- `crates/client/src/lib.rs`
|
||||||
|
- `crates/client/src/spawn.rs`
|
||||||
|
- `crates/client/src/ticket_role.rs`
|
||||||
|
- `crates/tui/src/dashboard/mod.rs`
|
||||||
|
- `crates/tui/src/spawn.rs`
|
||||||
|
- `crates/workspace-server/src/hosts.rs`
|
||||||
|
- `crates/workspace-server/src/server.rs`
|
||||||
|
|
||||||
|
## 検証結果
|
||||||
|
|
||||||
|
- `cargo test -p yoi-workspace-server`: 成功
|
||||||
|
- `cargo check -p yoi`: 成功
|
||||||
|
- `cd web/workspace && deno task check && deno task build`: 成功
|
||||||
|
- `cargo test -p client`: 成功(追加確認)
|
||||||
|
- `git diff --check`: 成功
|
||||||
|
|
||||||
|
## コミット
|
||||||
|
|
||||||
|
- 実装コミット: `217a4828d73ab553b5406cc7e22e43b1ec7be48e`
|
||||||
|
|
||||||
|
## 残リスク / 非ゴールとして残したもの
|
||||||
|
|
||||||
|
- `WorkspaceWorkerRuntime::spawn_worker` / `stop_worker` は typed boundary と request/result を用意した段階で、実際の Worker operation UI 完成、stream proxy、remote Host protocol、認可/権限、registry locking までは実装していない。
|
||||||
|
- low-level launcher は trusted in-process resolver からの追加 CLI args を受け取れるが、Ticket role などのドメイン概念は `PodProcessLaunchConfig` からは除外している。
|
||||||
|
- TS 型生成やフロントエンド API surface の追加は本 Ticket の非ゴールとして変更していない。
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@ pub mod ticket_role;
|
||||||
pub use runtime_command::PodRuntimeCommand;
|
pub use runtime_command::PodRuntimeCommand;
|
||||||
|
|
||||||
pub use pod_client::PodClient;
|
pub use pod_client::PodClient;
|
||||||
pub use spawn::{SpawnConfig, SpawnError, SpawnReady, spawn_pod};
|
pub use spawn::{
|
||||||
|
PodProcessLaunchConfig, PodProcessLaunchOptions, SpawnConfig, SpawnError, SpawnReady,
|
||||||
|
spawn_pod, spawn_pod_with_options,
|
||||||
|
};
|
||||||
pub use ticket_role::{
|
pub use ticket_role::{
|
||||||
TicketRef, TicketRoleLaunchContext, TicketRoleLaunchError, TicketRoleLaunchOptions,
|
TicketRef, TicketRoleLaunchContext, TicketRoleLaunchError, TicketRoleLaunchOptions,
|
||||||
TicketRoleLaunchPlan, TicketRoleLaunchResult, TicketRolePreRunWarning, launch_ticket_role_pod,
|
TicketRoleLaunchPlan, TicketRoleLaunchResult, TicketRolePreRunWarning, launch_ticket_role_pod,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const READY_PREFIX: &str = "YOI-READY\t";
|
||||||
const READY_TIMEOUT: Duration = Duration::from_secs(20);
|
const READY_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SpawnConfig {
|
pub struct PodProcessLaunchConfig {
|
||||||
pub runtime_command: PodRuntimeCommand,
|
pub runtime_command: PodRuntimeCommand,
|
||||||
/// `pod.name` として使う識別子。runtime ディレクトリ
|
/// `pod.name` として使う識別子。runtime ディレクトリ
|
||||||
/// (`manifest::paths::pod_runtime_dir`) の解決と、ready 行に乗る
|
/// (`manifest::paths::pod_runtime_dir`) の解決と、ready 行に乗る
|
||||||
|
|
@ -32,9 +32,6 @@ pub struct SpawnConfig {
|
||||||
/// Optional reusable Profile selector. Pod identity is always supplied
|
/// Optional reusable Profile selector. Pod identity is always supplied
|
||||||
/// separately with `--pod`; profile selection must not imply a name.
|
/// separately with `--pod`; profile selection must not imply a name.
|
||||||
pub profile: Option<String>,
|
pub profile: Option<String>,
|
||||||
/// Process-local Ticket role marker supplied only by Ticket role launches.
|
|
||||||
/// This does not alter prompts, manifests, or Ticket claim records.
|
|
||||||
pub ticket_role: Option<String>,
|
|
||||||
/// Explicit runtime workspace root. The child receives it via
|
/// Explicit runtime workspace root. The child receives it via
|
||||||
/// `--workspace` so startup does not infer workspace identity from the
|
/// `--workspace` so startup does not infer workspace identity from the
|
||||||
/// parent process cwd.
|
/// parent process cwd.
|
||||||
|
|
@ -48,6 +45,28 @@ pub struct SpawnConfig {
|
||||||
pub resume_from: Option<Uuid>,
|
pub resume_from: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct PodProcessLaunchOptions {
|
||||||
|
/// Extra child CLI arguments supplied by an upper resolver layer. The
|
||||||
|
/// low-level launch config intentionally does not model Ticket IDs,
|
||||||
|
/// Ticket roles, orchestration roles, executable authority, or raw
|
||||||
|
/// browser-provided profile/cwd/workspace inputs.
|
||||||
|
pub extra_args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PodProcessLaunchOptions {
|
||||||
|
pub fn with_hidden_arg(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
|
||||||
|
self.extra_args.extend([name.into(), value.into()]);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.extra_args.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SpawnConfig = PodProcessLaunchConfig;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SpawnReady {
|
pub struct SpawnReady {
|
||||||
pub pod_name: String,
|
pub pod_name: String,
|
||||||
|
|
@ -112,7 +131,7 @@ impl From<io::Error> for SpawnError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runtime_args(config: &SpawnConfig) -> Vec<String> {
|
fn runtime_args(config: &PodProcessLaunchConfig, options: &PodProcessLaunchOptions) -> Vec<String> {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"--workspace".to_string(),
|
"--workspace".to_string(),
|
||||||
config.workspace_root.display().to_string(),
|
config.workspace_root.display().to_string(),
|
||||||
|
|
@ -130,9 +149,7 @@ fn runtime_args(config: &SpawnConfig) -> Vec<String> {
|
||||||
args.extend(["--profile".to_string(), profile.clone()]);
|
args.extend(["--profile".to_string(), profile.clone()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(ticket_role) = &config.ticket_role {
|
args.extend(options.extra_args.clone());
|
||||||
args.extend(["--ticket-role".to_string(), ticket_role.clone()]);
|
|
||||||
}
|
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +157,21 @@ fn runtime_args(config: &SpawnConfig) -> Vec<String> {
|
||||||
///
|
///
|
||||||
/// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる
|
/// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる
|
||||||
/// (ready 行自体は除外される)。UI の表示更新や E2E ログ取得に使う。
|
/// (ready 行自体は除外される)。UI の表示更新や E2E ログ取得に使う。
|
||||||
pub async fn spawn_pod<F>(config: SpawnConfig, mut progress: F) -> Result<SpawnReady, SpawnError>
|
pub async fn spawn_pod<F>(
|
||||||
|
config: PodProcessLaunchConfig,
|
||||||
|
progress: F,
|
||||||
|
) -> Result<SpawnReady, SpawnError>
|
||||||
|
where
|
||||||
|
F: FnMut(&str),
|
||||||
|
{
|
||||||
|
spawn_pod_with_options(config, PodProcessLaunchOptions::default(), progress).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spawn_pod_with_options<F>(
|
||||||
|
config: PodProcessLaunchConfig,
|
||||||
|
options: PodProcessLaunchOptions,
|
||||||
|
mut progress: F,
|
||||||
|
) -> Result<SpawnReady, SpawnError>
|
||||||
where
|
where
|
||||||
F: FnMut(&str),
|
F: FnMut(&str),
|
||||||
{
|
{
|
||||||
|
|
@ -158,7 +189,7 @@ where
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::from(stderr_file))
|
.stderr(Stdio::from(stderr_file))
|
||||||
.process_group(0);
|
.process_group(0);
|
||||||
for arg in runtime_args(&config) {
|
for arg in runtime_args(&config, &options) {
|
||||||
command.arg(arg);
|
command.arg(arg);
|
||||||
}
|
}
|
||||||
let mut child = command
|
let mut child = command
|
||||||
|
|
@ -332,12 +363,11 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
|
||||||
fn base_config() -> SpawnConfig {
|
fn base_config() -> PodProcessLaunchConfig {
|
||||||
SpawnConfig {
|
PodProcessLaunchConfig {
|
||||||
runtime_command: PodRuntimeCommand::new("/bin/yoi", vec![OsString::from("pod")]),
|
runtime_command: PodRuntimeCommand::new("/bin/yoi", vec![OsString::from("pod")]),
|
||||||
pod_name: "explicit-pod".to_string(),
|
pod_name: "explicit-pod".to_string(),
|
||||||
profile: Some("project:companion".to_string()),
|
profile: Some("project:companion".to_string()),
|
||||||
ticket_role: None,
|
|
||||||
workspace_root: PathBuf::from("/work/other-project"),
|
workspace_root: PathBuf::from("/work/other-project"),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
|
|
@ -347,7 +377,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_args_keep_workspace_pod_and_profile_separate() {
|
fn runtime_args_keep_workspace_pod_and_profile_separate() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime_args(&base_config()),
|
runtime_args(&base_config(), &PodProcessLaunchOptions::default()),
|
||||||
vec![
|
vec![
|
||||||
"--workspace",
|
"--workspace",
|
||||||
"/work/other-project",
|
"/work/other-project",
|
||||||
|
|
@ -364,7 +394,7 @@ mod tests {
|
||||||
let mut config = base_config();
|
let mut config = base_config();
|
||||||
config.resume_from = Some(Uuid::nil());
|
config.resume_from = Some(Uuid::nil());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime_args(&config),
|
runtime_args(&config, &PodProcessLaunchOptions::default()),
|
||||||
vec![
|
vec![
|
||||||
"--workspace",
|
"--workspace",
|
||||||
"/work/other-project",
|
"/work/other-project",
|
||||||
|
|
@ -377,13 +407,16 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_args_do_not_include_child_cwd() {
|
fn runtime_args_include_upper_resolver_extra_args_without_child_cwd() {
|
||||||
let mut config = base_config();
|
let mut config = base_config();
|
||||||
config.ticket_role = Some("orchestrator".to_string());
|
|
||||||
config.cwd = Some(PathBuf::from("/work/main/.worktree/orchestration/yoi"));
|
config.cwd = Some(PathBuf::from("/work/main/.worktree/orchestration/yoi"));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime_args(&config),
|
runtime_args(
|
||||||
|
&config,
|
||||||
|
&PodProcessLaunchOptions::default()
|
||||||
|
.with_hidden_arg("--ticket-role", "orchestrator"),
|
||||||
|
),
|
||||||
vec![
|
vec![
|
||||||
"--workspace",
|
"--workspace",
|
||||||
"/work/other-project",
|
"/work/other-project",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ use thiserror::Error;
|
||||||
pub use ticket::config::TicketRole;
|
pub use ticket::config::TicketRole;
|
||||||
use ticket::config::{TicketConfig, TicketConfigError, TicketRoleLaunchConfigError};
|
use ticket::config::{TicketConfig, TicketConfigError, TicketRoleLaunchConfigError};
|
||||||
|
|
||||||
use crate::{PodClient, PodRuntimeCommand, SpawnConfig, SpawnError, SpawnReady, spawn_pod};
|
use crate::{
|
||||||
|
PodClient, PodProcessLaunchConfig, PodProcessLaunchOptions, PodRuntimeCommand, SpawnError,
|
||||||
|
SpawnReady, spawn_pod_with_options,
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_FIELD_CHARS: usize = 8_000;
|
const MAX_FIELD_CHARS: usize = 8_000;
|
||||||
const MAX_POD_NAME_CHARS: usize = 80;
|
const MAX_POD_NAME_CHARS: usize = 80;
|
||||||
|
|
@ -170,20 +173,24 @@ impl TicketRoleLaunchPlan {
|
||||||
pub fn spawn_config(
|
pub fn spawn_config(
|
||||||
&self,
|
&self,
|
||||||
runtime_command: PodRuntimeCommand,
|
runtime_command: PodRuntimeCommand,
|
||||||
) -> Result<SpawnConfig, TicketRoleLaunchError> {
|
) -> Result<PodProcessLaunchConfig, TicketRoleLaunchError> {
|
||||||
if self.profile == "inherit" {
|
if self.profile == "inherit" {
|
||||||
return Err(TicketRoleLaunchError::UnsupportedInheritProfile);
|
return Err(TicketRoleLaunchError::UnsupportedInheritProfile);
|
||||||
}
|
}
|
||||||
Ok(SpawnConfig {
|
Ok(PodProcessLaunchConfig {
|
||||||
runtime_command,
|
runtime_command,
|
||||||
pod_name: self.pod_name.clone(),
|
pod_name: self.pod_name.clone(),
|
||||||
profile: Some(self.profile.clone()),
|
profile: Some(self.profile.clone()),
|
||||||
ticket_role: Some(self.role.as_str().to_string()),
|
|
||||||
workspace_root: self.workspace_root.clone(),
|
workspace_root: self.workspace_root.clone(),
|
||||||
cwd: self.cwd.clone(),
|
cwd: self.cwd.clone(),
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn spawn_options(&self) -> PodProcessLaunchOptions {
|
||||||
|
PodProcessLaunchOptions::default()
|
||||||
|
.with_hidden_arg("--ticket-role", self.role.as_str().to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of executing a Ticket role launch.
|
/// Result of executing a Ticket role launch.
|
||||||
|
|
@ -191,9 +198,28 @@ impl TicketRoleLaunchPlan {
|
||||||
pub struct TicketRoleLaunchResult {
|
pub struct TicketRoleLaunchResult {
|
||||||
pub plan: TicketRoleLaunchPlan,
|
pub plan: TicketRoleLaunchPlan,
|
||||||
pub ready: SpawnReady,
|
pub ready: SpawnReady,
|
||||||
|
/// Evidence that the spawned worker accepted the initial Run request.
|
||||||
|
/// This is intentionally distinct from process readiness: a socket
|
||||||
|
/// snapshot only proves that the runtime is reachable, not that the
|
||||||
|
/// worker operation was durably queued/started.
|
||||||
|
pub acceptance_evidence: TicketRoleLaunchAcceptanceEvidence,
|
||||||
pub pre_run_warnings: Vec<TicketRolePreRunWarning>,
|
pub pre_run_warnings: Vec<TicketRolePreRunWarning>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TicketRoleLaunchAcceptanceEvidence {
|
||||||
|
pub pod_name: String,
|
||||||
|
pub accepted_run_segments: usize,
|
||||||
|
pub event: TicketRoleLaunchAcceptanceEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum TicketRoleLaunchAcceptanceEvent {
|
||||||
|
UserMessage,
|
||||||
|
UserSendInvokeStart,
|
||||||
|
TurnStart,
|
||||||
|
}
|
||||||
|
|
||||||
/// Non-fatal diagnostic produced by bounded pre-run launch actions.
|
/// Non-fatal diagnostic produced by bounded pre-run launch actions.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TicketRolePreRunWarning {
|
pub struct TicketRolePreRunWarning {
|
||||||
|
|
@ -369,7 +395,9 @@ where
|
||||||
F: FnMut(&str),
|
F: FnMut(&str),
|
||||||
{
|
{
|
||||||
let plan = plan_ticket_role_launch(context)?;
|
let plan = plan_ticket_role_launch(context)?;
|
||||||
let ready = spawn_pod(plan.spawn_config(runtime_command)?, progress).await?;
|
let spawn_config = plan.spawn_config(runtime_command)?;
|
||||||
|
let spawn_options = plan.spawn_options();
|
||||||
|
let ready = spawn_pod_with_options(spawn_config, spawn_options, progress).await?;
|
||||||
let mut client = PodClient::connect(&ready.socket_path)
|
let mut client = PodClient::connect(&ready.socket_path)
|
||||||
.await
|
.await
|
||||||
.map_err(|source| TicketRoleLaunchError::Connect {
|
.map_err(|source| TicketRoleLaunchError::Connect {
|
||||||
|
|
@ -377,10 +405,17 @@ where
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
let pre_run_warnings = run_pre_run_options_then_send_run(&mut client, &plan, &options).await?;
|
let pre_run_warnings = run_pre_run_options_then_send_run(&mut client, &plan, &options).await?;
|
||||||
wait_for_run_acceptance(&mut client, &plan.run_segments, RUN_ACCEPTANCE_TIMEOUT).await?;
|
let acceptance_event =
|
||||||
|
wait_for_run_acceptance(&mut client, &plan.run_segments, RUN_ACCEPTANCE_TIMEOUT).await?;
|
||||||
|
let acceptance_evidence = TicketRoleLaunchAcceptanceEvidence {
|
||||||
|
pod_name: ready.pod_name.clone(),
|
||||||
|
accepted_run_segments: plan.run_segments.len(),
|
||||||
|
event: acceptance_event,
|
||||||
|
};
|
||||||
Ok(TicketRoleLaunchResult {
|
Ok(TicketRoleLaunchResult {
|
||||||
plan,
|
plan,
|
||||||
ready,
|
ready,
|
||||||
|
acceptance_evidence,
|
||||||
pre_run_warnings,
|
pre_run_warnings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -471,18 +506,20 @@ async fn wait_for_run_acceptance(
|
||||||
client: &mut PodClient,
|
client: &mut PodClient,
|
||||||
expected_segments: &[Segment],
|
expected_segments: &[Segment],
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
) -> Result<(), TicketRoleLaunchError> {
|
) -> Result<TicketRoleLaunchAcceptanceEvent, TicketRoleLaunchError> {
|
||||||
let wait = async {
|
let wait = async {
|
||||||
loop {
|
loop {
|
||||||
let Some(event) = client.next_event().await else {
|
let Some(event) = client.next_event().await else {
|
||||||
return Err(TicketRoleLaunchError::RunAcceptanceClosed);
|
return Err(TicketRoleLaunchError::RunAcceptanceClosed);
|
||||||
};
|
};
|
||||||
match event {
|
match event {
|
||||||
Event::UserMessage { segments } if segments == expected_segments => return Ok(()),
|
Event::UserMessage { segments } if segments == expected_segments => {
|
||||||
|
return Ok(TicketRoleLaunchAcceptanceEvent::UserMessage);
|
||||||
|
}
|
||||||
Event::InvokeStart {
|
Event::InvokeStart {
|
||||||
kind: InvokeKind::UserSend,
|
kind: InvokeKind::UserSend,
|
||||||
}
|
} => return Ok(TicketRoleLaunchAcceptanceEvent::UserSendInvokeStart),
|
||||||
| Event::TurnStart { .. } => return Ok(()),
|
Event::TurnStart { .. } => return Ok(TicketRoleLaunchAcceptanceEvent::TurnStart),
|
||||||
Event::Error { code, message } => {
|
Event::Error { code, message } => {
|
||||||
return Err(TicketRoleLaunchError::RunRejected { code, message });
|
return Err(TicketRoleLaunchError::RunRejected { code, message });
|
||||||
}
|
}
|
||||||
|
|
@ -1026,8 +1063,12 @@ workflow = "ticket-review-workflow"
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(spawn.pod_name, "reviewer-fixed");
|
assert_eq!(spawn.pod_name, "reviewer-fixed");
|
||||||
assert_eq!(spawn.profile.as_deref(), Some("builtin:default"));
|
assert_eq!(spawn.profile.as_deref(), Some("builtin:default"));
|
||||||
assert_eq!(spawn.ticket_role.as_deref(), Some("reviewer"));
|
|
||||||
assert_eq!(spawn.workspace_root, temp.path());
|
assert_eq!(spawn.workspace_root, temp.path());
|
||||||
|
assert!(spawn.cwd.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
plan.spawn_options().extra_args,
|
||||||
|
vec!["--ticket-role".to_string(), "reviewer".to_string()]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ use client::ticket_role::{
|
||||||
TicketRoleLaunchOptions, TicketRoleLaunchResult, launch_ticket_role_pod,
|
TicketRoleLaunchOptions, TicketRoleLaunchResult, launch_ticket_role_pod,
|
||||||
launch_ticket_role_pod_with_options, plan_ticket_role_launch,
|
launch_ticket_role_pod_with_options, plan_ticket_role_launch,
|
||||||
};
|
};
|
||||||
use client::{PodRuntimeCommand, SpawnConfig, spawn_pod};
|
use client::{
|
||||||
|
PodProcessLaunchOptions, PodRuntimeCommand, SpawnConfig, spawn_pod, spawn_pod_with_options,
|
||||||
|
};
|
||||||
use crossterm::event::{
|
use crossterm::event::{
|
||||||
Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
||||||
poll, read,
|
poll, read,
|
||||||
|
|
@ -3281,7 +3283,6 @@ async fn restore_workspace_companion_pod(
|
||||||
runtime_command,
|
runtime_command,
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
profile: None,
|
profile: None,
|
||||||
ticket_role: None,
|
|
||||||
workspace_root: workspace_root.to_path_buf(),
|
workspace_root: workspace_root.to_path_buf(),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
|
|
@ -3298,7 +3299,6 @@ async fn spawn_workspace_companion_pod(
|
||||||
runtime_command,
|
runtime_command,
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
profile: None,
|
profile: None,
|
||||||
ticket_role: None,
|
|
||||||
workspace_root: workspace_root.to_path_buf(),
|
workspace_root: workspace_root.to_path_buf(),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
|
|
@ -3316,12 +3316,17 @@ async fn restore_orchestrator_pod(
|
||||||
runtime_command,
|
runtime_command,
|
||||||
pod_name: pod_name.to_string(),
|
pod_name: pod_name.to_string(),
|
||||||
profile: None,
|
profile: None,
|
||||||
ticket_role: Some("orchestrator".to_string()),
|
|
||||||
workspace_root: original_workspace_root.to_path_buf(),
|
workspace_root: original_workspace_root.to_path_buf(),
|
||||||
cwd: Some(workspace_root.to_path_buf()),
|
cwd: Some(workspace_root.to_path_buf()),
|
||||||
resume_from: None,
|
resume_from: None,
|
||||||
};
|
};
|
||||||
spawn_pod(config, |_| {}).await.map(|_| ())
|
spawn_pod_with_options(
|
||||||
|
config,
|
||||||
|
PodProcessLaunchOptions::default().with_hidden_arg("--ticket-role", "orchestrator"),
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_orchestrator_pod(
|
async fn spawn_orchestrator_pod(
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,6 @@ async fn wait_for_ready(
|
||||||
runtime_command: runtime_command.clone(),
|
runtime_command: runtime_command.clone(),
|
||||||
pod_name: form.name.clone(),
|
pod_name: form.name.clone(),
|
||||||
profile: form.selected_profile_selector(),
|
profile: form.selected_profile_selector(),
|
||||||
ticket_role: None,
|
|
||||||
workspace_root: form.cwd.clone(),
|
workspace_root: form.cwd.clone(),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
resume_from: form.resume_from,
|
resume_from: form.resume_from,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,118 @@ pub struct WorkerImplementation {
|
||||||
pub pod_name: String,
|
pub pod_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RuntimeList<T> {
|
||||||
|
pub items: Vec<T>,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WorkerLookupResult {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub worker: Option<WorkerSummary>,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browser-safe worker spawn request shape.
|
||||||
|
///
|
||||||
|
/// The request intentionally carries only workspace policy intents and stable
|
||||||
|
/// worker identifiers. Raw workspace roots, child cwd, executable path, and raw
|
||||||
|
/// profile selectors are resolved by the host/runtime service and never accepted
|
||||||
|
/// from Workspace API callers.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WorkerSpawnRequest {
|
||||||
|
pub intent: WorkerSpawnIntent,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub requested_worker_name: Option<String>,
|
||||||
|
pub acceptance: WorkerSpawnAcceptanceRequirement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum WorkerSpawnIntent {
|
||||||
|
WorkspaceCompanion,
|
||||||
|
WorkspaceOrchestrator,
|
||||||
|
TicketRole {
|
||||||
|
ticket_id: String,
|
||||||
|
role: TicketWorkerRole,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum TicketWorkerRole {
|
||||||
|
Intake,
|
||||||
|
Orchestrator,
|
||||||
|
Coder,
|
||||||
|
Reviewer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum WorkerSpawnAcceptanceRequirement {
|
||||||
|
SocketReady,
|
||||||
|
RunAccepted { expected_segments: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WorkerSpawnResult {
|
||||||
|
pub state: WorkerOperationState,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub worker: Option<WorkerSummary>,
|
||||||
|
pub acceptance_evidence: Vec<WorkerSpawnAcceptanceEvidence>,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WorkerOperationState {
|
||||||
|
Accepted,
|
||||||
|
Unsupported,
|
||||||
|
Rejected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WorkerSpawnAcceptanceEvidence {
|
||||||
|
pub kind: String,
|
||||||
|
pub detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WorkerStopRequest {
|
||||||
|
pub worker_id: String,
|
||||||
|
pub mode: WorkerStopMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WorkerStopMode {
|
||||||
|
Graceful,
|
||||||
|
Force,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WorkerStopResult {
|
||||||
|
pub state: WorkerOperationState,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WorkerProxyConnectPoint {
|
||||||
|
pub kind: String,
|
||||||
|
pub status: String,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WorkspaceWorkerRuntime: Send + Sync {
|
||||||
|
fn list_hosts(&self, limit: usize) -> RuntimeList<HostSummary>;
|
||||||
|
fn list_workers(&self, limit: usize) -> RuntimeList<WorkerSummary>;
|
||||||
|
fn worker(&self, worker_id: &str) -> WorkerLookupResult;
|
||||||
|
fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult;
|
||||||
|
fn stop_worker(&self, request: WorkerStopRequest) -> WorkerStopResult;
|
||||||
|
fn proxy_connect_points(&self, worker_id: &str) -> Vec<WorkerProxyConnectPoint>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct LocalRuntimeBridge {
|
pub struct LocalRuntimeBridge {
|
||||||
workspace_id: String,
|
workspace_id: String,
|
||||||
|
|
@ -247,6 +359,85 @@ impl LocalRuntimeBridge {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WorkspaceWorkerRuntime for LocalRuntimeBridge {
|
||||||
|
fn list_hosts(&self, limit: usize) -> RuntimeList<HostSummary> {
|
||||||
|
let (items, diagnostics) = LocalRuntimeBridge::list_hosts(self, limit);
|
||||||
|
RuntimeList { items, diagnostics }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_workers(&self, limit: usize) -> RuntimeList<WorkerSummary> {
|
||||||
|
let (items, diagnostics) = LocalRuntimeBridge::list_workers(self, limit);
|
||||||
|
RuntimeList { items, diagnostics }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worker(&self, worker_id: &str) -> WorkerLookupResult {
|
||||||
|
let RuntimeList {
|
||||||
|
items,
|
||||||
|
mut diagnostics,
|
||||||
|
} = WorkspaceWorkerRuntime::list_workers(self, 200);
|
||||||
|
let worker = items
|
||||||
|
.into_iter()
|
||||||
|
.find(|worker| worker.worker_id == worker_id);
|
||||||
|
if worker.is_none() {
|
||||||
|
diagnostics.push(RuntimeDiagnostic::new(
|
||||||
|
"worker_not_found",
|
||||||
|
"info",
|
||||||
|
format!("worker '{worker_id}' was not found on the local runtime"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
truncate_diagnostics(&mut diagnostics);
|
||||||
|
WorkerLookupResult {
|
||||||
|
worker,
|
||||||
|
diagnostics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult {
|
||||||
|
let diagnostic = RuntimeDiagnostic::new(
|
||||||
|
"worker_spawn_resolver_pending",
|
||||||
|
"info",
|
||||||
|
format!(
|
||||||
|
"worker spawn intent '{}' was accepted as a typed request shape, but local launch resolution is not implemented by this ticket",
|
||||||
|
worker_spawn_intent_label(&request.intent)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
WorkerSpawnResult {
|
||||||
|
state: WorkerOperationState::Unsupported,
|
||||||
|
worker: None,
|
||||||
|
acceptance_evidence: Vec::new(),
|
||||||
|
diagnostics: vec![diagnostic],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_worker(&self, request: WorkerStopRequest) -> WorkerStopResult {
|
||||||
|
WorkerStopResult {
|
||||||
|
state: WorkerOperationState::Unsupported,
|
||||||
|
diagnostics: vec![RuntimeDiagnostic::new(
|
||||||
|
"worker_stop_pending",
|
||||||
|
"info",
|
||||||
|
format!(
|
||||||
|
"worker stop for '{}' is reserved for the runtime service boundary and is not implemented by this ticket",
|
||||||
|
request.worker_id
|
||||||
|
),
|
||||||
|
)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_connect_points(&self, worker_id: &str) -> Vec<WorkerProxyConnectPoint> {
|
||||||
|
vec![WorkerProxyConnectPoint {
|
||||||
|
kind: "stream_proxy".to_string(),
|
||||||
|
status: "not_implemented".to_string(),
|
||||||
|
diagnostics: vec![RuntimeDiagnostic::new(
|
||||||
|
"worker_stream_proxy_pending",
|
||||||
|
"info",
|
||||||
|
format!(
|
||||||
|
"future stream/proxy connection point for '{worker_id}' is reserved without opening a protocol surface"
|
||||||
|
),
|
||||||
|
)],
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RuntimeDiagnostic {
|
impl RuntimeDiagnostic {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
code: impl Into<String>,
|
code: impl Into<String>,
|
||||||
|
|
@ -393,6 +584,14 @@ fn safe_metadata_label(value: &str) -> Option<String> {
|
||||||
Some(value.to_string())
|
Some(value.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn worker_spawn_intent_label(intent: &WorkerSpawnIntent) -> &'static str {
|
||||||
|
match intent {
|
||||||
|
WorkerSpawnIntent::WorkspaceCompanion => "workspace_companion",
|
||||||
|
WorkerSpawnIntent::WorkspaceOrchestrator => "workspace_orchestrator",
|
||||||
|
WorkerSpawnIntent::TicketRole { .. } => "ticket_role",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn stable_local_host_id(workspace_id: &str) -> String {
|
fn stable_local_host_id(workspace_id: &str) -> String {
|
||||||
format!("local-{}", sanitize_identifier(workspace_id, 96))
|
format!("local-{}", sanitize_identifier(workspace_id, 96))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ use axum::{Json, Router};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::hosts::{HostSummary, LocalRuntimeBridge, RuntimeDiagnostic, WorkerSummary};
|
use crate::hosts::{
|
||||||
|
HostSummary, LocalRuntimeBridge, RuntimeDiagnostic, WorkerSummary, WorkspaceWorkerRuntime,
|
||||||
|
};
|
||||||
use crate::identity::WorkspaceIdentity;
|
use crate::identity::WorkspaceIdentity;
|
||||||
use crate::records::{
|
use crate::records::{
|
||||||
LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary,
|
LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary,
|
||||||
|
|
@ -61,6 +63,7 @@ pub struct WorkspaceApi {
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
store: Arc<dyn ControlPlaneStore>,
|
store: Arc<dyn ControlPlaneStore>,
|
||||||
records: LocalProjectRecordReader,
|
records: LocalProjectRecordReader,
|
||||||
|
runtime: Arc<dyn WorkspaceWorkerRuntime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceApi {
|
impl WorkspaceApi {
|
||||||
|
|
@ -74,10 +77,16 @@ impl WorkspaceApi {
|
||||||
updated_at: config.workspace_created_at.clone(),
|
updated_at: config.workspace_created_at.clone(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
let runtime = Arc::new(LocalRuntimeBridge::new(
|
||||||
|
config.workspace_id.clone(),
|
||||||
|
config.workspace_root.clone(),
|
||||||
|
config.local_runtime_data_dir.clone(),
|
||||||
|
));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
records: LocalProjectRecordReader::new(config.workspace_root.clone()),
|
records: LocalProjectRecordReader::new(config.workspace_root.clone()),
|
||||||
config,
|
config,
|
||||||
store,
|
store,
|
||||||
|
runtime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,14 +94,6 @@ impl WorkspaceApi {
|
||||||
self.config.workspace_id.as_str()
|
self.config.workspace_id.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_runtime_bridge(&self) -> LocalRuntimeBridge {
|
|
||||||
LocalRuntimeBridge::new(
|
|
||||||
self.config.workspace_id.clone(),
|
|
||||||
self.config.workspace_root.clone(),
|
|
||||||
self.config.local_runtime_data_dir.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_repository_reader(&self) -> LocalRepositoryReader {
|
fn local_repository_reader(&self) -> LocalRepositoryReader {
|
||||||
LocalRepositoryReader::new(
|
LocalRepositoryReader::new(
|
||||||
self.config.workspace_root.clone(),
|
self.config.workspace_root.clone(),
|
||||||
|
|
@ -390,14 +391,13 @@ async fn list_hosts(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
|
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
|
||||||
let limit = api.config.max_records.min(200);
|
let limit = api.config.max_records.min(200);
|
||||||
let bridge = api.local_runtime_bridge();
|
let runtime_hosts = api.runtime.list_hosts(limit);
|
||||||
let (items, diagnostics) = bridge.list_hosts(limit);
|
|
||||||
Ok(Json(RuntimeListResponse {
|
Ok(Json(RuntimeListResponse {
|
||||||
workspace_id: api.config.workspace_id,
|
workspace_id: api.config.workspace_id,
|
||||||
limit,
|
limit,
|
||||||
items,
|
items: runtime_hosts.items,
|
||||||
source: "local_pod_metadata".to_string(),
|
source: "worker_runtime".to_string(),
|
||||||
diagnostics,
|
diagnostics: runtime_hosts.diagnostics,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,8 +411,13 @@ async fn list_host_workers(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(host_id): AxumPath<String>,
|
AxumPath(host_id): AxumPath<String>,
|
||||||
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
|
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
|
||||||
let bridge = api.local_runtime_bridge();
|
let runtime_hosts = api.runtime.list_hosts(1);
|
||||||
if host_id != bridge.host_id() {
|
let expected_host_id = runtime_hosts
|
||||||
|
.items
|
||||||
|
.first()
|
||||||
|
.map(|host| host.host_id.as_str())
|
||||||
|
.ok_or_else(|| Error::UnknownHost(host_id.clone()))?;
|
||||||
|
if host_id != expected_host_id {
|
||||||
return Err(Error::UnknownHost(host_id).into());
|
return Err(Error::UnknownHost(host_id).into());
|
||||||
}
|
}
|
||||||
workers_response(api).map(Json)
|
workers_response(api).map(Json)
|
||||||
|
|
@ -420,14 +425,13 @@ async fn list_host_workers(
|
||||||
|
|
||||||
fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSummary>> {
|
fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSummary>> {
|
||||||
let limit = api.config.max_records.min(200);
|
let limit = api.config.max_records.min(200);
|
||||||
let bridge = api.local_runtime_bridge();
|
let runtime_workers = api.runtime.list_workers(limit);
|
||||||
let (items, diagnostics) = bridge.list_workers(limit);
|
|
||||||
Ok(RuntimeListResponse {
|
Ok(RuntimeListResponse {
|
||||||
workspace_id: api.config.workspace_id,
|
workspace_id: api.config.workspace_id,
|
||||||
limit,
|
limit,
|
||||||
items,
|
items: runtime_workers.items,
|
||||||
source: "local_pod_metadata".to_string(),
|
source: "worker_runtime".to_string(),
|
||||||
diagnostics,
|
diagnostics: runtime_workers.diagnostics,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user