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}