Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled

Fork goose with custom MCP widgets, platform extensions (aider, git, web, search),
MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-14 21:30:20 +08:00
parent e5fd568e01
commit 4e21ca937a
359 changed files with 70658 additions and 56 deletions
+6
View File
@@ -420,6 +420,8 @@ derive_utoipa!(IconTheme as IconThemeSchema);
super::routes::prompts::save_prompt,
super::routes::prompts::reset_prompt,
super::routes::agent::start_agent,
super::routes::agent::harness_bootstrap,
super::routes::agent::harness_remember,
super::routes::agent::resume_agent,
super::routes::agent::stop_agent,
super::routes::agent::restart_agent,
@@ -646,6 +648,10 @@ derive_utoipa!(IconTheme as IconThemeSchema);
super::routes::agent::ImportAppRequest,
super::routes::agent::ImportAppResponse,
super::routes::agent::StartAgentRequest,
super::routes::agent::HarnessBootstrapRequest,
super::routes::agent::HarnessBootstrapResponse,
super::routes::agent::HarnessRememberRequest,
super::routes::agent::HarnessRememberResponse,
super::routes::agent::ResumeAgentRequest,
super::routes::agent::StopAgentRequest,
super::routes::agent::RestartAgentRequest,
+421 -2
View File
@@ -15,6 +15,7 @@ use goose::agents::{Container, ExtensionLoadResult};
use goose::goose_apps::{fetch_mcp_apps, GooseApp, McpAppCache};
use base64::Engine;
use goose::agents::platform_extensions::{chatrecall, projectmemory, PLATFORM_EXTENSIONS};
use goose::agents::reply_parts::is_tool_visible_to_app;
use goose::agents::ExtensionConfig;
use goose::config::resolve_extensions_for_new_session;
@@ -33,8 +34,10 @@ use rmcp::model::CallToolRequestParams;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::process::Command;
use tokio::time::{timeout, Duration};
use tokio_util::sync::CancellationToken;
use tracing::{error, warn};
@@ -75,6 +78,8 @@ pub struct StartAgentRequest {
recipe_deeplink: Option<String>,
#[serde(default)]
extension_overrides: Option<Vec<ExtensionConfig>>,
#[serde(default)]
enable_context_memory: bool,
}
#[derive(Deserialize, utoipa::ToSchema)]
@@ -177,6 +182,223 @@ pub struct ResumeAgentResponse {
pub extension_results: Option<Vec<ExtensionLoadResult>>,
}
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct HarnessBootstrapRequest {
session_id: String,
query: Option<String>,
#[serde(default)]
force: bool,
}
#[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct HarnessBootstrapResponse {
summary: String,
source: String,
refreshed: bool,
}
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct HarnessRememberRequest {
session_id: String,
content: String,
title: Option<String>,
}
#[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct HarnessRememberResponse {
remembered: bool,
}
const DEFAULT_BOOTSTRAP_QUERY: &str =
"project architecture conventions current goals decisions risks and recent work";
const DEFAULT_BOOTSTRAP_MAX_CHARS: usize = 6_000;
fn platform_extension_config(name: &str) -> Option<ExtensionConfig> {
PLATFORM_EXTENSIONS
.get(name)
.map(|definition| ExtensionConfig::Platform {
name: definition.name.to_string(),
description: definition.description.to_string(),
display_name: Some(definition.display_name.to_string()),
bundled: Some(true),
available_tools: Vec::new(),
})
}
fn append_context_memory_extensions(extensions: &mut Vec<ExtensionConfig>) {
for name in [chatrecall::EXTENSION_NAME, projectmemory::EXTENSION_NAME] {
if extensions.iter().any(|extension| extension.name() == name) {
continue;
}
if let Some(extension) = platform_extension_config(name) {
extensions.push(extension);
}
}
}
fn bootstrap_max_chars() -> usize {
std::env::var("GOOSE_HARNESS_BOOTSTRAP_MAX_CHARS")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|value| *value >= 1_000)
.unwrap_or(DEFAULT_BOOTSTRAP_MAX_CHARS)
}
fn truncate_chars(value: &str, max_chars: usize) -> String {
value.chars().take(max_chars).collect()
}
fn filter_recall_output(output: &str, max_chars: usize) -> String {
let filtered = output
.lines()
.filter(|line| {
let lower = line.to_ascii_lowercase();
!line.trim().is_empty()
&& !lower.contains("\trisk\t[monitor]")
&& !lower.contains("\tvault-note\tnew project")
})
.collect::<Vec<_>>()
.join("\n");
truncate_chars(filtered.trim(), max_chars)
}
fn harness_binary() -> PathBuf {
if let Some(path) = std::env::var_os("GOOSE_HARNESS_BIN") {
return PathBuf::from(path);
}
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_default();
home.join(".codex/harness/bin/harness")
}
async fn recall_harness(repo: &str, query: &str, working_dir: &Path) -> Option<String> {
let output = timeout(
Duration::from_secs(8),
Command::new(harness_binary())
.arg("recall")
.arg("--repo")
.arg(repo)
.arg("--query")
.arg(query)
.current_dir(working_dir)
.output(),
)
.await
.ok()?
.ok()?;
if !output.status.success() {
tracing::warn!(
"Harness recall failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
return None;
}
let recalled = filter_recall_output(
&String::from_utf8_lossy(&output.stdout),
bootstrap_max_chars() / 2,
);
(!recalled.is_empty()).then_some(recalled)
}
async fn remember_harness(
repo: &str,
title: &str,
content: &str,
working_dir: &Path,
) -> Result<(), String> {
let output = timeout(
Duration::from_secs(8),
Command::new(harness_binary())
.arg("remember")
.arg("--repo")
.arg(repo)
.arg("--kind")
.arg("conversation_memory")
.arg("--title")
.arg(title)
.arg("--content")
.arg(content)
.current_dir(working_dir)
.output(),
)
.await
.map_err(|_| "Harness remember timed out".to_string())?
.map_err(|error| format!("Failed to start harness remember: {error}"))?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
async fn read_project_context(working_dir: &Path, max_chars: usize) -> Option<String> {
let mut sections = Vec::new();
let mut remaining = max_chars;
for filename in ["AGENTS.md", ".tkmindhints", ".goosehints", "CLAUDE.md"] {
if remaining == 0 {
break;
}
let path = working_dir.join(filename);
let Ok(content) = tokio::fs::read_to_string(&path).await else {
continue;
};
let content = truncate_chars(content.trim(), remaining);
if content.is_empty() {
continue;
}
remaining = remaining.saturating_sub(content.chars().count());
sections.push(format!("## {filename}\n{content}"));
}
(!sections.is_empty()).then(|| sections.join("\n\n"))
}
async fn build_project_bootstrap(working_dir: &Path, query: &str) -> (String, String) {
let max_chars = bootstrap_max_chars();
let repo = working_dir
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.unwrap_or("unknown");
let recalled = recall_harness(repo, query, working_dir).await;
let recall_len = recalled
.as_deref()
.map(|value| value.chars().count())
.unwrap_or_default();
let project_context =
read_project_context(working_dir, max_chars.saturating_sub(recall_len)).await;
let mut sections = Vec::new();
let mut sources = Vec::new();
if let Some(recalled) = recalled {
sections.push(format!("## Recalled project knowledge\n{recalled}"));
sources.push("harness");
}
if let Some(project_context) = project_context {
sections.push(format!("## Project instructions\n{project_context}"));
sources.push("project_files");
}
(
truncate_chars(&sections.join("\n\n"), max_chars),
if sources.is_empty() {
"none".to_string()
} else {
sources.join("+")
},
)
}
#[derive(Serialize, utoipa::ToSchema)]
pub struct RestartAgentResponse {
pub extension_results: Vec<ExtensionLoadResult>,
@@ -207,6 +429,7 @@ async fn start_agent(
recipe_id,
recipe_deeplink,
extension_overrides,
enable_context_memory,
} = payload;
let original_recipe = if let Some(deeplink) = recipe_deeplink {
@@ -267,8 +490,11 @@ async fn start_agent(
let recipe_extensions = original_recipe
.as_ref()
.and_then(|r| r.extensions.as_deref());
let extensions_to_use =
let mut extensions_to_use =
resolve_extensions_for_new_session(recipe_extensions, extension_overrides);
if enable_context_memory {
append_context_memory_extensions(&mut extensions_to_use);
}
let mut extension_data = session.extension_data.clone();
let extensions_state = EnabledExtensionsState::new(extensions_to_use);
@@ -1359,9 +1585,166 @@ async fn import_app(
))
}
#[utoipa::path(
post,
path = "/agent/harness_bootstrap",
request_body = HarnessBootstrapRequest,
responses(
(status = 200, description = "Project memory bootstrap completed", body = HarnessBootstrapResponse),
(status = 404, description = "Session not found", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
security(
("api_key" = [])
),
tag = "Agent"
)]
async fn harness_bootstrap(
State(state): State<Arc<AppState>>,
Json(request): Json<HarnessBootstrapRequest>,
) -> Result<Json<HarnessBootstrapResponse>, ErrorResponse> {
let mut session = state
.session_manager()
.get_session(&request.session_id, false)
.await
.map_err(|_| ErrorResponse::not_found("Session not found"))?;
let mut enabled_extensions = EnabledExtensionsState::extensions_or_default(
Some(&session.extension_data),
Config::global(),
);
let previous_extension_count = enabled_extensions.len();
append_context_memory_extensions(&mut enabled_extensions);
if enabled_extensions.len() != previous_extension_count {
EnabledExtensionsState::new(enabled_extensions)
.to_extension_data(&mut session.extension_data)
.map_err(|error| ErrorResponse::internal(error.to_string()))?;
state
.session_manager()
.update(&request.session_id)
.extension_data(session.extension_data.clone())
.apply()
.await
.map_err(|error| ErrorResponse::internal(error.to_string()))?;
let agent = state
.get_agent_for_route(request.session_id.clone())
.await
.map_err(ErrorResponse::from)?;
agent.load_extensions_from_session(&session).await;
}
if !request.force {
if let Some(memory) =
projectmemory::ProjectMemoryState::from_extension_data(&session.extension_data)
{
if !memory.summary.trim().is_empty() {
return Ok(Json(HarnessBootstrapResponse {
summary: memory.summary,
source: memory.source.unwrap_or_else(|| "session".to_string()),
refreshed: false,
}));
}
}
}
let query = request
.query
.as_deref()
.map(str::trim)
.filter(|query| !query.is_empty())
.unwrap_or(DEFAULT_BOOTSTRAP_QUERY);
let (summary, source) = build_project_bootstrap(&session.working_dir, query).await;
if summary.is_empty() {
return Ok(Json(HarnessBootstrapResponse {
summary,
source,
refreshed: false,
}));
}
let memory = projectmemory::ProjectMemoryState {
summary: summary.clone(),
source: Some(source.clone()),
updated_at: Some(chrono::Utc::now().to_rfc3339()),
};
memory
.to_extension_data(&mut session.extension_data)
.map_err(|error| ErrorResponse::internal(error.to_string()))?;
state
.session_manager()
.update(&request.session_id)
.extension_data(session.extension_data)
.apply()
.await
.map_err(|error| ErrorResponse::internal(error.to_string()))?;
Ok(Json(HarnessBootstrapResponse {
summary,
source,
refreshed: true,
}))
}
#[utoipa::path(
post,
path = "/agent/harness_remember",
request_body = HarnessRememberRequest,
responses(
(status = 200, description = "Conversation context saved to harness", body = HarnessRememberResponse),
(status = 400, description = "Invalid memory content", body = ErrorResponse),
(status = 404, description = "Session not found", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
),
security(
("api_key" = [])
),
tag = "Agent"
)]
async fn harness_remember(
State(state): State<Arc<AppState>>,
Json(request): Json<HarnessRememberRequest>,
) -> Result<Json<HarnessRememberResponse>, ErrorResponse> {
let session = state
.session_manager()
.get_session(&request.session_id, false)
.await
.map_err(|_| ErrorResponse::not_found("Session not found"))?;
let content = request.content.trim();
if content.is_empty() {
return Err(ErrorResponse::bad_request("Memory content cannot be empty"));
}
if content.chars().count() > bootstrap_max_chars() {
return Err(ErrorResponse::bad_request("Memory content is too long"));
}
let repo = session
.working_dir
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.unwrap_or("unknown");
let title = request
.title
.as_deref()
.map(str::trim)
.filter(|title| !title.is_empty())
.unwrap_or("H5 conversation memory");
remember_harness(repo, title, content, &session.working_dir)
.await
.map_err(ErrorResponse::internal)?;
Ok(Json(HarnessRememberResponse { remembered: true }))
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/agent/start", post(start_agent))
.route("/agent/harness_bootstrap", post(harness_bootstrap))
.route("/agent/harness_remember", post(harness_remember))
.route("/agent/resume", post(resume_agent))
.route("/agent/restart", post(restart_agent))
.route("/agent/update_working_dir", post(update_working_dir))
@@ -1410,6 +1793,42 @@ mod tests {
}
}
#[test]
fn context_memory_extensions_are_added_once() {
let mut extensions = vec![frontend_extension()];
append_context_memory_extensions(&mut extensions);
append_context_memory_extensions(&mut extensions);
assert_eq!(
extensions
.iter()
.filter(|extension| extension.name() == chatrecall::EXTENSION_NAME)
.count(),
1
);
assert_eq!(
extensions
.iter()
.filter(|extension| extension.name() == projectmemory::EXTENSION_NAME)
.count(),
1
);
}
#[test]
fn recall_filter_removes_monitor_noise_and_limits_output() {
let output = concat!(
"harness-db\trisk\t[monitor] HTTP health\n",
"harness-db\tarchitecture\tUseful project fact\n",
);
let filtered = filter_recall_output(output, 40);
assert!(!filtered.contains("[monitor]"));
assert!(filtered.starts_with("harness-db\tarchitecture"));
assert!(filtered.chars().count() <= 40);
}
#[tokio::test]
async fn frontend_extensions_are_listed_and_rejected_cleanly_by_call_tool() {
let state = AppState::new(true).await.unwrap();
@@ -33,6 +33,7 @@ use serde_yaml;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::Duration,
};
use utoipa::ToSchema;
@@ -940,7 +941,17 @@ pub async fn get_provider_models(
let model_config = ModelConfig::new(&metadata.default_model)?.with_canonical_limits(&name);
let provider = goose::providers::create(&name, model_config, Vec::new()).await?;
let models_result = provider.fetch_recommended_model_info().await;
let models_result = tokio::time::timeout(
Duration::from_secs(10),
provider.fetch_recommended_model_info(),
)
.await
.map_err(|_| {
ErrorResponse::bad_request(format!(
"Timed out fetching models for provider '{}'. The provider may be unreachable.",
name
))
})?;
match models_result {
Ok(models) => Ok(Json(models)),