1use crate::{
2 Tool, ToolApprovalKind, ToolBackendKind, ToolCall, ToolCtx, ToolDescriptor, ToolError,
3 ToolExposureMode, ToolExposurePolicy, ToolIdempotencyClass, ToolOutputMode,
4 ToolReceiptPersistence, ToolResult, ToolSideEffectClass,
5};
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use stack_ids::{ArtifactId, ScopeKey};
10use std::sync::Arc;
11
12#[async_trait]
13pub trait MemorySearchPort: Send + Sync {
14 async fn search(
15 &self,
16 query: &str,
17 scope: Option<&ScopeKey>,
18 limit: usize,
19 ) -> Result<serde_json::Value, ToolError>;
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ArtifactContent {
24 pub artifact_id: ArtifactId,
25 pub content: String,
26}
27
28#[async_trait]
29pub trait ArtifactReader: Send + Sync {
30 async fn read(&self, artifact_id: &ArtifactId) -> Result<ArtifactContent, ToolError>;
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct VerificationJobRequest {
35 pub candidate_id: String,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub task_id: Option<String>,
38}
39
40#[async_trait]
41pub trait RunVerificationPort: Send + Sync {
42 async fn enqueue_verification(
43 &self,
44 request: VerificationJobRequest,
45 dry_run: bool,
46 execution_permit: &crate::ToolExecutionPermit,
47 ) -> Result<crate::ToolJobHandle, ToolError>;
48}
49
50#[async_trait]
51pub trait ApplyPatchPreviewPort: Send + Sync {
52 async fn preview_patch(
53 &self,
54 target: &str,
55 patch: &str,
56 execution_permit: &crate::ToolExecutionPermit,
57 ) -> Result<serde_json::Value, ToolError>;
58}
59
60#[async_trait]
61pub trait SubmitPatchPort: Send + Sync {
62 async fn submit_patch(
63 &self,
64 target: &str,
65 patch: &str,
66 approval_grant: &crate::ApprovalGrant,
67 execution_permit: &crate::ToolExecutionPermit,
68 ) -> Result<serde_json::Value, ToolError>;
69}
70
71fn scoped_execution_permit<'a>(
72 ctx: &'a ToolCtx,
73 tool_name: &str,
74 target_key: &str,
75) -> Result<&'a crate::ToolExecutionPermit, ToolError> {
76 let permit = ctx.execution_permit.as_ref().ok_or_else(|| {
77 ToolError::new(
78 crate::ToolErrorClass::ApprovalRequired,
79 format!("{tool_name} requires an execution permit"),
80 )
81 })?;
82 let scope = ctx.scope.as_ref().ok_or_else(|| {
83 ToolError::new(
84 crate::ToolErrorClass::Denied,
85 format!("{tool_name} requires a scoped context"),
86 )
87 })?;
88 if permit.scope().namespace() != scope.namespace || permit.scope().target_key() != target_key {
89 return Err(ToolError::new(
90 crate::ToolErrorClass::Denied,
91 format!("{tool_name} execution permit does not cover target {target_key}"),
92 ));
93 }
94 Ok(permit)
95}
96
97pub struct SearchMemoryTool<P> {
98 port: Arc<P>,
99 descriptor: ToolDescriptor,
100}
101
102impl<P> SearchMemoryTool<P> {
103 pub fn new(port: Arc<P>) -> Self {
105 Self {
106 port,
107 descriptor: ToolDescriptor {
108 name: "search_memory".into(),
109 version: "1.0.0".into(),
110 description: Some("Search projected memory within the active scope".into()),
111 backend_kind: ToolBackendKind::LocalFunction,
112 input_schema: json!({
113 "type": "object",
114 "required": ["query"],
115 "properties": {
116 "query": {"type": "string"},
117 "limit": {"type": "integer"}
118 },
119 "additionalProperties": false
120 }),
121 output_mode: ToolOutputMode::StructuredJson,
122 read_only: true,
123 side_effect_class: ToolSideEffectClass::ReadOnly,
124 idempotency_class: ToolIdempotencyClass::Idempotent,
125 approval_kind: ToolApprovalKind::None,
126 timeout_ms: 10_000,
127 concurrency_key: Some("search_memory".into()),
128 cache_ttl_ms: Some(5_000),
129 exposure_mode: ToolExposureMode::Auto,
130 mcp_surface_kind: crate::McpSurfaceKind::Tool,
131 exposure_policy: ToolExposurePolicy::default(),
132 receipt_persistence: ToolReceiptPersistence::Ephemeral,
133 rollback_contract: None,
134 effect_target: crate::EffectTargetSpec::default(),
135 output_size_limit_bytes: Some(128 * 1024),
136 provider_payload: None,
137 },
138 }
139 }
140}
141
142#[async_trait]
143impl<P> Tool for SearchMemoryTool<P>
144where
145 P: MemorySearchPort + 'static,
146{
147 fn descriptor(&self) -> &ToolDescriptor {
148 &self.descriptor
149 }
150
151 async fn invoke(&self, ctx: &ToolCtx, call: &ToolCall) -> Result<ToolResult, ToolError> {
152 let query = call
153 .arguments
154 .get("query")
155 .and_then(|value| value.as_str())
156 .unwrap_or_default();
157 let limit = call
158 .arguments
159 .get("limit")
160 .and_then(|value| value.as_u64())
161 .unwrap_or(5) as usize;
162 let results = self.port.search(query, ctx.scope.as_ref(), limit).await?;
163 Ok(ToolResult::json(results))
164 }
165}
166
167pub struct ReadArtifactTool<P> {
168 port: Arc<P>,
169 descriptor: ToolDescriptor,
170}
171
172impl<P> ReadArtifactTool<P> {
173 pub fn new(port: Arc<P>) -> Self {
175 Self {
176 port,
177 descriptor: ToolDescriptor {
178 name: "read_artifact".into(),
179 version: "1.0.0".into(),
180 description: Some("Read the content of a stored artifact".into()),
181 backend_kind: ToolBackendKind::LocalFunction,
182 input_schema: json!({
183 "type": "object",
184 "required": ["artifact_id"],
185 "properties": {
186 "artifact_id": {"type": "string"}
187 },
188 "additionalProperties": false
189 }),
190 output_mode: ToolOutputMode::Text,
191 read_only: true,
192 side_effect_class: ToolSideEffectClass::ReadOnly,
193 idempotency_class: ToolIdempotencyClass::Idempotent,
194 approval_kind: ToolApprovalKind::None,
195 timeout_ms: 10_000,
196 concurrency_key: Some("read_artifact".into()),
197 cache_ttl_ms: Some(5_000),
198 exposure_mode: ToolExposureMode::OptIn,
199 mcp_surface_kind: crate::McpSurfaceKind::Resource,
200 exposure_policy: ToolExposurePolicy::default(),
201 receipt_persistence: ToolReceiptPersistence::Ephemeral,
202 rollback_contract: None,
203 effect_target: crate::EffectTargetSpec::default(),
204 output_size_limit_bytes: Some(256 * 1024),
205 provider_payload: None,
206 },
207 }
208 }
209}
210
211#[async_trait]
212impl<P> Tool for ReadArtifactTool<P>
213where
214 P: ArtifactReader + 'static,
215{
216 fn descriptor(&self) -> &ToolDescriptor {
217 &self.descriptor
218 }
219
220 async fn invoke(&self, _ctx: &ToolCtx, call: &ToolCall) -> Result<ToolResult, ToolError> {
221 let artifact_id = call
222 .arguments
223 .get("artifact_id")
224 .and_then(|value| value.as_str())
225 .unwrap_or_default();
226 let artifact_id_parsed = ArtifactId::try_new(artifact_id).map_err(|e| {
227 ToolError::new(
228 crate::ToolErrorClass::InvalidArguments,
229 format!("invalid artifact_id: {e}"),
230 )
231 })?;
232 let content = self.port.read(&artifact_id_parsed).await?;
233 Ok(ToolResult::text(content.content))
234 }
235}
236
237pub struct RunVerificationTool<P> {
238 port: Arc<P>,
239 descriptor: ToolDescriptor,
240}
241
242impl<P> RunVerificationTool<P> {
243 pub fn new(port: Arc<P>) -> Self {
245 Self {
246 port,
247 descriptor: ToolDescriptor {
248 name: "run_verification".into(),
249 version: "1.0.0".into(),
250 description: Some(
251 "Enqueue a verification job and return a durable job handle".into(),
252 ),
253 backend_kind: ToolBackendKind::LocalFunction,
254 input_schema: json!({
255 "type": "object",
256 "required": ["candidate_id"],
257 "properties": {
258 "candidate_id": {"type": "string"},
259 "task_id": {"type": "string"}
260 },
261 "additionalProperties": false
262 }),
263 output_mode: ToolOutputMode::JobHandle,
264 read_only: false,
265 side_effect_class: ToolSideEffectClass::Analysis,
266 idempotency_class: ToolIdempotencyClass::BestEffort,
267 approval_kind: ToolApprovalKind::PolicyRequired,
268 timeout_ms: 30_000,
269 concurrency_key: Some("run_verification".into()),
270 cache_ttl_ms: None,
271 exposure_mode: ToolExposureMode::OptIn,
272 mcp_surface_kind: crate::McpSurfaceKind::Tool,
273 exposure_policy: ToolExposurePolicy::default(),
274 receipt_persistence: ToolReceiptPersistence::ForgeRaw,
275 rollback_contract: None,
276 effect_target: crate::EffectTargetSpec {
277 aliases: vec!["candidate_id".into()],
278 compound: Vec::new(),
279 },
280 output_size_limit_bytes: Some(8 * 1024),
281 provider_payload: None,
282 },
283 }
284 }
285}
286
287#[async_trait]
288impl<P> Tool for RunVerificationTool<P>
289where
290 P: RunVerificationPort + 'static,
291{
292 fn descriptor(&self) -> &ToolDescriptor {
293 &self.descriptor
294 }
295
296 async fn invoke(&self, ctx: &ToolCtx, call: &ToolCall) -> Result<ToolResult, ToolError> {
297 let request: VerificationJobRequest = serde_json::from_value(call.arguments.clone())
298 .map_err(|error| ToolError {
299 class: crate::ToolErrorClass::InvalidArguments,
300 message: error.to_string(),
301 retryable: false,
302 details: None,
303 })?;
304 let permit =
305 scoped_execution_permit(ctx, self.descriptor.name.as_str(), &request.candidate_id)?;
306 let handle = self
307 .port
308 .enqueue_verification(request, ctx.dry_run, permit)
309 .await?;
310 Ok(ToolResult::job_handle(handle))
311 }
312}
313
314pub struct ApplyPatchPreviewTool<P> {
315 port: Arc<P>,
316 descriptor: ToolDescriptor,
317}
318
319impl<P> ApplyPatchPreviewTool<P> {
320 pub fn new(port: Arc<P>) -> Self {
322 Self {
323 port,
324 descriptor: ToolDescriptor {
325 name: "apply_patch_preview".into(),
326 version: "1.0.0".into(),
327 description: Some("Preview a patch application without mutating the target".into()),
328 backend_kind: ToolBackendKind::LocalFunction,
329 input_schema: json!({
330 "type": "object",
331 "required": ["target", "patch"],
332 "properties": {
333 "target": {"type": "string"},
334 "patch": {"type": "string"}
335 },
336 "additionalProperties": false
337 }),
338 output_mode: ToolOutputMode::StructuredJson,
339 read_only: false,
340 side_effect_class: ToolSideEffectClass::PreviewWrite,
341 idempotency_class: ToolIdempotencyClass::Idempotent,
342 approval_kind: ToolApprovalKind::PolicyRequired,
343 timeout_ms: 10_000,
344 concurrency_key: Some("apply_patch_preview".into()),
345 cache_ttl_ms: None,
346 exposure_mode: ToolExposureMode::OptIn,
347 mcp_surface_kind: crate::McpSurfaceKind::Tool,
348 exposure_policy: ToolExposurePolicy::default(),
349 receipt_persistence: ToolReceiptPersistence::ForgeRaw,
350 rollback_contract: None,
351 effect_target: crate::EffectTargetSpec {
352 aliases: vec!["target".into()],
353 compound: Vec::new(),
354 },
355 output_size_limit_bytes: Some(64 * 1024),
356 provider_payload: None,
357 },
358 }
359 }
360}
361
362#[async_trait]
363impl<P> Tool for ApplyPatchPreviewTool<P>
364where
365 P: ApplyPatchPreviewPort + 'static,
366{
367 fn descriptor(&self) -> &ToolDescriptor {
368 &self.descriptor
369 }
370
371 async fn invoke(&self, _ctx: &ToolCtx, call: &ToolCall) -> Result<ToolResult, ToolError> {
372 let target = call
373 .arguments
374 .get("target")
375 .and_then(|value| value.as_str())
376 .unwrap_or_default();
377 let patch = call
378 .arguments
379 .get("patch")
380 .and_then(|value| value.as_str())
381 .unwrap_or_default();
382 let permit = scoped_execution_permit(_ctx, self.descriptor.name.as_str(), target)?;
383 let preview = self.port.preview_patch(target, patch, permit).await?;
384 Ok(ToolResult::json(preview))
385 }
386}
387
388pub struct SubmitPatchTool<P> {
389 port: Arc<P>,
390 descriptor: ToolDescriptor,
391}
392
393impl<P> SubmitPatchTool<P> {
394 pub fn new(port: Arc<P>) -> Self {
396 Self {
397 port,
398 descriptor: ToolDescriptor {
399 name: "submit_patch".into(),
400 version: "1.0.0".into(),
401 description: Some("Submit a patch to the target with explicit approval".into()),
402 backend_kind: ToolBackendKind::LocalFunction,
403 input_schema: json!({
404 "type": "object",
405 "required": ["target", "patch"],
406 "properties": {
407 "target": {"type": "string"},
408 "patch": {"type": "string"}
409 },
410 "additionalProperties": false
411 }),
412 output_mode: ToolOutputMode::StructuredJson,
413 read_only: false,
414 side_effect_class: ToolSideEffectClass::Write,
415 idempotency_class: ToolIdempotencyClass::NonIdempotent,
416 approval_kind: ToolApprovalKind::UserRequired,
417 timeout_ms: 10_000,
418 concurrency_key: Some("submit_patch".into()),
419 cache_ttl_ms: None,
420 exposure_mode: ToolExposureMode::Hidden,
421 mcp_surface_kind: crate::McpSurfaceKind::Tool,
422 exposure_policy: ToolExposurePolicy {
423 exposure_mode: ToolExposureMode::Hidden,
424 ..Default::default()
425 },
426 receipt_persistence: ToolReceiptPersistence::ForgeRaw,
427 rollback_contract: None,
428 effect_target: crate::EffectTargetSpec {
429 aliases: vec!["target".into()],
430 compound: Vec::new(),
431 },
432 output_size_limit_bytes: Some(64 * 1024),
433 provider_payload: None,
434 },
435 }
436 }
437}
438
439#[async_trait]
440impl<P> Tool for SubmitPatchTool<P>
441where
442 P: SubmitPatchPort + 'static,
443{
444 fn descriptor(&self) -> &ToolDescriptor {
445 &self.descriptor
446 }
447
448 async fn invoke(&self, ctx: &ToolCtx, call: &ToolCall) -> Result<ToolResult, ToolError> {
449 let target = call
450 .arguments
451 .get("target")
452 .and_then(|value| value.as_str())
453 .unwrap_or_default();
454 let patch = call
455 .arguments
456 .get("patch")
457 .and_then(|value| value.as_str())
458 .unwrap_or_default();
459 let permit = scoped_execution_permit(ctx, self.descriptor.name.as_str(), target)?;
460 let result = self
461 .port
462 .submit_patch(
463 target,
464 patch,
465 ctx.approval_grant.as_ref().ok_or_else(|| {
466 ToolError::new(
467 crate::ToolErrorClass::ApprovalRequired,
468 "submit_patch requires an approval grant",
469 )
470 })?,
471 permit,
472 )
473 .await?;
474 Ok(ToolResult::json(result))
475 }
476}
477
478pub struct StarterToolPorts<MS, AR, RV, PP, SP> {
479 pub memory_search: Arc<MS>,
480 pub artifact_reader: Arc<AR>,
481 pub verification: Arc<RV>,
482 pub patch_preview: Arc<PP>,
483 pub patch_submit: Arc<SP>,
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::{ToolCall, ToolCtx, ToolOriginKind, ToolRetryOwner};
490 use serde_json::json;
491 use stack_ids::{ArtifactId, AttemptId, TraceCtx, TrialId};
492
493 struct TestMemorySearch;
494
495 #[async_trait]
496 impl MemorySearchPort for TestMemorySearch {
497 async fn search(
498 &self,
499 query: &str,
500 scope: Option<&ScopeKey>,
501 limit: usize,
502 ) -> Result<serde_json::Value, ToolError> {
503 Ok(json!({
504 "query": query,
505 "scope": scope.map(|scope| scope.namespace.clone()),
506 "limit": limit,
507 }))
508 }
509 }
510
511 struct TestArtifactReader;
512
513 #[async_trait]
514 impl ArtifactReader for TestArtifactReader {
515 async fn read(&self, artifact_id: &ArtifactId) -> Result<ArtifactContent, ToolError> {
516 Ok(ArtifactContent {
517 artifact_id: artifact_id.clone(),
518 content: format!("artifact:{}", artifact_id.as_str()),
519 })
520 }
521 }
522
523 struct TestVerificationPort;
524
525 #[async_trait]
526 impl RunVerificationPort for TestVerificationPort {
527 async fn enqueue_verification(
528 &self,
529 request: VerificationJobRequest,
530 dry_run: bool,
531 execution_permit: &crate::ToolExecutionPermit,
532 ) -> Result<crate::ToolJobHandle, ToolError> {
533 Ok(crate::ToolJobHandle {
534 job_id: format!(
535 "job:{}:{}",
536 request.candidate_id,
537 execution_permit.execution_permit_id()
538 ),
539 status: Some(if dry_run { "dry_run" } else { "queued" }.into()),
540 })
541 }
542 }
543
544 struct TestPatchPreviewPort;
545
546 #[async_trait]
547 impl ApplyPatchPreviewPort for TestPatchPreviewPort {
548 async fn preview_patch(
549 &self,
550 target: &str,
551 patch: &str,
552 execution_permit: &crate::ToolExecutionPermit,
553 ) -> Result<serde_json::Value, ToolError> {
554 Ok(json!({
555 "target": target,
556 "patch_len": patch.len(),
557 "execution_permit_id": execution_permit.execution_permit_id(),
558 "preview": true,
559 }))
560 }
561 }
562
563 struct TestSubmitPatchPort;
564
565 #[async_trait]
566 impl SubmitPatchPort for TestSubmitPatchPort {
567 async fn submit_patch(
568 &self,
569 target: &str,
570 patch: &str,
571 approval_grant: &crate::ApprovalGrant,
572 execution_permit: &crate::ToolExecutionPermit,
573 ) -> Result<serde_json::Value, ToolError> {
574 Ok(json!({
575 "target": target,
576 "patch_len": patch.len(),
577 "approval_grant_id": approval_grant.grant_id,
578 "execution_permit_id": execution_permit.execution_permit_id(),
579 "submitted": true,
580 }))
581 }
582 }
583
584 fn execution_permit_for(target_key: &str) -> crate::ToolExecutionPermit {
585 crate::ToolExecutionPermit::new(
586 stack_ids::ExecutionPermitId::generate(),
587 stack_ids::PolicyDecisionId::generate(),
588 None,
589 "starter-tools-test",
590 target_key,
591 stack_ids::ContentDigest::compute(b"test-method"),
592 stack_ids::ContentDigest::compute(b"test-effect"),
593 Some(chrono::Utc::now() + chrono::Duration::minutes(5)),
594 uuid::Uuid::new_v4().to_string(),
595 )
596 }
597
598 fn tool_ctx() -> ToolCtx {
599 ToolCtx {
600 trace_ctx: TraceCtx::generate(),
601 attempt_id: AttemptId::generate(),
602 trial_id: TrialId::generate(),
603 deadline: None,
604 workload_class: None,
605 budget_context: None,
606 scope: Some(ScopeKey::namespace_only("starter-tools-test")),
607 dry_run: true,
608 approval_grant: Some(crate::ApprovalGrant::new(
609 "policy-1",
610 crate::ApprovalGrantScope {
611 namespace: "starter-tools-test".into(),
612 target_key: Some("src/lib.rs".into()),
613 tool_name: "submit_patch".into(),
614 effect_class: crate::ApprovalGrantEffectClass::Write,
615 planner_stage: Some(crate::ToolPlannerStage::Execution),
616 },
617 vec!["operator".into()],
618 "2026-03-12T00:00:00Z",
619 None,
620 crate::ApprovalGrantCitation {
621 applicability_context_id: stack_ids::ApplicabilityContextId::new(
622 "applicability-context-1",
623 ),
624 profile_set_id: stack_ids::ProfileSetId::new("profile-set-1"),
625 composition_receipt_id: stack_ids::CompositionReceiptId::new(
626 "composition-receipt-1",
627 ),
628 effective_constitution_id: stack_ids::EffectiveConstitutionId::new(
629 "effective-constitution-1",
630 ),
631 compiled_obligation_set_id: stack_ids::CompiledObligationSetId::new(
632 "compiled-obligation-set-1",
633 ),
634 },
635 )),
636 execution_permit: Some(execution_permit_for("src/lib.rs").into()),
637 idempotency_key: None,
638 caller: "starter-tools-test".into(),
639 planner_stage: crate::ToolPlannerStage::Execution,
640 parent_receipt_id: None,
641 family_receipt_id: None,
642 replay_parent_receipt_id: None,
643 remote_oracle_lease_id: None,
644 remote_slice_result_id: None,
645 attestation_envelope_id: None,
646 cross_runtime_replay_ticket_id: None,
647 retry_owner: Some(ToolRetryOwner::LlmPipeline),
648 }
649 }
650
651 #[tokio::test]
652 async fn starter_tools_return_expected_results() {
653 let ctx = tool_ctx();
654
655 let search_tool = SearchMemoryTool::new(Arc::new(TestMemorySearch));
656 let search_result = search_tool
657 .invoke(
658 &ctx,
659 &ToolCall::new(
660 "search_memory",
661 "1.0.0",
662 json!({"query": "tool runtime", "limit": 3}),
663 ToolOriginKind::Test,
664 ),
665 )
666 .await
667 .unwrap();
668 assert_eq!(search_result.payload["query"], "tool runtime");
669 assert_eq!(search_result.payload["limit"], 3);
670
671 let read_tool = ReadArtifactTool::new(Arc::new(TestArtifactReader));
672 let read_result = read_tool
673 .invoke(
674 &ctx,
675 &ToolCall::new(
676 "read_artifact",
677 "1.0.0",
678 json!({"artifact_id": "artifact-1"}),
679 ToolOriginKind::Test,
680 ),
681 )
682 .await
683 .unwrap();
684 assert_eq!(
685 read_result.display_text.as_deref(),
686 Some("artifact:artifact:artifact-1")
687 );
688
689 let verification_tool = RunVerificationTool::new(Arc::new(TestVerificationPort));
690 let mut verification_ctx = tool_ctx();
691 verification_ctx.execution_permit = Some(execution_permit_for("cand-1").into());
692 let verification_result = verification_tool
693 .invoke(
694 &verification_ctx,
695 &ToolCall::new(
696 "run_verification",
697 "1.0.0",
698 json!({"candidate_id": "cand-1"}),
699 ToolOriginKind::Test,
700 ),
701 )
702 .await
703 .unwrap();
704 assert!(verification_result.payload["job_id"]
705 .as_str()
706 .is_some_and(|job_id| job_id.starts_with("job:cand-1:")));
707
708 let preview_tool = ApplyPatchPreviewTool::new(Arc::new(TestPatchPreviewPort));
709 let preview_result = preview_tool
710 .invoke(
711 &ctx,
712 &ToolCall::new(
713 "apply_patch_preview",
714 "1.0.0",
715 json!({"target": "src/lib.rs", "patch": "*** Begin Patch"}),
716 ToolOriginKind::Test,
717 ),
718 )
719 .await
720 .unwrap();
721 assert_eq!(preview_result.payload["preview"], true);
722 assert!(preview_result.payload["execution_permit_id"].is_string());
723
724 let submit_tool = SubmitPatchTool::new(Arc::new(TestSubmitPatchPort));
725 let submit_result = submit_tool
726 .invoke(
727 &ctx,
728 &ToolCall::new(
729 "submit_patch",
730 "1.0.0",
731 json!({"target": "src/lib.rs", "patch": "*** Begin Patch"}),
732 ToolOriginKind::Test,
733 ),
734 )
735 .await
736 .unwrap();
737 assert_eq!(submit_result.payload["submitted"], true);
738 assert!(submit_result.payload["approval_grant_id"].is_string());
739 assert!(submit_result.payload["execution_permit_id"].is_string());
740 }
741}