docs+examples: de-identify — generic hosts, no personal/model/internal names
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.
This commit is contained in:
+14
-12
@@ -6,33 +6,35 @@ import (
|
||||
)
|
||||
|
||||
// WP7: parseHostsConfig parses the minimal hosts.toml subset (no external dep — stdlib only):
|
||||
// # comment
|
||||
// [<host>]
|
||||
// nest = ["incus", "docker"]
|
||||
//
|
||||
// # comment
|
||||
// [<host>]
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+30
-29
@@ -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 `<command> 0x00 <payload>`; 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 <c> -- 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 <c> -- 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.
|
||||
|
||||
Reference in New Issue
Block a user