1use vs_engine_webkit::{AuthBlob, CaptureScope, Viewport};
5use vs_protocol::{Ref, StateToken, Warning, WarningCode};
6
7use super::audit::AuditCtx;
8use super::responses::{
9 AuthClearResponse, AuthListResponse, AuthLoadResponse, AuthSaveResponse, CaptureResponse,
10 LayoutResponse, SkillListResponse, SkillShowResponse, ViewportResponse,
11};
12use super::Daemon;
13use crate::error::{DaemonError, Result};
14use crate::tokens;
15
16impl Daemon {
17 pub fn skill_list(&self, session_id: &str) -> Result<SkillListResponse> {
20 let ctx = AuditCtx::new("vs_skill", session_id).with_args(
21 "list".into(),
22 tokens::args_hash("vs_skill", &["list".into()]),
23 );
24 self.audit_call(ctx, |_ctx| {
25 self.require_session(session_id)?;
26 let mut names = Vec::new();
27 let entries = match std::fs::read_dir(&self.inner.skills_dir) {
28 Ok(it) => it,
29 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
30 return Ok(SkillListResponse { names });
31 }
32 Err(e) => return Err(DaemonError::Io(e)),
33 };
34 for entry in entries.flatten() {
35 if entry.file_type().is_ok_and(|t| t.is_dir()) {
36 if let Some(n) = entry.file_name().to_str() {
37 names.push(n.to_string());
38 }
39 }
40 }
41 names.sort();
42 Ok(SkillListResponse { names })
43 })
44 }
45
46 pub fn skill_show(&self, session_id: &str, name: &str) -> Result<SkillShowResponse> {
48 let ctx = AuditCtx::new("vs_skill", session_id).with_args(
49 format!("show {name}"),
50 tokens::args_hash("vs_skill", &["show".into(), name.to_string()]),
51 );
52 self.audit_call(ctx, |_ctx| {
53 self.require_session(session_id)?;
54 let path = self.inner.skills_dir.join(name).join("SKILL.md");
55 let body = std::fs::read_to_string(&path).map_err(|e| match e.kind() {
56 std::io::ErrorKind::NotFound => DaemonError::BadRequest(format!(
57 "skill not found: {name} (looked in {})",
58 path.display()
59 )),
60 _ => DaemonError::Io(e),
61 })?;
62 Ok(SkillShowResponse { body })
63 })
64 }
65
66 pub fn capture(
69 &self,
70 session_id: &str,
71 page_id: &str,
72 scope: CaptureScope,
73 ) -> Result<CaptureResponse> {
74 let scope_label = match &scope {
75 CaptureScope::Viewport => "viewport".to_string(),
76 CaptureScope::Element(r) => format!("ref:{r}"),
77 CaptureScope::FullPage => "full-page".to_string(),
78 };
79 let ctx = AuditCtx::new("vs_capture", session_id)
80 .with_page(page_id)
81 .with_args(
82 scope_label.clone(),
83 tokens::args_hash("vs_capture", &[scope_label]),
84 );
85 self.audit_call(ctx, |ctx| {
86 let engine_handle = self.engine_handle_for(session_id, page_id)?;
87 std::fs::create_dir_all(&self.inner.captures_dir).map_err(DaemonError::Io)?;
88 let path = self.inner.engine.capture(engine_handle, scope)?;
89 let token = self
90 .current_token(session_id, page_id)
91 .unwrap_or(StateToken::ZERO);
92 ctx.after_token = Some(token);
93 ctx.result_summary = Some(path.display().to_string());
94 Ok(CaptureResponse { path, token })
95 })
96 }
97
98 pub fn viewport(
102 &self,
103 session_id: &str,
104 page_id: &str,
105 viewport: Viewport,
106 ) -> Result<ViewportResponse> {
107 let arg = format!("{}x{}@{}dpr", viewport.width, viewport.height, viewport.dpr);
108 let ctx = AuditCtx::new("vs_viewport", session_id)
109 .with_page(page_id)
110 .with_args(arg.clone(), tokens::args_hash("vs_viewport", &[arg]));
111 self.audit_call(ctx, |ctx| {
112 let engine_handle = self.engine_handle_for(session_id, page_id)?;
113 self.inner.engine.set_viewport(engine_handle, viewport)?;
114 {
115 let mut sessions = self.inner.sessions.lock().expect("poisoned");
116 let page = sessions
117 .get_mut(session_id)
118 .ok_or_else(|| DaemonError::UnknownSession(session_id.to_string()))?
119 .pages
120 .get_mut(page_id)
121 .ok_or_else(|| DaemonError::UnknownPage(page_id.to_string()))?;
122 page.invalidate_baseline();
123 }
124 let tree = self.inner.engine.snapshot(engine_handle)?;
125 let (token, _form) = {
126 let mut sessions = self.inner.sessions.lock().expect("poisoned");
127 let page = sessions
128 .get_mut(session_id)
129 .ok_or_else(|| DaemonError::UnknownSession(session_id.to_string()))?
130 .pages
131 .get_mut(page_id)
132 .ok_or_else(|| DaemonError::UnknownPage(page_id.to_string()))?;
133 let result = page.apply_snapshot(tree);
134 page.invalidate_baseline();
137 result
138 };
139 ctx.after_token = Some(token);
140
141 let mut store = self.inner.store.lock().expect("poisoned");
142 store.update_page_token(page_id, &token.to_string(), "engine", None)?;
143 drop(store);
144 Ok(ViewportResponse {
145 token,
146 warnings: vec![Warning::with_args(
147 WarningCode::ViewportChanged,
148 vec![format!("{}x{}", viewport.width, viewport.height)],
149 )],
150 })
151 })
152 }
153
154 pub fn layout(
155 &self,
156 session_id: &str,
157 page_id: &str,
158 refs: Vec<Ref>,
159 ) -> Result<LayoutResponse> {
160 let arg_strs: Vec<String> = refs.iter().map(Ref::to_string).collect();
161 let ctx = AuditCtx::new("vs_layout", session_id)
162 .with_page(page_id)
163 .with_args(
164 arg_strs.join(" "),
165 tokens::args_hash("vs_layout", &arg_strs),
166 );
167 self.audit_call(ctx, |ctx| {
168 let engine_handle = self.engine_handle_for(session_id, page_id)?;
169 let boxes = self.inner.engine.layout(engine_handle, refs)?;
170 let token = self
171 .current_token(session_id, page_id)
172 .unwrap_or(StateToken::ZERO);
173 ctx.after_token = Some(token);
174 Ok(LayoutResponse { boxes, token })
175 })
176 }
177
178 pub fn auth_save(
179 &self,
180 session_id: &str,
181 page_id: &str,
182 name: &str,
183 ) -> Result<AuthSaveResponse> {
184 let ctx = AuditCtx::new("vs_auth", session_id)
185 .with_page(page_id)
186 .with_args(
187 format!("save {name}"),
188 tokens::args_hash("vs_auth", &["save".into(), name.to_string()]),
189 );
190 self.audit_call(ctx, |_ctx| {
191 let key = self.require_master_key()?;
192 let engine_handle = self.engine_handle_for(session_id, page_id)?;
193 let blob = self.inner.engine.save_auth(engine_handle)?;
194 let mut store = self.inner.store.lock().expect("poisoned");
195 store.save_auth(name, key, &blob.bytes)?;
196 Ok(AuthSaveResponse {
197 name: name.to_string(),
198 })
199 })
200 }
201
202 pub fn auth_load(
203 &self,
204 session_id: &str,
205 page_id: &str,
206 name: &str,
207 ) -> Result<AuthLoadResponse> {
208 let ctx = AuditCtx::new("vs_auth", session_id)
209 .with_page(page_id)
210 .with_args(
211 format!("load {name}"),
212 tokens::args_hash("vs_auth", &["load".into(), name.to_string()]),
213 );
214 self.audit_call(ctx, |ctx| {
215 let key = self.require_master_key()?;
216 let engine_handle = self.engine_handle_for(session_id, page_id)?;
217 let bytes = {
218 let mut store = self.inner.store.lock().expect("poisoned");
219 store.load_auth(name, key)?
220 };
221 self.inner
222 .engine
223 .load_auth(engine_handle, AuthBlob { bytes })?;
224 {
225 let mut sessions = self.inner.sessions.lock().expect("poisoned");
226 let page = sessions
227 .get_mut(session_id)
228 .ok_or_else(|| DaemonError::UnknownSession(session_id.to_string()))?
229 .pages
230 .get_mut(page_id)
231 .ok_or_else(|| DaemonError::UnknownPage(page_id.to_string()))?;
232 page.invalidate_baseline();
233 }
234 let token = self
235 .current_token(session_id, page_id)
236 .unwrap_or(StateToken::ZERO);
237 ctx.after_token = Some(token);
238 Ok(AuthLoadResponse {
239 token,
240 warnings: vec![Warning::with_args(
241 WarningCode::AuthLoaded,
242 vec![name.to_string()],
243 )],
244 })
245 })
246 }
247
248 pub fn auth_list(&self, session_id: &str) -> Result<AuthListResponse> {
249 let ctx = AuditCtx::new("vs_auth", session_id).with_args(
250 "list".into(),
251 tokens::args_hash("vs_auth", &["list".into()]),
252 );
253 self.audit_call(ctx, |_ctx| {
254 self.require_session(session_id)?;
255 let store = self.inner.store.lock().expect("poisoned");
256 let entries = store.list_auth()?;
257 Ok(AuthListResponse {
258 names: entries.into_iter().map(|m| m.name).collect(),
259 })
260 })
261 }
262
263 pub fn auth_clear(&self, session_id: &str, name: &str) -> Result<AuthClearResponse> {
264 let ctx = AuditCtx::new("vs_auth", session_id).with_args(
265 format!("clear {name}"),
266 tokens::args_hash("vs_auth", &["clear".into(), name.to_string()]),
267 );
268 self.audit_call(ctx, |_ctx| {
269 self.require_session(session_id)?;
270 let mut store = self.inner.store.lock().expect("poisoned");
271 store.delete_auth(name)?;
272 Ok(AuthClearResponse)
273 })
274 }
275}