Skip to content

control: log every route.open refusal and count them by daemon code (closes #94) - #96

Merged
ualtinok merged 4 commits into
cortexkit:masterfrom
iceteaSA:feat/route-open-refusal-info
Sep 8, 2026
Merged

control: log every route.open refusal and count them by daemon code (closes #94)#96
ualtinok merged 4 commits into
cortexkit:masterfrom
iceteaSA:feat/route-open-refusal-info

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Closes #94. Both pins from the ruling, plus one hardening the cross-family review forced.

What

Event. Every path in handle_route_open that refuses the open now goes through one helper (route_open_refusal_frameobserve_route_open_refusal), which emits

INFO subc_core::control: route.open refused code=… module_id=… connection_id=…

with connection_id = the requester. The supervised-but-absent branch adds state, enabled, live as separate tracing fields (state = %status.state, not prose), so a consumer's own log can be joined on them. 17 refusal returns route through the helper; invalid_project_root is left out deliberately — it rejects malformed input, it does not refuse an open.

Counter. route_open_refused_by_code in observability.rs, snapshotted into the existing server.describe counters object and rendered by ck daemon beside module_frames_dropped_no_route_by_module (summary line only when non-empty; verbose lists the map). No subc-control type changed; goldens byte-identical.

Counter keys are daemon vocabulary, never a module-chosen string. The relay-rejection site forwards the module's body.code on the wire as before, but counts under the fixed key module_rejected and carries the raw code on the event as module_code. The increment signature is &'static str with a debug_assert! against the closed ROUTE_OPEN_REFUSAL_COUNTER_CODES set. This is what the review caught: with the raw code as key, a module could grow the map for the daemon's lifetime and push \x1b]52;… through ck daemon to an operator's terminal — the same class the provenance renderers already escape. Both ck daemon renderers additionally go through terminal_safe_string/terminal_safe_json.

Tests (mutation-proved, each direction)

  • route_open_supervised_absence_emits_refusal_fields_and_counts_code — drives the supervised-absent branch, asserts the event's fields and the counter delta in one test. Severing info!.expect("route.open refusal event") panics; severing the increment → left: Null, right: {"module_warming": 1}.
  • route_open_module_rejection_uses_daemon_counter_key — relay rejection carrying "\x1b]52;c;AAAA\x07" lands as {"module_rejected": 1} and the raw string is asserted absent. Substituting &body.code as the key → left: {"\u{1b}]52;c;AAAA\u{7}": 1}.
  • route_open_refusal_counter_renderers_escape_terminal_controls — both renderers, hostile key, no byte < 0x20 in output.

Gates on 9c718c6b + these two commits: cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test -p subc-core 842 passed / 0 failed / 1 ignored. Workspace run shows only the #93 failure.

CONSUMER-IMPACT: none on both commits — log event + counter, no wire change.

Why now

Sunday's insula#14 exercise: the daemon's refusal code was the one fact neither end of the wire could attest, so the verdict rested on a state-machine argument with a stated residual. With this line, the daemon says which code it sent.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Previously, route.open refusals were not consistently observable. Now every refusal emits an INFO event and increments a daemon counter, giving operators the refusal code without changing the wire response (closes #94).

  • Events include requester connection_id; untrusted module_id and module_code fields use Debug formatting so terminal control characters stay out of logs. Supervised-but-absent refusals also include state, enabled, and live.
  • Exposes route_open_refused_by_code through server.describe and renders it in ck daemon beside frame-drop statistics.
  • Counter keys are fixed daemon vocabulary; module rejection codes still go out on the wire and in module_code, but count under module_rejected. Terminal renderers escape hostile values.

Written for commit 989a1b6. Summary will update on new commits.

Review in cubic

CONSUMER-IMPACT: none — log event + counter, no wire change
CONSUMER-IMPACT: none — counter key set is daemon vocabulary; no wire change

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/subc-core/src/control.rs Outdated
Comment thread crates/subc-core/src/control.rs Outdated
@subc-alfonso

subc-alfonso Bot commented Sep 8, 2026

Copy link
Copy Markdown

Read at 24adb39. Both pins landed as hard constraints and the module_rejected fixed-key hardening is the right call — a module must never be able to name a counter key. One change before merge, and it is cubic's two P2s read together: the two UNTRUSTED strings on the event — the requester's module_id (an unknown target is arbitrary bytes) and the module's module_code — are formatted with % (Display), which writes control bytes verbatim into subc.log. Format those two with ? (Debug), which escapes \x1b as \u{1b}; the daemon-vocabulary fields (code, state) stay %. Same class you already closed on the renderer side, one line each. Add the hostile bytes to route_open_module_rejection_uses_daemon_counter_key's event assertion so the log line is proven escaped, not just the counter key.

CI: the ubuntu red on your head is #93 (pre-existing; fix under two-arm test on a VM now), not this PR — windows is still running. I'll merge once windows is green and the ? change is up; tag 0.17.21 carries it together with #95.

CONSUMER-IMPACT: none — log field escaping only
CONSUMER-IMPACT: none — log field formatting only
@iceteaSA

iceteaSA commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

? change is up as 989a1b6f (on top of 5730148f, which had done the same with escape_debug() before your comment landed — converged to your form, escape_debug gone). Four sites: module_id = ?module_id in both refusal helpers, module_code = ?body.code and module_id = ?target_module_id at the relay-rejection site. code and state stay %.

Event proven escaped in route_open_module_rejection_uses_daemon_counter_key (control.rs:6839-6840): the captured module_code field has no byte < 0x20 and equals "\u{1b}]52;c;AAAA\u{7}" verbatim; same shape for a hostile module_id on the unknown-target path at :6772-6773. Swapping one ? back to % reddens the < 0x20 assertion.

One consequence of Debug worth a line in the docs when this ships: normal ids now log quoted — module_id="claustrum" — where every other module_id= field in the daemon's log is bare. Consumers joining on the refusal line should match the quoted form.

Gates: fmt, clippy -D warnings, cargo test -p subc-core 843/0/1 ignored; goldens untouched.

@subc-alfonso

subc-alfonso Bot commented Sep 8, 2026

Copy link
Copy Markdown

Your head 989a1b6 went red on windows too — that one is also not yours: release_index::tests::a_hanging_source_refuses_within_the_transport_deadline measured 22.5 s on a loaded hosted runner against a 20 s bound (powershell.exe cold start before the transport's 800 ms budget even begins). Master now has both fixes (#93 at 6c624d8, this bound at 6d764d6); rebase onto master and the matrix should read clean. I'll merge from there.

@ualtinok
ualtinok merged commit 3582e6f into cortexkit:master Sep 8, 2026
14 of 17 checks passed
@subc-alfonso

subc-alfonso Bot commented Sep 8, 2026

Copy link
Copy Markdown

Merged at 3582e6f from your head 989a1b6 (my gate: fmt, clippy native + windows-gnu, cargo test --workspace --all-targets 1266/0 on the merged tree; both reds on your head were master's and are fixed under it). One follow-up commit of mine, 4f81d4b, adds the three reasons that lived only in this PR's body — closed counter keys, the two Debug-formatted fields, and why invalid_project_root is not a refusal — as doc comments at the sites; a 461-line change with zero comments leaves the next reader with the code and none of the argument. Ships in subc-core-v0.17.21 with #95.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

route.open refusals leave no daemon-side trace: add an INFO event (code, module_id, connection_id) and a per-code counter

2 participants