1mod backend;
4mod registry;
5
6use std::error::Error as StdError;
7use std::fmt;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use http::StatusCode;
13use serde_json::Value;
14use zeroize::Zeroizing;
15
16pub use kcode_rust_bins::{ObjectStore, ObjectStoreError, ObjectStoreResult};
17
18use registry::RegistryState;
19
20pub const CREATE_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/create";
21pub const OPEN_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/open";
22pub const DOCS_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/docs";
23pub const WRITE_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/write";
24pub const WRITE_FILE_FREEFORM_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/write-file-freeform";
25pub const DELETE_FILE_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/delete-file";
26pub const CHECK_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/check";
27pub const PUBLISH_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/publish";
28pub const PREVIEW_WRITE_FILE_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/internal-preview-write-file";
29
30pub const CREATE_WEB_LIB_TOOL: &str = "kcode-web-libs/create";
31pub const OPEN_WEB_LIB_TOOL: &str = "kcode-web-libs/open";
32pub const DOCS_WEB_LIB_TOOL: &str = "kcode-web-libs/docs";
33pub const WRITE_WEB_LIB_TOOL: &str = "kcode-web-libs/write";
34pub const WRITE_FILE_FREEFORM_WEB_LIB_TOOL: &str = "kcode-web-libs/write-file-freeform";
35pub const DELETE_FILE_WEB_LIB_TOOL: &str = "kcode-web-libs/delete-file";
36pub const CHECK_WEB_LIB_TOOL: &str = "kcode-web-libs/check";
37pub const PUBLISH_WEB_LIB_TOOL: &str = "kcode-web-libs/publish";
38pub const PREVIEW_WRITE_FILE_WEB_LIB_TOOL: &str = "kcode-web-libs/internal-preview-write-file";
39
40pub const CREATE_RUST_BIN_TOOL: &str = "kcode-rust-bins/create";
41pub const OPEN_RUST_BIN_TOOL: &str = "kcode-rust-bins/open";
42pub const DOCS_RUST_BIN_TOOL: &str = "kcode-rust-bins/docs";
43pub const WRITE_RUST_BIN_TOOL: &str = "kcode-rust-bins/write";
44pub const WRITE_FILE_FREEFORM_RUST_BIN_TOOL: &str = "kcode-rust-bins/write-file-freeform";
45pub const DELETE_FILE_RUST_BIN_TOOL: &str = "kcode-rust-bins/delete-file";
46pub const CHECK_RUST_BIN_TOOL: &str = "kcode-rust-bins/check";
47pub const PUBLISH_RUST_BIN_TOOL: &str = "kcode-rust-bins/publish";
48pub const CALL_RUST_BIN_TOOL: &str = "kcode-rust-bins/call";
49pub const PREVIEW_WRITE_FILE_RUST_BIN_TOOL: &str = "kcode-rust-bins/internal-preview-write-file";
50
51pub const RUST_LIB_TOOLS: [&str; 8] = [
52 CREATE_RUST_LIB_TOOL,
53 OPEN_RUST_LIB_TOOL,
54 DOCS_RUST_LIB_TOOL,
55 WRITE_RUST_LIB_TOOL,
56 WRITE_FILE_FREEFORM_RUST_LIB_TOOL,
57 DELETE_FILE_RUST_LIB_TOOL,
58 CHECK_RUST_LIB_TOOL,
59 PUBLISH_RUST_LIB_TOOL,
60];
61
62pub const WEB_LIB_TOOLS: [&str; 8] = [
63 CREATE_WEB_LIB_TOOL,
64 OPEN_WEB_LIB_TOOL,
65 DOCS_WEB_LIB_TOOL,
66 WRITE_WEB_LIB_TOOL,
67 WRITE_FILE_FREEFORM_WEB_LIB_TOOL,
68 DELETE_FILE_WEB_LIB_TOOL,
69 CHECK_WEB_LIB_TOOL,
70 PUBLISH_WEB_LIB_TOOL,
71];
72
73pub const RUST_BIN_TOOLS: [&str; 9] = [
74 CREATE_RUST_BIN_TOOL,
75 OPEN_RUST_BIN_TOOL,
76 DOCS_RUST_BIN_TOOL,
77 WRITE_RUST_BIN_TOOL,
78 WRITE_FILE_FREEFORM_RUST_BIN_TOOL,
79 DELETE_FILE_RUST_BIN_TOOL,
80 CHECK_RUST_BIN_TOOL,
81 PUBLISH_RUST_BIN_TOOL,
82 CALL_RUST_BIN_TOOL,
83];
84
85const SESSION_LEASE: Duration = Duration::from_secs(24 * 60 * 60);
86const MAX_SESSION_ID_BYTES: usize = 128;
87
88pub struct Config {
89 pub rust_libraries_root: PathBuf,
90 pub web_libraries_root: PathBuf,
91 pub rust_binaries_root: PathBuf,
92 pub rust_binary_publications_root: PathBuf,
93 pub crates_io_registry_token: String,
94 pub object_store: Arc<dyn ObjectStore>,
95}
96
97#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
98pub enum ManagedSourceKind {
99 RustLibrary,
100 WebLibrary,
101 RustBinary,
102}
103
104impl ManagedSourceKind {
105 pub fn label(self) -> &'static str {
106 match self {
107 Self::RustLibrary => "Rust library",
108 Self::WebLibrary => "Web library",
109 Self::RustBinary => "Rust binary",
110 }
111 }
112
113 pub fn open_tool(self) -> &'static str {
114 match self {
115 Self::RustLibrary => OPEN_RUST_LIB_TOOL,
116 Self::WebLibrary => OPEN_WEB_LIB_TOOL,
117 Self::RustBinary => OPEN_RUST_BIN_TOOL,
118 }
119 }
120
121 pub(crate) fn code(self, suffix: &'static str) -> &'static str {
122 match (self, suffix) {
123 (Self::RustLibrary, "already_open") => "rust_lib_already_open",
124 (Self::RustLibrary, "not_open") => "rust_lib_not_open",
125 (Self::RustLibrary, "session_ending") => "rust_lib_session_ending",
126 (Self::WebLibrary, "already_open") => "web_lib_already_open",
127 (Self::WebLibrary, "not_open") => "web_lib_not_open",
128 (Self::WebLibrary, "session_ending") => "web_lib_session_ending",
129 (Self::RustBinary, "already_open") => "rust_bin_already_open",
130 (Self::RustBinary, "not_open") => "rust_bin_not_open",
131 (Self::RustBinary, "session_ending") => "rust_bin_session_ending",
132 _ => "managed_source_error",
133 }
134 }
135}
136
137#[derive(Clone, Debug, Eq, PartialEq)]
138pub struct SourceSnapshot {
139 pub kind: ManagedSourceKind,
140 pub name: String,
141 pub text: String,
142}
143
144#[derive(Clone, Debug, Eq, PartialEq)]
145pub struct ToolExecution {
146 pub text: String,
147 pub snapshot: Option<SourceSnapshot>,
148}
149
150#[derive(Debug)]
151pub struct ToolError {
152 pub status: StatusCode,
153 pub code: &'static str,
154 pub message: String,
155}
156
157#[derive(Clone)]
158pub struct Service {
159 pub(crate) inner: Arc<ServiceInner>,
160}
161
162pub(crate) struct ServiceInner {
163 pub(crate) rust_libraries_root: PathBuf,
164 pub(crate) web_libraries_root: PathBuf,
165 pub(crate) rust_binaries_root: PathBuf,
166 pub(crate) rust_binary_publications_root: PathBuf,
167 pub(crate) registry_token: Zeroizing<String>,
168 pub(crate) object_store: Arc<dyn ObjectStore>,
169 pub(crate) registry: Arc<RegistryState>,
170}
171
172impl Service {
173 pub fn open(config: Config) -> Result<Self, ToolError> {
174 let registry_token = config.crates_io_registry_token.trim();
175 if registry_token.is_empty() {
176 return Err(ToolError::unavailable(
177 "registry_token_unavailable",
178 "The crates.io registry token is empty.",
179 ));
180 }
181
182 Ok(Self {
183 inner: Arc::new(ServiceInner {
184 rust_libraries_root: checked_root(
185 &config.rust_libraries_root,
186 "managed Rust libraries",
187 )?,
188 web_libraries_root: checked_root(
189 &config.web_libraries_root,
190 "managed Web libraries",
191 )?,
192 rust_binaries_root: checked_root(
193 &config.rust_binaries_root,
194 "managed Rust binaries",
195 )?,
196 rust_binary_publications_root: checked_root(
197 &config.rust_binary_publications_root,
198 "Rust binary publications",
199 )?,
200 registry_token: Zeroizing::new(registry_token.to_owned()),
201 object_store: config.object_store,
202 registry: Arc::new(RegistryState::default()),
203 }),
204 })
205 }
206
207 pub fn web_libraries_root(&self) -> &Path {
208 &self.inner.web_libraries_root
209 }
210
211 pub async fn execute(
212 &self,
213 session_id: impl Into<String>,
214 tool_name: impl Into<String>,
215 arguments: Value,
216 ) -> Result<ToolExecution, ToolError> {
217 let session_id = session_id.into();
218 validate_session_id(&session_id)?;
219 let tool_name = tool_name.into();
220 let service = self.clone();
221 tokio::task::spawn_blocking(move || {
222 service.execute_blocking(&session_id, &tool_name, arguments)
223 })
224 .await
225 .map_err(|error| {
226 tracing::error!(error=%error, "managed-source tool worker failed");
227 ToolError::internal("The managed-source tool worker stopped unexpectedly.")
228 })?
229 }
230
231 pub async fn release(&self, session_id: impl Into<String>) -> Result<usize, ToolError> {
232 let session_id = session_id.into();
233 validate_session_id(&session_id)?;
234 let service = self.clone();
235 tokio::task::spawn_blocking(move || service.release_blocking(&session_id))
236 .await
237 .map_err(|error| {
238 tracing::error!(error=%error, "managed-source release worker failed");
239 ToolError::internal("The managed-source release worker stopped unexpectedly.")
240 })?
241 }
242}
243
244impl ToolExecution {
245 pub(crate) fn plain(text: impl Into<String>) -> Self {
246 Self {
247 text: text.into(),
248 snapshot: None,
249 }
250 }
251
252 pub(crate) fn snapshot(snapshot: SourceSnapshot) -> Self {
253 Self {
254 text: snapshot.text.clone(),
255 snapshot: Some(snapshot),
256 }
257 }
258
259 pub(crate) fn with_snapshot(text: impl Into<String>, snapshot: SourceSnapshot) -> Self {
260 Self {
261 text: text.into(),
262 snapshot: Some(snapshot),
263 }
264 }
265}
266
267impl ToolError {
268 pub(crate) fn invalid(message: impl Into<String>) -> Self {
269 Self {
270 status: StatusCode::BAD_REQUEST,
271 code: "invalid_arguments",
272 message: message.into(),
273 }
274 }
275
276 pub(crate) fn not_found(code: &'static str, message: impl Into<String>) -> Self {
277 Self {
278 status: StatusCode::NOT_FOUND,
279 code,
280 message: message.into(),
281 }
282 }
283
284 pub(crate) fn conflict(code: &'static str, message: impl Into<String>) -> Self {
285 Self {
286 status: StatusCode::CONFLICT,
287 code,
288 message: message.into(),
289 }
290 }
291
292 pub(crate) fn unprocessable(code: &'static str, message: impl Into<String>) -> Self {
293 Self {
294 status: StatusCode::UNPROCESSABLE_ENTITY,
295 code,
296 message: message.into(),
297 }
298 }
299
300 pub(crate) fn unavailable(code: &'static str, message: impl Into<String>) -> Self {
301 Self {
302 status: StatusCode::SERVICE_UNAVAILABLE,
303 code,
304 message: message.into(),
305 }
306 }
307
308 pub(crate) fn internal(message: impl Into<String>) -> Self {
309 Self {
310 status: StatusCode::INTERNAL_SERVER_ERROR,
311 code: "dev_tools_internal_error",
312 message: message.into(),
313 }
314 }
315}
316
317impl fmt::Display for ToolError {
318 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
319 write!(formatter, "{}: {}", self.code, self.message)
320 }
321}
322
323impl StdError for ToolError {}
324
325pub fn proposed_write_snapshot(tool_name: &str, arguments: &Value) -> Option<SourceSnapshot> {
326 backend::proposed_write_snapshot(tool_name, arguments)
327}
328
329fn checked_root(path: &Path, label: &str) -> Result<PathBuf, ToolError> {
330 std::fs::create_dir_all(path).map_err(|error| {
331 ToolError::unavailable(
332 "dev_tools_storage_unavailable",
333 format!("Could not create {label} root {}: {error}", path.display()),
334 )
335 })?;
336 let metadata = std::fs::symlink_metadata(path).map_err(|error| {
337 ToolError::unavailable(
338 "dev_tools_storage_unavailable",
339 format!("Could not inspect {label} root {}: {error}", path.display()),
340 )
341 })?;
342 if metadata.file_type().is_symlink() || !metadata.is_dir() {
343 return Err(ToolError::unprocessable(
344 "dev_tools_invalid_root",
345 format!("{label} root {} must be a real directory.", path.display()),
346 ));
347 }
348 std::fs::canonicalize(path).map_err(|error| {
349 ToolError::unavailable(
350 "dev_tools_storage_unavailable",
351 format!(
352 "Could not canonicalize {label} root {}: {error}",
353 path.display()
354 ),
355 )
356 })
357}
358
359fn validate_session_id(session_id: &str) -> Result<(), ToolError> {
360 if session_id.is_empty()
361 || session_id.len() > MAX_SESSION_ID_BYTES
362 || !session_id
363 .bytes()
364 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b':'))
365 {
366 return Err(ToolError::invalid("The hidden tool session ID is invalid."));
367 }
368 Ok(())
369}