openlatch-client 0.5.2

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
//! Where install remembers the endpoint an agent was pointed at *before* we
//! wired it to the model relay — so uninstall can put it back.
//!
//! Until this existed the wiring was a one-way door on both conventions: the
//! writer overwrote `ANTHROPIC_BASE_URL` unconditionally and the remover only
//! knew how to delete the key, so a customer whose Claude Code pointed at a
//! corporate gateway lost it on install and never got it back. The headers on
//! the same path were always additive in both directions; only the base URL
//! was careless. This makes the two consistent, and gives Codex's
//! `model_provider` the same guarantee from the start.
//!
//! # Why a sibling file, and not a field on `StateEntry`
//!
//! [`crate::core::hook_state::StateEntry`] carries a `hook_event` and is
//! upserted **once per hook event** — twelve rows per agent. A prior endpoint
//! is a per-**agent** fact, so an additive field there would write it twelve
//! times with no defined tie-break, and the re-install rule below would then
//! depend on which of the twelve rows happened to be read.
//!
//! # The re-install rule, which the caller owns
//!
//! Recording unconditionally is wrong. On a re-install the value on disk is
//! already ours, so a second install would overwrite the recorded prior with
//! our own value — and uninstall would then "restore" a pointer at a provider
//! table it has just deleted. Callers therefore record **only** when the
//! current value is not already ours; see `hooks::write_model_relay_config`.

use std::collections::BTreeMap;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::error::{OlError, ERR_STATE_FILE_CORRUPT, ERR_STATE_FILE_WRITE_FAILED};

/// One agent's record.
///
/// A struct rather than a bare `Option<String>` on the wire so the file can
/// grow a second per-agent fact without a format break. `prior: null` is a
/// meaningful value — *the agent named no endpoint before we wired it* — and
/// is not the same as having no entry at all.
///
/// That growth happened once: a provider slot served by its own relay
/// endpoint carries an [`EndpointRecord`] instead. Both fields default, so a
/// file written before endpoints existed still parses, and a build that
/// predates them ignores the field it does not know.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct Entry {
    /// The endpoint the agent named before OpenLatch wired it, or `null` when
    /// it named none.
    #[serde(default)]
    prior: Option<String>,
    /// The slot's endpoint, for an entry keyed by provider slot.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    endpoint: Option<EndpointRecord>,
}

/// A value as it sat in an agent's file, in the representation it had there.
///
/// Three shapes because Cline stores "no URL" three ways: the key absent, the
/// key `null`, and — in the legacy bundle's settings UI — the key `""`. They
/// mean the same thing to the agent and they are NOT the same bytes, so a
/// restore that normalised them would leave the file different from the one we
/// found.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum SlotValue {
    /// The key was not there.
    Absent,
    /// The key held `null`.
    Null,
    /// The key held a string, possibly empty.
    Text(String),
}

impl SlotValue {
    /// Whether the agent reads this as "no URL configured": absent, `null`, or
    /// blank text.
    pub fn is_unset(&self) -> bool {
        match self {
            Self::Absent | Self::Null => true,
            Self::Text(t) => t.trim().is_empty(),
        }
    }

    /// The text, when there is a non-blank one.
    pub fn text(&self) -> Option<&str> {
        match self {
            Self::Text(t) if !t.trim().is_empty() => Some(t.as_str()),
            _ => None,
        }
    }
}

/// Where a provider slot's wiring stands.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SlotState {
    /// Recorded, about to be (or being) written. The record exists BEFORE the
    /// value commits, so a crash in between leaves a record naming a port the
    /// file may or may not hold — never a relay URL with no record.
    Pending,
    /// The file holds `last_written`.
    Wired,
    /// Handed back. A tombstone, kept so a port is not re-issued to another
    /// origin while an editor may still dial it, and so a teardown cannot be
    /// undone by a wiring pass that races it.
    Released,
}

/// Why a slot was handed back.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReleasedBy {
    /// `uninstall`, or the relay switched off. Not re-wired by a daemon that
    /// was already running when it happened.
    Teardown,
    /// The wiring pass itself: the slot stopped being safe or stopped existing
    /// (a Vertex selection sharing the key, a deleted provider entry). Re-wired
    /// as soon as that changes.
    Wiring,
}

/// What proved the agent uses a slot's endpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Proof {
    /// A request arrived on the endpoint.
    Traffic,
    /// The editor saved a file still carrying our value — it can only do that
    /// once it has loaded it. Proves the editor HOLDS the URL, not that its
    /// requests follow it (an organisation's remote configuration can shadow a
    /// loaded value in memory).
    EditorSave,
}

/// What OpenLatch knows about one provider slot it wired to a relay endpoint.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EndpointRecord {
    /// The endpoint port, from the relay's block.
    pub port: u16,
    /// The origin the endpoint forwards to (scheme, host, port).
    pub origin: String,
    /// What the slot held before OpenLatch first wrote it, in its original
    /// representation. What uninstall puts back.
    pub prior: SlotValue,
    /// The exact value OpenLatch last wrote, so the next pass can tell our own
    /// write from an editor's revert or a developer's change.
    pub last_written: Option<String>,
    /// The file the slot lives in.
    pub file: std::path::PathBuf,
    /// Where the wiring stands.
    pub state: SlotState,
    /// Why it was released, when it was.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub released_by: Option<ReleasedBy>,
    /// Unix seconds of the last write or release.
    pub changed_at: u64,
    /// Unix seconds of the first proof the agent dials the endpoint.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub proven_at: Option<u64>,
    /// What that proof was. A traffic proof replaces an editor-save one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub proven_by: Option<Proof>,
    /// The wire format the slot's provider speaks, when known
    /// ([`crate::model_relay::wire_format::WireFormat::as_str`]).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub family: Option<String>,
    /// The `relay_wiring_pending` event emitted when the slot was written, until
    /// the editor is proven to use it — kept here so the heal links to it even
    /// when a different daemon process sees the proof.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub pending_event: Option<String>,
    /// The `relay_wiring_misconfigured` event, while the editor holds our URL
    /// and uses the provider but no request reaches the endpoint. Cleared, with
    /// a heal, by the first request.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub misconfigured_event: Option<String>,
}

impl EndpointRecord {
    /// Pending or wired: the slot names, or is about to name, our endpoint.
    pub fn is_live(&self) -> bool {
        matches!(self.state, SlotState::Pending | SlotState::Wired)
    }

    /// Whether the endpoint's most recent request (unix seconds) came after the
    /// slot was last written. One from before a revert was re-applied says
    /// nothing: the editor that saved the old value holds it until it restarts.
    pub fn served_since_written(&self, last_request_unix: Option<u64>) -> bool {
        last_request_unix.is_some_and(|at| at >= self.changed_at)
    }
}

/// The whole file: agent wire type → entry.
type Records = BTreeMap<String, Entry>;

/// `$OPENLATCH_DIR/model-relay-endpoints.json`.
///
/// Through [`crate::config::openlatch_dir`], **never a literal `~/.openlatch`**:
/// that function is the seam every isolated instance depends on, and hardcoding
/// the home path would make a sandboxed run rewrite the developer's real
/// record.
fn record_path() -> PathBuf {
    crate::config::openlatch_dir().join("model-relay-endpoints.json")
}

/// Read the file, distinguishing "there is none" from "it could not be read".
///
/// `Ok(None)` means the file is absent, which is the ordinary first-install
/// state. `Err` means it exists and could not be read or parsed, and the two
/// callers want opposite things from that:
///
/// * A TEARDOWN must not be blocked by it. The worst case there is that a prior
///   endpoint goes unrestored, and refusing to unwire would leave the agent
///   pointed at a listener that is going away — so `take` degrades to "no
///   record" and says so in a warning.
/// * A WRITE must not proceed over it. `record` used to inherit the same
///   degradation, which meant one unreadable byte turned into "the journal is
///   empty" and the next `store` overwrote the OTHER agent's restoration
///   record with a single entry. Losing an unrelated agent's prior endpoint to
///   our own parse failure is not a degradation, it is data loss.
fn load() -> Result<Option<Records>, OlError> {
    let raw = match std::fs::read_to_string(record_path()) {
        Ok(raw) => raw,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(e) => {
            return Err(OlError::new(
                ERR_STATE_FILE_CORRUPT,
                format!("cannot read {}: {e}", record_path().display()),
            ))
        }
    };
    serde_json::from_str(&raw).map(Some).map_err(|e| {
        OlError::new(
            ERR_STATE_FILE_CORRUPT,
            format!("{} is not valid JSON: {e}", record_path().display()),
        )
    })
}

/// Write the file back, owner-only.
///
/// Through the shared [`crate::fs_secure::restrict_to_owner`] helper rather
/// than a `cfg` branch of its own — the record names a customer's internal
/// gateway host, which is not something to leave world-readable under an
/// `OPENLATCH_DIR` that points somewhere with a permissive ACL.
///
/// Callers go through [`mutate`], which holds the lock around the whole
/// read-modify-write.
fn store(records: &Records) -> Result<(), OlError> {
    let path = record_path();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|e| {
            OlError::new(
                ERR_STATE_FILE_WRITE_FAILED,
                format!("Cannot create the OpenLatch directory: {e}"),
            )
        })?;
    }
    let content = serde_json::to_string_pretty(records).map_err(|e| {
        OlError::new(
            ERR_STATE_FILE_WRITE_FAILED,
            format!("Cannot serialize the model relay endpoint record: {e}"),
        )
    })?;
    crate::fs_secure::write_preserving_mode(&path, content.as_bytes()).map_err(|e| {
        OlError::new(
            ERR_STATE_FILE_WRITE_FAILED,
            format!("Cannot write the model relay endpoint record: {e}"),
        )
    })?;
    let _ = crate::fs_secure::restrict_to_owner(&path);
    Ok(())
}

/// How old a record lock must be before it is taken to belong to a process
/// that died holding it. The sections it guards take milliseconds.
const LOCK_STALE_AFTER: std::time::Duration = std::time::Duration::from_secs(30);

/// Read-modify-write the whole file under its lock.
///
/// Two writers exist at once in practice — a running daemon's wiring pass and
/// the CLI's `init` or `uninstall` — and each rewrites the whole map, so an
/// unlocked pair interleaving loses one side's entry.
///
/// A read failure PROPAGATES: see [`load`] for why writing over an unreadable
/// file would destroy another agent's record.
fn mutate<T>(f: impl FnOnce(&mut Records) -> T) -> Result<T, OlError> {
    let path = record_path();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|e| {
            OlError::new(
                ERR_STATE_FILE_WRITE_FAILED,
                format!("Cannot create the OpenLatch directory: {e}"),
            )
        })?;
    }
    let lock = path.with_extension("json.lock");
    crate::fs_secure::with_lockfile(&lock, LOCK_STALE_AFTER, || {
        let mut records = load()?.unwrap_or_default();
        let before = records.clone();
        let out = f(&mut records);
        if !same_records(&before, &records) {
            store(&records)?;
        }
        Ok(out)
    })
    .map_err(|e| {
        OlError::new(
            ERR_STATE_FILE_WRITE_FAILED,
            format!("Cannot lock the model relay endpoint record: {e}"),
        )
    })?
}

/// Whether two maps would serialise identically, so an unchanged map is not
/// rewritten.
fn same_records(a: &Records, b: &Records) -> bool {
    serde_json::to_string(a).ok() == serde_json::to_string(b).ok()
}

/// Every endpoint record whose key starts with `prefix`, in key order.
///
/// A read failure propagates. The wiring pass must not act on an empty view of
/// a file it could not read: that view has no records, so every slot would look
/// unwired and every port free.
pub fn endpoint_records(prefix: &str) -> Result<Vec<(String, EndpointRecord)>, OlError> {
    Ok(load()?
        .unwrap_or_default()
        .into_iter()
        .filter(|(key, _)| key.starts_with(prefix))
        .filter_map(|(key, entry)| entry.endpoint.map(|rec| (key, rec)))
        .collect())
}

/// Every entry under `prefix` that carries a v1 prior and no endpoint record:
/// `(key, prior)`.
///
/// For reclaiming what a retired wiring convention recorded. A read failure
/// propagates, for the reason [`endpoint_records`] gives.
pub fn prior_records(prefix: &str) -> Result<Vec<(String, Option<String>)>, OlError> {
    Ok(load()?
        .unwrap_or_default()
        .into_iter()
        .filter(|(key, entry)| key.starts_with(prefix) && entry.endpoint.is_none())
        .map(|(key, entry)| (key, entry.prior))
        .collect())
}

/// Write `key`'s endpoint record, replacing any previous one.
pub fn put_endpoint(key: &str, record: EndpointRecord) -> Result<(), OlError> {
    mutate(|records| {
        records.insert(
            key.to_string(),
            Entry {
                prior: None,
                endpoint: Some(record),
            },
        );
    })
}

/// Change `key`'s endpoint record in place, returning the result, or `None`
/// when there is no such record.
pub fn update_endpoint(
    key: &str,
    change: impl FnOnce(&mut EndpointRecord),
) -> Result<Option<EndpointRecord>, OlError> {
    mutate(|records| {
        let rec = records.get_mut(key)?.endpoint.as_mut()?;
        change(rec);
        Some(rec.clone())
    })
}

/// The port `key` should serve on, from `block`.
///
/// **Sticky.** A slot keeps the port it has, because a running editor keeps the
/// URL it read at start: moving the slot would strand that editor on a port
/// nobody serves. After that, a port no record has ever named. A port that only
/// a RELEASED record names is handed out last — an editor started before the
/// release may still dial it, and a port reused for another provider would send
/// that editor's traffic, and its credential, to the wrong origin.
///
/// `None` when every port in the block is held by a live record.
pub fn allocate_port(
    key: &str,
    block: std::ops::RangeInclusive<u16>,
    records: &[(String, EndpointRecord)],
) -> Option<u16> {
    if let Some((_, own)) = records.iter().find(|(k, _)| k == key) {
        if block.contains(&own.port) {
            return Some(own.port);
        }
    }
    let live = |port: u16| {
        records
            .iter()
            .any(|(k, r)| k != key && r.port == port && r.is_live())
    };
    let named = |port: u16| records.iter().any(|(k, r)| k != key && r.port == port);
    if let Some(port) = block.clone().find(|p| !named(*p)) {
        return Some(port);
    }
    // Exhausted: the least recently released tombstone.
    block.filter(|p| !live(*p)).min_by_key(|p| {
        records
            .iter()
            .filter(|(_, r)| r.port == *p)
            .map(|(_, r)| r.changed_at)
            .max()
            .unwrap_or(0)
    })
}

/// Record what `agent` pointed at before we wired it.
///
/// `prior` is `None` when the agent named no endpoint at all — a value worth
/// storing, because uninstall then knows to *remove* the key rather than leave
/// ours behind.
///
/// **Only ever called when the value on disk is not already ours.** See the
/// module doc: recording on a re-install destroys the real prior.
pub fn record(agent: &str, prior: Option<String>) -> Result<(), OlError> {
    // PROPAGATE a read failure rather than degrading to an empty journal. The
    // degradation is right for `take` and wrong here: `store` below writes the
    // whole map back, so treating an unreadable file as empty would replace
    // the OTHER agent's restoration record with a single entry. Losing an
    // unrelated agent's prior endpoint to our own parse failure is data loss,
    // not a graceful degradation.
    mutate(|records| {
        records.insert(
            agent.to_string(),
            Entry {
                prior,
                endpoint: None,
            },
        );
    })
}

/// Read `agent`'s record WITHOUT removing it.
///
/// The consuming [`take`] cannot be used before a rewrite that might fail: it
/// deletes the entry first, so a failed rename would leave the agent pointed
/// at us with its real prior endpoint gone for good. Callers peek, rewrite,
/// and only then [`forget`].
pub fn peek(agent: &str) -> Option<Option<String>> {
    match load() {
        Ok(Some(records)) => records.get(agent).map(|e| e.prior.clone()),
        Ok(None) => None,
        Err(e) => {
            tracing::warn!(
                agent,
                code = %e.code,
                error = %e.message,
                "model_relay endpoint record unreadable — treating as no prior rather than \
                 blocking the teardown"
            );
            None
        }
    }
}

/// Drop `agent`'s record, after the rewrite that consumed it succeeded.
///
/// Best effort by design: the customer's file is already correct by the time
/// this runs, and failing the teardown over the bookkeeping would be the tail
/// wagging the dog. A surviving record is harmless — the next uninstall finds
/// the pointer is no longer ours and leaves it alone.
pub fn forget(agent: &str) {
    if let Err(e) = mutate(|records| {
        records.remove(agent);
    }) {
        tracing::warn!(agent, error = %e.message, "could not clear the record");
    }
}

/// Take `agent`'s record, removing it.
///
/// Three outcomes, and collapsing any two of them loses something:
///
/// - `Some(Some(v))` — restore `v`.
/// - `Some(None)` — the agent named no endpoint before us; remove ours.
/// - `None` — no record at all. Nothing to restore.
///
/// Removing the entry is what makes uninstall **idempotent**: it runs two or
/// three times per `openlatch uninstall` (the command itself, `run_stop`'s
/// net, and the daemon's own teardown), and a record that survived the first
/// call would have the second one restore a pointer that is already back.
///
/// The file itself is left in place even when it empties out.
pub fn take(agent: &str) -> Option<Option<String>> {
    // Degrades on an unreadable journal, deliberately and loudly: a teardown
    // must not be blocked by bookkeeping. See `load`'s doc for why `record`
    // does the opposite.
    match load() {
        Ok(Some(_)) => {}
        Ok(None) => return None,
        Err(e) => {
            tracing::warn!(
                agent,
                code = %e.code,
                error = %e.message,
                "model relay endpoint record unreadable — no prior will be restored"
            );
            return None;
        }
    }
    match mutate(|records| records.remove(agent)) {
        Ok(entry) => entry.map(|e| e.prior),
        Err(e) => {
            // Best effort: the restore is the point, and a record that could
            // not be cleared is re-read on the next uninstall pass, where the
            // ownership guard has already stopped it. Without the lock the
            // value cannot be read consistently, so it is read without it.
            tracing::warn!(
                code = %e.code,
                error = %e.message,
                agent,
                "could not clear the recorded prior model endpoint"
            );
            load().ok().flatten()?.get(agent).map(|e| e.prior.clone())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Redirect `OPENLATCH_DIR` at a tempdir for the body of `f`, under **the**
    /// lock for that variable.
    fn with_openlatch_dir<T>(f: impl FnOnce() -> T) -> T {
        let _guard = crate::config::OPENLATCH_DIR_ENV_LOCK
            .lock()
            .unwrap_or_else(|e| e.into_inner());
        let tmp = tempfile::tempdir().expect("tempdir");
        let prev = std::env::var_os("OPENLATCH_DIR");
        std::env::set_var("OPENLATCH_DIR", tmp.path());
        let out = f();
        match prev {
            Some(v) => std::env::set_var("OPENLATCH_DIR", v),
            None => std::env::remove_var("OPENLATCH_DIR"),
        }
        out
    }

    /// The three outcomes are three outcomes. `Some(None)` says "there was no
    /// endpoint"; `None` says "we never looked" — and the remover acts on them
    /// differently.
    #[test]
    fn take_distinguishes_an_absent_record_from_a_recorded_absence() {
        with_openlatch_dir(|| {
            assert_eq!(take("claude-code"), None, "nothing recorded yet");

            record("claude-code", None).expect("record");
            assert_eq!(take("claude-code"), Some(None), "a recorded absence");
            assert_eq!(take("claude-code"), None, "take removes the entry");

            record("codex-cli", Some("corporate-gateway".into())).expect("record");
            assert_eq!(take("codex-cli"), Some(Some("corporate-gateway".into())));
            assert_eq!(take("codex-cli"), None, "and removes that one too");
        });
    }

    fn record_on(port: u16, state: SlotState, changed_at: u64) -> EndpointRecord {
        EndpointRecord {
            port,
            origin: "http://127.0.0.1:11434/".into(),
            prior: SlotValue::Absent,
            last_written: Some(format!("http://127.0.0.1:{port}")),
            file: std::path::PathBuf::from("/tmp/globalState.json"),
            state,
            released_by: None,
            changed_at,
            proven_at: None,
            family: None,
            pending_event: None,
            proven_by: None,
            misconfigured_event: None,
        }
    }

    /// A file written before endpoint records existed still parses, and its
    /// entries keep their meaning beside the new ones.
    #[test]
    fn records_v1_entries_still_parse() {
        with_openlatch_dir(|| {
            std::fs::write(
                record_path(),
                r#"{"claude-code":{"prior":"https://gw.example"},"codex-cli":{"prior":null}}"#,
            )
            .expect("seed v1");
            put_endpoint(
                "cline:gs:shared:ollamaBaseUrl",
                record_on(7601, SlotState::Wired, 1),
            )
            .expect("put");

            assert_eq!(peek("claude-code"), Some(Some("https://gw.example".into())));
            assert_eq!(peek("codex-cli"), Some(None));
            let endpoints = endpoint_records("cline:").expect("read");
            assert_eq!(endpoints.len(), 1);
            assert_eq!(endpoints[0].1.port, 7601);
            assert!(
                endpoint_records("claude").expect("read").is_empty(),
                "a v1 entry is not an endpoint"
            );
        });
    }

    #[test]
    fn slot_values_round_trip_in_their_original_representation() {
        for v in [
            SlotValue::Absent,
            SlotValue::Null,
            SlotValue::Text(String::new()),
            SlotValue::Text("https://gw.corp/anthropic".into()),
        ] {
            let json = serde_json::to_string(&v).expect("ser");
            assert_eq!(serde_json::from_str::<SlotValue>(&json).expect("de"), v);
        }
        assert!(SlotValue::Absent.is_unset() && SlotValue::Null.is_unset());
        assert!(SlotValue::Text("  ".into()).is_unset());
        assert!(!SlotValue::Text("http://x".into()).is_unset());
    }

    #[test]
    fn ports_are_sticky_and_never_reassigned_live() {
        let block = 7601..=7604;
        let records = vec![
            ("a".to_string(), record_on(7601, SlotState::Wired, 1)),
            ("b".to_string(), record_on(7602, SlotState::Pending, 1)),
        ];
        assert_eq!(allocate_port("a", block.clone(), &records), Some(7601));
        assert_eq!(allocate_port("b", block.clone(), &records), Some(7602));
        assert_eq!(allocate_port("c", block.clone(), &records), Some(7603));
        // A record whose port left the block (the relay port moved) is re-issued.
        let moved = vec![("a".to_string(), record_on(9000, SlotState::Wired, 1))];
        assert_eq!(allocate_port("a", block, &moved), Some(7601));
    }

    #[test]
    fn a_tombstoned_port_is_reused_only_when_the_block_is_exhausted() {
        let block = 7601..=7603;
        let mut records = vec![
            ("a".to_string(), record_on(7601, SlotState::Released, 5)),
            ("b".to_string(), record_on(7602, SlotState::Wired, 1)),
        ];
        assert_eq!(
            allocate_port("c", block.clone(), &records),
            Some(7603),
            "a never-named port first"
        );
        records.push(("c".to_string(), record_on(7603, SlotState::Wired, 1)));
        assert_eq!(
            allocate_port("d", block.clone(), &records),
            Some(7601),
            "then the released one"
        );
        records[0].1.state = SlotState::Wired;
        assert_eq!(allocate_port("d", block, &records), None, "all live: none");
    }

    /// Concurrent writers keep each other's entries.
    #[test]
    fn concurrent_record_writers_keep_both_entries() {
        with_openlatch_dir(|| {
            std::thread::scope(|scope| {
                for i in 0..8u16 {
                    scope.spawn(move || {
                        put_endpoint(
                            &format!("cline:pj:p{i}"),
                            record_on(7601 + i, SlotState::Wired, 1),
                        )
                        .expect("put");
                    });
                }
            });
            assert_eq!(endpoint_records("cline:pj:").expect("read").len(), 8);
        });
    }

    /// Two agents, two independent records: taking one must not disturb the
    /// other, which is the whole reason this is keyed per agent.
    #[test]
    fn records_are_per_agent() {
        with_openlatch_dir(|| {
            record("claude-code", Some("https://gw.example".into())).expect("record");
            record("codex-cli", Some("corporate-gateway".into())).expect("record");

            assert_eq!(take("claude-code"), Some(Some("https://gw.example".into())));
            assert_eq!(
                take("codex-cli"),
                Some(Some("corporate-gateway".into())),
                "the other agent's record must survive the first take"
            );
        });
    }
}