@@ -134,6 +134,28 @@ pub struct ConfigArgs {
134134 #[ arg( long = "per-user-secret-quota" , env = "PER_USER_SECRET_QUOTA" ) ]
135135 pub per_user_secret_quota_bytes : Option < u64 > ,
136136
137+ /// Inactivity TTL, in seconds, after which a HOSTED user's entire
138+ /// per-user data is reclaimed by a background sweep (#4561, P5 of #4381).
139+ /// Keeps a public "try Freenet" node a transient demo with bounded storage:
140+ /// a visitor who walks away has their namespace reclaimed after this many
141+ /// real-calendar seconds of inactivity (durable across restarts). Default:
142+ /// 2_592_000 (30 days). `0` disables the sweep entirely. Has NO effect
143+ /// outside hosted mode — Local single-user data is never enumerated or
144+ /// reclaimed (it lives outside the `users/<id>/` tree the sweep touches).
145+ #[ arg( long = "per-user-inactive-ttl" , env = "PER_USER_INACTIVE_TTL" ) ]
146+ pub per_user_inactive_ttl_secs : Option < u64 > ,
147+
148+ /// How often, in seconds, the inactive-user reclaim sweep runs (#4561).
149+ /// Only relevant when hosted mode is on and `per-user-inactive-ttl` is
150+ /// non-zero. Default: 3_600 (hourly) — far finer than the 30-day TTL, so
151+ /// reclamation lag is negligible while keeping the sweep's disk-walk cost
152+ /// trivial. Must be > 0; `0` is treated as the default.
153+ #[ arg(
154+ long = "inactive-user-sweep-interval" ,
155+ env = "INACTIVE_USER_SWEEP_INTERVAL"
156+ ) ]
157+ pub inactive_user_sweep_interval_secs : Option < u64 > ,
158+
137159 /// Byte budget for the compiled-WASM **contract** module cache. The
138160 /// **delegate** cache gets a fraction of this value
139161 /// (`DELEGATE_MODULE_CACHE_BUDGET_DIVISOR`, currently 1/4), so the combined
@@ -206,6 +228,8 @@ impl Default for ConfigArgs {
206228 max_blocking_threads : None ,
207229 max_hosting_storage : None ,
208230 per_user_secret_quota_bytes : None ,
231+ per_user_inactive_ttl_secs : None ,
232+ inactive_user_sweep_interval_secs : None ,
209233 module_cache_budget_bytes : None ,
210234 shutdown_drain_secs : None ,
211235 telemetry : Default :: default ( ) ,
@@ -461,6 +485,10 @@ impl ConfigArgs {
461485 . get_or_insert ( cfg. max_hosting_storage ) ;
462486 self . per_user_secret_quota_bytes
463487 . get_or_insert ( cfg. per_user_secret_quota_bytes ) ;
488+ self . per_user_inactive_ttl_secs
489+ . get_or_insert ( cfg. per_user_inactive_ttl_secs ) ;
490+ self . inactive_user_sweep_interval_secs
491+ . get_or_insert ( cfg. inactive_user_sweep_interval_secs ) ;
464492 self . module_cache_budget_bytes
465493 . get_or_insert ( cfg. module_cache_budget_bytes ) ;
466494 self . shutdown_drain_secs
@@ -899,6 +927,9 @@ impl ConfigArgs {
899927 . ws_api
900928 . per_user_export_min_interval_secs
901929 . unwrap_or_else ( default_per_user_export_min_interval_secs) ,
930+ // Runtime-only: resolve the secrets dir for this mode so the WS
931+ // serve layer can stamp per-user activity markers (#4561).
932+ secrets_dir : config_paths. secrets_dir ( mode) ,
902933 } ,
903934 secrets,
904935 log_level : self . log_level . unwrap_or ( tracing:: log:: LevelFilter :: Info ) ,
@@ -915,6 +946,23 @@ impl ConfigArgs {
915946 per_user_secret_quota_bytes : self
916947 . per_user_secret_quota_bytes
917948 . unwrap_or ( crate :: wasm_runtime:: DEFAULT_PER_USER_SECRET_QUOTA_BYTES as u64 ) ,
949+ per_user_inactive_ttl_secs : self
950+ . per_user_inactive_ttl_secs
951+ . unwrap_or ( default_per_user_inactive_ttl_secs ( ) ) ,
952+ inactive_user_sweep_interval_secs : {
953+ // `0` means "use the default" (an interval of 0 is meaningless —
954+ // the sweep would otherwise floor it to 1s and hammer the disk).
955+ // Remap here so the resolved value always reflects the documented
956+ // semantics, rather than relying on a downstream `.max(1)`.
957+ let v = self
958+ . inactive_user_sweep_interval_secs
959+ . unwrap_or ( default_inactive_user_sweep_interval_secs ( ) ) ;
960+ if v == 0 {
961+ default_inactive_user_sweep_interval_secs ( )
962+ } else {
963+ v
964+ }
965+ } ,
918966 module_cache_budget_bytes : self
919967 . module_cache_budget_bytes
920968 . unwrap_or_else ( crate :: wasm_runtime:: default_module_cache_budget_bytes) ,
@@ -1054,6 +1102,23 @@ pub struct Config {
10541102 rename = "per-user-secret-quota"
10551103 ) ]
10561104 pub per_user_secret_quota_bytes : u64 ,
1105+ /// Inactivity TTL in seconds after which a HOSTED user's entire per-user
1106+ /// data is reclaimed by a background sweep (#4561, P5 of #4381). Durable,
1107+ /// real-calendar time (survives restarts). Default 2_592_000 (30 days);
1108+ /// `0` disables the sweep. No effect outside hosted mode — Local
1109+ /// single-user data is never enumerated or reclaimed.
1110+ #[ serde(
1111+ default = "default_per_user_inactive_ttl_secs" ,
1112+ rename = "per-user-inactive-ttl"
1113+ ) ]
1114+ pub per_user_inactive_ttl_secs : u64 ,
1115+ /// How often (seconds) the inactive-user reclaim sweep runs (#4561). Only
1116+ /// relevant in hosted mode with a non-zero TTL. Default 3_600 (hourly).
1117+ #[ serde(
1118+ default = "default_inactive_user_sweep_interval_secs" ,
1119+ rename = "inactive-user-sweep-interval"
1120+ ) ]
1121+ pub inactive_user_sweep_interval_secs : u64 ,
10571122 /// Byte budget for the compiled-WASM **contract** module cache. The
10581123 /// delegate cache gets a fraction of this
10591124 /// (`DELEGATE_MODULE_CACHE_BUDGET_DIVISOR`), so the combined ceiling is
@@ -1124,6 +1189,21 @@ fn default_per_user_secret_quota_bytes() -> u64 {
11241189 crate :: wasm_runtime:: DEFAULT_PER_USER_SECRET_QUOTA_BYTES as u64
11251190}
11261191
1192+ /// Default inactive-user TTL (30 days). Resolves to
1193+ /// [`crate::wasm_runtime::DEFAULT_PER_USER_INACTIVE_TTL_SECS`], the single
1194+ /// source of truth, so the operator-facing default and the sweep's fallback
1195+ /// never drift.
1196+ const fn default_per_user_inactive_ttl_secs ( ) -> u64 {
1197+ crate :: wasm_runtime:: DEFAULT_PER_USER_INACTIVE_TTL_SECS
1198+ }
1199+
1200+ /// Default inactive-user sweep interval (1 hour). Far finer than the 30-day
1201+ /// TTL, so reclamation lag is negligible while the periodic disk walk stays
1202+ /// cheap.
1203+ const fn default_inactive_user_sweep_interval_secs ( ) -> u64 {
1204+ 3_600
1205+ }
1206+
11271207/// Default contract-module cache byte budget, scaled to system RAM
11281208/// (`clamp(total_ram / 8, 64 MiB, 1.5 GiB)`).
11291209///
@@ -2022,6 +2102,21 @@ pub struct WebsocketApiConfig {
20222102 rename = "per-user-export-min-interval-secs"
20232103 ) ]
20242104 pub per_user_export_min_interval_secs : u64 ,
2105+
2106+ /// Resolved secrets directory for this node. RUNTIME-ONLY, NOT persisted
2107+ /// (`#[serde(skip)]`, like `TelemetryConfig::is_test_environment`): it is
2108+ /// derived from the full `Config` in `build()` (`config.secrets_dir()`), so
2109+ /// serializing/round-tripping a `WebsocketApiConfig` standalone leaves it
2110+ /// empty and `build()` repopulates it.
2111+ ///
2112+ /// The WS serve layer injects it as an `Extension` so the per-user
2113+ /// last-activity marker (#4561, P5 of #4381, inactive-user TTL) can be
2114+ /// stamped at the same `<base>/users/<user_id>/.last_seen` location the
2115+ /// reclaim sweep reads. Empty (the default on the standalone test paths)
2116+ /// disables stamping, which is correct for non-hosted/test composition that
2117+ /// has no secrets tree to mark.
2118+ #[ serde( skip) ]
2119+ pub secrets_dir : std:: path:: PathBuf ,
20252120}
20262121
20272122#[ inline]
@@ -2067,6 +2162,7 @@ impl From<SocketAddr> for WebsocketApiConfig {
20672162 per_user_op_rate_limit : default_per_user_op_rate_limit ( ) ,
20682163 per_user_op_burst : default_per_user_op_burst ( ) ,
20692164 per_user_export_min_interval_secs : default_per_user_export_min_interval_secs ( ) ,
2165+ secrets_dir : std:: path:: PathBuf :: new ( ) ,
20702166 }
20712167 }
20722168}
@@ -2085,6 +2181,7 @@ impl Default for WebsocketApiConfig {
20852181 per_user_op_rate_limit : default_per_user_op_rate_limit ( ) ,
20862182 per_user_op_burst : default_per_user_op_burst ( ) ,
20872183 per_user_export_min_interval_secs : default_per_user_export_min_interval_secs ( ) ,
2184+ secrets_dir : std:: path:: PathBuf :: new ( ) ,
20882185 }
20892186 }
20902187}
@@ -3986,6 +4083,8 @@ mod tests {
39864083 max_blocking_threads : None ,
39874084 max_hosting_storage : None ,
39884085 per_user_secret_quota_bytes : None ,
4086+ per_user_inactive_ttl_secs : None ,
4087+ inactive_user_sweep_interval_secs : None ,
39894088 module_cache_budget_bytes : None ,
39904089 shutdown_drain_secs : None ,
39914090 telemetry : Default :: default ( ) ,
@@ -4044,6 +4143,9 @@ mod tests {
40444143 per_user_op_rate_limit : 33 ,
40454144 per_user_op_burst : 77 ,
40464145 per_user_export_min_interval_secs : 17 ,
4146+ // serde-skip runtime field; repopulated by build() and not
4147+ // asserted in the round-trip (bound to `_` in the destructure).
4148+ secrets_dir : std:: path:: PathBuf :: new ( ) ,
40474149 } ,
40484150 secrets : base. secrets . clone ( ) ,
40494151 log_level : tracing:: log:: LevelFilter :: Debug ,
@@ -4055,6 +4157,8 @@ mod tests {
40554157 max_blocking_threads : 7 ,
40564158 max_hosting_storage : 123_456_789 ,
40574159 per_user_secret_quota_bytes : 7_654_321 ,
4160+ per_user_inactive_ttl_secs : 1_234_567 ,
4161+ inactive_user_sweep_interval_secs : 7_200 ,
40584162 module_cache_budget_bytes : 987_654_321 ,
40594163 telemetry : TelemetryConfig {
40604164 enabled : false ,
@@ -4090,6 +4194,8 @@ mod tests {
40904194 max_blocking_threads,
40914195 max_hosting_storage,
40924196 per_user_secret_quota_bytes,
4197+ per_user_inactive_ttl_secs,
4198+ inactive_user_sweep_interval_secs,
40934199 module_cache_budget_bytes,
40944200 telemetry,
40954201 shutdown_drain_secs,
@@ -4111,6 +4217,14 @@ mod tests {
41114217 per_user_secret_quota_bytes, seed. per_user_secret_quota_bytes,
41124218 "per_user_secret_quota_bytes"
41134219 ) ;
4220+ assert_eq ! (
4221+ per_user_inactive_ttl_secs, seed. per_user_inactive_ttl_secs,
4222+ "per_user_inactive_ttl_secs"
4223+ ) ;
4224+ assert_eq ! (
4225+ inactive_user_sweep_interval_secs, seed. inactive_user_sweep_interval_secs,
4226+ "inactive_user_sweep_interval_secs"
4227+ ) ;
41144228 assert_eq ! (
41154229 module_cache_budget_bytes, seed. module_cache_budget_bytes,
41164230 "module_cache_budget_bytes"
@@ -4216,6 +4330,7 @@ mod tests {
42164330 per_user_op_rate_limit,
42174331 per_user_op_burst,
42184332 per_user_export_min_interval_secs,
4333+ secrets_dir : _, // serde-skip runtime field, repopulated by build()
42194334 } = ws_api;
42204335 assert_eq ! ( ws_address, seed. ws_api. address, "ws_api.address" ) ;
42214336 assert_eq ! ( ws_port, seed. ws_api. port, "ws_api.port" ) ;
0 commit comments