You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/25 08:25:27 UTC
(maka) 01/02: feat(workhub): configure terminal task creation and model recovery
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 97acb4cc57dc6762e38c21a7e1d50c689899822b
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Fri Sep 25 16:24:26 2026 +0800
feat(workhub): configure terminal task creation and model recovery
Use caller-authorized setup, exact assignment details and isolated composition slots for terminal workflows.
Generated-by: Codex
---
crates/workhub/src/decision.rs | 16 +-
crates/workhub/src/plugin/remote.rs | 51 +-
crates/workhub/src/plugin/terminal.rs | 190 ++++-
crates/workhub/src/plugin/terminal/setup.rs | 1111 +++++++++++++++++++++++++++
4 files changed, 1331 insertions(+), 37 deletions(-)
diff --git a/crates/workhub/src/decision.rs b/crates/workhub/src/decision.rs
index 91574d4e47..a1aa71d297 100644
--- a/crates/workhub/src/decision.rs
+++ b/crates/workhub/src/decision.rs
@@ -108,7 +108,11 @@ pub(super) struct Record {
}
impl super::plugin::Manager {
- pub(super) async fn configure_creation(&self, creation: Creation) -> Result<(), Error> {
+ pub(super) async fn configure_creation(
+ &self,
+ creation: Creation,
+ revision: Option<u64>,
+ ) -> Result<(), Error> {
let authorization::Target::Workspace { sandbox_mode, .. } = &creation.authorization else {
return Err(invalid("New work requires explicit workspace consent"));
};
@@ -117,12 +121,10 @@ impl super::plugin::Manager {
}
creation.settings.validate().map_err(invalid)?;
self.access.commands(&creation.authorization).await?;
- let repository = &self.assignments.repository;
- let revision = repository
- .read::<Creation>("creation")
- .await?
- .map(|(revision, _)| revision);
- repository.put("creation", revision, &creation).await
+ self.assignments
+ .repository
+ .put("creation", revision, &creation)
+ .await
}
pub(super) async fn candidates(&self) -> Result<crate::candidates::Candidates, Error> {
diff --git a/crates/workhub/src/plugin/remote.rs b/crates/workhub/src/plugin/remote.rs
index 910971bd1a..9f0360228f 100644
--- a/crates/workhub/src/plugin/remote.rs
+++ b/crates/workhub/src/plugin/remote.rs
@@ -306,21 +306,14 @@ impl Method for Call {
}
Action::Creation => {
let creation: crate::decision::Creation = decode(input)?;
- let authority = caller
- .views
- .authorize(Authorization {
- operation_id: uuid::Uuid::new_v4(),
- title: "Choose WorkHub task defaults".into(),
- target: creation.authorization.clone(),
- capabilities: [Capability::Executions].into(),
- })
- .await?;
- let result = manager.configure_creation(creation).await.map_err(failure);
- authority
- .finish()
+ let revision = manager
+ .assignments
+ .repository
+ .read::<crate::decision::Creation>("creation")
.await
- .map_err(|_| Error::CleanupUnconfirmed)?;
- result.map(|()| Value::Null)
+ .map_err(failure)?
+ .map(|(revision, _)| revision);
+ configure_creation(&manager, &caller, creation, revision).await
}
Action::Decide => {
#[derive(Deserialize)]
@@ -514,6 +507,34 @@ impl Method for Call {
})
}
}
+/// Both clients use the same authority and configuration boundary. The terminal
+/// can retain its observed revision across editing and consent.
+pub(super) async fn configure_creation(
+ manager: &Manager,
+ caller: &Caller,
+ creation: crate::decision::Creation,
+ revision: Option<u64>,
+) -> Result<Value, Error> {
+ let _wake = Wake(manager);
+ let authority = caller
+ .views
+ .authorize(Authorization {
+ operation_id: uuid::Uuid::new_v4(),
+ title: "Choose WorkHub task defaults".into(),
+ target: creation.authorization.clone(),
+ capabilities: [Capability::Executions].into(),
+ })
+ .await?;
+ let result = manager
+ .configure_creation(creation, revision)
+ .await
+ .map_err(failure);
+ authority
+ .finish()
+ .await
+ .map_err(|_| Error::CleanupUnconfirmed)?;
+ result.map(|()| Value::Null)
+}
fn decode<T: DeserializeOwned>(input: Value) -> Result<T, Error> {
serde_json::from_value(input).map_err(|e| Error::Invalid(e.to_string()))
}
@@ -530,7 +551,7 @@ fn empty(input: Value) -> Result<(), Error> {
fn command(error: CommandError) -> Error {
failure(error.into())
}
-fn failure(error: WorkhubError) -> Error {
+pub(super) fn failure(error: WorkhubError) -> Error {
match error {
WorkhubError::Storage(maka_plugins::storage::StoreError::OutcomeUnknown(reason))
| WorkhubError::Execution(CommandError::OutcomeUnknown(reason)) => {
diff --git a/crates/workhub/src/plugin/terminal.rs b/crates/workhub/src/plugin/terminal.rs
index 70a1eb6a67..c11abd05fa 100644
--- a/crates/workhub/src/plugin/terminal.rs
+++ b/crates/workhub/src/plugin/terminal.rs
@@ -32,7 +32,7 @@ use maka_plugins::{
contributions::Staged,
remote::{Caller, Error, Method, key},
terminal_ui::{
- Context, Descriptor, Text, VERSION,
+ Context, Descriptor, Placement, Text, VERSION,
app::{self, App, Cx, Submission, Words},
view::{self, Action, Confirm, Node, Reply, Role, Tone, View, build::*},
},
@@ -41,23 +41,46 @@ use serde::Deserialize;
use serde_json::{Value, json};
use std::sync::Arc;
+mod setup;
+
pub(super) fn publish(
staged: &mut Staged,
manager: Arc<Manager>,
package: &str,
) -> Result<(), String> {
let endpoint = app::endpoint(
- Hub(manager),
+ Hub(manager.clone()),
Descriptor::new(Text::plain("WorkHub"), Context::Application)
.icon("◈", "H")
.order(10),
)
- .map_err(|error| error.to_string())?;
+ .map_err(|error| error.to_string())?
+ .requiring_host_paths();
staged
.insert(
key(package, "terminal").map_err(|error| error.to_string())?,
endpoint,
)
+ .map_err(|error| error.to_string())?;
+ staged
+ .insert(
+ key(package, "task-models").map_err(|error| error.to_string())?,
+ app::endpoint(
+ setup::SessionModels(Hub(manager)),
+ Descriptor::new(
+ Text::localized(
+ "WorkHub task models",
+ "WorkHub 任务模型",
+ "WorkHub 任務模型",
+ ),
+ Context::Session,
+ )
+ .placement(Placement::Panel)
+ .icon("◈", "H")
+ .order(40),
+ )
+ .map_err(|error| error.to_string())?,
+ )
.map_err(|error| error.to_string())
}
@@ -132,9 +155,42 @@ struct Place {
filter: Option<String>,
after: Option<String>,
assignment: Option<String>,
+ setup: Option<setup::Route>,
}
impl Hub {
+ async fn detail(&self, id: &str) -> Result<Option<Summary>, Error> {
+ let assignment = self
+ .0
+ .assignments
+ .repository
+ .read::<crate::assignment::Assignment>(
+ &crate::assignment::key(id).map_err(super::remote::failure)?,
+ )
+ .await
+ .map_err(super::remote::failure)?;
+ Ok(assignment.map(|(_, assignment)| Summary {
+ operation_id: assignment.request.operation_id,
+ title: match assignment.request.target {
+ crate::assignment::Target::Create { request } => request.name,
+ crate::assignment::Target::Existing { .. } => {
+ assignment.request.content.text.chars().take(160).collect()
+ }
+ },
+ source: Invocation {
+ session_id: assignment.request.source.session_id,
+ },
+ delivery: assignment.delivery.map(|delivery| Delivery {
+ receipt: Receipt {
+ invocation: Invocation {
+ session_id: delivery.invocation().session_id.clone(),
+ },
+ },
+ }),
+ retired: assignment.retired,
+ control: assignment.control,
+ }))
+ }
async fn call<T: for<'de> Deserialize<'de>>(
&self,
action: Call,
@@ -182,19 +238,32 @@ impl App for Hub {
let this = Hub(self.0.clone());
Box::pin(async move {
let words = &cx.words;
+ let place: Place = serde_json::from_value(route).unwrap_or_default();
+ if let Some(route) = place.setup {
+ return setup::read(&this, route, &cx).await;
+ }
if !this.ready(&cx.caller).await? {
return Ok(welcome(words));
}
- let query: Query = this.call(Call::Query, Value::Null, &cx.caller).await?;
- let place: Place = serde_json::from_value(route).unwrap_or_default();
- let (page, feedback) = this.board(&cx.caller, place.after.clone()).await?;
if let Some(id) = &place.assignment {
- let summary = page.entries.iter().find(|entry| &entry.operation_id == id);
- let state = feedback.iter().find(|item| &item.id == id);
- return Ok(assignment(words, id, summary, state));
+ let summary = this.detail(id).await?;
+ let feedback: Vec<Feedback> =
+ this.call(Call::Feedback, json!([id]), &cx.caller).await?;
+ return Ok(assignment(words, id, summary.as_ref(), feedback.first()));
}
+ let query: Query = this.call(Call::Query, Value::Null, &cx.caller).await?;
+ let (page, feedback) = this.board(&cx.caller, place.after.clone()).await?;
let filter = place.filter.unwrap_or_else(|| "active".into());
- Ok(board(words, &query, &page, &feedback, &filter))
+ let mut view = board(
+ words,
+ &query,
+ &page,
+ &feedback,
+ &filter,
+ place.after.as_deref(),
+ );
+ setup::entries(&this, &mut view, &cx).await?;
+ Ok(view)
})
}
@@ -210,7 +279,16 @@ impl App for Hub {
}),
error => Err(error),
};
+ if submission.action == "search" || submission.action == "save-target" {
+ return match setup::submit(&this, submission, &cx).await {
+ Err(Error::Invalid(message)) => Ok(Reply::Rejected {
+ message: clean(&message, false).chars().take(256).collect(),
+ }),
+ result => result,
+ };
+ }
match submission.action.as_str() {
+ "discovery" => setup::discovery(&this, submission, &cx).await,
"setup" => {
let Some(grant) = submission.grant else {
return Ok(Reply::Consent {
@@ -310,6 +388,8 @@ fn welcome(words: &Words) -> View {
Tone::Subtle,
),
row("setup", vec![button("setup", "setup", Role::Primary)]),
+ link("coordinator-model", words.t("Choose coordinator model", "选择协调器模型", "選擇協調器模型"),
+ json!({"setup":{"purpose":{"kind":"coordinator"}}})).into(),
],
),
)
@@ -330,7 +410,14 @@ fn state(words: &Words, state: Option<&str>) -> (String, Tone, &'static str) {
(words.t(en, zh_cn, zh_tw), tone, group)
}
-fn board(words: &Words, query: &Query, page: &Page, feedback: &[Feedback], filter: &str) -> View {
+fn board(
+ words: &Words,
+ query: &Query,
+ page: &Page,
+ feedback: &[Feedback],
+ filter: &str,
+ after: Option<&str>,
+) -> View {
let mut children = vec![];
let mut actions = vec![];
match &query.coordinator_session_id {
@@ -414,7 +501,7 @@ fn board(words: &Words, query: &Query, page: &Page, feedback: &[Feedback], filte
let mut row = link(
format!("work-{}", entry.operation_id).replace('/', ":"),
view::build::clean(&entry.title, false),
- json!({"assignment": entry.operation_id, "filter": filter}),
+ json!({"assignment": entry.operation_id, "filter": filter, "after": after}),
)
.meta(label)
.tone(tone);
@@ -525,9 +612,29 @@ fn assignment(
session: summary.source.session_id.clone(),
},
});
+ if !summary.retired {
+ children.push(
+ link(
+ "model",
+ words.t("Change task model", "更换任务模型", "更換任務模型"),
+ json!({"setup":{"purpose":{"kind":"delegation","assignment":id}}}),
+ )
+ .into(),
+ );
+ // Only stable task/session identities cross the extension boundary.
+ // A filler owns its own data; it receives no result text or authority.
+ children.push(slot(
+ format!("extensions-{}", crate::repository::digest(&id).expect("assignment identity serializes")),
+ "workhub.task.detail",
+ json!({"assignmentId":id,"sourceSessionId":summary.source.session_id,
+ "sessionId":summary.delivery.as_ref().map(|delivery| &delivery.receipt.invocation.session_id)}),
+ ));
+ }
let mut actions = vec![];
let stopped = feedback.is_some_and(|item| item.state == "aborted");
- if summary.control.is_some() {
+ if summary.retired {
+ // Retired assignments keep their history but offer no new controls.
+ } else if summary.control.is_some() {
// A stop or resume is already on its way; another waits for it.
children.push(text(
"applying",
@@ -630,13 +737,14 @@ mod tests {
result_preview: Some("Which branch?".into()),
},
];
- let active = board(&words, &query, &page, &feedback, "active");
+ let active = board(&words, &query, &page, &feedback, "active", None);
active.validate().unwrap();
let text = serde_json::to_string(&active).unwrap();
assert!(text.contains("Task a") && !text.contains("Task b"));
assert!(text.contains("\"kind\":\"session\"") && text.contains("Could not reach"));
let attention =
- serde_json::to_string(&board(&words, &query, &page, &feedback, "attention")).unwrap();
+ serde_json::to_string(&board(&words, &query, &page, &feedback, "attention", None))
+ .unwrap();
assert!(attention.contains("Task b") && attention.contains("Which branch?"));
let detail = assignment(&words, "a", page.entries.first(), feedback.first());
detail.validate().unwrap();
@@ -650,5 +758,57 @@ mod tests {
.destructive
);
assert!(serde_json::to_string(&detail).unwrap().contains("worker-a"));
+ let Node::Column { children, .. } = &detail.root else {
+ panic!("task detail column");
+ };
+ let context = children
+ .iter()
+ .find_map(|node| match node {
+ Node::Slot { name, context, .. } if name == "workhub.task.detail" => Some(context),
+ _ => None,
+ })
+ .unwrap();
+ assert_eq!(
+ context,
+ &json!({"assignmentId":"a","sourceSessionId":"coordinator","sessionId":"worker-a"})
+ );
+ let slot_key = |view: &View| {
+ let Node::Column { children, .. } = &view.root else {
+ panic!("task detail")
+ };
+ children
+ .iter()
+ .find_map(|node| match node {
+ Node::Slot { key, .. } => Some(key.clone()),
+ _ => None,
+ })
+ .unwrap()
+ };
+ let second = assignment(&words, "b", page.entries.get(1), feedback.get(1));
+ second.validate().unwrap();
+ assert_ne!(slot_key(&detail), slot_key(&second));
+ assert_eq!(
+ slot_key(&detail),
+ slot_key(&assignment(
+ &words,
+ "a",
+ page.entries.first(),
+ feedback.get(1)
+ ))
+ );
+
+ let mut retired = summary("retired");
+ retired.retired = true;
+ let history = assignment(&words, "retired", Some(&retired), feedback.first());
+ history.validate().unwrap();
+ assert!(history.actions.is_empty());
+ let Node::Column { children, .. } = history.root else {
+ panic!("task history")
+ };
+ assert!(
+ !children
+ .iter()
+ .any(|node| matches!(node, Node::Slot { .. }))
+ );
}
}
diff --git a/crates/workhub/src/plugin/terminal/setup.rs b/crates/workhub/src/plugin/terminal/setup.rs
new file mode 100644
index 0000000000..ff7caf4994
--- /dev/null
+++ b/crates/workhub/src/plugin/terminal/setup.rs
@@ -0,0 +1,1111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use super::*;
+use crate::decision::Creation;
+use crate::plugin::remote::failure;
+use maka_plugins::{authorization::Id, execution::Target, executor, llm};
+use maka_runtime::execution::{CollaborationMode, SandboxMode, WorkspaceTarget};
+use serde::Serialize;
+
+#[derive(Clone, Deserialize, Serialize)]
+#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
+pub(super) enum Purpose {
+ Creation,
+ Coordinator,
+ Delegation { assignment: String },
+}
+
+#[derive(Clone, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub(super) struct Route {
+ purpose: Purpose,
+ #[serde(default)]
+ query: String,
+ #[serde(default)]
+ target: Option<Target>,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct Delegation {
+ revision: Option<u64>,
+ authorization: Scope,
+ name: String,
+}
+
+#[derive(Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct Stamp {
+ operation: uuid::Uuid,
+ revision: Option<u64>,
+}
+
+fn failed(error: impl std::fmt::Display) -> Error {
+ Error::Provider(error.to_string())
+}
+
+fn bounded(value: &str, limit: usize) -> String {
+ let value = clean(value, false);
+ value[..value.floor_char_boundary(limit.min(value.len()))].to_owned()
+}
+
+fn route(route: &Route) -> Value {
+ json!({"setup": route})
+}
+
+fn title(words: &Words, purpose: &Purpose) -> String {
+ match purpose {
+ Purpose::Creation => words.t("New task setup", "新任务设置", "新任務設定"),
+ Purpose::Coordinator => words.t("Coordinator model", "协调器模型", "協調器模型"),
+ Purpose::Delegation { .. } => words.t("Task model", "任务模型", "任務模型"),
+ }
+}
+
+pub(super) async fn entries(hub: &Hub, view: &mut View, cx: &Cx) -> Result<(), Error> {
+ let Node::Column { children, .. } = &mut view.root else {
+ return Ok(());
+ };
+ let creation = hub
+ .0
+ .assignments
+ .repository
+ .read::<Creation>("creation")
+ .await
+ .map_err(failure)?;
+ let mut entry = link(
+ "creation",
+ title(&cx.words, &Purpose::Creation),
+ json!({"setup":{"purpose":{"kind":"creation"}}}),
+ );
+ if let Some((_, creation)) = creation {
+ if let Scope::Workspace { workspace, .. } = creation.authorization {
+ let location = match workspace {
+ WorkspaceTarget::HostPath { path } => path,
+ WorkspaceTarget::Project { project_id } => project_id,
+ };
+ entry = entry.detail(bounded(&location, 1024));
+ }
+ } else {
+ entry = entry
+ .detail(cx.t(
+ "Choose where new tasks run",
+ "选择新任务的运行位置",
+ "選擇新任務的執行位置",
+ ))
+ .tone(Tone::Warning);
+ }
+ children.insert(0, entry.into());
+ children.push(
+ link(
+ "coordinator-model",
+ title(&cx.words, &Purpose::Coordinator),
+ json!({"setup":{"purpose":{"kind":"coordinator"}}}),
+ )
+ .into(),
+ );
+ view.actions.push(action(
+ "discovery",
+ cx.t("Task discovery access", "任务发现授权", "任務探索授權"),
+ ));
+ children.push(row(
+ "discovery",
+ vec![button("discovery", "discovery", Role::Normal)],
+ ));
+ Ok(())
+}
+
+pub(super) async fn read(hub: &Hub, route: Route, cx: &Cx) -> Result<View, Error> {
+ let delegation = match &route.purpose {
+ Purpose::Delegation { assignment } => Some(
+ hub.call::<Delegation>(
+ Call::DelegationModel,
+ json!({"assignmentId":assignment}),
+ &cx.caller,
+ )
+ .await?,
+ ),
+ _ => None,
+ };
+ if let Some(Delegation {
+ authorization: Scope::Session { session_id },
+ ..
+ }) = &delegation
+ && cx.caller.session_id.as_ref() != Some(session_id)
+ {
+ return Ok(session_handoff(&cx.words, session_id));
+ }
+ let Some(target) = route.target.as_ref() else {
+ let models: llm::Choices = hub
+ .call(Call::Models, json!({"query":route.query}), &cx.caller)
+ .await?;
+ let executors = if matches!(route.purpose, Purpose::Creation) {
+ Some(
+ hub.call::<executor::Choices>(
+ Call::Executors,
+ json!({"query":route.query}),
+ &cx.caller,
+ )
+ .await?,
+ )
+ } else {
+ None
+ };
+ return Ok(picker(&cx.words, &route, models, executors));
+ };
+ target.validate().map_err(failed)?;
+ if !matches!(route.purpose, Purpose::Creation) && !matches!(target, Target::Model { .. }) {
+ return Err(Error::Invalid("Recovery requires a model".into()));
+ }
+ let creation = hub
+ .0
+ .assignments
+ .repository
+ .read::<Creation>("creation")
+ .await
+ .map_err(failure)?;
+ form(&cx.words, &route, creation, delegation.as_ref())
+}
+
+fn session_handoff(words: &Words, session: &str) -> View {
+ view_of(&words.t("Task model", "任务模型", "任務模型"), "session", vec![], column("root", vec![
+ text("explanation", words.t(
+ "Open this session, then choose WorkHub task models in its panels to change the model.",
+ "打开此会话,再从会话面板选择 WorkHub 任务模型以更换模型。",
+ "開啟此工作階段,再從工作階段面板選擇 WorkHub 任務模型以更換模型。"), Tone::Muted),
+ Node::Item { key: "session".into(), title: words.t("Open task session", "打开任务会话", "開啟任務工作階段"), detail: String::new(), meta: String::new(), tone: Tone::Accent, current: false, target: view::Target::Session { session: session.into() } },
+ ]))
+}
+
+pub(super) struct SessionModels(pub(super) Hub);
+
+impl SessionModels {
+ async fn route(&self, route: &Value, cx: &Cx) -> Result<Option<Route>, Error> {
+ let session = cx.session()?;
+ if route.is_null() {
+ return Ok(None);
+ }
+ let place: Place = serde_json::from_value(route.clone()).map_err(failed)?;
+ let Some(setup) = place.setup else {
+ return Ok(None);
+ };
+ let Purpose::Delegation { assignment } = &setup.purpose else {
+ return Err(Error::Invalid(
+ "Open task model recovery from this Session".into(),
+ ));
+ };
+ let choice: Delegation = self
+ .0
+ .call(
+ Call::DelegationModel,
+ json!({"assignmentId":assignment}),
+ &cx.caller,
+ )
+ .await?;
+ if choice.authorization
+ != (Scope::Session {
+ session_id: session.into(),
+ })
+ {
+ return Err(Error::Invalid(
+ "The task belongs to another authorization scope".into(),
+ ));
+ }
+ Ok(Some(setup))
+ }
+}
+
+impl App for SessionModels {
+ fn read(&self, route: Value, cx: Cx) -> BoxFuture<'static, Result<View, Error>> {
+ let this = Self(Hub(self.0.0.clone()));
+ Box::pin(async move {
+ if let Some(setup) = this.route(&route, &cx).await? {
+ return read(&this.0, setup, &cx).await;
+ }
+ let session = cx.session()?;
+ let place: Place = serde_json::from_value(route).unwrap_or_default();
+ let (assignments, mut after) = this
+ .0
+ .0
+ .assignments
+ .repository
+ .scan::<crate::assignment::Assignment>("assignments/", place.after)
+ .await
+ .map_err(failure)?;
+ if assignments.len() > 32 {
+ after = Some(
+ crate::assignment::key(&assignments[31].request.operation_id)
+ .map_err(failure)?,
+ );
+ }
+ let mut children: Vec<Node> = assignments.into_iter().take(32).filter(|assignment| {
+ !assignment.retired && assignment.request.authorization == (Scope::Session { session_id: session.into() })
+ }).enumerate().map(|(index, assignment)| {
+ link(format!("task-{index}"), bounded(&assignment.request.content.text, 256),
+ json!({"setup":{"purpose":{"kind":"delegation","assignment":assignment.request.operation_id}}})).into()
+ }).collect();
+ if children.is_empty() {
+ children.push(text(
+ "empty",
+ cx.t(
+ "No delegated tasks for this session on this page.",
+ "此页没有属于此会话的委派任务。",
+ "此頁沒有屬於此工作階段的委派任務。",
+ ),
+ Tone::Muted,
+ ));
+ }
+ if let Some(after) = after {
+ children.push(
+ link(
+ "more",
+ cx.t("Older tasks", "更早的任务", "更早的任務"),
+ json!({"after":after}),
+ )
+ .into(),
+ );
+ }
+ Ok(view_of(
+ &cx.t(
+ "WorkHub task models",
+ "WorkHub 任务模型",
+ "WorkHub 任務模型",
+ ),
+ "tasks",
+ vec![],
+ column("root", children),
+ ))
+ })
+ }
+ fn submit(&self, submission: Submission, cx: Cx) -> BoxFuture<'static, Result<Reply, Error>> {
+ let this = Self(Hub(self.0.0.clone()));
+ Box::pin(async move {
+ this.route(&submission.route, &cx)
+ .await?
+ .ok_or_else(|| Error::Invalid("Choose a delegated task".into()))?;
+ submit(&this.0, submission, &cx).await
+ })
+ }
+}
+
+fn picker(
+ words: &Words,
+ current: &Route,
+ models: llm::Choices,
+ executors: Option<executor::Choices>,
+) -> View {
+ let mut items = Vec::new();
+ let incomplete = !models.complete
+ || models.models.len() > 20
+ || executors
+ .as_ref()
+ .is_some_and(|page| !page.complete || page.executors.len() > 12);
+ for (index, model) in models.models.into_iter().take(20).enumerate() {
+ let target = Target::Model {
+ model: model.model,
+ thinking_level: model.default_thinking_level,
+ };
+ items.push(
+ link(
+ format!("model-{index}"),
+ bounded(&model.display_name, 256),
+ route(&Route {
+ target: Some(target),
+ ..current.clone()
+ }),
+ )
+ .detail(bounded(&model.connection_name, 1024))
+ .into(),
+ );
+ }
+ if let Some(executors) = executors {
+ for (index, executor) in executors.executors.into_iter().take(12).enumerate() {
+ items.push(
+ link(
+ format!("executor-{index}"),
+ bounded(&executor.display_name, 256),
+ route(&Route {
+ target: Some(Target::Executor {
+ executor_id: executor.id,
+ settings: Default::default(),
+ }),
+ ..current.clone()
+ }),
+ )
+ .detail(words.t("Plugin executor", "插件执行器", "外掛執行器"))
+ .into(),
+ );
+ }
+ }
+ let mut children = vec![
+ input(
+ "query",
+ "query",
+ words.t(
+ "Find a model or executor",
+ "查找模型或执行器",
+ "尋找模型或執行器",
+ ),
+ ),
+ row("search", vec![button("search", "search", Role::Normal)]),
+ ];
+ if items.is_empty() {
+ children.push(text(
+ "empty",
+ words.t(
+ "No available choices. Configure a connection or refine the search.",
+ "没有可用选项。请配置连接或调整搜索条件。",
+ "沒有可用選項。請設定連線或調整搜尋條件。",
+ ),
+ Tone::Muted,
+ ));
+ } else {
+ children.push(scroll("choices", 20, stack("items", items)));
+ }
+ if incomplete {
+ children.push(text(
+ "more",
+ words.t(
+ "Refine the search to see more choices.",
+ "缩小搜索范围以查看更多选项。",
+ "縮小搜尋範圍以查看更多選項。",
+ ),
+ Tone::Subtle,
+ ));
+ }
+ View {
+ fields: vec![line("query", ¤t.query, 512)],
+ ..view_of(
+ &title(words, ¤t.purpose),
+ "choices",
+ vec![Action {
+ fields: vec!["query".into()],
+ ..action("search", words.t("Search", "搜索", "搜尋"))
+ }],
+ column("root", children),
+ )
+ }
+}
+
+fn form(
+ words: &Words,
+ current: &Route,
+ creation: Option<(u64, Creation)>,
+ delegation: Option<&Delegation>,
+) -> Result<View, Error> {
+ let mut fields = Vec::new();
+ let target = current
+ .target
+ .as_ref()
+ .ok_or_else(|| Error::Invalid("Choose an execution target".into()))?;
+ let label = match target {
+ Target::Model { model, .. } => format!("{} · {}", model.connection_slug, model.model),
+ Target::Executor { executor_id, .. } => executor_id.as_str().to_owned(),
+ };
+ let mut children = vec![text("target", clean(&label, false), Tone::Strong)];
+ if matches!(current.purpose, Purpose::Creation) {
+ let (kind, location, mode, collaboration) =
+ match creation.as_ref().map(|(_, creation)| creation) {
+ Some(Creation {
+ authorization:
+ Scope::Workspace {
+ workspace,
+ sandbox_mode,
+ },
+ settings,
+ }) => {
+ let (kind, location) = match workspace {
+ WorkspaceTarget::HostPath { path } => ("path", path.clone()),
+ WorkspaceTarget::Project { project_id } => ("project", project_id.clone()),
+ };
+ (kind, location, *sandbox_mode, settings.collaboration_mode)
+ }
+ _ => (
+ "path",
+ String::new(),
+ SandboxMode::WorkspaceWrite,
+ CollaborationMode::Agent,
+ ),
+ };
+ fields.extend([
+ choice(
+ "location-kind",
+ kind,
+ vec![
+ (
+ "path".into(),
+ words.t("Host directory", "Host 目录", "Host 目錄"),
+ ),
+ (
+ "project".into(),
+ words.t("Project ID", "项目 ID", "專案 ID"),
+ ),
+ ],
+ ),
+ line("location", location, 4096),
+ choice(
+ "sandbox",
+ if mode == SandboxMode::ReadOnly {
+ "read-only"
+ } else {
+ "workspace-write"
+ },
+ vec![
+ ("read-only".into(), words.t("Read only", "只读", "唯讀")),
+ (
+ "workspace-write".into(),
+ words.t("Write in workspace", "可写工作区", "可寫工作區"),
+ ),
+ ],
+ ),
+ ]);
+ children.extend([
+ input(
+ "location-kind",
+ "location-kind",
+ words.t("Workspace", "工作区", "工作區"),
+ ),
+ input(
+ "location",
+ "location",
+ words.t("Directory or project ID", "目录或项目 ID", "目錄或專案 ID"),
+ ),
+ input(
+ "sandbox",
+ "sandbox",
+ words.t("Access", "访问权限", "存取權限"),
+ ),
+ ]);
+ if matches!(target, Target::Model { .. }) {
+ fields.push(choice(
+ "collaboration",
+ if collaboration == CollaborationMode::Plan {
+ "plan"
+ } else {
+ "agent"
+ },
+ vec![
+ ("agent".into(), words.t("Act", "执行", "執行")),
+ ("plan".into(), words.t("Plan first", "先规划", "先規劃")),
+ ],
+ ));
+ children.push(input(
+ "collaboration",
+ "collaboration",
+ words.t("Task mode", "任务模式", "任務模式"),
+ ));
+ }
+ children.push(text(
+ "scope",
+ words.t(
+ "These defaults apply to new tasks. Existing tasks keep their settings.",
+ "这些默认设置用于新任务。已有任务保留其设置。",
+ "這些預設設定用於新任務。既有任務保留其設定。",
+ ),
+ Tone::Subtle,
+ ));
+ } else if let Some(choice) = delegation {
+ children.push(text("task", clean(&choice.name, false), Tone::Muted));
+ children.push(text(
+ "scope",
+ words.t(
+ "The same delegated task keeps its original dispatch identity.",
+ "继续使用同一委派任务及其原始派发标识。",
+ "繼續使用同一委派任務及其原始派發識別。",
+ ),
+ Tone::Subtle,
+ ));
+ }
+ let stamp = Stamp {
+ operation: uuid::Uuid::new_v4(),
+ revision: if matches!(current.purpose, Purpose::Creation) {
+ creation.as_ref().map(|(revision, _)| *revision)
+ } else {
+ delegation.and_then(|value| value.revision)
+ },
+ };
+ children.push(row(
+ "save",
+ vec![button("save-target", "save-target", Role::Primary)],
+ ));
+ Ok(View {
+ actions: vec![Action {
+ fields: fields.iter().map(|field| field.id.clone()).collect(),
+ // These domain methods do not expose exact operation receipts.
+ // In particular, reading the same current value cannot prove success.
+ ..action(
+ "save-target",
+ words.t("Use this configuration", "使用此配置", "使用此設定"),
+ )
+ }],
+ fields,
+ ..view_of(
+ &title(words, ¤t.purpose),
+ &serde_json::to_string(&stamp).map_err(failed)?,
+ vec![],
+ column("root", children),
+ )
+ })
+}
+
+fn creation_input(
+ submission: &Submission,
+ target: &Target,
+) -> Result<(Scope, CollaborationMode), Error> {
+ let location = submission.text("location")?.trim();
+ if location.is_empty() || location.len() > 4096 || location.chars().any(char::is_control) {
+ return Err(Error::Invalid(
+ "Enter a workspace directory or project ID".into(),
+ ));
+ }
+ let workspace = match submission.text("location-kind")? {
+ "path" => WorkspaceTarget::HostPath {
+ path: location.into(),
+ },
+ "project" => WorkspaceTarget::Project {
+ project_id: location.into(),
+ },
+ _ => return Err(Error::Invalid("Invalid workspace kind".into())),
+ };
+ let sandbox_mode = match submission.text("sandbox")? {
+ "read-only" => SandboxMode::ReadOnly,
+ "workspace-write" => SandboxMode::WorkspaceWrite,
+ _ => return Err(Error::Invalid("Invalid workspace access".into())),
+ };
+ let collaboration = match target {
+ Target::Executor { .. } => CollaborationMode::Agent,
+ Target::Model { .. } => match submission.text("collaboration")? {
+ "agent" => CollaborationMode::Agent,
+ "plan" => CollaborationMode::Plan,
+ _ => return Err(Error::Invalid("Invalid task mode".into())),
+ },
+ };
+ Ok((
+ Scope::Workspace {
+ workspace,
+ sandbox_mode,
+ },
+ collaboration,
+ ))
+}
+
+async fn consent(
+ access: &crate::Access,
+ caller: &Caller,
+ target: &Scope,
+ grant: Option<Id>,
+ operation: uuid::Uuid,
+ title: String,
+) -> Result<Option<Reply>, Error> {
+ let capabilities = [if *target == Scope::Profile {
+ Capability::ReadSessions
+ } else {
+ Capability::Executions
+ }]
+ .into();
+ let request = Authorization {
+ operation_id: operation,
+ title,
+ target: target.clone(),
+ capabilities,
+ };
+ request.validate().map_err(failed)?;
+ // A remembered background grant is not the foreground caller's authority.
+ // Check that caller before opening any grant or changing plugin state.
+ let authority = caller.views.authorize(request.clone()).await?;
+ authority
+ .finish()
+ .await
+ .map_err(|_| Error::CleanupUnconfirmed)?;
+ let id = match grant {
+ Some(id) => Some(id),
+ None => access.remembered(target).await.map_err(failure)?,
+ };
+ if let Some(id) = id {
+ match access.authorizations.open(id).await {
+ Ok(authorized) => {
+ let matches = authorized.grant.request.target == *target
+ && request
+ .capabilities
+ .is_subset(&authorized.grant.request.capabilities);
+ authorized
+ .call
+ .finish()
+ .await
+ .map_err(|_| Error::CleanupUnconfirmed)?;
+ if !matches {
+ return Err(Error::Invalid(
+ "Authorization does not match this configuration".into(),
+ ));
+ }
+ if grant.is_some() {
+ access.remember(id).await.map_err(failure)?;
+ }
+ return Ok(None);
+ }
+ Err(
+ maka_plugins::execution::CommandError::Denied
+ | maka_plugins::execution::CommandError::Revoked,
+ ) if grant.is_none() => {}
+ Err(error) => return Err(failure(error.into())),
+ }
+ }
+ Ok(Some(Reply::Consent { request }))
+}
+
+pub(super) async fn discovery(hub: &Hub, submission: Submission, cx: &Cx) -> Result<Reply, Error> {
+ if let Some(reply) = consent(
+ &hub.0.access,
+ &cx.caller,
+ &Scope::Profile,
+ submission.grant,
+ uuid::Uuid::new_v4(),
+ cx.t(
+ "Allow WorkHub to discover tasks",
+ "允许 WorkHub 发现任务",
+ "允許 WorkHub 探索任務",
+ ),
+ )
+ .await?
+ {
+ return Ok(reply);
+ }
+ Ok(Reply::Applied { route: Value::Null })
+}
+
+pub(super) async fn submit(hub: &Hub, submission: Submission, cx: &Cx) -> Result<Reply, Error> {
+ let place: Place = serde_json::from_value(submission.route.clone()).map_err(failed)?;
+ let mut current = place
+ .setup
+ .ok_or_else(|| Error::Invalid("Open task setup first".into()))?;
+ if submission.action == "search" {
+ current.query = submission.text("query")?.trim().to_owned();
+ llm::Search {
+ query: current.query.clone(),
+ }
+ .validate()
+ .map_err(failed)?;
+ current.target = None;
+ return Ok(Reply::Applied {
+ route: route(¤t),
+ });
+ }
+ let stamp: Stamp = serde_json::from_str(&submission.revision).map_err(failed)?;
+ let target = current
+ .target
+ .as_ref()
+ .ok_or_else(|| Error::Invalid("Choose an execution target".into()))?;
+ target.validate().map_err(failed)?;
+ let (scope, collaboration) = match ¤t.purpose {
+ Purpose::Creation => creation_input(&submission, target)?,
+ Purpose::Coordinator => (workspace(), CollaborationMode::Agent),
+ Purpose::Delegation { assignment } => {
+ let choice: Delegation = hub
+ .call(
+ Call::DelegationModel,
+ json!({"assignmentId":assignment}),
+ &cx.caller,
+ )
+ .await?;
+ if choice.revision != stamp.revision {
+ return Ok(Reply::Conflict);
+ }
+ (choice.authorization, CollaborationMode::Agent)
+ }
+ };
+ if let Some(reply) = consent(
+ &hub.0.access,
+ &cx.caller,
+ &scope,
+ submission.grant,
+ stamp.operation,
+ title(&cx.words, ¤t.purpose),
+ )
+ .await?
+ {
+ return Ok(reply);
+ }
+ let result = match ¤t.purpose {
+ Purpose::Creation => {
+ let creation: Creation = hub.call(Call::Template, json!({"authorization":scope,"collaborationMode":collaboration,"target":target}), &cx.caller).await?;
+ crate::plugin::remote::configure_creation(&hub.0, &cx.caller, creation, stamp.revision).await
+ }
+ Purpose::Coordinator => hub.call::<Value>(Call::SelectModel, json!(target), &cx.caller).await,
+ Purpose::Delegation { assignment } => hub.call::<Value>(Call::SelectDelegationModel, json!({"assignmentId":assignment,"expectedRevision":stamp.revision,"target":target}), &cx.caller).await,
+ };
+ result?;
+ Ok(Reply::Applied { route: Value::Null })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use maka_runtime::execution::{ModelBinding, ThinkingLevel};
+
+ /// Every method is a trap: denial must happen before restoring an old grant,
+ /// reading plugin state, or acquiring any execution capability.
+ struct Untouched;
+ impl maka_plugins::storage::Store for Untouched {
+ fn read(
+ &self,
+ _: String,
+ ) -> BoxFuture<
+ '_,
+ Result<Option<maka_plugins::storage::Record>, maka_plugins::storage::StoreError>,
+ > {
+ panic!("denied caller read plugin state")
+ }
+ fn scan(
+ &self,
+ _: maka_plugins::storage::Scan,
+ ) -> BoxFuture<'_, Result<maka_plugins::storage::Page, maka_plugins::storage::StoreError>>
+ {
+ panic!("denied caller scanned plugin state")
+ }
+ fn batch(
+ &self,
+ _: Vec<maka_plugins::storage::Mutation>,
+ ) -> BoxFuture<
+ '_,
+ Result<Vec<maka_plugins::storage::Record>, maka_plugins::storage::StoreError>,
+ > {
+ panic!("denied caller changed plugin state")
+ }
+ }
+ impl maka_plugins::execution::Access for Untouched {
+ fn restore(
+ &self,
+ _: Id,
+ ) -> BoxFuture<
+ '_,
+ Result<
+ Arc<dyn maka_plugins::execution::Commands>,
+ maka_plugins::execution::CommandError,
+ >,
+ > {
+ panic!("denied caller restored execution authority")
+ }
+ fn acquire(
+ &self,
+ _: maka_plugins::call::Scope,
+ ) -> BoxFuture<
+ '_,
+ Result<
+ Arc<dyn maka_plugins::execution::Commands>,
+ maka_plugins::execution::CommandError,
+ >,
+ > {
+ panic!("denied caller acquired execution authority")
+ }
+ }
+ impl maka_plugins::authorization::Access for Untouched {
+ fn open(
+ &self,
+ _: Id,
+ ) -> BoxFuture<
+ '_,
+ Result<maka_plugins::authorization::Authorized, maka_plugins::execution::CommandError>,
+ > {
+ panic!("denied caller borrowed a remembered grant")
+ }
+ }
+ #[derive(Default)]
+ struct Denied(std::sync::Mutex<Vec<Authorization>>);
+ impl maka_plugins::remote::Views for Denied {
+ fn authorize(
+ &self,
+ request: Authorization,
+ ) -> BoxFuture<'_, Result<maka_plugins::call::Owned, Error>> {
+ self.0.lock().unwrap().push(request);
+ Box::pin(async {
+ Err(Error::Provider(
+ "Current caller cannot authorize execution".into(),
+ ))
+ })
+ }
+ fn session(&self) -> BoxFuture<'_, Result<maka_plugins::remote::SessionView, Error>> {
+ panic!("unneeded Session view")
+ }
+ fn workspace(
+ &self,
+ _: maka_plugins::remote::WorkspaceViewInput,
+ ) -> BoxFuture<'_, Result<maka_plugins::remote::SessionView, Error>> {
+ panic!("unneeded workspace view")
+ }
+ fn query_database(
+ &self,
+ _: maka_plugins::filesystem::database::Read,
+ ) -> BoxFuture<
+ '_,
+ Result<
+ Vec<maka_plugins::filesystem::database::Table>,
+ maka_plugins::filesystem::database::Error,
+ >,
+ > {
+ panic!("unneeded database view")
+ }
+ }
+
+ #[tokio::test]
+ async fn remembered_grants_never_replace_the_current_callers_authority() {
+ let untouched = Arc::new(Untouched);
+ let access = crate::Access {
+ repository: Arc::new(crate::Repository::new(untouched.clone())),
+ executions: untouched.clone(),
+ authorizations: untouched,
+ };
+ let views = Arc::new(Denied::default());
+ let scope = Scope::Session {
+ session_id: "worker-a".into(),
+ };
+ for session in [None, Some("worker-a".to_owned())] {
+ let caller = Caller {
+ connection_id: uuid::Uuid::new_v4(),
+ client_instance_id: "restricted".into(),
+ document_id: uuid::Uuid::new_v4(),
+ session_id: session,
+ access: maka_plugins::remote::Access::HostPaths,
+ views: views.clone(),
+ resources: Arc::default(),
+ cancellation: tokio_util::sync::CancellationToken::new(),
+ };
+ let result = consent(
+ &access,
+ &caller,
+ &scope,
+ Some(Id(uuid::Uuid::new_v4())),
+ uuid::Uuid::new_v4(),
+ "Choose task model".into(),
+ )
+ .await;
+ assert!(
+ matches!(result, Err(Error::Provider(message)) if message == "Current caller cannot authorize execution")
+ );
+ }
+ let requests = views.0.lock().unwrap();
+ assert_eq!(requests.len(), 2);
+ assert!(requests.iter().all(|request| request.target == scope
+ && request.capabilities == [Capability::Executions].into()));
+ }
+
+ #[test]
+ fn session_scoped_repair_hands_off_without_offering_a_foreign_write() {
+ for locale in ["en", "zh-CN", "zh-TW"] {
+ let view = session_handoff(&Words::new(locale), "worker-a");
+ view.validate().unwrap();
+ assert!(view.actions.is_empty());
+ assert!(view.fields.is_empty());
+ let Node::Column { children, .. } = view.root else {
+ panic!("handoff")
+ };
+ assert!(children.iter().any(|node| matches!(node, Node::Item { target: view::Target::Session { session }, .. } if session == "worker-a")));
+ }
+ }
+
+ fn model(index: usize) -> llm::Choice {
+ llm::Choice {
+ model: ModelBinding {
+ connection_id: format!("connection-{index}"),
+ connection_slug: "local".into(),
+ model: format!("model-{index}"),
+ },
+ connection_name: "Local".into(),
+ display_name: format!("Model {index}"),
+ thinking_levels: vec![ThinkingLevel::High],
+ default_thinking_level: Some(ThinkingLevel::High),
+ is_default: index == 0,
+ }
+ }
+
+ fn target() -> Target {
+ Target::Model {
+ model: model(0).model,
+ thinking_level: Some(ThinkingLevel::High),
+ }
+ }
+
+ #[test]
+ fn choices_are_bounded_and_navigation_keeps_the_exact_binding() {
+ for locale in ["en", "zh-CN", "zh-TW"] {
+ let current = Route {
+ purpose: Purpose::Creation,
+ query: "model".into(),
+ target: None,
+ };
+ let view = picker(
+ &Words::new(locale),
+ ¤t,
+ llm::Choices {
+ revision: 42,
+ complete: true,
+ models: (0..25).map(model).collect(),
+ },
+ Some(executor::Choices {
+ revision: 7,
+ complete: true,
+ executors: vec![],
+ }),
+ );
+ view.validate().unwrap();
+ let Node::Column { children, .. } = &view.root else {
+ panic!("column")
+ };
+ let Node::Scroll { child, .. } = &children[2] else {
+ panic!("choices")
+ };
+ let Node::Column {
+ children: choices, ..
+ } = child.as_ref()
+ else {
+ panic!("items")
+ };
+ assert_eq!(choices.len(), 20);
+ let Node::Item {
+ target: view::Target::Route { route },
+ ..
+ } = &choices[0]
+ else {
+ panic!("choice")
+ };
+ let place: Place = serde_json::from_value(route.clone()).unwrap();
+ assert_eq!(place.setup.unwrap().target, Some(target()));
+ assert!(matches!(children.last(), Some(Node::Text { key, .. }) if key == "more"));
+ }
+ }
+
+ #[test]
+ fn task_model_form_freezes_cas_without_promising_value_based_recovery() {
+ let current = Route {
+ purpose: Purpose::Delegation {
+ assignment: "task-a".into(),
+ },
+ query: String::new(),
+ target: Some(target()),
+ };
+ for locale in ["en", "zh-CN", "zh-TW"] {
+ let view = form(
+ &Words::new(locale),
+ ¤t,
+ None,
+ Some(&Delegation {
+ revision: Some(9),
+ authorization: Scope::Session {
+ session_id: "worker-a".into(),
+ },
+ name: "Task A".into(),
+ }),
+ )
+ .unwrap();
+ view.validate().unwrap();
+ let stamp: Stamp = serde_json::from_str(&view.revision).unwrap();
+ assert_eq!(stamp.revision, Some(9));
+ assert!(view.action("save-target").unwrap().recovery.is_none());
+ }
+ }
+
+ #[test]
+ fn creation_fields_preserve_workspace_permissions_and_executor_mode() {
+ let mut submission = Submission {
+ route: Value::Null,
+ revision: String::new(),
+ action: "save-target".into(),
+ grant: None,
+ fields: [
+ ("location-kind".into(), json!("path")),
+ ("location".into(), json!("/tmp/work")),
+ ("sandbox".into(), json!("read-only")),
+ ("collaboration".into(), json!("plan")),
+ ]
+ .into(),
+ };
+ let (scope, mode) = creation_input(&submission, &target()).unwrap();
+ assert_eq!(mode, CollaborationMode::Plan);
+ assert_eq!(
+ scope,
+ Scope::Workspace {
+ workspace: WorkspaceTarget::HostPath {
+ path: "/tmp/work".into()
+ },
+ sandbox_mode: SandboxMode::ReadOnly
+ }
+ );
+ let saved = Creation {
+ authorization: scope.clone(),
+ settings: maka_plugins::execution::RootSettings {
+ target: target(),
+ sandbox_mode: SandboxMode::ReadOnly,
+ approval_policy: maka_runtime::execution::ApprovalPolicy::OnRequest,
+ collaboration_mode: CollaborationMode::Plan,
+ behavior: Default::default(),
+ bound_tools: None,
+ instructions: None,
+ },
+ };
+ let view = form(
+ &Words::new("en"),
+ &Route {
+ purpose: Purpose::Creation,
+ query: String::new(),
+ target: Some(target()),
+ },
+ Some((17, saved)),
+ None,
+ )
+ .unwrap();
+ view.validate().unwrap();
+ let stamp: Stamp = serde_json::from_str(&view.revision).unwrap();
+ assert_eq!(stamp.revision, Some(17));
+ assert!(
+ matches!(&view.fields.iter().find(|field| field.id == "sandbox").unwrap().control, view::Control::Choice { value, .. } if value == "read-only")
+ );
+ assert!(view.action("save-target").unwrap().recovery.is_none());
+ let executor = Target::Executor {
+ executor_id: "fixture.agent".to_owned().try_into().unwrap(),
+ settings: Default::default(),
+ };
+ assert_eq!(
+ creation_input(&submission, &executor).unwrap().1,
+ CollaborationMode::Agent
+ );
+ for locale in ["en", "zh-CN", "zh-TW"] {
+ form(
+ &Words::new(locale),
+ &Route {
+ purpose: Purpose::Creation,
+ query: String::new(),
+ target: Some(executor.clone()),
+ },
+ None,
+ None,
+ )
+ .unwrap()
+ .validate()
+ .unwrap();
+ }
+ submission
+ .fields
+ .insert("sandbox".into(), json!("danger-full-access"));
+ assert!(creation_input(&submission, &target()).is_err());
+ submission
+ .fields
+ .insert("sandbox".into(), json!("workspace-write"));
+ submission.fields.insert("location".into(), json!(" "));
+ assert!(creation_input(&submission, &target()).is_err());
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org