selfware 0.6.7

Your personal AI workshop — software you own, software that lasts
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
{
  "component": "config",
  "tier": "full",
  "loop_stage": "foundation",
  "summary": "The config component is the loop's foundation substrate. crate::config::loader::load merges Config from defaults -> TOML -> env -> model-defaults profile, then derive_context_budget computes the usable context/token budget, provenance attaches a ConfigSource per field, trust gates checkout-local configs, and validation rejects unrunnable settings at startup. The api_key module resolves credentials through a strict hierarchy (env -> keyring -> TOML) behind transport guards that refuse to leak the key to insecure or spoofed endpoints. Every loop-shaping knob the driver reads — context_length, agent.token_budget, max_iterations, step_timeout_secs, model, safety, concurrency — originates here before the state machine starts.",
  "loop_objects": ["Config", "AgentConfig", "SafetyConfig", "ModelProfile", "ModelDefaultsProfile", "ConcurrencyConfig", "ConfigSource", "ConfigSources", "ApiKeySource", "RedactedString", "Budget"],
  "context_basis": "Recommendations formed with config read in the context of the full engine (~600k budget framing), grounded in loader.rs precedence and trust gating, mod.rs Config/derive_context_budget, api_key.rs credential guards, provenance.rs ConfigSources, trust.rs, validation.rs, types.rs ConcurrencyConfig, and model_profiles.rs.",
  "examples": [
    {
      "id": "config-01",
      "title": "Layer config in precedence order",
      "loop_stage": "foundation",
      "pattern": "layered-precedence",
      "intent": "Resolve every setting from the highest-priority source that defines it.",
      "how_it_shapes_the_loop": "loader::load merges defaults -> TOML -> env -> model-defaults profile, so each layer overrides the last; the final Config is the single object the entire loop reads for budget, model, and safety before the state machine starts.",
      "loop_objects_touched": ["Config", "ConfigSources"],
      "wiring": {
        "inputs_from": ["defaults", "selfware.toml", "env vars", "model profile"],
        "outputs_to": ["merged Config", "Agent::new"]
      },
      "touch_interaction": {
        "gesture": "spread",
        "canvas_action": "Spreading the foundation node fans out the four precedence layers as stacked translucent sheets.",
        "visual": "Higher-priority layers sit on top and glow; overridden values below them are dimmed."
      },
      "mini_scenario": "SELFWARE_MODEL in the env overrides the TOML model, and the loop runs against the env-chosen model on its very first reason turn.",
      "pitfall": "Assuming the TOML file always wins hides that env vars silently override it for the running loop."
    },
    {
      "id": "config-02",
      "title": "Fill only unset fields from the model profile",
      "loop_stage": "foundation",
      "pattern": "profile-fill-unset",
      "intent": "Apply sensible model defaults without clobbering explicit user choices.",
      "how_it_shapes_the_loop": "model_profiles::match_profile finds the first glob-matching ModelDefaultsProfile and apply_profile fills only user-unset fields (native FC, streaming, temperature, max_tokens, extra_body), tuning the reason stage per model.",
      "loop_objects_touched": ["Config", "ModelDefaultsProfile"],
      "wiring": {
        "inputs_from": ["config.model", "builtin_profiles"],
        "outputs_to": ["filled Config fields"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tapping the model node applies its matching profile, filling empty slots and leaving set ones untouched.",
        "visual": "Auto-filled fields glow amber (profile-sourced); user-set fields stay solid blue."
      },
      "mini_scenario": "A glm-5.2 model matches its builtin profile, enabling native function calling and thinking without the user setting either.",
      "pitfall": "A profile that overwrites explicitly-set fields silently changes the loop's behavior against the user's intent."
    },
    {
      "id": "config-03",
      "title": "Derive the context budget for the loop",
      "loop_stage": "foundation",
      "pattern": "budget-derivation",
      "intent": "Compute the usable conversation budget the whole loop lives within.",
      "how_it_shapes_the_loop": "Config::derive_context_budget reserves a 20% safety margin, clamps max_tokens to what fits, and enforces a conversation floor, returning the max_context_tokens and output reservation every reason turn and the compressor respect.",
      "loop_objects_touched": ["Config", "Budget", "AgentConfig"],
      "wiring": {
        "inputs_from": ["context_length", "max_tokens", "token_safety_margin"],
        "outputs_to": ["history compressor", "reason turns"]
      },
      "touch_interaction": {
        "gesture": "pinch",
        "canvas_action": "Pinching the foundation node reveals the budget breakdown into margin, output reservation, and conversation budget.",
        "visual": "Three segments render: hatched margin, reserved output, and green usable conversation budget."
      },
      "mini_scenario": "At startup the loop derives max_context_tokens from context_length so history compaction targets the right size before turn one.",
      "pitfall": "Deriving from max_tokens (the output cap) instead of context_length undersizes the conversation budget."
    },
    {
      "id": "config-04",
      "title": "Fall back to a conservative context for unknown models",
      "loop_stage": "foundation",
      "pattern": "safe-default-fallback",
      "intent": "Avoid overflowing an unrecognized model's real window.",
      "how_it_shapes_the_loop": "When the model matches no profile and the user did not set context_length, the loader applies UNKNOWN_MODEL_CONTEXT_LENGTH (32_768), keeping the loop's requests inside a window it can rely on.",
      "loop_objects_touched": ["Config", "Budget"],
      "wiring": {
        "inputs_from": ["unrecognized model name"],
        "outputs_to": ["context_length = 32768", "budget derivation"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flicking an unknown-model node's context slot snaps it down to the conservative fallback value.",
        "visual": "The context bar renders half-width with a 'fallback' badge to signal the safe default."
      },
      "mini_scenario": "A brand-new model with no profile gets a 32k context so the loop does not overrun an unknown window on its first request.",
      "pitfall": "Assuming a large default for an unknown model can silently exceed its real window and 400 every turn."
    },
    {
      "id": "config-05",
      "title": "Derive the agent token budget",
      "loop_stage": "foundation",
      "pattern": "conversation-budget-share",
      "intent": "Give the loop a conversation token budget proportional to the context.",
      "how_it_shapes_the_loop": "If agent.token_budget is unset the loader derives it as 60% of context_length, and validation ensures token_safety_margin < token_budget, bounding how much history the loop can carry before compaction triggers.",
      "loop_objects_touched": ["AgentConfig", "Budget", "Config"],
      "wiring": {
        "inputs_from": ["context_length"],
        "outputs_to": ["agent.token_budget", "compaction triggers"]
      },
      "touch_interaction": {
        "gesture": "two-finger-rotate",
        "canvas_action": "Rotating the budget dial sets the conversation share; the default sits at 60% of context.",
        "visual": "A ring shows the token_budget as a fraction of the full context window."
      },
      "mini_scenario": "With context_length 32768 and no explicit budget, the loop gets a 19660-token conversation budget (32768 * 3 / 5).",
      "pitfall": "Setting token_safety_margin above token_budget fails validation and the loop cannot start."
    },
    {
      "id": "config-06",
      "title": "Resolve the API key by hierarchy",
      "loop_stage": "foundation",
      "pattern": "credential-hierarchy",
      "intent": "Pick the credential from the most trustworthy available source.",
      "how_it_shapes_the_loop": "api_key resolution tries SELFWARE_API_KEY, then OPENROUTER_API_KEY (only when is_openrouter_endpoint matches), then load_api_key_from_keyring, then plaintext TOML — recording the winning ApiKeySource so the reason stage authenticates without hard-coded secrets.",
      "loop_objects_touched": ["RedactedString", "ApiKeySource", "Config"],
      "wiring": {
        "inputs_from": ["env", "OS keyring", "selfware.toml"],
        "outputs_to": ["authorized ApiClient"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-pressing the credential node shows the resolution chain with the winning source highlighted.",
        "visual": "Each source is a rung; the first that resolves lights green, the rest grey out."
      },
      "mini_scenario": "No env var is set, so the loop pulls the key from the OS keyring scoped to the endpoint before the first reason turn.",
      "pitfall": "Falling through to a plaintext TOML key without warning normalizes storing secrets in the repo."
    },
    {
      "id": "config-07",
      "title": "Refuse the key over an insecure remote endpoint",
      "loop_stage": "foundation",
      "pattern": "credential-transport-guard",
      "intent": "Never transmit the key over plaintext HTTP to a remote host.",
      "how_it_shapes_the_loop": "is_insecure_remote_endpoint and assert_credential_endpoint_safe block authorization when the endpoint is plaintext HTTP to a non-local host, so authorize_request cannot leak the key on the wire during any reason turn.",
      "loop_objects_touched": ["Config", "RedactedString"],
      "wiring": {
        "inputs_from": ["endpoint URL", "resolved api_key"],
        "outputs_to": ["authorized request or hard refusal"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tapping the endpoint node runs the safety check; unsafe endpoints show a broken-lock stamp that blocks the key.",
        "visual": "Safe endpoints show a green padlock; insecure remotes show a red broken lock and disable the send edge."
      },
      "mini_scenario": "A config points at http://remote-host; the guard refuses to attach the key and the loop halts before leaking it.",
      "pitfall": "Whitelisting a remote host over http 'just for testing' can exfiltrate the credential in one request."
    },
    {
      "id": "config-08",
      "title": "Reject a userinfo-spoofed endpoint",
      "loop_stage": "foundation",
      "pattern": "host-spoof-guard",
      "intent": "Prevent a URL like localhost@attacker from tricking the local-endpoint check.",
      "how_it_shapes_the_loop": "endpoint_has_userinfo rejects URLs carrying a userinfo component so an attacker cannot disguise a remote host as local and coax the loop into sending the key without TLS.",
      "loop_objects_touched": ["Config"],
      "wiring": {
        "inputs_from": ["endpoint URL"],
        "outputs_to": ["rejected config"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-pressing the endpoint node parses the URL and flags any userinfo component as spoof.",
        "visual": "A spoofed URL highlights the userinfo segment in red and blocks the node."
      },
      "mini_scenario": "An endpoint http://localhost@attacker.com is rejected before the is_local_endpoint check can be fooled.",
      "pitfall": "A naive 'contains localhost' host check without userinfo rejection is trivially bypassed."
    },
    {
      "id": "config-09",
      "title": "Match the OpenRouter host exactly",
      "loop_stage": "foundation",
      "pattern": "exact-host-match",
      "intent": "Only apply OpenRouter-specific behavior to the genuine host.",
      "how_it_shapes_the_loop": "is_openrouter_endpoint requires an exact host match, so OPENROUTER_API_KEY use and usage.cost inclusion never leak to a lookalike subdomain during a reason turn.",
      "loop_objects_touched": ["Config", "RedactedString"],
      "wiring": {
        "inputs_from": ["endpoint host"],
        "outputs_to": ["OpenRouter key + cost behavior"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tapping the endpoint node shows whether it exactly matches the OpenRouter host.",
        "visual": "An exact match shows the OpenRouter badge; a lookalike shows a warning and no badge."
      },
      "mini_scenario": "A lookalike host openrouter.ai.evil.com does not match, so the OpenRouter key is never sent to it.",
      "pitfall": "A substring host match sends the OpenRouter key to any domain containing the string."
    },
    {
      "id": "config-10",
      "title": "Redact secrets from any dump",
      "loop_stage": "foundation",
      "pattern": "redaction-by-default",
      "intent": "Keep credentials out of logs, traces, and error output.",
      "how_it_shapes_the_loop": "RedactedString renders as [REDACTED] in Debug/Display and redact_config_secrets walks serialized JSON to scrub the api_key and env values, so the loop's observability channel never leaks credentials.",
      "loop_objects_touched": ["RedactedString", "Config"],
      "wiring": {
        "inputs_from": ["Config with secrets"],
        "outputs_to": ["redacted logs / traces"]
      },
      "touch_interaction": {
        "gesture": "double-tap",
        "canvas_action": "Double-tapping the config node opens a safe view where secret fields show as masked chips.",
        "visual": "Secret fields render as dotted [REDACTED] chips that never reveal the value."
      },
      "mini_scenario": "A debug dump of the request body shows the api_key as [REDACTED] instead of the real token.",
      "pitfall": "Logging the raw Config or request body before redaction leaks the key into persistent traces."
    },
    {
      "id": "config-11",
      "title": "Track each value's provenance",
      "loop_stage": "learn",
      "pattern": "value-provenance",
      "intent": "Know where every effective setting came from.",
      "how_it_shapes_the_loop": "ConfigSources maps each dotted field to a ConfigSource (Default/ConfigFile/EnvVar/Profile/CliArg/AutoConfig) as each layer merges, so source_of makes the loop's foundation transparent and debuggable after the fact.",
      "loop_objects_touched": ["ConfigSources", "ConfigSource", "Config"],
      "wiring": {
        "inputs_from": ["each merge layer in loader::load"],
        "outputs_to": ["source_of lookups", "diagnostics"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-pressing any config field reveals a provenance tag showing its origin layer.",
        "visual": "Each field wears a small origin badge: gear for default, terminal for env, file for TOML, robot for auto."
      },
      "mini_scenario": "A user wonders why temperature is 1.0; source_of shows it came from the model profile, not their TOML.",
      "pitfall": "Losing provenance makes an unexpected setting impossible to trace back to its layer."
    },
    {
      "id": "config-12",
      "title": "Gate a checkout-local config behind trust",
      "loop_stage": "control",
      "pattern": "workspace-trust-gate",
      "intent": "Stop an untrusted repo's selfware.toml from weakening the loop's guardrails.",
      "how_it_shapes_the_loop": "When loader::load sees a checkout-local selfware.toml and trust::is_config_trusted returns false, it resets hooks, MCP, yolo, and permission grants and refuses remote endpoint overrides — so a cloned repo cannot silently escalate the loop's privileges.",
      "loop_objects_touched": ["Config", "SafetyConfig"],
      "wiring": {
        "inputs_from": ["checkout-local TOML", "~/.selfware/trusted_repos"],
        "outputs_to": ["restricted Config"]
      },
      "touch_interaction": {
        "gesture": "long-press",
        "canvas_action": "Long-pressing the foundation node shows the trust verdict; untrusted repos display their reset privileged fields.",
        "visual": "Untrusted config renders behind a caution border; reset fields flash back to their safe defaults."
      },
      "mini_scenario": "A cloned repo's selfware.toml tries to register hooks; being untrusted, the loader strips them before the loop runs.",
      "pitfall": "Trusting a checkout-local config by default lets a malicious repo add hooks that run arbitrary commands."
    },
    {
      "id": "config-13",
      "title": "Add a repo to the trust list",
      "loop_stage": "control",
      "pattern": "explicit-trust-grant",
      "intent": "Let the user opt a specific checkout into full config privileges.",
      "how_it_shapes_the_loop": "trust::add_trusted_config appends the config's canonical path to trusted_repos_file (~/.selfware/trusted_repos) so is_config_trusted returns true on later loads, allowing that repo's config to set hooks and endpoints for the loop.",
      "loop_objects_touched": ["Config", "SafetyConfig"],
      "wiring": {
        "inputs_from": ["user trust command", "canonical config path"],
        "outputs_to": ["trusted_repos file"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tapping the trust toggle on a config node adds its canonical path to the trust list.",
        "visual": "The caution border turns into a solid green trust ring once granted."
      },
      "mini_scenario": "The user trusts their own project, so its selfware.toml's hooks and MCP servers now configure the loop.",
      "pitfall": "Trusting a path by non-canonical form lets a symlink dodge the check next time."
    },
    {
      "id": "config-14",
      "title": "Validate the config before the loop starts",
      "loop_stage": "verify",
      "pattern": "startup-validation-gate",
      "intent": "Fail fast on a config that would break the loop.",
      "how_it_shapes_the_loop": "validation::validate hard-errors on bad URLs, empty model, zero/oversized token budgets, negative temperature, and invalid globs, so an unrunnable loop is rejected at startup rather than mid-turn.",
      "loop_objects_touched": ["Config", "AgentConfig", "SafetyConfig"],
      "wiring": {
        "inputs_from": ["merged Config"],
        "outputs_to": ["startup success or hard error"]
      },
      "touch_interaction": {
        "gesture": "tap",
        "canvas_action": "Tapping the validate gate runs the checks; failing fields light red and block the loop from launching.",
        "visual": "A checklist overlay marks each rule pass/fail; any red halts the start edge."
      },
      "mini_scenario": "A config with max_tokens 0 is rejected by validate, so the loop never starts in a broken budget state.",
      "pitfall": "Deferring validation to runtime surfaces a config error deep in a turn instead of at a clean startup."
    },
    {
      "id": "config-15",
      "title": "Restrict a generated config to fit without clamping",
      "loop_stage": "verify",
      "pattern": "no-silent-clamp",
      "intent": "Ensure wizard/auto-config output actually fits the window.",
      "how_it_shapes_the_loop": "check_generated_context_fit requires max_tokens to fit without runtime clamping for generated configs, so an auto-config cannot rely on a silent clamp that later surprises the loop's budget derivation.",
      "loop_objects_touched": ["Config", "Budget"],
      "wiring": {
        "inputs_from": ["auto-generated Config"],
        "outputs_to": ["accepted config or error"]
      },
      "touch_interaction": {
        "gesture": "pinch",
        "canvas_action": "Pinching an auto-config node checks whether its budget fits without clamping; overshoot is flagged.",
        "visual": "The budget bar turns red at the point it would need clamping, blocking acceptance."
      },
      "mini_scenario": "The wizard proposes a max_tokens that would be clamped; the strict check rejects it so the user picks a fitting value.",
      "pitfall": "Letting a generated config rely on runtime clamping hides the real output budget from the user."
    },
    {
      "id": "config-16",
      "title": "Bound concurrency from config",
      "loop_stage": "control",
      "pattern": "concurrency-bounds",
      "intent": "Cap the loop's parallel fanout from a single source of truth.",
      "how_it_shapes_the_loop": "ConcurrencyConfig (max_streams, max_tools, max_global, each validated to 1..=256) feeds the tool dispatch governor, so the act stage's parallelism is bounded by validated config.",
      "loop_objects_touched": ["ConcurrencyConfig", "Config"],
      "wiring": {
        "inputs_from": ["concurrency config section"],
        "outputs_to": ["tool dispatch governor"]
      },
      "touch_interaction": {
        "gesture": "two-finger-rotate",
        "canvas_action": "Rotating the concurrency dial sets max concurrent tools; the value is clamped to the valid range.",
        "visual": "The dial refuses to move past 256 or below 1, snapping into the allowed band."
      },
      "mini_scenario": "max_tools is set to 4, so a parallel read batch never runs more than four tools at once in the act stage.",
      "pitfall": "A concurrency value outside 1..=256 fails validation and the loop refuses to start."
    },
    {
      "id": "config-17",
      "title": "Select a named model profile for a subtask",
      "loop_stage": "reason",
      "pattern": "profile-scoped-model",
      "intent": "Route certain turns to a specialized model (coder, vision).",
      "how_it_shapes_the_loop": "Config.models holds named ModelProfiles with their own endpoint, context_length, and modalities; supports_vision lets the loop pick a vision-capable profile for a multimodal verify turn instead of the default model.",
      "loop_objects_touched": ["ModelProfile", "Config"],
      "wiring": {
        "inputs_from": ["named profile", "turn modality"],
        "outputs_to": ["reason turn model selection"]
      },
      "touch_interaction": {
        "gesture": "draw-connection",
        "canvas_action": "User draws an edge from a named profile node to a turn to route it to that model.",
        "visual": "Vision-capable profiles show a camera badge; the routed turn adopts the profile's color."
      },
      "mini_scenario": "A screenshot-verification turn is routed to the vision profile so the model can actually see the image.",
      "pitfall": "Routing a multimodal turn to a text-only profile silently drops the image from the request."
    },
    {
      "id": "config-18",
      "title": "Cap iterations and step timeout",
      "loop_stage": "control",
      "pattern": "loop-bounds-from-config",
      "intent": "Give the loop's state machine its iteration and per-step limits.",
      "how_it_shapes_the_loop": "AgentConfig.max_iterations and step_timeout_secs (both validated > 0) seed the agent loop's iteration ceiling and per-turn timeout, directly bounding how long the control loop can run before it must fail or recover.",
      "loop_objects_touched": ["AgentConfig", "Config", "Budget"],
      "wiring": {
        "inputs_from": ["agent config section"],
        "outputs_to": ["AgentLoop::new", "per-step timeout"]
      },
      "touch_interaction": {
        "gesture": "drag",
        "canvas_action": "Dragging the two control sliders sets max iterations and per-step timeout; values below 1 snap back.",
        "visual": "Two gauges show the iteration ceiling and step timeout that bound the loop."
      },
      "mini_scenario": "max_iterations 40 seeds the agent loop so the state machine fails cleanly after 40 execution turns.",
      "pitfall": "A zero max_iterations fails validation; a huge one lets a runaway loop burn the entire budget."
    },
    {
      "id": "config-19",
      "title": "Merge extra_body for backend extensions",
      "loop_stage": "reason",
      "pattern": "backend-extension-merge",
      "intent": "Pass backend-specific request knobs without losing user overrides.",
      "how_it_shapes_the_loop": "extra_body from the matching ModelDefaultsProfile merges key-by-key with the user's, with the user winning, so backend extensions (enable_thinking, top_p) reach the reason turn while respecting explicit settings.",
      "loop_objects_touched": ["Config", "ModelDefaultsProfile"],
      "wiring": {
        "inputs_from": ["profile extra_body", "user extra_body"],
        "outputs_to": ["request body extensions"]
      },
      "touch_interaction": {
        "gesture": "spread",
        "canvas_action": "Spreading the model node fans out extra_body keys; user-set keys sit above profile-set ones.",
        "visual": "Merged keys show their source; user-overridden keys carry a 'user wins' marker."
      },
      "mini_scenario": "The profile sets enable_thinking=true but the user sets top_p; both reach the request via the key-by-key merge.",
      "pitfall": "Replacing extra_body wholesale instead of merging drops the profile's needed backend knobs."
    },
    {
      "id": "config-20",
      "title": "Warn on high-risk soft values",
      "loop_stage": "verify",
      "pattern": "soft-warning-signal",
      "intent": "Flag risky-but-legal settings without blocking the loop.",
      "how_it_shapes_the_loop": "validation emits warnings (http to a remote host, extreme temperature, very long step timeout, empty api_key) so the loop starts but the operator sees the foundation risks it is running on.",
      "loop_objects_touched": ["Config", "SafetyConfig"],
      "wiring": {
        "inputs_from": ["merged Config"],
        "outputs_to": ["warning log", "startup"]
      },
      "touch_interaction": {
        "gesture": "flick",
        "canvas_action": "Flicking sideways on the validate gate cycles through amber warning chips for risky values that still pass.",
        "visual": "Warnings render amber (non-blocking) versus red errors (blocking) so the two are distinct."
      },
      "mini_scenario": "An empty api_key produces a warning; the loop still starts for a local endpoint but the risk is visible.",
      "pitfall": "Treating a soft warning as a hard error blocks legitimate local setups that need no key."
    }
  ]
}