Surfaced by CIRISConformance while building the CC 5.3.4 node fixture (the /v1/accord-holders gate beside CIRISServer#248) on ciris-server 0.5.198 / persist v41.2.0.
What I tried
Server 0.5's config lives in CEG: config:* knobs are scores rows on the config:v1 dimension at cohort_scope: self, self-emitted by the node key (graph_config.rs), and compose resolves them at the config_resolution phase before the edge is built, so net.listen_addr can move the node off 0.0.0.0:4242. A conformance harness cannot use POST /v1/config for that (it needs the node already up, on the port the harness is trying to move it off), so it pre-writes the rows into the node's own database before the first boot:
home = tempfile.mkdtemp(); os.makedirs(f"{home}/identity"); os.makedirs(f"{home}/data")
open(f"{home}/identity/ed25519.seed", "wb").write(secrets.token_bytes(32))
open(f"{home}/identity/ml_dsa_65.seed", "wb").write(secrets.token_bytes(32))
e = ciris_persist.Engine(f"sqlite:///{home}/data/ciris_engine.db", "probe-node",
local_key_id="probe-node", local_key_path=f"{home}/identity/ed25519.seed",
local_pqc_key_id="probe-node-pqc", local_pqc_key_path=f"{home}/identity/ml_dsa_65.seed")
kid = e.register_self_federation_key("node", "probe", None, None, None) # probe-node-<b32>
for key, value in (("net.listen_addr", "127.0.0.1:44412"), ("transport.node", False)):
env = {"attesting_key_id": kid, "attested_key_id": kid, "dimension": "config:v1", "score": 1.0,
"asserted_at": "2026-05-28T14:00:00.000Z", "witness_relation": "self",
"cohort_scope": "self", "key": key, "value": value}
e.emit_attestation_self(json.dumps({"attestation_type": "scores", "attested_key_id": kid,
"cohort_scope": "self", "attestation_envelope": env})) # ADMITTED
ciris_persist.reset_engine()
ciris_server.serve_with_python_adapter(Adapter(), home=home, key_id="probe-node")
The rows admit and read back exactly as graph_config::set_config would have written them (attestation_type=scores, cohort_scope=self, envelope.dimension=config:v1, key, value); list_attestations_by(kid) returns them; node_identity() after boot reports the same key_id the pre-write registered (local_derived_key_id on both sides — graph_config::self_key_id is that call).
What happens
INFO ciris_server::compose: resolved initial config:* snapshot (Server 0.5 — knobs now live in CEG, not env)
initial_config=ResolvedConfig { transport_node: true, store_and_forward: true, … listen_addr: "0.0.0.0:4242" … }
ERROR build reticulum transport: … reticulum node start: I/O error: Address already in use (os error 98)
The snapshot is the baked defaults; the pre-written rows are not seen, so the edge binds 0.0.0.0:4242 (held by another node on this host) and boot fails. No warning names the rows as rejected or filtered.
The question
Is a first boot meant to be able to read config:* rows written by the node key before compose ever ran? If yes, this is a defect somewhere between live_config_rows (self_key_id + AttestationFilter{attesting_key_id, scores, dimension_prefixes ["config:"]} + build_caller_admission + scope-gated list_attestations) and the store — the by-attester listing sees the rows, the scoped one apparently does not. If no — if the only supported path is POST /v1/config on a running node — then a node can never be moved off :4242 on a host where that port is taken, which is every developer box running a second node, and a harness on such a host cannot stand a node up at all. Either answer is fine; I need to know which, because the CC 5.3.4 gate (the /v1/accord-holders shape, with the signature half strict-xfail on #248) is written and waiting on a bootable node.
Surfaced by CIRISConformance while building the CC 5.3.4 node fixture (the
/v1/accord-holdersgate beside CIRISServer#248) on ciris-server 0.5.198 / persist v41.2.0.What I tried
Server 0.5's config lives in CEG:
config:*knobs arescoresrows on theconfig:v1dimension atcohort_scope: self, self-emitted by the node key (graph_config.rs), andcomposeresolves them at theconfig_resolutionphase before the edge is built, sonet.listen_addrcan move the node off0.0.0.0:4242. A conformance harness cannot usePOST /v1/configfor that (it needs the node already up, on the port the harness is trying to move it off), so it pre-writes the rows into the node's own database before the first boot:The rows admit and read back exactly as
graph_config::set_configwould have written them (attestation_type=scores,cohort_scope=self,envelope.dimension=config:v1,key,value);list_attestations_by(kid)returns them;node_identity()after boot reports the samekey_idthe pre-write registered (local_derived_key_idon both sides —graph_config::self_key_idis that call).What happens
The snapshot is the baked defaults; the pre-written rows are not seen, so the edge binds
0.0.0.0:4242(held by another node on this host) and boot fails. No warning names the rows as rejected or filtered.The question
Is a first boot meant to be able to read
config:*rows written by the node key before compose ever ran? If yes, this is a defect somewhere betweenlive_config_rows(self_key_id+AttestationFilter{attesting_key_id, scores, dimension_prefixes ["config:"]}+build_caller_admission+ scope-gatedlist_attestations) and the store — the by-attester listing sees the rows, the scoped one apparently does not. If no — if the only supported path isPOST /v1/configon a running node — then a node can never be moved off:4242on a host where that port is taken, which is every developer box running a second node, and a harness on such a host cannot stand a node up at all. Either answer is fine; I need to know which, because the CC 5.3.4 gate (the/v1/accord-holdersshape, with the signature half strict-xfail on #248) is written and waiting on a bootable node.