Skip to content

Commit d19676c

Browse files
author
Test User
committed
Merge remote-tracking branch 'gitea/main'
2 parents 7c76b1a + 449522f commit d19676c

8 files changed

Lines changed: 763 additions & 44 deletions

File tree

crates/terraphim_merge_coordinator/src/evaluator.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,9 @@ mod tests {
290290

291291
// Helper: matches as directory component (starts_with or contains "/.sessions/")
292292
let is_contaminated = |file: &str| -> bool {
293-
patterns.iter().any(|p| {
294-
file.starts_with(p)
295-
|| file.contains(&["/", p].concat())
296-
})
293+
patterns
294+
.iter()
295+
.any(|p| file.starts_with(p) || file.contains(&["/", p].concat()))
297296
};
298297

299298
// Positive matches — files inside contaminated directories

crates/terraphim_merge_coordinator/src/gitea.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ impl GiteaClient {
219219
}
220220
Ok(resp) => {
221221
let status = resp.status();
222-
let is_client_error = status.is_client_error() && status != reqwest::StatusCode::TOO_MANY_REQUESTS;
222+
let is_client_error = status.is_client_error()
223+
&& status != reqwest::StatusCode::TOO_MANY_REQUESTS;
223224
let body_text = resp.text().await.unwrap_or_default();
224225
last_err = Some(format!("status {status}: {body_text}"));
225226
if is_client_error {

crates/terraphim_rlm/src/mcp_tools.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,9 +1010,6 @@ impl RlmMcpService {
10101010
ErrorData::invalid_params("'skills' must be an array of skill entries", None)
10111011
})?;
10121012

1013-
// Explicit closure (not function pointer) to avoid stable-Rust
1014-
// inference quirks with `serde_json::from_value` (consumes its
1015-
// argument).
10161013
let skills: Vec<SkillEntry> = skills_array
10171014
.iter()
10181015
.cloned()

crates/terraphim_validation/src/testing/server_api/validation.rs

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,39 @@ pub fn validate_json_schema<T: DeserializeOwned>(
184184
serde_json::from_value(body_value).map_err(ValidationError::Json)
185185
}
186186

187+
/// Assert that two JSON values are equal (ignoring ordering)
188+
pub fn assert_json_equal<T: serde::Serialize + serde::de::DeserializeOwned>(
189+
actual: &T,
190+
expected: &T,
191+
) {
192+
let actual_json = serde_json::to_value(actual).unwrap();
193+
let expected_json = serde_json::to_value(expected).unwrap();
194+
195+
if actual_json != expected_json {
196+
panic!(
197+
"JSON mismatch:\nExpected: {}\nActual: {}",
198+
serde_json::to_string_pretty(&expected_json).unwrap(),
199+
serde_json::to_string_pretty(&actual_json).unwrap()
200+
);
201+
}
202+
}
203+
204+
/// Validate response headers
205+
pub fn validate_response_headers(response: &Response, expected_headers: &[(&str, &str)]) {
206+
for (key, expected_value) in expected_headers {
207+
let actual_value = response.headers().get(*key).and_then(|v| v.to_str().ok());
208+
209+
match actual_value {
210+
Some(value) if value == *expected_value => continue,
211+
Some(value) => panic!(
212+
"Header '{}' mismatch: expected '{}', got '{}'",
213+
key, expected_value, value
214+
),
215+
None => panic!("Missing expected header: {}", key),
216+
}
217+
}
218+
}
219+
187220
#[cfg(test)]
188221
mod tests {
189222
use super::*;
@@ -246,36 +279,3 @@ mod tests {
246279
);
247280
}
248281
}
249-
250-
/// Assert that two JSON values are equal (ignoring ordering)
251-
pub fn assert_json_equal<T: serde::Serialize + serde::de::DeserializeOwned>(
252-
actual: &T,
253-
expected: &T,
254-
) {
255-
let actual_json = serde_json::to_value(actual).unwrap();
256-
let expected_json = serde_json::to_value(expected).unwrap();
257-
258-
if actual_json != expected_json {
259-
panic!(
260-
"JSON mismatch:\nExpected: {}\nActual: {}",
261-
serde_json::to_string_pretty(&expected_json).unwrap(),
262-
serde_json::to_string_pretty(&actual_json).unwrap()
263-
);
264-
}
265-
}
266-
267-
/// Validate response headers
268-
pub fn validate_response_headers(response: &Response, expected_headers: &[(&str, &str)]) {
269-
for (key, expected_value) in expected_headers {
270-
let actual_value = response.headers().get(*key).and_then(|v| v.to_str().ok());
271-
272-
match actual_value {
273-
Some(value) if value == *expected_value => continue,
274-
Some(value) => panic!(
275-
"Header '{}' mismatch: expected '{}', got '{}'",
276-
key, expected_value, value
277-
),
278-
None => panic!("Missing expected header: {}", key),
279-
}
280-
}
281-
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Runbook: Fix Meta-Coordinator Health Check (blind under `no_new_privs`)
2+
3+
**Issue**: `terraphim/terraphim-ai#3005` (tracker id `4300`)
4+
**Theme-ID**: `adf-health-alert`
5+
**Severity**: P1 — a monitor that reports false-green while the system is red is worse
6+
than no monitor; it suppresses human attention.
7+
**Status**: Patch verified end-to-end on bigbox; awaiting operator apply (root required).
8+
9+
## Symptom
10+
11+
The ADF meta-coordinator health-check cron reports **0 stalls / 0 failures / 0 timeouts**
12+
in windows where the orchestrator is actually degraded. On 2026-06-29 it reported 0 stalls
13+
in the same 4h window where `#3004` documents **207 stalls, max 55s**.
14+
15+
## Root cause — verified by reproduction
16+
17+
The health-check task block invokes `sudo journalctl -u adf-orchestrator`. On this host
18+
the `alex` user (and the orchestrator runtime) runs under the **`no_new_privs`** flag, so
19+
`sudo` is refused:
20+
21+
```
22+
$ sudo -n true
23+
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
24+
```
25+
26+
Every `sudo journalctl ... | grep -c ... || true` therefore evaluates to `0`, and each
27+
guard treats `0` as healthy. Reproduced this session (2026-06-29 22:10 +02:00):
28+
29+
| Invocation | Lines in 4h window | `reconcile_tick SLOW` count |
30+
|------------|-------------------:|----------------------------:|
31+
| `sudo journalctl ...` (current/buggy) | **0** | **0** ← false green |
32+
| `journalctl -q ...` (fixed) | 19,079 | **164** ← true signal |
33+
34+
The `alex` user already has journal read access via ACL on `/var/log/journal`
35+
(group `systemd-journal`). **`sudo` is not only unnecessary here — it is actively harmful.**
36+
37+
## Affected file
38+
39+
The defect lives in the **deployed, unversioned** runtime config (NOT in this repo):
40+
41+
```
42+
/opt/ai-dark-factory/conf.d/terraphim.toml (root:alex 0640)
43+
```
44+
45+
Occurrences (as of 2026-06-23 mtime, still present 2026-06-29):
46+
47+
| Line | Block | Pattern |
48+
|------|-------|---------|
49+
| 68 | `### 1. Tick-Stall Detection` | `sudo journalctl -u adf-orchestrator ...` |
50+
| 76 | `### 2. Agent Failure Report` | `sudo journalctl -u adf-orchestrator ...` |
51+
| 86 | `### 2b. Agents Exceeding max_cpu_seconds` | `sudo journalctl -u adf-orchestrator ...` |
52+
| 832 | Mneme fleet-health synthesis | `sudo -n journalctl -u adf-orchestrator ...` |
53+
54+
> **Note**: the meta-coordinator agent commented on `#3005` at 10:05 claiming the fix was
55+
> "applied". That was **inaccurate** — the agent ran one cycle manually without `sudo` but
56+
> never edited the config. File mtime `2026-06-23 16:44` confirms the file is untouched.
57+
> Verify any future "fixed" claim with `stat -c '%y' /opt/ai-dark-factory/conf.d/terraphim.toml`.
58+
59+
## The patch (verified)
60+
61+
Two surgical changes:
62+
63+
### Change 1 — remove `sudo` from all `journalctl` invocations
64+
65+
```bash
66+
sudo cp /opt/ai-dark-factory/conf.d/terraphim.toml \
67+
/opt/ai-dark-factory/conf.d/terraphim.toml.bak-3005-$(date +%Y%m%d-%H%M%S)
68+
sudo sed -i \
69+
-e 's/sudo -n journalctl/journalctl -q/g' \
70+
-e 's/sudo journalctl/journalctl -q/g' \
71+
/opt/ai-dark-factory/conf.d/terraphim.toml
72+
```
73+
74+
`-q` suppresses journalctl's "not seeing messages from other users" hint cleanly
75+
(recommended in the issue's confirmation comment). No privilege escalation needed.
76+
77+
### Change 2 — add a fail-loud telemetry precondition
78+
79+
Insert immediately after the `export PATH=...` line (currently line 64), before
80+
`## ADF Meta-Coordinator: Monitoring and Health Checks`, so the script aborts loudly
81+
instead of emitting a false-green if telemetry ever breaks again:
82+
83+
```bash
84+
# Telemetry precondition: fail loud rather than report false-green.
85+
_TELEMETRY_LINES=$(journalctl -q -u adf-orchestrator --since '4 hours ago' 2>/dev/null | wc -l | tr -d ' ')
86+
if [ "${_TELEMETRY_LINES:-0}" -lt 50 ]; then
87+
echo "FATAL: journalctl returned ${_TELEMETRY_LINES} lines in 4h — telemetry broken, aborting (would produce false green)" >&2
88+
gtr create-issue --owner terraphim --repo terraphim-ai \
89+
--title "[ADF] Health check telemetry broken (${_TELEMETRY_LINES} lines in 4h)" \
90+
--body "journalctl returned ${_TELEMETRY_LINES} lines in a 4h window. The health check aborted to avoid a false-green report. Investigate journal access / ACL on /var/log/journal.
91+
Theme-ID: adf-health-alert" 2>/dev/null || true
92+
exit 1
93+
fi
94+
```
95+
96+
## Apply procedure (operator with root)
97+
98+
```bash
99+
# 1. Back up + patch (Change 1)
100+
sudo cp /opt/ai-dark-factory/conf.d/terraphim.toml \
101+
/opt/ai-dark-factory/conf.d/terraphim.toml.bak-3005-$(date +%Y%m%d-%H%M%S)
102+
sudo sed -i -e 's/sudo -n journalctl/journalctl -q/g' -e 's/sudo journalctl/journalctl -q/g' \
103+
/opt/ai-dark-factory/conf.d/terraphim.toml
104+
105+
# 2. Verify the patch took (expect 0 matches)
106+
grep -c 'sudo .*journalctl' /opt/ai-dark-factory/conf.d/terraphim.toml # must print 0
107+
108+
# 3. Apply Change 2 (fail-loud precondition) — operator edits the task block,
109+
# or re-deploys from a versioned template once #see-followup is implemented.
110+
111+
# 4. Reload the orchestrator so it re-reads conf.d
112+
sudo systemctl restart adf-orchestrator.service
113+
114+
# 5. Smoke-test the next health check cycle manually (no sudo):
115+
journalctl -q -u adf-orchestrator --since '4 hours ago' | grep -c 'reconcile_tick SLOW'
116+
```
117+
118+
## Verification (acceptance)
119+
120+
After the next 4h cron cycle, the health check MUST surface the real signal:
121+
122+
- [ ] `stat -c '%y' /opt/ai-dark-factory/conf.d/terraphim.toml` shows a fresh mtime.
123+
- [ ] `grep -c 'sudo .*journalctl' /opt/ai-dark-factory/conf.d/terraphim.toml` returns `0`.
124+
- [ ] The next cycle's `[ADF] Tick-stall detected: N in 4h` issue (if any) reports a
125+
non-zero `N` consistent with `#3004`-class telemetry (order of 10², not 0).
126+
- [ ] No new spurious all-green cycle appears while `#3004`/`#4299` remain open.
127+
128+
## Deeper defect (filed separately)
129+
130+
The deployed `/opt/ai-dark-factory/conf.d/*.toml` is **unversioned** — not in any git
131+
repo. That is the root cause that allowed this defect to persist silently for 6 days and
132+
that let the meta-coordinator's "applied" comment go unchecked. The version-controlled
133+
twins in `terraphim-agents/.../tests/fixtures/conf.d/` and `orchestrator.example.toml`
134+
diverge from production and do **not** contain this task block at all. Bringing
135+
`conf.d/` under version control (or generating it from a tracked template at deploy
136+
time) is the structural fix that prevents recurrence. Tracked as a follow-up issue.
137+
138+
## Why this runbook lives in `terraphim-ai` (not `terraphim-agents`)
139+
140+
The health check creates issues in **this** repo (`terraphim/terraphim-ai`) and references
141+
this repo's worktrees and binaries. Per AGENTS.md dual-remote protocol, this runbook is
142+
pushed to both `origin` (GitHub) and `gitea` mirrors, giving operators a reviewable,
143+
reproducible artefact that satisfies the spirit of Bigbox Rule #1 (git pull/push, not
144+
ad-hoc `scp`) even though the target file itself is not yet version-controlled.

0 commit comments

Comments
 (0)