Skip to main content

forge_primitives/
server.rs

1//! The `[server]` section of `forge.toml`.
2
3use std::net::{IpAddr, Ipv4Addr};
4
5use serde::Deserialize;
6
7/// The `[server]` block of `forge.toml`: whether the socket's listener
8/// starts with forge, where it binds, and how large a served page's
9/// structured tool record may be.
10///
11/// On unless it is turned off, so a restart leaves the socket serving
12/// without a key being added first.
13#[derive(Debug, Clone, Deserialize)]
14#[serde(default, deny_unknown_fields)]
15pub struct ServerConfig {
16    pub enabled: bool,
17    pub port: u16,
18    /// Loopback by default: an address on a network interface is a
19    /// deliberate line in `forge.toml`, never something on-by-default
20    /// reaches.
21    pub bind: IpAddr,
22    /// How large a frame's `tool_use_result` may be in anything a client is
23    /// served, in KB (#2374).
24    ///
25    /// **A record over it is reduced first**, keeping only the keys a client
26    /// draws (a mutation's patch and outside-edit mark, a question's answers
27    /// and notes); it becomes an `omitted` marker only when that reduction is
28    /// still over the cap. Every path to a client is bounded - a page, a
29    /// `more`, the live stream and both repairs - while the transcript keeps
30    /// the record whole and the visible tool-result block draws as always.
31    /// `0` serves the records whole, for a reader whose changes carry patches
32    /// beyond the default.
33    pub tool_record_cap_kb: u64,
34    /// How many bytes of conversation a session's first snapshot carries, in
35    /// KB.
36    ///
37    /// **The first snapshot is what a seat switch waits on**, and twenty
38    /// turns of a large conversation are megabytes: seconds over a network,
39    /// from a frame a client spends milliseconds on locally. The newest
40    /// turns are kept while they fit and the rest is paged through `more` as
41    /// it always was, so nothing is dropped and the order does not move.
42    /// `0` serves the page the turn count asks for, for a reader who would
43    /// rather have the whole window in one frame.
44    pub first_frame_cap_kb: u64,
45    /// How many bytes of conversation a `more` page carries, in KB.
46    ///
47    /// **A page past the first frame is unbounded by its turn count alone**:
48    /// the page contract is whole turns, so one large turn crossed whole and
49    /// a `more` answer could be tens of megabytes. The newest whole turns are
50    /// kept while they fit, the newest rides even when it alone is over, and
51    /// the cursor names the history above the page as it always did - nothing
52    /// is dropped and the walk above it still reaches every turn.
53    /// `0` serves the page the turn count asks for.
54    pub more_page_cap_kb: u64,
55    /// Extra browser origins allowed to open the socket, beside the shell's
56    /// own, the server's own, and any page served on the user's own machine -
57    /// the `localhost` name or any loopback IP address, any port, http or
58    /// https, in every build. A web client served from another address is one
59    /// only the operator knows; an empty list allows none.
60    pub allowed_origins: Vec<String>,
61}
62
63/// The port the server binds when `[server] port` is absent. Distinct
64/// from the gateway's 8787, the other listener a boot brings up.
65pub const DEFAULT_SERVER_PORT: u16 = 8790;
66
67/// The served `tool_use_result` cap when `[server] tool_record_cap_kb` is
68/// absent. See `ServerConfig::tool_record_cap_kb`.
69pub const DEFAULT_TOOL_RECORD_CAP_KB: u64 = 64;
70
71/// The first snapshot's conversation budget when `[server]
72/// first_frame_cap_kb` is absent. See `ServerConfig::first_frame_cap_kb`.
73pub const DEFAULT_FIRST_FRAME_CAP_KB: u64 = 512;
74
75/// A `more` page's conversation budget when `[server] more_page_cap_kb` is
76/// absent. See `ServerConfig::more_page_cap_kb`.
77pub const DEFAULT_MORE_PAGE_CAP_KB: u64 = 512;
78
79impl Default for ServerConfig {
80    fn default() -> Self {
81        Self {
82            enabled: true,
83            port: DEFAULT_SERVER_PORT,
84            bind: IpAddr::V4(Ipv4Addr::LOCALHOST),
85            tool_record_cap_kb: DEFAULT_TOOL_RECORD_CAP_KB,
86            first_frame_cap_kb: DEFAULT_FIRST_FRAME_CAP_KB,
87            more_page_cap_kb: DEFAULT_MORE_PAGE_CAP_KB,
88            allowed_origins: Vec::new(),
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    /// The shipped default, and the opt-out: absent means 64 KB, and `0`
98    /// means the records cross whole. **The default is pinned as a literal**,
99    /// because the book documents 64 and comparing it to the constant would
100    /// move with it.
101    #[test]
102    fn the_served_record_cap_defaults_to_64_kb_and_zero_means_whole() {
103        assert_eq!(ServerConfig::default().tool_record_cap_kb, 64, "the documented default");
104        assert_eq!(DEFAULT_TOOL_RECORD_CAP_KB, 64, "and the constant the default is built from");
105        let parsed: ServerConfig =
106            serde_json::from_str(r#"{"tool_record_cap_kb": 0}"#).expect("the key parses");
107        assert_eq!(parsed.tool_record_cap_kb, 0);
108    }
109
110    /// The shipped default, and the opt-out: absent means a 512 KB `more`
111    /// page, and `0` means the turn count alone decides it. Pinned as a
112    /// literal for the same reason as the other caps'.
113    #[test]
114    fn the_more_page_cap_defaults_to_512_kb_and_zero_means_the_count() {
115        assert_eq!(ServerConfig::default().more_page_cap_kb, 512, "the documented default");
116        assert_eq!(DEFAULT_MORE_PAGE_CAP_KB, 512, "and the constant the default is built from");
117        let parsed: ServerConfig =
118            serde_json::from_str(r#"{"more_page_cap_kb": 0}"#).expect("the key parses");
119        assert_eq!(parsed.more_page_cap_kb, 0);
120    }
121
122    /// The shipped default, and the opt-out: absent means a 512 KB first
123    /// page, and `0` means the turn count alone decides it. Pinned as a
124    /// literal for the same reason as the record cap's.
125    #[test]
126    fn the_first_frame_cap_defaults_to_512_kb_and_zero_means_the_count() {
127        assert_eq!(ServerConfig::default().first_frame_cap_kb, 512, "the documented default");
128        assert_eq!(DEFAULT_FIRST_FRAME_CAP_KB, 512, "and the constant the default is built from");
129        let parsed: ServerConfig =
130            serde_json::from_str(r#"{"first_frame_cap_kb": 0}"#).expect("the key parses");
131        assert_eq!(parsed.first_frame_cap_kb, 0);
132    }
133}