idunn-launch answers presence only: the splice, the unix child endpoint and --listener are deleted; the child binds the member bind and the probe reaches it over TCP

PresenceAcceptor serves one connection at a time within a 1 s deadline and never ends on an accept error; a dead presence thread fails the unit. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-23 01:28:30 +02:00
commit 9173a0e8e6
5 changed files with 393 additions and 532 deletions

View file

@ -154,8 +154,11 @@ An upstream daemon that does not speak Idunn's runtime contract is deployed
with `idunn-launch` as its executable artifact and the upstream tree as a
required adjacent artifact; the launcher finds the child inside the release that
holds it. The launcher is the unit's process: it binds the
candidate endpoint, answers Idunn's challenge, renders the child's config,
starts a stateful child only once it holds the write lease, and signs presence.
candidate endpoint and answers Idunn's challenge there, renders the child's
config, starts a stateful child only once it holds the write lease, and signs
presence. Members never pass through it: the binding's `[route.member]` makes
Idunn route a second listener straight to the child, which binds the address
Idunn gives it in `GAMECULT_IDUNN_MEMBER_BIND`.
```toml
# recipe fragment — the upstream tree is built or fetched as an artifact
@ -166,13 +169,31 @@ arguments = [
{ kind = "literal", value = "upstream/bin/daemon" },
{ kind = "literal", value = "--template" },
{ kind = "literal", value = "config.tmpl" },
{ kind = "literal", value = "--listener" },
{ kind = "literal", value = "unix" },
{ kind = "literal", value = "--health-path" },
{ kind = "literal", value = "/health" },
{ kind = "literal", value = "--health-contract" },
{ kind = "literal", value = "example.presence-launcher.v1" },
]
required_environment = [
"GAMECULT_IDUNN_RUNTIME_BUNDLE",
"GAMECULT_IDUNN_CANDIDATE_BIND",
"GAMECULT_IDUNN_MEMBER_BIND",
]
```
```toml
# binding fragment — members connect to the member endpoint, which Idunn
# forwards to the private port derived from the candidate: member
# private_port_start + (candidate port - route private_port_start)
[route]
stable_endpoint = "tcp://10.77.0.1:8008"
private_port_start = 18831
private_port_end = 18839
# ...
[route.member]
stable_endpoint = "tcp://10.77.0.1:8448"
private_port_start = 18841
```
## Contracts

View file

@ -83,24 +83,30 @@ activation key.
Idunn owns admission, lease, route and continuity, as before.
- **Inputs:** the runtime bundle, the activation credential and provider
identity descriptors, the process write lease, the systemd credentials the
binding names, and the child's health over its private listener.
binding names, and the child's health over TCP at the member bind.
- **Outputs:** signed `gamecult.runtime_presence_health.v2` (Warming, Active
and failed to Odin; route observations on the candidate endpoint), one child
process, and a rendered config in the unit's private tmp that is never read
back.
- **Route:** the launcher, not the child, binds the candidate endpoint. A first
byte of 0x00 is an Idunn challenge frame and is answered by the launcher;
every other byte is copied untouched to the child's private listener.
- **Route:** Idunn owns both listeners of the route: the Expected route's
stable endpoint (presence) and, when the binding has `[route.member]`, the
member endpoint, rendered as a second server block in the one nginx fragment.
The launcher binds the candidate endpoint and answers Idunn's challenge there,
one connection at a time within a one-second deadline; any other first byte
closes the connection. The child binds the member bind Idunn sets in
`GAMECULT_IDUNN_MEMBER_BIND`, derived from the candidate port. Member bytes
never touch the launcher.
- **Lease:** with `write_lease_required` the launcher starts the child only
after it holds the lease (bound to this Expected, this activation and one of
its own Warming proofs) and keeps the lease store share-locked while the
child lives. Losing the lease stops the child.
- **Config:** one template from the release is rendered with the closed
placeholder set `{{child.listen}}`, `{{credential:NAME}}`,
placeholder set `{{child.listen}}`, `{{child.listen_host}}`,
`{{child.listen_port}}` (all three the member bind), `{{credential:NAME}}`,
`{{credential_path:NAME}}` and `{{state_root}}`. Any other placeholder
refuses to start.
- **Forbidden:** the child binding the candidate endpoint; a child of a
lease-requiring target running without a held lease; any presence not signed
- **Forbidden:** member traffic through the launcher; the child binding the
candidate endpoint; a child of a lease-requiring target running without a held lease; any presence not signed
by both keys.
- **State kind `external-directory`:** a process-owned directory imposed by an
external runtime contract. Same writer, recovery and startup rules as

View file

@ -7,7 +7,7 @@ fn main() -> std::process::ExitCode {
use anyhow::Context;
use idunn_daemon::deployment::{
IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT, IDUNN_RUNTIME_BUNDLE_ENVIRONMENT,
IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT,
IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT, IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT,
};
use idunn_daemon::launcher::{self, *};
@ -26,6 +26,11 @@ fn main() -> std::process::ExitCode {
.context("the Idunn runtime bundle is not set")?;
let bind = std::env::var(IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT)
.context("the Idunn candidate bind is not set")?;
let member = launcher::parse_member_bind(
&std::env::var(IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT)
.context("the Idunn member bind is not set")?,
&bind,
)?;
let (activation, provider) = launcher::inherited_authority_files()?;
let authority = RuntimeAuthority::open(bundle.as_ref(), activation, provider)?;
authority.require_candidate_bind(&bind)?;
@ -47,10 +52,9 @@ fn main() -> std::process::ExitCode {
sink: std::sync::Arc::new(sink),
lease: std::sync::Arc::new(lease),
spawner: Box::new(ProcessSpawner),
route_bounds: RouteBounds::default(),
health: Box::new(move |endpoint| {
health: Box::new(move |member| {
std::sync::Arc::new(HttpHealthProbe {
endpoint: endpoint.clone(),
member,
path: health_path.clone(),
})
}),
@ -64,6 +68,7 @@ fn main() -> std::process::ExitCode {
&contract,
authority,
route,
member,
&std::env::temp_dir(),
ports,
&STOP,

View file

@ -657,9 +657,7 @@ pub struct ProcessWriteLeaseBinding {
impl ProcessWriteLeaseBinding {
pub fn lock_path(&self) -> PathBuf {
let mut lock_path = self.record_path.as_os_str().to_os_string();
lock_path.push(".lock");
PathBuf::from(lock_path)
crate::drivers::authority_lock_path(&self.record_path)
}
}

File diff suppressed because it is too large Load diff