Merge origin/main (bounded_detail hotfix) into idunn/reincarnation-c2

Conflict in begin_post_fencing_abort: kept c2's now_millis binding and
the hotfix's bounded_detail in place of truncate. The owner-wait note
written by wait_for_owner now goes through bounded_detail too; every
other c2/c2b/c2c detail writer already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-19 19:34:20 +02:00
commit e5298584a4
2 changed files with 191 additions and 14 deletions

View file

@ -0,0 +1,61 @@
# Deployment provenance instead of an approval gate: target
Status: target, Self, 2026-10-19 (campaign `idunn-deploy-provenance` in
Eureka's mind). It states the ends. The map will live in
`deploy-provenance-map.md`.
## Why
Operator, 2026-10-19:
> The rule about the deployment brake was instituted by a cautious agent, but
> it doesn't actually suit how GameCult operates. The whole point of Eureka is
> to give the Mind, Persona and Self agency over its own Body. It's requiring
> my approval for every deployment that violates our principles. Yes, we
> certainly want to know which agent approved the deployment and why (this is
> the authority provenance which we might want to add to the transaction), but
> that's not the same as gating every deploy on my approval.
On the same day a StreamPixels deploy (tx-b40c7240) waited on the brake
because only the root-held operator identity could release it. The agent
running the deploy could neither proceed nor record why it should.
`F:\Projects\CLAUDE.md` says a deployment brake *may* gate artifact, revision,
configuration, schema, unit or authority changes. It does not say that
operator approval is the gate.
## Invariants
1. **No deployment waits on a per-transaction operator approval.** An agent
acting for the Mind, Persona or Self may admit a deployment of a target it
is authorized for.
2. **Every transaction carries typed authority provenance.** That is: who
approved it (agent or instance, and faculty), under which session or
ruling, and why. It is written before Fencing, it is immutable, and
`idunn status` and history show it. It is part of the transaction
document, not a log line.
3. **A brake that survives is a stop, not a gate.** The operator or an
authorized agent can halt deployment of a target, and the halt is itself
provenance-bearing. The deployment brake and the continuity lifecycle brake
stay separate (`F:\Projects\CLAUDE.md` Infrastructure). Neither gates Idunn
itself, and the deployment brake never gates continuity.
4. **Existing invariants hold.** These are `idunn-reincarnation` target r2,
`route-continuity-target.md` 1-7 and `authority-map.md`.
## Scope
In:
- The deployment-brake release path in Idunn and `idunn-provision`.
- The provenance record on the transaction.
- `idunn status` and history projection of the provenance.
- The gamecult-ops runbooks that describe a per-transaction operator release
(`heimdall-discord-launch.md`, `idunn-host-raven.md`, the StreamPixels
runbook).
Out:
- `idunn-reincarnation`. This campaign is mapped after that campaign's cut 2
merges, and coordinates with it rather than bundling.
- Harness permission rules on the workstation, which are the operator's.
- Heimdall's onboarding failures.

View file

@ -1330,7 +1330,7 @@ impl TargetSupervision {
fn defer_continuity(&mut self, now: u64, reason: &str) {
self.continuity_deferred_until = Some(now.saturating_add(CONTINUITY_DEFERRAL_MILLIS));
self.continuity_deferral_reason = Some(reason.to_owned());
self.continuity_deferral_reason = Some(bounded_detail(reason));
}
fn validate(&self) -> Result<()> {
@ -2015,7 +2015,7 @@ impl DeploymentTransaction {
fn rejected(command: &DeploymentCommand, error: anyhow::Error, now: u64) -> Result<Self> {
let mut transaction = Self::new(command, command.selector.clone(), 0, None, now)?;
let detail = truncate(&format!("{error:#}"), 2048);
let detail = bounded_detail(&format!("{error:#}"));
transaction.enter_phase(DeploymentPhase::Complete, now);
transaction.last_error = Some(detail.clone());
transaction.pre_fencing_abort = Some(PreFencingAbort {
@ -3221,7 +3221,7 @@ fn cancel(store_path: &Path, command_id: &str, requested_by: &str) -> Result<()>
} else {
next.post_fencing_abort = Some(post_fencing_abort_intent(&next, &error));
}
next.last_error = Some(error);
next.last_error = Some(bounded_detail(&error));
next.updated_at_unix_millis = now_millis()?;
replace_transaction(store_path, current, &next)?;
println!("{command_id} cancellation requested; Idunn is performing exact candidate cleanup");
@ -5958,7 +5958,7 @@ impl Engine {
/// record's `last_error`, which status renders. Recording is best effort:
/// a store too broken to take the note is already in stderr.
fn note_fault(&self, what: &str, transaction_id: &str, error: &anyhow::Error) {
let detail = truncate(&format!("{error:#}"), 2048);
let detail = bounded_detail(&format!("{error:#}"));
let offered = {
let mut reports = self.fault_reports.lock().expect("fault report mutex");
reports
@ -5985,6 +5985,8 @@ impl Engine {
/// Write `detail` as the record's `last_error`, touching nothing else and
/// not archiving: a terminal record whose archive is the fault stays put.
fn record_last_error(&self, transaction_id: &str, detail: &str) -> Result<()> {
let detail = bounded_detail(detail);
let detail = detail.as_str();
let snapshot = ControlSnapshot::read(&self.options.state_store)?;
let Some(stored) = snapshot
.transactions
@ -6694,7 +6696,7 @@ impl Engine {
current.value.target
);
let mut deferred = supervision.clone();
deferred.defer_continuity(now, &truncate(&format!("{error:#}"), 2048));
deferred.defer_continuity(now, &bounded_detail(&format!("{error:#}")));
self.write_target_supervision(
snapshot.supervision_for(&current.value.target),
&deferred,
@ -8552,7 +8554,7 @@ impl Engine {
held.target
);
}
let detail = waiting_for_owner(&held.target, owner_id);
let detail = bounded_detail(&waiting_for_owner(&held.target, owner_id));
if held.last_error.as_deref() == Some(detail.as_str()) && held.phase_deadline.is_none() {
return Ok(());
}
@ -8588,11 +8590,12 @@ impl Engine {
current: &Stored<DeploymentTransaction>,
detail: &str,
) -> Result<()> {
if current.value.last_error.as_deref() == Some(detail) {
let detail = bounded_detail(detail);
if current.value.last_error.as_deref() == Some(detail.as_str()) {
return Ok(());
}
let mut next = current.value.clone();
next.last_error = Some(detail.into());
next.last_error = Some(detail);
next.updated_at_unix_millis = now_millis()?;
replace_transaction(&self.options.state_store, current, &next)
}
@ -9663,7 +9666,7 @@ impl Engine {
current: &Stored<DeploymentTransaction>,
error: &anyhow::Error,
) -> Result<()> {
let detail = truncate(&format!("{error:#}"), 2048);
let detail = bounded_detail(&format!("{error:#}"));
if current.value.last_error.as_deref() == Some(detail.as_str()) {
return Ok(());
}
@ -9723,8 +9726,8 @@ impl Engine {
current.value.post_fencing_abort.is_none(),
"post-fencing abort intent is already durable"
);
let abort = post_fencing_abort_intent(&current.value, &truncate(&format!("{error:#}"), 2048));
let now = now_millis()?;
let abort = post_fencing_abort_intent(&current.value, &bounded_detail(&format!("{error:#}")));
self.persist_same_phase(current, |next| {
next.post_fencing_abort = Some(abort);
next.retime(now);
@ -10101,7 +10104,7 @@ fn incumbent_was_stopped_during_fencing(fencing: &FencingEvidence) -> bool {
fn pre_fencing_abort_intent(transaction: &DeploymentTransaction, error: &str) -> PreFencingAbort {
PreFencingAbort {
error: truncate(error, 2048),
error: bounded_detail(error),
candidate_cleanup: transaction.candidate_cleanup_owed(),
topology_reconciliation: transaction.abort_topology_reconciliation(),
source_cleanup: if transaction.command_kind == CommandKind::Deploy {
@ -10117,7 +10120,7 @@ fn post_fencing_abort_intent(
error: &str,
) -> PostFencingAbort {
PostFencingAbort {
error: truncate(error, 2048),
error: bounded_detail(error),
route_restoration: if transaction.route_preflight.is_some() {
CleanupEvidence::Pending
} else {
@ -10390,8 +10393,30 @@ fn require_detail(value: &str, label: &str) -> Result<()> {
Ok(())
}
fn truncate(value: &str, length: usize) -> String {
value.chars().take(length).collect()
const DETAIL_LIMIT: usize = 2048;
const DETAIL_HEAD: usize = 512;
const DETAIL_ELISION: &str = "
...[elided]...
";
const DETAIL_EMPTY: &str = "(no detail)";
/// The only producer of persisted free-text details; `require_detail` is the
/// only judge, and this output always satisfies it. NUL is dropped, empty
/// input gets a fixed placeholder, and an over-long value keeps its head and
/// its tail (where a build log's error is) cut on char boundaries.
fn bounded_detail(value: &str) -> String {
let value = value.replace('', "");
if value.is_empty() {
return DETAIL_EMPTY.to_owned();
}
if value.len() <= DETAIL_LIMIT {
return value;
}
let head = value.floor_char_boundary(DETAIL_HEAD);
let tail = value.ceil_char_boundary(
value.len() - (DETAIL_LIMIT - DETAIL_HEAD - DETAIL_ELISION.len()),
);
format!("{}{DETAIL_ELISION}{}", &value[..head], &value[tail..])
}
fn sha256_id(bytes: &[u8]) -> String {
@ -19368,6 +19393,97 @@ GAMECULT_RUNTIME_PRESENCE_IDENTITY = "/etc/gamecult/service/runtime-presence-ide
Ok(())
}
/// A source whose resolve step fails with a fixed message, as a build or
/// resolve step does when its output is a long log.
struct FailingSource {
message: String,
}
impl SourcePort for FailingSource {
fn resolve(
&self,
_: &OperatorBinding,
_: &str,
_: u64,
) -> Result<crate::drivers::ResolvedSource> {
bail!("{}", self.message)
}
fn freeze(&self, _: &str, _: &CompiledDeploymentPlan) -> Result<FrozenSourceReceipt> {
bail!("this source freezes nothing")
}
fn observe_frozen(
&self,
_: &CompiledDeploymentPlan,
_: &FrozenSourceReceipt,
) -> Result<crate::drivers::FrozenSource> {
bail!("this source freezes nothing")
}
fn cleanup(&self, _: &str, _: Option<&FrozenSourceReceipt>) -> Result<()> {
Ok(())
}
}
#[test]
fn bounded_detail_output_always_satisfies_require_detail() -> Result<()> {
for unit in ["a", "\u{e9}", "\u{2500}", "\u{1f600}"] {
for bytes in [0, 1, 511, 512, 513, 2047, 2048, 2049, 2600, 9000] {
// A prefix of one byte offsets every multibyte char against the cuts.
for prefix in ["", "x", "xx", "xxx"] {
let input = format!("{prefix}{}", unit.repeat(bytes / unit.len()));
let output = bounded_detail(&input);
require_detail(&output, "detail")?;
assert!(output.len() <= 2048, "{} bytes", output.len());
}
}
}
let nul = bounded_detail("a\0b\0");
require_detail(&nul, "detail")?;
assert_eq!(nul, "ab");
assert_eq!(bounded_detail("\0"), bounded_detail(""));
require_detail(&bounded_detail(""), "detail")?;
let exact = "a".repeat(2048);
assert_eq!(bounded_detail(&exact), exact);
Ok(())
}
#[test]
fn bounded_detail_keeps_the_tail_of_a_long_error() {
let input = format!("first line\n{}\nthe last line", "\u{2500}".repeat(5000));
let output = bounded_detail(&input);
assert!(output.starts_with("first line"));
assert!(output.ends_with("the last line"));
assert!(output.contains(DETAIL_ELISION));
assert!(output.len() > 1024, "the budget was not used: {}", output.len());
}
#[test]
fn a_sealing_failure_with_a_long_multibyte_error_aborts_and_frees_the_target() -> Result<()> {
// 3000 chars of 3-byte box drawing: 9000 bytes, the shape of a build log.
let log = format!("{}\nthe real error is here", "\u{2500}".repeat(3000));
let mut world = sealing_world(&unrouted_recipe())?;
world.engine.source = Arc::new(FailingSource { message: log });
for _ in 0..8 {
world.engine.resume_backoff.lock().unwrap().clear();
let _ = world.engine.run_scheduler_tick();
}
assert!(
resident(&world).is_err(),
"the failed transaction still owns the target"
);
let done = latest(&world)?;
let TransactionCompletion::FailedBeforeFencing { error } =
done.completion.context("the aborted transaction did not complete")?
else {
bail!("the transaction did not fail before fencing");
};
assert!(error.len() <= 2048, "{}", error.len());
assert!(error.ends_with("the real error is here"), "the tail was lost");
Ok(())
}
#[test]
fn admission_takes_a_recipe_that_declares_its_proof() -> Result<()> {
let recipe = unrouted_recipe().replace(