From 20a6608f0e8517af8132bf610cb9346fc968c1ed Mon Sep 17 00:00:00 2001 From: "Claude (noether)" Date: Thu, 23 Jul 2026 13:17:06 +0200 Subject: [PATCH] =?UTF-8?q?docs+examples:=20de-identify=20=E2=80=94=20gene?= =?UTF-8?q?ric=20hosts,=20no=20personal/model/internal=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Public repo hygiene: scrub local identifiers from the design doc and the usage examples/tests. docs/design-nested.md loses the hostname, the maintainer name, a model name, and internal system/agent names (the rejected shared-store router is now described generically, not by product name); its stale wire line is corrected to netstring(argc) while there. Usage comments and test fixtures switch real fleet hostnames/container names to host1/host2/app/db. No behaviour change; all tests green. --- cmd/sic/hostscfg_test.go | 26 ++++++++-------- cmd/sic/main.go | 8 ++--- cmd/sic/parsetarget_test.go | 6 ++-- cmd/sic/resolveverbs_test.go | 4 +-- cmd/sic/verbfor_test.go | 4 +-- docs/design-nested.md | 59 ++++++++++++++++++------------------ 6 files changed, 55 insertions(+), 52 deletions(-) diff --git a/cmd/sic/hostscfg_test.go b/cmd/sic/hostscfg_test.go index 9b7eb9c..e41b45b 100644 --- a/cmd/sic/hostscfg_test.go +++ b/cmd/sic/hostscfg_test.go @@ -6,33 +6,35 @@ import ( ) // WP7: parseHostsConfig parses the minimal hosts.toml subset (no external dep — stdlib only): -// # comment -// [] -// nest = ["incus", "docker"] +// +// # comment +// [] +// nest = ["incus", "docker"] +// // into map[host] -> ordered runtime types. Comments and blank lines are ignored; an empty nest // list is a present host with zero hops. func TestParseHostsConfig(t *testing.T) { data := "# fleet nest config\n" + - "[boltzmann]\n" + + "[host1]\n" + "nest = [\"incus\", \"docker\"]\n" + "\n" + - "[data]\n" + + "[host2]\n" + "nest = [\"pct\"]\n" + "\n" + - "[bosch]\n" + + "[host3]\n" + "nest = []\n" cfg, err := parseHostsConfig(data) if err != nil { t.Fatalf("unexpected err: %v", err) } - if !reflect.DeepEqual(cfg["boltzmann"], []string{"incus", "docker"}) { - t.Fatalf("boltzmann nest = %#v, want [incus docker]", cfg["boltzmann"]) + if !reflect.DeepEqual(cfg["host1"], []string{"incus", "docker"}) { + t.Fatalf("host1 nest = %#v, want [incus docker]", cfg["host1"]) } - if !reflect.DeepEqual(cfg["data"], []string{"pct"}) { - t.Fatalf("data nest = %#v, want [pct]", cfg["data"]) + if !reflect.DeepEqual(cfg["host2"], []string{"pct"}) { + t.Fatalf("host2 nest = %#v, want [pct]", cfg["host2"]) } - b, ok := cfg["bosch"] + b, ok := cfg["host3"] if !ok || len(b) != 0 { - t.Fatalf("bosch must be present with an empty nest; got %#v ok=%v", b, ok) + t.Fatalf("host3 must be present with an empty nest; got %#v ok=%v", b, ok) } } diff --git a/cmd/sic/main.go b/cmd/sic/main.go index 0cee2ab..78dbe5c 100644 --- a/cmd/sic/main.go +++ b/cmd/sic/main.go @@ -13,15 +13,15 @@ // // sic host echo hi # run on the host // sic host touch 'a b' # ONE file -// sic dcw2/noether cat /etc/os-release # into the 'noether' guest on dcw2 -// sic data/ct110/app id # host -> pct ct110 -> docker app (two hops) +// sic host1/app cat /etc/os-release # into the 'app' guest on host1 +// sic host2/ct/svc id # host -> pct ct -> docker svc (two hops) // sic --sh host 'echo hi | wc -c' # shell line (space-join is intended here) // // Hop runtimes come from /etc/sic/hosts.toml, one stanza per host: // -// [dcw2] +// [host1] // nest = ["incus"] -// [data] +// [host2] // nest = ["pct", "docker"] package main diff --git a/cmd/sic/parsetarget_test.go b/cmd/sic/parsetarget_test.go index 6a55f88..f2077b7 100644 --- a/cmd/sic/parsetarget_test.go +++ b/cmd/sic/parsetarget_test.go @@ -14,9 +14,9 @@ func TestParseTarget(t *testing.T) { host string hops []string }{ - {"boltzmann", "boltzmann", nil}, - {"boltzmann/memory", "boltzmann", []string{"memory"}}, - {"boltzmann/memory/stash-stash-1", "boltzmann", []string{"memory", "stash-stash-1"}}, + {"host1", "host1", nil}, + {"host1/app", "host1", []string{"app"}}, + {"host1/app/db", "host1", []string{"app", "db"}}, } for _, tc := range cases { host, hops := parseTarget(tc.in) diff --git a/cmd/sic/resolveverbs_test.go b/cmd/sic/resolveverbs_test.go index 81f4d0a..8333a02 100644 --- a/cmd/sic/resolveverbs_test.go +++ b/cmd/sic/resolveverbs_test.go @@ -9,11 +9,11 @@ import ( // Too many hops for the declared nest depth is a hard error (fail-loud); an unknown runtime // propagates verbFor's error. Zero hops -> zero verbs (a bare host is a single v2 frame). func TestResolveVerbs(t *testing.T) { - got, err := resolveVerbs([]string{"memory", "stash-stash-1"}, []string{"incus", "docker"}) + got, err := resolveVerbs([]string{"app", "db"}, []string{"incus", "docker"}) if err != nil { t.Fatalf("unexpected err: %v", err) } - want := []string{"incus exec memory --", "docker exec stash-stash-1"} + want := []string{"incus exec app --", "docker exec db"} if !reflect.DeepEqual(got, want) { t.Fatalf("resolveVerbs = %#v, want %#v", got, want) } diff --git a/cmd/sic/verbfor_test.go b/cmd/sic/verbfor_test.go index 194a773..485e8ec 100644 --- a/cmd/sic/verbfor_test.go +++ b/cmd/sic/verbfor_test.go @@ -10,8 +10,8 @@ func TestVerbFor(t *testing.T) { cases := []struct { runtime, name, want string }{ - {"incus", "memory", "incus exec memory --"}, - {"docker", "stash-stash-1", "docker exec stash-stash-1"}, + {"incus", "app", "incus exec app --"}, + {"docker", "db", "docker exec db"}, {"pct", "107", "pct exec 107 --"}, } for _, tc := range cases { diff --git a/docs/design-nested.md b/docs/design-nested.md index 0e2bb75..b7c6b38 100644 --- a/docs/design-nested.md +++ b/docs/design-nested.md @@ -1,9 +1,8 @@ # sic — nested (multi-hop) design & chain-resolution decision -Status: **Accepted** — 2026-07-23, bullpen design session (room msgs 865–877; -@architect / @skeptic on deepseek-v4-flash-dspark, initiated by Markus). -This records a decision and its rejected alternatives *for the audit trail* — a future -reader should see that mneme-as-router was considered and killed for cause, not overlooked. +Status: **Accepted** — 2026-07-23. This records a decision and its rejected alternatives +*for the audit trail* — a future reader should see that a shared-store router was considered +and killed for cause, not overlooked. ## Goal @@ -22,8 +21,8 @@ repeats. There is **no separate `sic-run` binary** — see Decision 3. Construct and up-front; unrolling is iterative and distributed. One frame = preamble (`MAGIC 0x00`, then 4-byte big-endian `len32`) + a djb netstring whose -content is ` 0x00 `; the payload of a non-terminal frame is itself a full -frame for the next hop. +content is `netstring(argc)` + argc argv netstrings + `payload`; the payload of a non-terminal +frame is itself a full frame for the next hop. ## Decision 1 — chain resolution: **static per-client config** @@ -32,40 +31,41 @@ frame for the next hop. concrete verb + address at each layer. **Fail loud on any mismatch** (unknown host, more hops than the config declares) — never guess. -### Rejected: mneme as the chain router +### Rejected: a shared memory store as the chain router -Resolving chains by querying the fleet-memory store (mneme) was proposed and **rejected**. -Even with the bootstrap circularity removed (mneme's own address is a flat local setting, so a -lookup is one direct call, not a nested one), it loses on four counts: +Resolving chains by querying a shared, mutable fleet-memory store was proposed and **rejected**. +Even with the bootstrap circularity removed (the store's own address is a flat local setting, so +a lookup is one direct call, not a nested one), it loses on four counts: -- **Trust (dispositive).** mneme is a *mutable shared* store. Any container with write access - poisons one chain entry and every subsequent `sic` call through that chain execs the - attacker's verb on the target host — a supply-chain RCE vector. Static config is - immutable-after-deploy and auditable once. +- **Trust (dispositive).** Such a store is *mutable and shared*. Any writer poisons one chain + entry and every subsequent `sic` call through that chain execs the attacker's verb on the + target host — a supply-chain RCE vector. Static config is immutable-after-deploy and auditable + once. - **Staleness fails the wrong way.** A moved/renamed container makes the stored chain lie, and the client *silently* execs into the wrong container. Static config fails loud (connection - refused); mneme turns a hard error into quiet wrong-target execution. -- **Availability.** mneme lives on boltzmann, which sleeps; resolution would die with it, and a + refused); a mutable store turns a hard error into quiet wrong-target execution. +- **Availability.** The store may live on a host that sleeps; resolution would die with it, and a local cache to cover that is just a slower, staler config file. -- **No net benefit.** The client needs a local mneme *address* regardless, so mneme only adds a - fragile network hop + cache to do what a flat file read already does. +- **No net benefit.** The client needs a local store *address* regardless, so the store only adds + a fragile network hop + cache to do what a flat file read already does. -mneme remains the fleet **memory** store — this decision is narrowly about *routing*, nothing else. +A shared memory store may still be useful for *memory* — this decision is narrowly about +*routing*, nothing else. ## Decision 2 — migration: **daemon dual-read, daemon-first** The v2 daemon hard-requires the `0x00` magic; the deployed v1 client sends a digit first, so a -naive v2 deploy breaks every existing call on 30+ live hosts. Instead the daemon **sniffs the -first byte** — `0x00` → v2 parser, digit → v1-legacy parser — a once-per-connection branch, -~20 lines, zero perf cost. Ship the v2 daemon everywhere first; roll clients independently, no -flag day. +naive v2 deploy breaks every existing call on the many live hosts already running v1. Instead the +daemon **sniffs the first byte** — `0x00` → v2 parser, digit → v1-legacy parser — a +once-per-connection branch, ~20 lines, zero perf cost. Ship the v2 daemon everywhere first; roll +clients independently, no flag day. ## Decision 3 — the chain link is `sicd` itself (no `sic-run` binary) An earlier draft proposed a separate `sic-run` helper as the per-hop peeler. It is **not built**: `sicd` is already a pure v2 frame-peeler, so the inner-hop command is just `sicd` -(`incus exec -- sicd`), and one binary deploys instead of two (Markus' call: deployment -simplicity). **Verified empirically, not assumed** — `sicd` invoking `sicd` was run against a +(`incus exec -- sicd`), and one binary deploys instead of two (a deployment-simplicity +decision). **Verified empirically, not assumed** — `sicd` invoking `sicd` was run against a two-layer nested frame (`build sicd; frame("sicd", frame("cat", payload)) | sicd`): the payload reached the inner hop, exit status propagated, stderr clean. sicd calling sicd peels correctly, so nothing about a nested hop needs behaviour sicd lacks. The only prerequisite is that **sicd is @@ -74,15 +74,15 @@ present in every container a chain traverses** (a packaging/rollout item, not ne ## Backward compat & the two original bugs - A **bare `--`** must survive into the innermost argv untouched (the payload is opaque bytes; - no hop interprets it) — closes marfrit/sic#1. + no hop interprets it) — closes the double-dash bug (#1). - **stdin must be forwarded** into/through the frame — closes the silent zero-byte-file bug. - A **bare host** with no `/` stays a single v1-style hop (backward compatible). ## Escape hatch (documented, not built) If redeploying clients on chain changes ever costs more than the trust of a live store, -distribute **signed manifests out-of-band** — deterministic like static config, updatable like -mneme, without the mutable-store poison vector. Future work; explicitly not today's problem. +distribute **signed manifests out-of-band** — deterministic like static config, updatable like a +live store, without the mutable-store poison vector. Future work; explicitly not today's problem. ## Consequences @@ -90,4 +90,5 @@ mneme, without the mutable-store poison vector. Future work; explicitly not toda - No network dependency and no mutable store in the routing path — routing is deterministic and auditable. - Next: implement `cmd/sic` (config read + recursive frame build + `--`/stdin fixes) and the - daemon dual-read; ensure sicd is deployed into target containers (no `sic-run` binary); both are Go, grindable via @godev. + daemon dual-read; ensure sicd is deployed into target containers (no `sic-run` binary); both + are Go.