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
//! The op instant `T` — one clock read per op, three consumers (§8, bl-8b98).
//!
//! An op reads the wall clock exactly ONCE, here, and every timestamp it authors
//! derives from that single instant `T`: the frontmatter `created`/`updated` ints
//! ([`crate::mutate`]), core's own seal-commit date ([`crate::git`] sets
//! `GIT_AUTHOR_DATE`/`GIT_COMMITTER_DATE`), and the delivery squash
//! ([`crate::plugin`] exports the same dates into every plugin's spawn env, so the
//! plugin's `commit-tree` inherits them). Before this the three agreed only by
//! luck — three independent reads landing in the same second — a
//! single-source-of-truth violation this dissolves.
//!
//! `T` resolves down a fail-open ladder:
//!
//! ```text
//! clock_provider (a conf-set LOCAL value → a resolved bin; the product seam) >
//! BALLS_CLOCK (an i64 env, the edge TEST seam — deterministic core tests) >
//! the system clock ([`crate::log::wall`]; the default — today's behaviour)
//! ```
//!
//! The provider is a DIRECTLY-SET local value (bl-cfe3): an absolute path, or a
//! PATH-resolved name, living in the per-machine LOCAL-TRUST layer (the per-clone
//! `binding.toml` / XDG, [`crate::config::clock_provider`]) and set by `bl conf`
//! — NOT a landing-config name bound via `install`. The clock is box-local (§1),
//! cosmetic, and fail-open, so it needs none of the shared-schedule/RCE-consent
//! machinery of the hook `bin/<name>` indirection, and it NEVER travels on
//! `install` (§4).
//!
//! The provider is the ONLY rung that can fail, and it is NON-FATAL: a value that
//! resolves to no binary, a non-zero exit, or unparseable output logs a note and
//! falls to the next rung. This is the deliberate asymmetry with hook dispatch,
//! where a dangling bin ABORTS (§6): a hook is load-bearing, the op clock is
//! cosmetic with a sane default, so it degrades instead of blocking. With NOTHING
//! set the ladder bottoms at the system clock — byte-identical to pre-bl-8b98
//! behaviour.
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use crate::config;
use crate::edge::Edge;
use crate::log::wall;
/// The op instant plus an optional fail-open note. `note` is `Some` when a
/// configured provider could not be honoured; the seal path emits it through the
/// op [`crate::log::Log`] (never a bare `eprintln` — it must be threshold-gated
/// and persisted like every other op record, bl-bfcc).
pub struct Instant {
/// Unix seconds — the §3 convention, stamped into frontmatter and every
/// commit the op authors.
pub t: i64,
/// A fail-open diagnostic to log, or `None` when the ladder resolved cleanly.
pub note: Option<String>,
}
/// Resolve the op instant for `edge` — read this checkout's LOCAL-TRUST
/// `clock_provider` (the per-clone `binding.toml` / XDG,
/// [`config::clock_provider`]) and run the fail-open [`resolve`] ladder,
/// resolving a named value against this box's binaries with [`locate`]. The
/// impure wrapper: it does the config + filesystem reads, while [`resolve`]
/// stays pure over its inputs (the provider string + a resolver closure) so the
/// ladder is unit-tested exhaustively.
pub fn for_op(edge: &Edge) -> io::Result<Instant> {
let clone = edge.xdg.clone_dir(&edge.invocation_path);
let provider = config::clock_provider(&clone.binding(), &edge.xdg.user_config());
Ok(resolve(provider.as_deref(), |name| locate(name, edge), edge.balls_clock, wall))
}
/// Resolve a `clock_provider` value to THIS box's binary (bl-cfe3): an absolute
/// path is used verbatim (when it is a file); any other value is a name resolved
/// beside `bl` first (the [`crate::seed`] sibling rule), then on `$PATH`
/// (`edge.path_dirs`) — the SAME "this machine" lookup a plugin binary gets, minus
/// the `bin/<name>` symlink, because the clock never travels on `install`. No hit
/// ⇒ `None`, and [`resolve`] falls open.
fn locate(value: &str, edge: &Edge) -> Option<PathBuf> {
let p = Path::new(value);
if p.is_absolute() {
return p.is_file().then(|| p.to_path_buf());
}
edge.exe_dir.iter().chain(edge.path_dirs.iter()).map(|d| d.join(value)).find(|p| p.is_file())
}
/// The fail-open ladder, pure over its inputs (`system` injected as a `fn`
/// pointer like [`crate::log::Log`]'s clock, so tests are deterministic and this
/// does no hidden time read; `locate` injected so the filesystem lookup is a
/// closure the tests stub). A `provider` value resolving to a bin that prints one
/// parseable unix-seconds line wins; ANY provider failure (unresolvable, non-zero
/// exit, unparseable) falls through carrying a note; then the `balls_clock` test
/// seam; then the `system` clock.
#[must_use]
pub fn resolve(provider: Option<&str>, locate: impl Fn(&str) -> Option<PathBuf>, balls_clock: Option<i64>, system: fn() -> i64) -> Instant {
let fallback = |note| Instant { t: balls_clock.unwrap_or_else(system), note };
let Some(value) = provider else {
return fallback(None);
};
let Some(bin) = locate(value) else {
return fallback(Some(format!(
"clock_provider {value} not found (not an absolute path, not beside bl or on PATH) — using the system clock"
)));
};
match probe(&bin) {
Ok(t) => Instant { t, note: None },
Err(e) => fallback(Some(format!("clock_provider {value}: {e} — using the system clock"))),
}
}
/// Run a provider bin: one unix-seconds `i64` line on stdout, exit 0. Anything
/// else (non-zero exit, empty/non-integer output) is an error the caller turns
/// into a fail-open note. `retry_busy` absorbs the ETXTBSY a freshly-written bin
/// can throw under parallel test spawns (bl-6cd9).
fn probe(bin: &Path) -> io::Result<i64> {
let child = crate::plugin::retry_busy(|| {
Command::new(bin).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()
})?;
let out = child.wait_with_output()?;
if !out.status.success() {
return Err(io::Error::other(format!("provider exited {}", out.status)));
}
let text = String::from_utf8_lossy(&out.stdout);
let line = text.lines().next().unwrap_or_default().trim();
line.parse::<i64>().map_err(|_| io::Error::other(format!("provider printed non-integer {line:?}")))
}
/// The `GIT_AUTHOR_DATE`/`GIT_COMMITTER_DATE` pair pinning a git commit to the op
/// instant `t`. `@<unix>` (no offset) fixes the INSTANT while letting git supply
/// the LOCAL timezone offset for display — byte-identical to how git stamps an
/// un-dated commit today, only the instant is now `t` instead of a fresh read.
/// The single home of this format, shared by core's seal ([`crate::git`]) and the
/// plugin spawn env ([`crate::plugin`]), so the store commit and the delivery
/// squash carry the SAME date.
#[must_use]
pub fn git_date_env(t: i64) -> [(&'static str, String); 2] {
let date = format!("@{t}");
[("GIT_AUTHOR_DATE", date.clone()), ("GIT_COMMITTER_DATE", date)]
}
#[cfg(test)]
#[path = "clock_tests.rs"]
mod tests;