Skip to main content

chio_guards/
behavioral_sequence.rs

1//! Behavioral sequence guard -- enforces tool ordering policies using the session journal.
2//!
3//! This guard checks the tool invocation sequence recorded in the session journal
4//! against configurable ordering policies:
5//!
6//! - **Required predecessors**: tool X can only run after tool Y has been invoked.
7//! - **Forbidden sequences**: tool X cannot be invoked immediately after tool Y.
8//! - **Max consecutive**: limits on how many times the same tool can run in a row.
9//! - **Required first tool**: the first tool in a session must match a specific name.
10//!
11//! The guard fails closed: if the session journal is unavailable, access is denied.
12
13use std::collections::{HashMap, HashSet};
14use std::sync::Arc;
15
16use chio_http_session::SessionJournal;
17#[cfg(test)]
18use chio_kernel::Verdict;
19use chio_kernel::{Guard, GuardContext, GuardDecision, KernelError};
20
21// ---------------------------------------------------------------------------
22// SequencePolicy
23// ---------------------------------------------------------------------------
24
25/// Policy configuration for the behavioral sequence guard.
26#[derive(Clone, Debug, Default)]
27pub struct SequencePolicy {
28    /// Tools that must have been invoked before a given tool can run.
29    /// Map from tool_name to set of required predecessor tools.
30    pub required_predecessors: HashMap<String, HashSet<String>>,
31    /// Forbidden immediate transitions: (from_tool, to_tool) pairs.
32    /// If the last invoked tool is `from_tool`, then `to_tool` is denied.
33    pub forbidden_transitions: Vec<(String, String)>,
34    /// Maximum consecutive invocations of the same tool.
35    /// None means unlimited.
36    pub max_consecutive: Option<u32>,
37    /// If set, the first tool in the session must match this name.
38    pub required_first_tool: Option<String>,
39}
40
41// ---------------------------------------------------------------------------
42// BehavioralSequenceGuard
43// ---------------------------------------------------------------------------
44
45/// Guard that enforces tool ordering policies using the session journal.
46pub struct BehavioralSequenceGuard {
47    journal: Arc<SessionJournal>,
48    policy: SequencePolicy,
49}
50
51impl BehavioralSequenceGuard {
52    /// Create a new guard with the given journal and policy.
53    pub fn new(journal: Arc<SessionJournal>, policy: SequencePolicy) -> Self {
54        Self { journal, policy }
55    }
56}
57
58impl Guard for BehavioralSequenceGuard {
59    fn name(&self) -> &str {
60        "behavioral-sequence"
61    }
62
63    fn evaluate(&self, ctx: &GuardContext) -> Result<GuardDecision, KernelError> {
64        let tool_name = &ctx.request.tool_name;
65
66        let snapshot = self.journal.snapshot().map_err(|e| {
67            KernelError::Internal(format!(
68                "behavioral-sequence guard journal error (fail-closed): {e}"
69            ))
70        })?;
71
72        // Check required first tool. "Have any tools run yet" is a cumulative
73        // property, so consult the journal's cumulative O(1) last-tool field
74        // (`current_streak_tool`), which is `None` only before the first record,
75        // NOT the bounded `tool_sequence` ring. When `journal_entry_cap` is 0 the
76        // ring stores no tool names (capacity 0 = disabled), so `tool_sequence`
77        // would report EVERY call as the first and mis-fire this check; the
78        // cumulative field is correct at any entry cap.
79        if snapshot.current_streak_tool.is_none() {
80            if let Some(ref required_first) = self.policy.required_first_tool {
81                if tool_name != required_first {
82                    return Ok(GuardDecision::deny(Vec::new()));
83                }
84            }
85        }
86
87        // Check required predecessors. "Has this tool ever been invoked" is a
88        // cumulative property, so consult the journal's cumulative `tool_counts`
89        // (which survives ring eviction) rather than the
90        // bounded `tool_sequence` tail. A predecessor invoked once and then
91        // pushed out of the retained window is still known to have run, so a
92        // workflow that runs setup once and then more than `journal_entry_cap`
93        // other calls is no longer falsely denied when a dependent tool needs
94        // that evicted predecessor. `tool_counts` cannot grow without bound: the
95        // journal caps its distinct-key set fail-closed
96        // (`journal_tool_counts_cap`). Legitimate registry-bounded predecessors
97        // stay recorded, but a predecessor that overflowed the cap is absent here
98        // and therefore denies (fail-closed) rather than being falsely treated as
99        // invoked.
100        if let Some(required) = self.policy.required_predecessors.get(tool_name) {
101            for req in required {
102                if !snapshot.tool_counts.contains_key(req) {
103                    return Ok(GuardDecision::deny(Vec::new()));
104                }
105            }
106        }
107
108        // Check forbidden transitions. The "last invoked tool" comes from the
109        // journal's cumulative O(1) last-tool field (`current_streak_tool`), NOT
110        // the bounded `tool_sequence` tail: when `journal_entry_cap` is 0 the ring
111        // stores no tool names (capacity 0 = disabled), so `tool_sequence.last()`
112        // is always None and a forbidden transition would silently never fire
113        // (fail-OPEN), letting a memory-budget setting disable a transition-deny
114        // policy. The cumulative field tracks the most recent recorded tool at any
115        // entry cap, so the check holds fail-closed.
116        if let Some(last_tool) = snapshot.current_streak_tool.as_deref() {
117            for (from, to) in &self.policy.forbidden_transitions {
118                if last_tool == from && tool_name == to {
119                    return Ok(GuardDecision::deny(Vec::new()));
120                }
121            }
122        }
123
124        // Check max consecutive. The count of prior consecutive same-tool
125        // invocations comes from the journal's cumulative O(1) streak counter
126        // (`current_streak_tool` + `current_streak_len`), NOT the bounded
127        // `tool_sequence` tail. When `journal_entry_cap` is smaller than
128        // `max_consecutive`, the ring evicts the older part of a same-tool streak,
129        // so counting the retained tail would undercount and ALLOW a call that
130        // must be DENIED. The cumulative counter survives ring eviction, so the
131        // streak limit holds regardless of the entry cap. If the
132        // request tool differs from the current-streak tool, no prior consecutive
133        // run exists for it (it would start a fresh streak).
134        if let Some(max_consec) = self.policy.max_consecutive {
135            let prior_streak =
136                if snapshot.current_streak_tool.as_deref() == Some(tool_name.as_str()) {
137                    snapshot.current_streak_len
138                } else {
139                    0
140                };
141            if prior_streak >= u64::from(max_consec) {
142                return Ok(GuardDecision::deny(Vec::new()));
143            }
144        }
145
146        Ok(GuardDecision::allow())
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use chio_http_session::RecordParams;
154
155    fn make_journal(session_id: &str) -> Arc<SessionJournal> {
156        Arc::new(SessionJournal::new(session_id.to_string()))
157    }
158
159    fn record(journal: &SessionJournal, tool: &str) {
160        journal
161            .record(RecordParams {
162                tool_name: tool.to_string(),
163                server_id: "srv".to_string(),
164                agent_id: "agent".to_string(),
165                bytes_read: 0,
166                bytes_written: 0,
167                delegation_depth: 0,
168                allowed: true,
169            })
170            .expect("record");
171    }
172
173    fn make_ctx_for_tool(
174        tool_name: &str,
175    ) -> (
176        chio_kernel::ToolCallRequest,
177        chio_core::capability::scope::ChioScope,
178        String,
179        String,
180    ) {
181        let kp = chio_core::crypto::Keypair::generate();
182        let scope = chio_core::capability::scope::ChioScope::default();
183        let agent_id = kp.public_key().to_hex();
184        let server_id = "srv-test".to_string();
185
186        let cap_body = chio_core::capability::token::CapabilityTokenBody {
187            id: "cap-test".to_string(),
188            issuer: kp.public_key(),
189            subject: kp.public_key(),
190            scope: scope.clone(),
191            issued_at: 0,
192            expires_at: u64::MAX,
193            delegation_chain: vec![],
194            aggregate_invocation_budget: None,
195        };
196        let cap =
197            chio_core::capability::token::CapabilityToken::sign(cap_body, &kp).expect("sign cap");
198
199        let request = chio_kernel::ToolCallRequest {
200            request_id: "req-test".to_string(),
201            capability: cap,
202            tool_name: tool_name.to_string(),
203            server_id: server_id.clone(),
204            agent_id: agent_id.clone(),
205            arguments: serde_json::json!({}),
206            dpop_proof: None,
207            execution_nonce: None,
208            governed_intent: None,
209            approval_token: None,
210            approval_tokens: Vec::new(),
211            threshold_approval_proposal: None,
212            supplemental_authorization: None,
213            model_metadata: None,
214            federated_origin_kernel_id: None,
215        };
216
217        (request, scope, agent_id, server_id)
218    }
219
220    fn guard_ctx<'a>(
221        request: &'a chio_kernel::ToolCallRequest,
222        scope: &'a chio_core::capability::scope::ChioScope,
223        agent_id: &'a String,
224        server_id: &'a String,
225    ) -> chio_kernel::GuardContext<'a> {
226        chio_kernel::GuardContext {
227            request,
228            scope,
229            agent_id,
230            server_id,
231            session_filesystem_roots: None,
232            matched_grant_index: None,
233        }
234    }
235
236    #[test]
237    fn guard_name() {
238        let journal = make_journal("sess-1");
239        let guard = BehavioralSequenceGuard::new(journal, SequencePolicy::default());
240        assert_eq!(guard.name(), "behavioral-sequence");
241    }
242
243    #[test]
244    fn empty_policy_allows_all() {
245        let journal = make_journal("sess-1");
246        record(&journal, "read_file");
247        record(&journal, "bash");
248
249        let guard = BehavioralSequenceGuard::new(journal, SequencePolicy::default());
250        let (request, scope, agent_id, server_id) = make_ctx_for_tool("write_file");
251        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
252        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Allow);
253    }
254
255    #[test]
256    fn required_predecessor_enforced() {
257        let journal = make_journal("sess-pred");
258        // No tools invoked yet.
259
260        let mut required = HashMap::new();
261        required.insert(
262            "write_file".to_string(),
263            HashSet::from(["read_file".to_string()]),
264        );
265
266        let guard = BehavioralSequenceGuard::new(
267            journal.clone(),
268            SequencePolicy {
269                required_predecessors: required,
270                ..SequencePolicy::default()
271            },
272        );
273
274        // write_file without read_file predecessor should deny.
275        let (request, scope, agent_id, server_id) = make_ctx_for_tool("write_file");
276        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
277        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Deny);
278
279        // After read_file is invoked, write_file should be allowed.
280        record(&journal, "read_file");
281        let (request2, scope2, agent_id2, server_id2) = make_ctx_for_tool("write_file");
282        let ctx2 = guard_ctx(&request2, &scope2, &agent_id2, &server_id2);
283        assert_eq!(guard.evaluate(&ctx2).expect("ok"), Verdict::Allow);
284    }
285
286    #[test]
287    fn required_predecessor_survives_journal_ring_eviction() {
288        // The required-predecessor check asks "has this tool ever been invoked",
289        // which is cumulative. Once the
290        // bounded tool_sequence ring evicts the setup call, the check must still
291        // resolve it via the cumulative tool_counts, so a long workflow (setup
292        // once, then more than journal_entry_cap other calls) is not falsely
293        // denied when a dependent tool needs the evicted predecessor.
294        let cap = 4;
295        let journal = Arc::new(SessionJournal::with_entry_cap(
296            "sess-evict".to_string(),
297            cap,
298        ));
299        // Run the required predecessor once.
300        record(&journal, "read_file");
301        // Then run well over `cap` other calls, evicting "read_file" from the ring.
302        for _ in 0..(cap * 3) {
303            record(&journal, "bash");
304        }
305
306        let mut required = HashMap::new();
307        required.insert(
308            "write_file".to_string(),
309            HashSet::from(["read_file".to_string()]),
310        );
311        let guard = BehavioralSequenceGuard::new(
312            journal.clone(),
313            SequencePolicy {
314                required_predecessors: required,
315                ..SequencePolicy::default()
316            },
317        );
318
319        // Precondition: the retained ring no longer holds the evicted
320        // predecessor, but the cumulative tool_counts still records it.
321        let snapshot = journal.snapshot().expect("snapshot");
322        assert!(
323            !snapshot.tool_sequence.iter().any(|t| t == "read_file"),
324            "test precondition: read_file must have been evicted from the ring"
325        );
326        assert!(
327            snapshot.tool_counts.contains_key("read_file"),
328            "cumulative tool_counts must still record the evicted predecessor"
329        );
330
331        // write_file requires read_file, which ran once but was evicted; it must
332        // still be ALLOWED because the predecessor is cumulatively known; a
333        // sequence-based check would falsely DENY here.
334        let (request, scope, agent_id, server_id) = make_ctx_for_tool("write_file");
335        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
336        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Allow);
337    }
338
339    #[test]
340    fn required_predecessor_denies_when_predecessor_overflowed_tool_counts_cap() {
341        // The cumulative tool_counts map is distinct-key bounded fail-closed. A
342        // predecessor whose tool name overflowed the cap is absent from
343        // tool_counts, so the required-predecessor check must DENY (treat it as
344        // never-invoked) rather than falsely allow. This gives the distinct-key
345        // bound fail-closed teeth that the cumulative predecessor check depends on.
346        let journal = Arc::new(SessionJournal::with_caps(
347            "sess-overflow".to_string(),
348            1024,
349            1,
350        ));
351        // Fill the single distinct-key slot with an unrelated tool, then invoke
352        // the required predecessor -- which overflows the cap and is dropped from
353        // the cumulative counts even though it ran.
354        record(&journal, "filler");
355        record(&journal, "read_file");
356
357        let snapshot = journal.snapshot().expect("snapshot");
358        assert!(
359            snapshot.tool_sequence.iter().any(|t| t == "read_file"),
360            "test precondition: read_file ran and is in the sequence ring"
361        );
362        assert!(
363            !snapshot.tool_counts.contains_key("read_file"),
364            "test precondition: read_file overflowed the distinct-key cap"
365        );
366
367        let mut required = HashMap::new();
368        required.insert(
369            "write_file".to_string(),
370            HashSet::from(["read_file".to_string()]),
371        );
372        let guard = BehavioralSequenceGuard::new(
373            journal,
374            SequencePolicy {
375                required_predecessors: required,
376                ..SequencePolicy::default()
377            },
378        );
379
380        // read_file "ran" but overflowed the cap, so it is unknown to the check:
381        // write_file must be DENIED fail-closed.
382        let (request, scope, agent_id, server_id) = make_ctx_for_tool("write_file");
383        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
384        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Deny);
385    }
386
387    #[test]
388    fn required_predecessor_within_tool_counts_cap_still_allows() {
389        // The bound must not regress legitimate (registry-bounded) workflows: a
390        // predecessor that fits under the distinct-key cap stays recorded and the
391        // dependent tool is allowed.
392        let journal = Arc::new(SessionJournal::with_caps(
393            "sess-within-cap".to_string(),
394            1024,
395            8,
396        ));
397        record(&journal, "read_file");
398        assert!(journal
399            .snapshot()
400            .expect("snapshot")
401            .tool_counts
402            .contains_key("read_file"));
403
404        let mut required = HashMap::new();
405        required.insert(
406            "write_file".to_string(),
407            HashSet::from(["read_file".to_string()]),
408        );
409        let guard = BehavioralSequenceGuard::new(
410            journal,
411            SequencePolicy {
412                required_predecessors: required,
413                ..SequencePolicy::default()
414            },
415        );
416
417        let (request, scope, agent_id, server_id) = make_ctx_for_tool("write_file");
418        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
419        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Allow);
420    }
421
422    #[test]
423    fn forbidden_transition_enforced() {
424        let journal = make_journal("sess-trans");
425        record(&journal, "bash");
426
427        let guard = BehavioralSequenceGuard::new(
428            journal,
429            SequencePolicy {
430                forbidden_transitions: vec![("bash".to_string(), "write_file".to_string())],
431                ..SequencePolicy::default()
432            },
433        );
434
435        // bash -> write_file is forbidden.
436        let (request, scope, agent_id, server_id) = make_ctx_for_tool("write_file");
437        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
438        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Deny);
439
440        // bash -> read_file is fine.
441        let (request2, scope2, agent_id2, server_id2) = make_ctx_for_tool("read_file");
442        let ctx2 = guard_ctx(&request2, &scope2, &agent_id2, &server_id2);
443        assert_eq!(guard.evaluate(&ctx2).expect("ok"), Verdict::Allow);
444    }
445
446    #[test]
447    fn forbidden_transition_enforced_at_zero_entry_cap() {
448        // A memory budget that sets journal_entry_cap = 0 disables the
449        // entries/tool_sequence rings (capacity 0 = stores nothing), so
450        // snapshot.tool_sequence.last() is always None. The forbidden-transition
451        // check must NOT silently stop firing: it reads the journal's cumulative
452        // O(1) last-tool field, which survives at any cap. Reading
453        // sequence.last() instead would let cap 0 disable the transition deny.
454        let journal = Arc::new(SessionJournal::with_entry_cap(
455            "sess-zero-cap".to_string(),
456            0,
457        ));
458        record(&journal, "bash");
459
460        // The ring really stores nothing at cap 0 (the test only bites if the
461        // tool_sequence tail is empty here)...
462        let snapshot = journal.snapshot().expect("snapshot");
463        assert!(
464            snapshot.tool_sequence.is_empty(),
465            "entry_cap 0 must leave the bounded tool_sequence empty for this test to bite"
466        );
467        // ...but the cumulative last-tool field still tracks `bash`.
468        assert_eq!(snapshot.current_streak_tool.as_deref(), Some("bash"));
469
470        let guard = BehavioralSequenceGuard::new(
471            Arc::clone(&journal),
472            SequencePolicy {
473                forbidden_transitions: vec![("bash".to_string(), "write_file".to_string())],
474                ..SequencePolicy::default()
475            },
476        );
477
478        let (request, scope, agent_id, server_id) = make_ctx_for_tool("write_file");
479        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
480        assert_eq!(
481            guard.evaluate(&ctx).expect("ok"),
482            Verdict::Deny,
483            "forbidden transition bash -> write_file must fire even at entry_cap 0"
484        );
485
486        // A non-forbidden transition from the same cumulative last-tool still allows.
487        let (request2, scope2, agent_id2, server_id2) = make_ctx_for_tool("read_file");
488        let ctx2 = guard_ctx(&request2, &scope2, &agent_id2, &server_id2);
489        assert_eq!(guard.evaluate(&ctx2).expect("ok"), Verdict::Allow);
490    }
491
492    #[test]
493    fn max_consecutive_enforced() {
494        let journal = make_journal("sess-consec");
495        record(&journal, "read_file");
496        record(&journal, "read_file");
497        record(&journal, "read_file");
498
499        let guard = BehavioralSequenceGuard::new(
500            journal,
501            SequencePolicy {
502                max_consecutive: Some(3),
503                ..SequencePolicy::default()
504            },
505        );
506
507        // 4th consecutive read_file should be denied.
508        let (request, scope, agent_id, server_id) = make_ctx_for_tool("read_file");
509        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
510        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Deny);
511
512        // A different tool should be fine.
513        let (request2, scope2, agent_id2, server_id2) = make_ctx_for_tool("write_file");
514        let ctx2 = guard_ctx(&request2, &scope2, &agent_id2, &server_id2);
515        assert_eq!(guard.evaluate(&ctx2).expect("ok"), Verdict::Allow);
516    }
517
518    #[test]
519    fn max_consecutive_resets_on_different_tool() {
520        let journal = make_journal("sess-reset");
521        record(&journal, "read_file");
522        record(&journal, "read_file");
523        record(&journal, "bash"); // Breaks the streak
524        record(&journal, "read_file");
525
526        let guard = BehavioralSequenceGuard::new(
527            journal,
528            SequencePolicy {
529                max_consecutive: Some(3),
530                ..SequencePolicy::default()
531            },
532        );
533
534        // Only 1 consecutive read_file after bash, so this should pass.
535        let (request, scope, agent_id, server_id) = make_ctx_for_tool("read_file");
536        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
537        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Allow);
538    }
539
540    #[test]
541    fn max_consecutive_survives_journal_ring_eviction() {
542        // When `journal_entry_cap` is smaller than `max_consecutive`, the bounded
543        // `tool_sequence` ring evicts the older part of a same-tool streak. If the
544        // guard counted only the retained tail it would undercount and ALLOW a
545        // call that must be DENIED. The cumulative O(1) streak counter survives
546        // ring eviction, so the streak limit is enforced regardless of the cap.
547        let cap = 4;
548        let journal = Arc::new(SessionJournal::with_entry_cap(
549            "sess-streak-evict".to_string(),
550            cap,
551        ));
552        // 10 consecutive calls: max_consecutive allows exactly 10, the 11th must
553        // deny. The ring only retains `cap` (4) of them.
554        for _ in 0..10 {
555            record(&journal, "read_file");
556        }
557
558        // Precondition: the retained ring holds only `cap` entries, far fewer than
559        // the 10-long streak, but the cumulative streak counter records all 10.
560        let snapshot = journal.snapshot().expect("snapshot");
561        assert_eq!(
562            snapshot.tool_sequence.len(),
563            cap,
564            "test precondition: the ring must have evicted the older streak prefix"
565        );
566        assert_eq!(snapshot.current_streak_tool.as_deref(), Some("read_file"));
567        assert_eq!(snapshot.current_streak_len, 10);
568
569        let guard = BehavioralSequenceGuard::new(
570            journal,
571            SequencePolicy {
572                max_consecutive: Some(10),
573                ..SequencePolicy::default()
574            },
575        );
576
577        // The 11th consecutive read_file must be DENIED. Counting the retained
578        // 4-entry tail (4 >= 10 is false) would falsely ALLOW it.
579        let (request, scope, agent_id, server_id) = make_ctx_for_tool("read_file");
580        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
581        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Deny);
582    }
583
584    #[test]
585    fn required_first_tool_enforced() {
586        let journal = make_journal("sess-first");
587
588        let guard = BehavioralSequenceGuard::new(
589            journal,
590            SequencePolicy {
591                required_first_tool: Some("init".to_string()),
592                ..SequencePolicy::default()
593            },
594        );
595
596        // First tool must be "init".
597        let (request, scope, agent_id, server_id) = make_ctx_for_tool("read_file");
598        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
599        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Deny);
600
601        let (request2, scope2, agent_id2, server_id2) = make_ctx_for_tool("init");
602        let ctx2 = guard_ctx(&request2, &scope2, &agent_id2, &server_id2);
603        assert_eq!(guard.evaluate(&ctx2).expect("ok"), Verdict::Allow);
604    }
605
606    #[test]
607    fn required_first_tool_only_applies_to_first() {
608        let journal = make_journal("sess-first-only");
609        record(&journal, "init"); // First tool is correct.
610
611        let guard = BehavioralSequenceGuard::new(
612            journal,
613            SequencePolicy {
614                required_first_tool: Some("init".to_string()),
615                ..SequencePolicy::default()
616            },
617        );
618
619        // Subsequent tools can be anything.
620        let (request, scope, agent_id, server_id) = make_ctx_for_tool("read_file");
621        let ctx = guard_ctx(&request, &scope, &agent_id, &server_id);
622        assert_eq!(guard.evaluate(&ctx).expect("ok"), Verdict::Allow);
623    }
624}