1.0.0-RC: hexagonal core/adapter/compose rewrite - #20
Open
pgodwin wants to merge 350 commits into
Open
Conversation
Third M-ng2 slice: wire SMB direct-hosting over IPX (Microsoft "NWLink direct
hosting", IPX socket 0x0550, NetBIOS-LESS). Until now the IPX mini-router was built
only when the NetBIOS service was present (for NB-IPX session traffic on 0x0455),
so a NetBIOS-free build with SMB + an IPX port never reached the direct-IPX
transport even though core/service/smb already implemented it (NewDirectIPX +
HandleDatagram on the ipxrouter SocketHandler).
crossWireTransports no longer early-returns when NetBIOS is absent. The IPX family
now carries TWO independent transports off one mini-router, and the router is built
whenever an IPX port exists AND at least one consumer was built:
- NB-IPX session traffic on 0x0455 — registered when NetBIOS is present.
- SMB direct-hosted-over-IPX on 0x0550 — registered when SMB is present,
independent of NetBIOS.
The NetBEUI family, the mailslot/datagram path, and the SMB-over-NetBIOS session
consumer stay NetBIOS-gated (they genuinely need the NetBIOS layer). Teardown is
already lifecycle-safe: NewDirectIPX self-registers on the SMB service's closers, so
SMB Stop tears its circuits down — compose owns no extra teardown.
Verified: gofmt, vet -tags all, build all-tags + default, archtest, cs-tinygo amd64
gate, full all-tags + default suites. New transports_test.go case proves the IPX
port is attached (mini-router built off the SMB consumer alone) with NO NetBIOS
service present.
This completes the wireable M-ng2 transport seams. Remaining M-ng2 items are
correctly deferred to later milestones: IPXGW SetIPXRouter waits on the MacIPX
gateway becoming a registered component (M5/M8 MacIP cutover — today still the macip
placeholder); AFP-over-ASP is already wired (reg_afp SetRouter + crossWireRouter).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive REAL ports over in-memory FrameLink pairs (adapter/link/inmem) through the
REAL M-ng2 cross-wire and assert the replies that come back out — no test doubles
between the wire and the command core. Where transports_test.go injects frames
straight into the delivery callback (a wiring unit test), these exercise the WHOLE
path: the frameport read loop, the port's own Ethernet/LLC decode+encode, the
mini-router dispatch, the session engine, the command core, and the link.
Two cases in compose/runtime/integration_test.go:
- NetBEUI session CALL: a real core/port/netbeui port over an inmem link,
cross-wired to a real NetBIOS+SMB stack, answers a NAME_QUERY (CALL) for its
file-server name with NAME_RECOGNIZED. Path: client → peer link → port read
loop → NetBEUI mini-router → NBF engine → port.Send → peer link → client.
- SMB direct-hosted-over-IPX NEGOTIATE (NetBIOS-LESS, socket 0x0550): a real
core/port/ipx port over an inmem link, cross-wired to a real SMB stack with NO
NetBIOS, answers an SMB NEGOTIATE with the reply flag set. Path: client → peer
link → IPX port read loop → IPX mini-router → direct-IPX transport → SMB
command core → reply → port.Send → peer link → client.
The frames are built from EXPORTED protocol types only (nbf.Frame.Encode,
ipxproto.Datagram.Encode, smbproto.Header.Encode + the SMB WCT/BCC wire layout), so
the integration test owns no package-internal test helper. Each reader runs with a
2s timeout so a wiring regression fails fast instead of hanging.
Verified: gofmt, vet -tags all, build all-tags + default, archtest, cs-tinygo amd64
gate, full all-tags suite, and -race on the integration tests (clean).
Note: an AFP login→OpenVol→Enumerate integration over the DDP/ATP/ASP stack is a
worthwhile follow-on (a heavier harness needing a memfs volume); AFP-over-ASP/DDP is
already covered in its own service packages, and these two cases establish the
inmem-link end-to-end pattern through real router+services that M-ng3 calls for.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the last compose-wiring item of M8a: when both SMB and the browser service are built, install the browser as SMB's BrowseProvider (§3-ter) so the IPC$ \PIPE\LANMAN NetServerEnum2 RAP call answers from the live browse list instead of the no-provider empty-success fallback. smbBrowseBridge in crossWireTransports copies the browser's ServerEntries into smb.BrowseServer rows and forwards Available — smb.BrowseServer mirrors browser.ServerEntry field-for-field, so the bridge is a pure copy with no package coupling (neither imports the other; compose owns the shim, exactly like smbSessionBridge). It is wired independent of NetBIOS, since SMB serves NetServerEnum2 over any transport including direct-TCP :445. This completes the M8a compose-wiring noted alongside SetSessionConsumer/ SetDatagramConsumer. The rest of the core M8a slice list (AFP/SMB volume+share config sections, config→ShareSpec mapper, supervisor Reconfigure→share.Manager, server identity §4-bis, §10d/§10e FS bus + host-watcher, Model.Validate Apply hook, secret-param masking) already landed in earlier commits; the only remaining M8a- adjacent piece is the SPA's secret-input password field, which is M8-spa front-end. Verified: gofmt, vet -tags all, build all-tags + default, cs-tinygo amd64 gate, full all-tags suite. transports_test.go asserts the bridge forwards Available and copies the browse list field-for-field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e proof)
Two standalone client binaries that drive the SAME core codecs and adapters the
server uses, proving the protocol layers compose into a client as cleanly as a
server (T1's "protocol-reuse proof").
cmd/csecho — an AppleTalk Echo Protocol (AEP) client over LToUDP. It opens the real
adapter/link/ltoudp multicast link, wraps it with the real adapter/link/framing LLAP
framer, and sends a DDP type-4/socket-4 echo request (command byte 1) built straight
from core/protocol/ddp + the aep.* consts, then waits for the reflected reply
(command byte 2) and prints it. Flags: -iface/-net/-src/-dst/-count/-timeout/-data.
No server, no router — just the wire stack, over the simplest real transport (UDP
multicast, no pcap/NIC).
cmd/csnetsend — a NetBIOS Messenger ("net send" / WinPopup) payload builder. It
assembles exactly the bytes messenger.Service.SendMessage puts on the wire:
messenger.Message{From,To,Text}.Marshal() wrapped in
mailslot.Write{Name:NameMessenger}.Marshal() (the \MAILSLOT\MESSNGR
SMB_COM_TRANSACTION envelope), then prints a hex dump and optionally writes the bytes
to a file (-o). Per the M7g/T1 "core send half only" decision, it stops at the
mailslot payload the service hands the netbios.SendDatagram seam; the outer NetBIOS
datagram framing + transport (NBT/NetBEUI/NB-IPX) is M7b2.
main_test.go round-trips the assembled payload back through the SAME core codecs
(mailslot.Unmarshal → messenger.Unmarshal), proving what the client builds is exactly
what the server parses.
Verified: gofmt, vet, build all-tags + default, archtest, cs-tinygo amd64 gate, cmd
test suites (default + all-tags); csnetsend run produces a correct \MAILSLOT\MESSNGR
transaction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ssicstack-ng Bind a control.Plane over the runtime's supervisor and expose it through the new-ring HTTP control adapter (adapter/control/http: JSON API + SSE), opt-in via a new -http flag (empty = disabled). This is the parity gate for the M10 cutover: the new ring now serves the management API (/status, /config, /reconfigure, /save, start/stop/restart, users, /setup first-run gate, /subscribe SSE) that the legacy internal/app + service/webui provided, so deleting the legacy runtime no longer means losing the control surface. The Plane is built from rt.Supervisor() (which satisfies control.Supervisor) plus the SAME file Store + TOML Codec the config loaded through, and the telemetry bus — the single contract every front-end shares (§14). The SPA (M8-spa) will layer over this exact surface. The server starts after the supervisor is up and stops before teardown so no control call races shutdown. Verified: gofmt, vet, build default/pcap/all + full all-tags, and a live smoke test — `classicstack-ng -http 127.0.0.1:PORT` boots, serves the API, and /status returns 409 (the first-run setup gate, no admin configured), then shuts down cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cli) Point every classicstack binary at compose/runtime via a new shared run-core, cmd/internal/cli, so the legacy internal/app run-core is no longer imported by anything — the safe precondition for the M10 deletion (the binaries never break across the cutover). cmd/internal/cli is the COMPOSE EDGE: it chooses the concrete adapters compose/ runtime deliberately does not import — the file config Store, the TOML Codec, the pcap LinkOpener, the serial opener, the HTTP control server — so the runtime ring stays adapter-agnostic and cgo-free. It exposes Main(Version) and Run(ctx, args, Version), mirroring the legacy app.* signatures so the service/daemon wrappers thread the same struct through unchanged. Run is config-file driven (server.toml + the named-instance model + the web-admin control plane); the legacy per-transport flag wall is superseded, so Run takes only the cross-cutting flags every entry point shares: -config, -http, -version. It blank-imports the component set so the build-tagged init()s self-register (the §8 replacement for *_disabled.go). Repointed: - cmd/classicstack → cli.Main (thin entry, build vars only) - cmd/classicstack-ng → cli.Main (now an alias of classicstack) - cmd/classicstack-svc → cli.Run / cli.Version (Windows SCM wrapper) - cmd/classicstackd → cli.Run / cli.Version (Unix daemon + macOS LaunchAgent) internal/app is now orphaned (grep confirms zero importers outside itself). The legacy tree it pulls in (router/port/service/netlog/pkg/control/logbuf/metrics/ service/webui) is therefore reachable from nothing — the M10 deletion follows. Verified: gofmt, build all-tags + default, cs-tinygo amd64 gate; cross-compiled the platform-tagged wrappers (linux + darwin classicstackd, windows classicstack-svc) against cli; a prior live smoke test confirmed -http serves the control API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ring
The strangler-fig cutover. With every binary now on the new-ring run-core
(cmd/internal/cli → compose/runtime, M9) and the legacy internal/app fully
orphaned, delete the entire legacy stack it pulled in:
internal/ the legacy run-core (Supervisor, wireXxx hooks, *_disabled.go)
router/ port/ the legacy routing engine + transport ports
service/ the legacy AFP/SMB/NetBIOS/MacIP/… services (+ macgarden cache)
netlog/ the legacy logger — superseded by core/log (logging cutover)
config/ the legacy koanf config — superseded by core/config + adapter/config/toml
capture/ the legacy capture glue — superseded by adapter/capture
pkg/control legacy control plane → core/control + adapter/control/*
pkg/logbuf legacy log ring → core/log sinks
pkg/metrics legacy stats hub → core/bus + compose/stats
pkg/logging legacy slog wiring → core/log
pkg/{appledouble,cnid,encoding,hwaddr,serialport,shortname,status,telemetry,vfs}
legacy support pkgs → core/ equivalents
The new ring (core/ + compose/ + adapter/ + cmd/) is now the whole product:
config → compose/runtime build+supervise → cross-wired data path (DDP router +
IPX/NetBEUI mini-routers + NetBIOS/SMB/AFP/browser/messenger) → web-admin control
plane, driven by classicstack / -ng / -svc / -daemon over cmd/internal/cli.
Kept deliberately: protocol/ (atp/ddp/llap + root) and pkg/binutil — a
self-contained legacy-codec leaf with NO dependency on anything deleted — because
the cmd/pcapdiff capture-diff diagnostic still decodes through them. Porting
pcapdiff onto core/protocol (which has no exported LLAP decoder; LLAP framing lives
unexported in adapter/link/framing) is a follow-on, tracked separately; it does not
block the runtime cutover.
go mod tidy pruned 24 dependencies the legacy tree alone pulled (goquery, koanf,
tailscale.com + wireguard/netlink, sspi, …); the new ring carries its own
adapter/config/toml and a reflection-free core.
Verified: gofmt, vet -tags all, build all-tags + default, archtest, cs-tinygo amd64
gate, and the FULL all-tags + default test suites — all green with the legacy tree
gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…trol adapters
The control-plane contract add the SPA needs to render per-share forms: until now
ListFSTypes was a Supervisor placeholder returning nil, and the param schema (which
keys, which are required, which are Secret) was reachable only inside core/fs.
- core/fs.Types() — sorted list of registered fs_type names (the dropdown source).
- Supervisor.ListFSTypes now returns fs.Types() instead of nil.
- control.Plane.ParamsFor(fsType) []ParamInfo — the schema half of ListFSTypes,
a JSON-friendly mirror of fs.Param{Key,Required,Secret,Doc}. The plane reads the
fs registry directly (a pure lookup, no supervisor state). A UI renders the chosen
backend's form from it and shows Secret keys as password fields (the server already
masks/unmasks Secret values on the Config/Reconfigure round-trip).
Plumbed through every control transport so all three stay at parity: the inproc
Client interface + Adapter, the HTTP server (GET /params_for?fs_type=) + AdapterClient,
and the ubus server case + AdapterClient.
Verified: gofmt, vet -tags all, build all-tags, the control parity test (exercises
inproc/http/ubus together), cs-tinygo amd64 gate, and a live smoke test of the
fs-type endpoints through classicstack-ng -http.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fresh, minimal single-page web-admin embedded in the HTTP control adapter
(adapter/control/http/spa), served under //go:build webui||all. Built with NATIVE
Web Components — custom elements + a tiny el() DOM helper, NO jQuery, no framework,
no build step. It drives the same JSON/SSE control API every front-end shares.
Views (cs-app routes on the first-run gate):
- cs-setup — first-run admin creation (POST /setup), shown on a 409 gate.
- cs-dashboard — component status table with Start/Stop/Restart per unit and a
Refresh; reads GET /status, posts /start|/stop|/restart.
- cs-config — the live masked config model (GET /config) as formatted JSON.
Auth flow: the page's static assets (/, /app.js, /app.css) are exempted from the
auth gate so the page can LOAD, then its JS probes /status — 409 → setup screen,
401 → the browser's Basic-auth prompt (then reload), 200 → dashboard. Every data
route stays gated; the assets carry no secrets.
Wiring: spa.go embeds the three files (embed.FS, webui||all only) and mountSPA
serves index.html's bytes directly at "/" (not via a path rewrite, which would
301-redirect to /index.html) with the file server handling named assets; the
specific API routes win over "/" by ServeMux longest-prefix. spa_stub.go (!webui
&&!all) makes mountSPA a no-op and exempts no path, so a headless/API-only build
(and TinyGo) embeds zero HTML — the §8 build-tag gate, the new-ring analogue of the
legacy service/webui embed.
The fs.Param.Secret → password-field hint (the last M8-spa-adjacent item) rides the
ParamsFor contract added in the previous commit; the share-form UI that renders it
is a follow-on enhancement over this baseline.
Verified: gofmt, vet -tags all, build all/default/webui + full all-tags suite,
archtest, cs-tinygo amd64 gate (SPA does not leak in), and a live smoke test — GET /
returns the cs-app HTML (200), /app.js + /app.css serve with correct content-types,
and /status stays 409-gated pre-setup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the last legacy holdout. cmd/pcapdiff (a capture-diff diagnostic) was the only thing still importing the legacy protocol/ (atp/ddp/llap + root) and pkg/binutil leaf that the M10 cutover deliberately kept alive. With pcapdiff gone, both are fully orphaned, so all three are deleted together — the repo is now purely the new ring (core/ + compose/ + adapter/ + cmd/) plus assets, with no legacy package surviving. go mod tidy reports no change: the leaf used only the stdlib, so nothing external drops — what's removed is the last internal coupling to the pre-refactor codec tree. Verified: gofmt, vet -tags all, build all-tags + default, cs-tinygo amd64 gate, full all-tags + default test suites — all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more standalone AppleTalk client utilities over the LToUDP+LLAP wire
stack, completing the netatalk diagnostic-tool trio alongside csecho (aecho):
- csnbp (nbplkup): NBP name lookup. Sends a Broadcast Request for an
object:type@zone pattern and prints each matching node's address. Omitted
fields wildcard ('=' object/type, '*' zone).
- csgetzones (getzones): ZIP zone-list query over ATP. Pages GetZoneList
TReq/TResp until the router's last-flag; -local for GetLocalZones, -my for
GetMyZone.
Both are T1 "protocol-reuse proofs" like csecho — they drive the SAME core
codecs/constants the server uses (core/protocol/nbp, core/service/zip) over
adapter/link/framing + adapter/link/ltoudp; no server or router code.
Adds nbp.BuildLkUp, the request-side partner of BuildLkUpRply, so the client
encodes through the core codec rather than hand-rolling wire bytes (DTO rule).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… responder Two more legacy-network diagnostic utilities plus the server-side piece IPX ping needs: IPX ping (IPXPING equivalent): - core/protocol/ipx/diag: the IPX Diagnostic codec (socket 0x0456) — self- serialising request (exclusion list) / response (component summary) DTOs. Observation-based; documented in spec/errata.md per the project rule. - core/service/ipxdiag: the IPX Diagnostic Responder — a SocketHandler on the IPX mini-router that answers reachability probes (the IPX analogue of the AEP responder). Wired in crossWireTransports; registry entry gated ipxdiag||all, built sink-less and injected the mini-router as its egress, like the browser. - cmd/csipxping: the client — sends a diagnostic request over the pcap NIC link (IPX rides Ethernet, not LToUDP) and reports per-reply round-trip time. Enhanced net view (browser browse-list): - core/service/browser: serverRecord/ServerEntry now retain the announced OS and browser-protocol versions and comment (off the HostAnnouncement frame), not just the type bits. SMB's NetServerEnum2 bridge is unaffected (still copies Name/Type/Comment). - cmd/csnetview: a standalone passive listener that decodes Host/LocalMaster/ Domain announcements off the wire across NetBEUI (NBF) and IPX (NBIPX/NMPI), chaining the same core codecs the server uses, and prints discovered hosts with protocol, protocol address, OS/app version, role, and comment. All four utilities (csecho/csnbp/csgetzones/csipxping) now drive the same core codecs the server uses; csipxping/csnetview compile in the default build via the pcap stub and link libpcap under -tags pcap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ashtalk/pcap)
csecho/csnbp/csgetzones hardwired LToUDP. Factor the transport open into a
shared cmd/internal/atlink helper that picks the segment transport behind one
-transport flag and returns a framed link.DatagramLink:
- ltoudp (default): LToUDP multicast + LLAP framing (unchanged behaviour)
- tashtalk: a serial TashTalk adapter (-device/-baud) + LLAP framing
- pcap: an EtherTalk NIC via libpcap (-iface) + Ethernet/SNAP framing
(needs -tags pcap; errors cleanly without it via the pcap stub)
All three converge on link.DatagramLink — the type each client already drove
after framing — so the clients change only at the open site; default runs are
identical to before. atlink lives under cmd/internal (the cmd edge) because it
imports the adapter ring, which core and compose/runtime must not.
Also drops the deprecated cmd/classicstack-ng alias — it is just classicstack
now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
server.toml.example was stale (legacy [Bridge]/[AFP]/[WebUI] sections, pkg/vfs).
Regenerate it from the LIVE config schema (verified by round-tripping through the
real TOML codec): well-known [identity]/[logging]/[router]/[bridge], the
[[interface]] namespace, repeated [[ethertalk]]/[[ltoudp]]/[[tashtalk]]/[[ipx]]/
[[netbeui]] transports, and [[afpvolumes]]/[[smbshares]]. Documents that the
web-admin credential is first-run-only and the web-admin API is the -http flag.
OpenWRT packaging under openwrt/:
- Makefile: golang-package build of ./cmd/classicstack (tags afp smb netbios
ipx netbeui macip pcap; +libpcap dep), installs the init + UCI config.
- files/classicstack.init: procd service, runs the daemon against the UCI
config, reads an init-only `config classicstack 'init'` block (enable /
http_addr / respawn) the daemon ignores.
- files/classicstack.config: the UCI mirror of server.toml.example (verified
round-trip through the UCI codec), installed as /etc/config/classicstack.
- README.md: build/install/run notes.
Make the codec selectable by config path (cli.pickCodec): UCI for an /etc/config
path or a *.uci/*.config file, TOML otherwise — so ONE binary reads server.toml
on a desktop and /etc/config/classicstack on a router with no separate build. The
web-admin Save uses the same codec, so a UCI-loaded config is rewritten as UCI.
The TOML repeated-section idiom stays array-of-tables ([[afpvolumes]]) rather
than named sub-tables ([afpvolumes.Public]): both are valid TOML, but the array
form maps cleanly to the same named-instance model the UCI codec uses, keeping
the two codecs symmetric.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…instance CRUD, stats, config sections, gateways, diagnostics
The "web UI regressed" symptom traced to the M10 cutover deleting the legacy
pkg/control + service/webui tree and rebuilding the new-ring control plane
thinner than the original: contracts the UI needs were dropped, and several
services/sections the legacy UI edited were never wired into the new ring. This
restores them as backend-first layers feeding the SPA.
L0 — register the orphaned DDP services: rtmp/zip/nbp/aep existed but had no
compose factory, so the router never routed or had zones. Add reg_{rtmp,zip,nbp,
aep}.go (+ bundle components for RTMP/ZIP) and Router hardDeps; the runtime's
crossWireRouter auto-registers them on their sockets.
L1 — repeated-section CRUD: Supervisor.Reconfigure mis-wrote NamedSection
instances as singletons (volumes/shares could not be created). Route them to
Model.Lists; add AddInstance/RemoveInstance on the Supervisor + Plane; add
component.Describable so Status() carries Kind/Props.
L2 — stats producers: components implement component.Statful but nothing
published. Add a supervisor stats flush (poll Statful nodes + StatsEmitter push)
feeding the existing compose/stats collector + SSE; align SPA metric keys; add a
gated expvar metrics sink (adapter/metrics, perfcounters tag).
L3 — config sections + service wiring:
- SMB/NetBIOS transport binding lists (serversection.go / section.go) gate the
compose transport cross-wire per family.
- adapter/smbtcp: a real direct-TCP/NBT SMB listener over the SessionConsumer
seam (net-new; NBT was a stub even on main). Windows-safe: binds only an
explicit tcp_addr, bind failure is non-fatal (OS owns :445).
- Capture (pcap) per-interface section + cmd-edge link.Capture tee.
- InterfaceSection.Backend (pcap/tap/tun) dispatch.
- AFP extension-map: parser/marshal + Volume Finder type/creator defaulting +
per-volume ExtMapPath.
- MacIP: placeholder -> real gateway (section + NBP wiring; AppleTalk-only until
an IP-egress adapter lands, surfaced in Props).
- IPXGW (MacIPX): unregistered -> real gateway (reg_ipxgw.go + section + NBP +
IPX-mini-router wiring).
L4 (partial) — control-plane surface across all three adapters (http/ubus/inproc,
parity-enforced): instance add/remove endpoints; real diagnostics (compose/diag
reads the router's zone table, replacing the unavailable stub).
Verified: go test -tags all ./... (0 failures), headless + all + all,perfcounters
builds, gofmt + vet clean, conformance harness green, and live smoke tests
(services running, SSE stats frames, SMB-TCP bind cases, MacIP/IPXGW Props,
/list_zones 200-not-501).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continues the web-admin restore: the control-plane surface the SPA's config forms and editors need, all live-verified. - Extension-map editor surface (adapter/extmap): GET/POST /extmap read, validate (round-trip through afp.ParseExtensionMap), and write with a numbered backup — the file half core does not do. Server-side handlers (a path is server-local), not on the transport-agnostic Client contract. - Config download: control.Plane.MarshalConfig serialises the masked model through the codec; GET /config_download serves a faithful server.toml backup (the JSON Config() shape cannot). - NIC picker: Supervisor.SetInterfaceEnumerator + runtime Options thread an injected enumerator (cmd-edge pcap.ListDevices) into ListInterfaces, keeping the supervisor pcap-free; the stub/no-permission case degrades to empty. - Serial picker: adapter/serial.ListPorts (ported from the legacy pkg/serialport — Windows SERIALCOMM registry, /dev glob on Unix) + GET /list_serial_ports. - Path picker: GET /browse_path lists directories under a cleaned dir (+ parent) for the volume/share path chooser; dirs-only, no file contents, auth-gated. Verified: go test -tags all ./... (0 failures), headless + all builds, gofmt + vet clean, and a live smoke test — /list_interfaces, /list_serial_ports (2 real COM ports), /browse_path (62 dirs + parent), /extmap, /config_download. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rid, TOML download Builds the front-end on the restored control-plane surface so the backend work is operable from the browser. - Volume/share editors (cs-instance-editor): a table per repeated-section key (AFPVolumes/SMBShares) with Add / Edit / Delete driving /add_instance, /remove_instance, /reconfigure; the owning service reconciles live. The path field carries a Browse… directory picker (openPathBrowser over /browse_path) and fs_type a dropdown from /list_fs_types. Forms key off the Go FIELD names the JSON uses (VName/FSType/Path), not the toml names. - Extension-map grid (cs-extmap): a 3-column Extension/Creator/Type editor over /extmap, parsing the Netatalk format on load and serialising rows back on save (with the server's numbered backup); add/delete rows, editable file path. - Config download switched to /config_download (faithful server.toml via the codec) rather than the JSON model shape. - Diagnostics gains a Serial Ports probe; the dashboard already surfaces the new Kind/Props and live stats from the backend layers. Verified: node --check, go build (headless + all), full suite (0 failures), and a live E2E — create an AFP volume through /add_instance (VName round-trips), remove it (count → 0), /config_download serves application/toml, and the page serves app.js with cs-instance-editor / cs-extmap / the path browser embedded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…IP gateway Recover web-admin functionality and the MacIP IP-egress dropped in the M10 legacy cutover, plus the control-plane/SPA surface for editing it. MacIP gateway (adapter/macipgw): full NAT mode (OS-socket ICMP/UDP/TCP), a DHCP-relay adaptor, and a proxy-ARP etherlink so the router-member gateway egresses real IP and listens for inbound. Core gains an optional AddressAssigner seam (async DHCP assignment off the inbound loop) and the cgo-bearing egress opener is injected at the cmd edge so compose stays cgo-free. Control plane / adapters: - afp.ServerSection (advertised name/zone + ddp/tcp transport bindings), registered + wired into the AFP service (Describable props). - Diagnostics gains RegisteredNames (NBP table) and MacIPLeases drill-downs, threaded core/control -> compose/diag -> inproc/http/ubus adapters. - Interface-namespace CRUD (SetInterface/RemoveInterface) through plane -> supervisor (referencing-port reconcile) -> all three control adapters. - User store wired at last: registry.BuildUserStore (always-compiled hook) -> supervisor.SetUserStore + AFP/SMB authenticators. It was defined but never called, silently leaving services guest-only. - runport now implements Describable (Kind "port" + seed range/zone props). SPA (native web components): - One FIELD_META registry drives full labels + descriptions on every form; the dashboard cog modal and the config page share one sectionEditor. - Dashboard groups components by role (AppleTalk router / file services / transports); SMB-TCP nests under SMB by its dependency. - Model-aware widgets: Router members + bridge members as checkbox lists, a transport port's interface as a dropdown, SMB/NetBIOS/AFP transport bindings as checkboxes, and EtherTalk/LToUDP/TashTalk zone + network seed fields surfaced. Interface-namespace CRUD panel; clickable NBP-name and MacIP-lease drill-down modals. Verified: go build (all / headless / all+webui+pcap), go test -tags all, go vet, gofmt, node --check all clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…spaces
Add a NetWare 3.x-compatible NCP file server so NETx/VLM/Client32 (DOS,
Win 3.x/9x), MacIPX, and OS/2 NetWare requesters can attach and use shares.
Build tag `ncp` (and `all`); reuses the AFP/SMB storage + auth + bus seams.
- core/protocol/ncp: NCP request/reply framing, SAP, and the function-0x57
name-space DTOs (NW_HPATH, INFO_MSK_* bits, build_dir_info entry builder).
- core/service/ncp: connection table, DOS file/dir engine (0x14-0x4D), bindery
login over core/auth (guest fallback), NCP-over-IPX transport (socket 0x0451),
SAP advertiser (0x0452), and dashboard stats (connected machines / logged-in
users / open files / throughput).
- Name spaces: function 0x57 serves OS/2 + Mac long filenames, reusing the AFP
FilenameCodec + NameEngine (DOS 8.3 / Mac 31-char MacRoman / OS2 native long).
- core/fs.ResolveFold: shared case-insensitive store-path resolver so mis-cased
long names still resolve on a case-sensitive host (NFS stays case-sensitive).
- compose wiring (reg_ncp, transports wireIPX/wireAuthenticator), config
([[ncpvolumes]]), client tool csncpinfo (SLIST-style SAP probe), spec/17-ncp.md.
DTOs and function codes verified field-for-field against github.com/davidrg/mars_nwe
(src/nwconn.c, src/nwbind.c, src/namspace.c, include/{net,namspace}.h).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cross-service naming Add an EtherDFS server (The Ethernet DOS File System): DOS clients map a remote drive to a drive letter over raw Ethernet (EtherType 0xEDF5, no IP/TCP/NetBIOS). The service embeds the frameport raw-frame base (own NIC link, no cross-wire) and serves the full read/write AL_* opcode set over the shared fs/share seam, with 8.3 short names, FAT attributes, sequence-dedup, and a per-client open-file table. core/protocol/etherdfs frame header + BSD checksum + opcodes/FCB/path + DTOs core/port/etherdfs frameport-based port, EtherType demux, reply framing core/service/etherdfs service/dispatch/drive/session over the storage seam compose/registry single-factory wiring + conformance stager adapter/control/http/spa EtherDFS Drives grid + [EtherDFS] singleton panel spec/18-etherdfs.md wire format, opcodes, errata cross-links Add a per-share DOS-attribute + naming layer shared by EtherDFS, SMB, NCP, and AFP: - metastore.DOSAttrStore: definitive per-share store (sqlite/mem) keyed d/a/<path>, with a Samba-compatible XATTR_DOSINFO v3 value codec so the metastore, sidecar, and user.DOSATTRIB xattr all share one on-disk format (and interop with Samba). - DOS-attr backends selected per share by dos_attr_backend (auto|metastore|sidecar| native|xattr): Windows-native passthrough, Samba xattr (linux/darwin, xattr tag), an all-filesystems sidecar, always caching in the metastore. auto = native → xattr → sidecar. Host-path resolution via the new fs.HostPather seam (local_fs). - BuildShare assembles the store and exposes it via fs.DOSAttred / fs.Named on the built ForkFS; SMB persists/serves attrs via TRANS2_SET/QUERY_*_INFO, EtherDFS via AL_SETATTR/GETATTR. - Name engine: case-insensitive lookup with preserved stored case (Windows-FS semantics), one generator on Win/Mac/Linux; 8.3 uppercased, 31-char medium keeps case. AFP long names now flow through the medium engine; NCP 8.3 through the name engine (unique, reversible) instead of raw truncation. dos_attr_backend is wired on all four service config sections. Docs: spec/16-storage-seam.md + errata (Samba DOSATTRIB interop, casing) + server.toml.example. Verified: gofmt + go vet -tags all clean; builds for no-tags, all, sqlite, GOOS=linux/darwin -tags xattr; full test suite green (incl. new codec, backend, casing, and SMB/EtherDFS attribute-persistence tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he new ring Recover the macintoshgarden.org abandonware archive as a read-only AFP volume — a backend listed in .refactor/00-DESIGN.md (fs/macgarden) and whose CatSearch behaviour spec/errata.md was designed around, but which was dropped in the M10 cutover and never ported. It lives entirely in adapter/macgarden (it does real HTTP I/O, so it cannot be in core) and self-registers fs_type "macgarden" into the core/fs factory registry. AFP is fully decoupled: it knows only core/fs.FileSystem + the optional core/fs.CatSearcher capability and never imports this package or names the type — the registry virtualises the backend away. (The legacy port leaked afp.* error types into the FS layer; those are replaced with package-local errors here.) - dom.go: a tiny CSS-selector engine over golang.org/x/net/html (child/adjacent/ attribute/compound/list selectors) so the refactor ring does NOT re-add the goquery + cascadia modules it dropped. Proven against the captured fixtures. - client.go: the HTTP scraper (categories / search / item pages / ranged reads) on the new dom engine, logging through a core/log shim (was netlog). - fs.go: the virtual tree (Apps/ Games/ search/) as a core/fs.FileSystem + CatSearcher; FPCatSearch becomes an upstream archive query materialised as virtual folders, paging through an opaque CatSearchCursor. Read-only: all mutating methods return fs.ErrPermission. - stub.go: in an afp/smb build WITHOUT the macgarden tag, registers the fs_type with an actionable "rebuild with -tags macgarden" error (vs. a generic miss). - reg_macgarden.go: blank-imports the backend under afp||smb||all so either the real backend (macgarden/all) or the stub links; never in a no-file-service build. Verified: go build all / headless / afp-stub, go test, go vet, gofmt — all clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The local_fs backend reported 0/0 ("unknown") for volume size; fill in the
real host-volume total/free at the VFS level and SATURATE it to each
protocol's field width at the consumer (the protocol-specific cap belongs
where the field width is known, not in core/fs which cannot tell who is
asking).
core/fs (VFS, real uncapped bytes):
- DiskUsage resolves the path under the share root and delegates to a
build-tagged diskUsage helper: statfs(2) on unix, GetDiskFreeSpaceExW on
Windows (both via stdlib syscall, no x/sys added to the core ring), and a
0/0 fallback for unsupported GOOS / TinyGo so the file services keep
compiling on the cs-tinygo gate.
Per-protocol caps (saturate, never wrap):
- AFP: the old uint32(free/total) cast WRAPPED a >4 GiB disk (a 6 GiB volume
reported 2 GiB). Replaced with sat32, capping at 0xFFFFFFFF, the AFP 2.x
BytesFree/Total 32-bit field limit (the 64-bit ExtBytes fields are AFP 3.x,
not implemented here).
- SMB (clamp16, ~2 GiB unit ceiling) and NCP (block-scaling loop) already
saturate correctly; added tests to lock them.
Tests: real DiskUsage non-zero + traversal guard; sat32 (6 GiB/1 TiB capped,
not wrapped); clamp16. gofmt/vet clean; core/fs cross-compiles
windows/linux/darwin; full core suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e ring The core import-graph gate (core/internal/archtest) was red: several core packages transitively pulled reflect / encoding/binary, which the no-reflection rule (TinyGo + allocation discipline, §1) forbids. Two root causes, both from features that landed after the rule was last enforced: - core/fs/dosattr_native_windows.go imported golang.org/x/sys/windows for three calls (GetFileAttributes/SetFileAttributes/UTF16PtrFromString) + the FILE_ATTRIBUTE_* consts; x/sys/windows pulls encoding/binary -> reflect. Swapped to stdlib syscall (already a permitted core dep), which carries all of them. This cleared core/fs and everything downstream of it (afp/smb/ share/control/etherdfs/ncp). - The extension-map parser (afp/extmap.go, extmap_parse.go) and the MacIP config section (macip/section.go) used fmt.Errorf/Fprintf; fmt pulls reflect too. Replaced with errors.New + strconv.Quote/Itoa and a hand-built strings.Builder for the Netatalk `.ext "TYPE" "CRTR"` line — the same fmt-free idiom the rest of core uses. No behaviour change (error text and on-disk format identical). archtest now passes fresh (-count=1); gofmt/vet clean; build all/headless + core/fs cross-compile (windows/linux/darwin) green; full core test suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rchive RAM) Add an optional `zipfs` fs_type backend (adapter/zipfs, -tags zipfs||all) that serves an entire share tree from a single .zip archive — the canonical "VFS structure works standalone" check: it pins appledouble forks + the mem metastore so it exercises the whole §9 storage seam with no host directory and no sqlite. Memory model: the archive is never loaded into RAM (a 2 GiB volume must not cost 2 GiB), and no long-lived OS handle is held between calls (it would lock the file on Windows and pin across a slow client). Reads stream — each read handle owns a short-lived zip.Reader and inflates on demand behind a forward cursor (backward ReadAt reopens). Writes stage in a host temp file (random-access there); on flush the archive is rebuilt by streaming member-by-member old→new (Writer.Copy copies unchanged members raw, no re-deflate), then atomic rename. Peak RAM ≈ one buffer. Lives in adapter/ because archive/zip pulls compress/flate → reflect, forbidden in the core ring; mirrors the macgarden backend's wiring (real backend + disabled stub + reg_zipfs.go blank-import). Validates the existing read-only-zipfs ⇒ appledouble-fork-only constraint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ice Stop
The share lifecycle had no close hook, so a backend owning a GC-invisible
resource leaked: macgarden's scraper goroutine and zipfs's per-handle archive
fds were never released at teardown.
Add `fs.FSCloser interface { Close() error }` as an OPTIONAL capability (like
HostPather/Coded/DOSAttred), not a method on FileSystem — existing backends are
unaffected. `fs.CloseFS(f)` calls it when present, else no-ops. shareFS forwards
Close to the base backend; share.Share.Close threads it through; and each file
service's share wrapper (smb.Share, afp.Volume, etherdfs.Drive, ncp.Volume)
exposes Close.
Wire the call at DEFINITIVE teardown only — every file service's Stop() snapshots
its live shares/volumes under lock, unlocks, then closes each. NOT called from
RemoveShare/UpdateShare, which keep the in-flight contract (a session holding the
displaced share rides it out; GC reclaims it) — closing there would pull the FS
out from under a live handle.
macgarden and zipfs now assert `_ fs.FSCloser`. Tests: fs.TestFSCloserSeam
(no-op + direct + through-shareFS forwarding) and smb.TestStopClosesShares
(Stop closes the live share; RemoveShare does not).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… backend Two boundary cleanups from the storage-seam review. 1. Per-backend validators (Open-Closed). validateShareSpec no longer hardcodes plugin names (`if fsType == "hfs-image"`, `"zipfs"`, …). RegisterFSWithValidator lets a backend declare its own fs_type×codec / fs_type×fork constraints via a Validator(SpecConstraints) error; BuildShare calls the registered validator for the share's fs_type instead of branching on the type string. zipfs now declares its read-only⇒appledouble rule from its own package; the core knows no plugin names. Only a genuinely cross-component rule owned by no single backend (macroman-native codec × xattr fork) remains in core. 2. Remove the readOnlyFS wrapper. Read-only is now enforced INSIDE memFS (a readOnly bool guarding its mutators + Capabilities), exactly how local_fs and zipfs already honour spec.ReadOnly. The old wrapper re-listed every method and would silently drop any optional capability the inner FS gained (HostPather, CatSearcher, …) unless hand-updated to forward it. Folding the policy into the backend removes that whole bug class — one concrete value carries every capability it implements, read-only or not. Tests: validator hook exercised via test-only hfs-image/zipfs factories; TestReadOnlyMemFSEnforcesAndPreservesCapabilities asserts RO rejects mutations, reports ReadOnly, and still satisfies CatSearcher. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hardcoded forkEngineByName switch with a self-registration registry mirroring RegisterFS, and make a fork adapter MANDATORY for every share. Pure refactor — every previously-valid share builds identically; all fork tests pass unchanged. - New core/fs/fork_registry.go: RegisterForkAdapter(name, factory) + forkAdapterByName(name, base), guarded like fsFactories. The set of fork backends is now the set linked into the build. - Built-in adapters self-register from init(): appledouble (+ auto/native aliases) in fork.go, ads in fork_ads.go, xattr in fork_xattr.go. native carries a TODO(phase4) — it aliases appledouble until the real host-fork adapter lands. - nullForkEngine -> noForkAdapter, exposed as NewNoForkAdapter() and registered as "nofork" (aliases "null"/"none"): the EXPLICIT "this share has no resource forks" choice, so a fork-less share is deliberate, not a silent fallback. NewNullForkEngine kept as a deprecated alias for existing callers. - BuildShare resolves the adapter via forkAdapterByName (default appledouble, unknown = hard error); ForkFS doc states the always-present-adapter invariant. Also lands .refactor/fork-adapter-phases.md (the phase prompts for this refactor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se 2) The AppleDouble adapter hardcoded the Netatalk "._name" sidecar location. Make each sidecar LAYOUT its own registered fork adapter that inherits one base AppleDouble engine and overrides only where the sidecar lives, so an OS-X-created zip (__MACOSX/dir/._name) and a .AppleDouble/ folder volume work. The AppleDouble byte format (core/appledouble) is identical across all variants — only the container location moves. - appleDoubleForkEngine is the shared BASE: it holds an injected sidecar func(path) string and contains all fork logic. The variants differ only in that function. - Registered adapters: appledouble-default (._name), appledouble-osxzip (__MACOSX/…), appledouble-dir (dir/.AppleDouble/name) — consts ForkAppleDoubleDefault / ...OSXZip / ...Dir. Plain "appledouble" (+ "auto"/ "native") aliases appledouble-default, so existing configs and the default are unchanged. - Selection is by ADAPTER NAME (ForkBackend) through the registry — no Extra config key, no layout-strategy interface. An unknown name is a hard error as before. Caveat for a later slice: osxzip / dir layouts imply an intermediate dir; memfs/ zipfs auto-vivify parent keys, local_fs would need MkdirAll on sidecar write. Tests: fork_layout_test.go — per-layout sidecar-path mapping; the registry resolves each variant + the aliases to the right layout; per-layout fork+FinderInfo round-trip with location assertions; Move/DeleteMetadata follow the layout; BuildShare selects a variant by name and "appledouble" stays the default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion (phase 3)
Make the fork adapter the owner of its metadata containers and let a same-host-path
peer follow them, so an AFP+EtherDFS/SMB pair sharing one host path stays
metadata-consistent across a rename/delete.
- New optional fs.ForkContainers{ MetadataPaths(storePath) []string }: the
AppleDouble base returns its single sidecar path (whatever layout placed it);
ads/xattr/nofork keep metadata with the file and return nil (don't implement it).
shareFS forwards the capability. shareFS.Rename/Remove already delegate the
container move/delete to the adapter (MoveMetadata/DeleteMetadata) — the adapter
owns what its containers are; docs clarified.
- share.Reactor coordination: NamedPath gains an optional FS fs.ForkFS field (AFP/
SMB/NCP set it from v.FS()/sh.FS()). On a foreign OpRename under a shared root,
Reactor.coordinate re-derives the new name's shortname via fs.Named so the peer's
NameEngine mapping is fresh and stable; MetadataPathsFor(np, hostPath) surfaces the
sidecars the peer must re-stat (host->store conversion + ForkContainers) for the
deferred wire-push slice.
Wire push (AFP attention / SMB CHANGE_NOTIFY) stays DEFERRED — this lands the
in-memory metadata + shortname consistency and the seam the push will consume.
Tests: fork_containers_test.go (capability present on appledouble per layout,
absent/nil for ride-with-file adapters, forwarded through shareFS);
reactor_coord_test.go (MetadataPathsFor host->store + outside/no-FS nil; coordinate
re-derives a stable 8.3 shortname on foreign rename; nil-FS no-op).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Updates the TinyGo amd64 build gate from 0.34.0. No Go-version conflict (0.41.0 supports the go.mod 1.23.0); the gate script has no separate pin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…or (P1) adapter/link/framing/localtalk.go kept its own ddpChecksum copy only because core/protocol/ddp's checksum was unexported. Export it as ddp.Checksum and have stampChecksum call it directly. Added a characterization test (TestLocalTalk_CalcChecksumStampsWire) first: CalcChecksum/stampChecksum had zero test coverage before this change (no test in the package ever set CalcChecksum true), so the refactor is now provably behavior-preserving rather than unverified.
client.go, clientfileops.go, namespace.go, ncp.go, and sap.go each hand-rolled their own BE16/BE32/LE16/LE32 append (and, in clientfileops.go, decode) helpers, byte-for-byte duplicates of core/binaryprimitives.AppendBE16/32, AppendLE16/32, and BE16/BE32. Deleted the local appendBE16/appendBE32/ beU16b/appendLE16/appendLE32/be16/be32 helpers and repointed every call site at binaryprimitives, including the scattered inline byte(v>>8), byte(v) header-encoding in client.go/ncp.go/sap.go. Added core/protocol/ncp/namespace_test.go first: ParseHPath, HPath.BaseHandle, and DirEntryInfo.MarshalDirInfo had no dedicated protocol-layer test (only indirect happy-path coverage via core/service/ncp/namespace_test.go), so the error paths (truncated NW_HPATH, overrunning component lengths) and the exact per-field wire layout MarshalDirInfo emits are now pinned before the encode helpers underneath them changed.
core/csnet already had these from P0; round out the packages an external developer reaches for first: ddp (encode/decode round trip), nbp (lookup/reply build+parse), client/link (ParseSpec, RandomMAC), and client/atalk (Addr formatting, Endpoint Send/receive over a loopback DatagramLink). These double as compiler-checked usage samples and as tests (go test verifies the // Output: comments).
A standalone AFP file server: one AppleTalk node, no router services (no RTMP/ZIP, no compose/registry), serving a single read-only in-memory volume built entirely in Go. Demonstrates the three things an embedder needs: implementing fs.FileSystem from scratch and registering it via fs.RegisterFS (memfs.go), wiring router+NBP+AFP by hand (main.go), and opening a link transport the same way compose/registry does but without the config.Section plumbing (transport.go, plus a pcap-tagged variant / no-pcap stub). Builds clean under both the default and -tags pcap; runs end-to-end (binds LToUDP, starts AFP, shuts down on SIGINT) verified locally. NBP name resolution against it could not be exercised end-to-end in this sandbox (UDP multicast loopback between two processes doesn't work here at all, even for a trivial two-line send/receive using the existing ltoudp adapter with no custom code) — the wiring itself matches compose/registry/reg_localtalk.go's production pattern exactly; live verification against a real Mac/emulator is still worth doing per the plan's verification section.
main.go.bak was a sed(1) backup left over from local testing; not meant to be tracked.
Add "Extending ClassicStack — the server SDK" (§6, renumbering Credits to §7), parallel in shape to §5's client-SDK section: a package map, a walkthrough of examples/memfs-afp-server's three pieces (FileSystem-from-scratch, hand-wired transport, hand-wired router/NBP/AFP), and the SetZone gotcha the example's own comments warn about. Cross-link the new example from README.md.
Pre-1.0, so no compatibility guarantee applies yet, but the P0/P1 work already landed touches public API (ddp.Checksum export) that a downstream importer of core/protocol/* would want announced rather than silently discovered. Backfills entries for v0.1.0-v0.3.0 as placeholders (no prior changelog existed) and records the Unreleased P0/P1/P1b changes so far.
docs.yml now installs gomarkdoc and runs it over the packages meant
for external reuse (core/protocol/..., core/service/..., client/...,
plus the shared core/{csnet,binaryprimitives,config,fs,encoding,link,
buf} packages — adapter/* and cmd/* excluded as internal composition/
CLI layer), writing one page per package into site/content/reference/
with a minimal injected Hugo frontmatter. Also widen the workflow's
trigger paths to those source trees so the reference regenerates when
they change, and point Set up Go at the root go.mod (was site/go.mod)
so `go list`/`go install` run against the real module.
The generated pages aren't committed (gitignored, apart from the
hand-written _index.md landing page) — verified locally end-to-end:
ran the exact script, then `hugo --minify --gc` against the real repo
content, and confirmed the reference pages render with correct
per-package titles.
release-main.yml's build-embedded job (Pico/Pico W/Pico 2/Pico 2 W + best-effort WT32-ETH01) had its outputs pulled into the release by the release job's blanket download-artifact (no name: filter), so every tagged release shipped embedded .uf2/.bin zips alongside the desktop builds. Compile validation for these targets already runs independently on every PR (pr-ci.yml's own build-embedded job, plus refactor-harness.yml's tinygo-gate.sh), so dropping the job from the release workflow loses no CI signal -- only the artifact attachment. Deleted the job entirely (removing it from needs: alone would still let it run and upload in the same workflow run, given the blanket download-artifact) and dropped it from the release job's needs: list. The tag-only trigger gating (a separate part of this plan item) was already correct -- release-main.yml only runs on push: tags: ['v*'], and compute-release-metadata.sh fails closed on a non-tag ref -- so no change was needed there.
The Wireshark Lua dissectors (etherdfs/ltoudp/macipx/mactcp/netboot) existed but were never packaged into any release artifact. - Windows installer: new opt-in "wiresharklua" task (unchecked by default, matching the "tray" task's pattern), copying *.lua to this user's Wireshark "Personal Lua Plugins" folder (%APPDATA%\Wireshark\plugins) via a Tasks-guarded [Files] entry -- no [Code] logic needed. Untestable here (ISCC is Windows-only); reviewed carefully against the file's existing conventions. - All other platforms (plus the plain Windows zip, which shares this script): unconditional tools/ folder in the release archive, alongside the existing README/server.toml.example/extmap.conf copy. Verified locally end-to-end for all three packaging paths (Linux tar.gz, macOS zip, Windows zip via pwsh) -- each archive now contains a tools/ directory with all five .lua files.
expandStuffItClassic was a stub that always returned ErrUnsupportedFormat -- every .sit file routed through the Finder web UI's "expand archive" feature has always failed, despite Sniff/expandStuffIt doing real signature detection first. isStuffIt was also checking the wrong magic bytes for StuffIt 5 (SIT5/SITD instead of the real "StuffIt (c)1997-..." banner), so real SIT5 archives weren't even recognised by content. - adapter/archive/stuffit.go: rewritten to parse and extract via github.com/ObsoleteMadness/StuffIt-Go/stuffit, entirely in memory (stuffit.NewParser over a bytes.Reader, no temp file). A small ForkWriter/DirectoryWriter adapter (stuffitSink) feeds entries into the shared tree builder. isStuffIt now matches the real "SIT!" and "StuffIt (c)1997-" signatures. - adapter/archive/tree.go (new): the flat-path-to-nested-[]Node assembly zip.go already had (ensureParentDirs/buildZipTree) pulled out into a shared treeBuilder, so stuffit.go doesn't grow a second copy. zip.go now builds its tree through the same type. One behavioral side effect of sharing the logic: addDir now backfills ancestor directories for an explicitly-listed empty directory entry too (the original zip-only ensureParentDirs call was skipped on that path), which only ever matters for a zip with a nested empty directory and no explicit parent entries -- a strict fix, not a behavior change anything could have depended on. - Test-first per the working agreement: adapter/archive had zero tests before this change. Added stuffit_test.go against four real archives (two classic SIT!, two StuffIt 5/Arsenic, one nested) from the classicstack-web submodule's already-vetted StuffIt fixtures -- first capturing the "always ErrUnsupportedFormat" baseline against the stub, confirmed passing, then flipped to assert the real extracted tree once the fix landed. Also added zip_test.go (nested dirs, AppleDouble sidecar merge) since this change touched zip.go's internals directly. - No docs mention StuffIt as unsupported/TODO anywhere in this repo (checked) -- the plan's doc-update item is a no-op here. go.mod: manual edit (not `go mod tidy`) to move the new dependency into the direct require block -- tidy itself errors out on an unrelated, pre-existing hardware/peripherals/sdcard TinyGo package resolution issue and would have made unrelated changes trying to route around it.
Pre-existing bug carried over verbatim from the original zip.go
buildZipTree/roots loop this session's earlier refactor (P1e)
generalized into tree.go: the outer loop iterated every entry in
b.dirs — nested paths included — as a potential root, relying on a
`seen` map populated only by a PARENT's own recursive build to skip
children. Since map iteration order is undefined, a nested dir (e.g.
"a/b") visited before its parent ("a") had nothing in `seen` yet, so
it was wrongly emitted as its own top-level root; when "a" was
processed later it built "b" again, correctly, as a real child --
net result, a duplicate/misplaced node, nondeterministically (~1 in
6 runs on the two-level fixture in zip_test.go).
Caught by TestExpandZip_NestedDirs (added in the P1e commit) failing
intermittently under `go test -count=N`. Fix: the outer loop now
skips any path containing "/" outright -- only a genuine top-level
directory is ever a root, exactly mirroring the existing top-level
filter on the files loop just below it. Verified with 50 repeated
runs (`go test -count=50`), all clean.
core/service/afp/parms.go redefined its own fdBitmap*/fileBitmap*/ dirBitmap* constants -- an exact byte-for-byte duplicate (same names minus casing, same bit values) of core/protocol/afp's already-exported FDBitmap*/FileBitmap*/DirBitmap* constants in afp.go. Per the plan's staged approach (constants first as the near-zero-risk half of the AFP DTO-duplication cleanup, struct unification separately since that's the higher-blast-radius half): deleted the private const block and repointed every one of its 18 usages, across 11 files, at the protocol package (aliased `protocol`, matching the core/service/smb -> core/protocol/smb convention already used elsewhere in this codebase, since core/protocol/afp and core/service/afp share the plain package name "afp"). Careful about the false-positive collision here: catsearch.go/ handlers.go/dispatch_test.go also have LOCAL VARIABLES literally named `fileBitmap`/`dirBitmap` (the request-parsed runtime values, completely unrelated to the exported constants of almost the same name) -- confirmed the replacement only touched the 18 specific full constant identifiers (fdBitmapAttributes, fileBitmapFileNum, etc.), never the bare local-variable names, before running it. No new tests added for this pass: the existing service/afp test suite (parms_test.go, catsearch_test.go, dispatch_test.go, golden_test.go, forkio_test.go, and the rest -- 109 passing subtests) already exercises every bitmap path this touches and is the safety net a pure constant-value rename needs; nothing here changes behavior. Full build/vet/test/archtest swept clean after.
core/service/afp.ServerInfo (handlers.go) and core/protocol/afp.ServerInfo (srvrinfo.go) were field-for-field identical -- ServerName, MachineType, AFPVersions, UAMs, Flags -- one for the server's FPGetSrvrInfo pack side (serverInfoBlock), one for the client's parse side (ParseServerInfo). No server-only fields to carry over, and nothing in the package builds it via a positional/unkeyed struct literal (checked), so this unifies as a plain type alias rather than a deeper merge: `type ServerInfo = protocol.ServerInfo`. External callers are unaffected -- grepped the whole repo; nothing outside core/service/afp itself references afp.ServerInfo or SetServerInfo, and an alias is source-compatible with the old type's zero value/field-assignment usage anyway. Also folded the duplicate srvrInfoSupportsSrvrMsg flag constant (same 0x0008 value as the already-exported protocol.SrvrInfoSupportsSrvrMsg) into its two call sites (afp.go, message_test.go). serverInfoBlock's marshal logic stays in core/service/afp as-is -- it's genuinely server-only (core/protocol/afp has no corresponding Marshal, only ParseServerInfo), so there's no duplicate there to fold; moving it into the protocol package would be a bigger change than this DTO-unification pass calls for. Existing coverage (109 subtests across parms_test.go, message_test.go, dispatch_test.go, golden_test.go, conn_test.go) already exercised every path this touches; ran it before and after as the safety net for what is otherwise a pure rename with no behavior change.
…rt 3)
Per the middle-ground approach: rather than forcing a full merge of
each pair (the server side packs/parses live against Volume state
with no equivalent typed struct on the client side -- a materially
bigger, riskier rewrite of the hottest AFP wire paths for what turned
out to be a small amount of true duplication once inspected closely),
pull out just the genuinely shared header shape each pair opens with
and have both sides embed it. Params/Entries bodies and the
PathType/name handling (which really do differ -- MoveAndRenameRequest
sends one shared PathType across all three names, FPMoveAndRenameReq
parses three independent ones off the wire) are untouched.
- New core/protocol/afp.FileDirBitmaps{FileBitmap, DirBitmap} with a
Marshal helper, embedded by GetFileDirParmsReply and EnumerateReply
(client-parse side) and by core/service/afp's FPGetFileDirParmsRes
and FPEnumerateRes (server-marshal side, now calling
FileDirBitmaps.Marshal instead of hand-rolling the same two
AppendBE16 calls).
- New core/protocol/afp.MoveHeader{VolID, SrcDirID, DstDirID},
embedded by MoveAndRenameRequest (client) and FPMoveAndRenameReq
(server) -- the server's field was previously named VolumeID;
renamed to VolID (nothing else read it, confirmed by grep) so both
sides share one name too, not just one shape.
Every construction site updated to the nested-embed literal form
(core/protocol/afp/commands.go, core/service/afp/handlers.go +
models_test.go, client/afp/filesystem.go). Verified byte-exact:
the existing golden tests (TestFPGetFileDirParmsRes_Header,
TestFPEnumerateRes_Header/_MarshalGolden, TestFileDirParmsHeader_*)
all still pass unchanged, plus the full 109-subtest service/afp suite,
core/protocol/afp, and client/afp. Full repo build/vet/test/archtest
swept clean.
zip.go and stuffit.go already got coverage in the P1e StuffIt work; archive.go (the Sniff/Expand dispatch), binhex.go, and macbinary.go had none. Tests use real samples already present via the classicstack-web submodule (a real BinHex-4.0-wrapped GIF, a real MacBinary-wrapped StuffIt download) rather than synthetic wrapper bytes, so they exercise the actual decoders end-to-end -- probed each sample first (a throwaway local test, discarded) to get the real expected Name/fork-length/FinderInfo values before writing assertions against them, rather than asserting whatever the code happened to currently produce. - binhex_test.go: looksBinHex detection + rejection, expandBinHex against the real sample (checks the decoded GIF8 magic), a synthetic corrupt-but-plausible input, and unsupported input. - macbinary_test.go: isMacBinary detection + rejection (including a one-byte-short header and a zip-signature false lead), expandMacBinary against the real sample (which is itself a MacBinary-wrapped StuffIt archive -- checks the unwrapped SIT! magic and the FinderInfo type/creator), truncated and unsupported input. - archive_test.go: Sniff's three independent paths (extension, Finder type code, content sniff) and its false case; Expand's dispatch to all four formats through the one public entry point; the unsupported-format case; and a corrupt-zip case confirming Expand propagates ErrCorrupt directly rather than falling through the other three decoders and masking it as ErrUnsupportedFormat. Verified with `go test -count=20`, no flakes. Full repo build/vet/test swept clean after.
This package (checksum/fragmentation/NAT engine for the MacIP gateway) had zero tests. - iputil_test.go: RawChecksum against an independently-verified worked example plus the self-verify and odd-length-padding properties; TransportChecksum against a by-hand pseudo-header computation plus its own self-verify property; BuildIPv4Packet's fixed fields and checksum; FragmentIPv4's single-piece passthrough, DF-bit refusal, offset/MF-flag/8-byte-alignment chaining across a real multi-fragment split with payload reassembly, and the too-short-to-parse guard. - osnat_test.go: allocICMPNatID's uniqueness, used-id skipping, and wraparound; Forward's drop paths (too-short packet, unsupported protocol, ICMP with no raw socket available) via a directly constructed *OSNAT that bypasses New()'s real ICMP socket and background goroutines entirely; and the three TCP segment-building helpers (sendTCPSYNACK/sendTCPSegment/sendTCPRST) against a directly-constructed flow, checking ports/seq/ack/flags/payload/MSS option and a self-verifying IP checksum. osnat.go's actual flow-handling logic (forwardICMP/forwardUDP/ handleTCP's live-socket paths) is intentionally not covered here -- it's built entirely around real OS sockets (net.DialUDP, TCP dial, a raw ICMP socket) with no injection seam, and adding one would be a production-code redesign beyond what a baseline-coverage pass calls for. What's covered is everything reachable without a real socket: every pure helper, the dispatch/guard logic, and the packet-building that's actually exercised by the TCP write-back paths. Verified with `go test -race -count=5`, no flakes. Full repo build/vet/test swept clean after.
adapter/control/ubus had no _test.go of its own -- it did have real indirect coverage via adapter/control/parity_test.go (Status, Subscribe, Start, Config, ListFSTypes, ListZones, Users CRUD), but Stop/Restart/HostInfo/ShareBackends/ParamsFor/Reconfigure/AddInstance/ RemoveInstance/Save/ListInterfaces/SetInterface/RemoveInterface, the four DiagProvider-backed drill-downs, and the wire-protocol edge cases (unknown method, malformed JSON) were untouched. Built on the same real-Plane-over-real-Supervisor pattern parity_test.go already established (control.New + supervisor.New) rather than hand-writing a fake against control.Plane's large interface, using a real Unix domain socket in t.TempDir() throughout. Two real findings surfaced while writing these (not bugs -- both are now documented in the test comments rather than "fixed" away): - AdapterClient.Config() can't round-trip a model whose Sections/Lists actually hold populated config.Section values -- plain encoding/json can't unmarshal into an interface with no concrete type to target. Tests that need to observe a mutation verify server-side against the live *config.Model instead (safe: the RPC response is only sent after the handler's mutation completes, so there's a real happens-before by the time the client call returns). - port.Section implements config.NamedSection, so Reconfigure on one routes it through Model.Lists, not Model.Sections (setSectionLocked's documented NamedSection branch) -- TestReconfigure follows that real routing rather than asserting a singleton path a NamedSection never takes. Verified with `go test -race -count=10`, no flakes. Full repo build/vet/test swept clean after.
The only untested pair in core/protocol/core/service per the plan's survey (every other protocol/service pair already had coverage). - core/protocol/rip/rip_test.go: Marshal/Unmarshal round trip pinned to exact wire bytes, the append-style contract, a bare (no-entry) packet both directions, the too-short-for-a-header rejection, and the trailing-partial-entry-ignored tolerance (Ethernet minimum-frame padding). - core/service/rip/rip_test.go: a fakeSender records/channels every IPXSender.Send call, so the Responder's actual behavior is checked without a real IPX network. Covers SetNetworks' zero-entry filter; HandleDatagram answering an owned network, the wildcard matching every owned network, silently ignoring an unowned network or a Response packet (this responder doesn't learn routes), and safely dropping nil/malformed input; Start's immediate directly-served broadcast and Stop's shutdown broadcast at HopsUnreachable, both to the IPX broadcast address; Start/Stop idempotency (no panic on a second Stop's channel double-close); and the no-owned-networks no-op case. Verified with `go test -race -count=15` across both packages, no flakes. Full repo build/vet/test/archtest swept clean after.
client/trace is process-wide singleton state (one shared stderr sink, a muted-scope set, an extra-sinks slice) driving every client transport's wire narration, and had zero tests. Covers: SetVerbose toggling Verbose()/a Logger's Trace-enabled state; SetScope muting one named scope independently of the global verbose toggle (and restoring it); the documented AddSink contract that an extra sink's own threshold can capture records even while the shared verbose toggle is off (client log files keep working with -v absent); AddSink(nil) being a safe no-op rather than a future nil-pointer panic on Write; CloseExtraSinks actually closing sinks and stopping the fan-out to them; and SetLevel driving the same shared threshold SetVerbose does, at an arbitrary level. Since this is real global mutable state shared across every test in the package, each test resets it via t.Cleanup (SetVerbose(false) + CloseExtraSinks) and uses a scope name unique to itself, so ordering never matters -- verified with `go test -race -count=10 -shuffle=on`, no flakes. Full repo build/vet/test swept clean after.
Per the plan's own investigation of this package (a documented, correctly-scoped compatibility shim over client/link with no independent protocol logic): just the flag-binding surface, low priority. Covers Flags' defaults and argument parsing, plus Open's one piece of real logic -- srcNode 0 with Claim false must error rather than silently asserting the invalid node 0. This closes out P3 (baseline tests for previously-untested packages): adapter/archive, adapter/macipgw/nat, adapter/control/ubus, core/protocol/rip + core/service/rip, client/trace, and this package.
The capability-tag table (afp/smb/pcap/webui/...) was already well-maintained; the embedded-target tags (tinygo/pico/picow/pico2/ pico2w/esp32/wt32eth01) were only a one-line aside pointing at a design charter, not documented as a family a reader could act on. Added a real "Embedded target tags" subsection: what core/tinygo means as the implicit umbrella tag (and why there's no separate "embedded" tag -- Go has no tag-alias mechanism), the pico/pico2/ picow/pico2w relationship to TinyGo's -target vs -tags split, the esp32/wt32eth01 scope including the documented ESP-IDF linking gap, and the concrete build/CI entry points (scripts/build_pico.sh, scripts/build_wt32eth01.sh, make tinygo-gate). Kept a pointer to .refactor/00-DESIGN.md for the deeper design rationale rather than duplicating it. Also pointed core/csnet's doc comment at the new table, since it's the package whose !tinygo/tinygo split this documents most directly. Verified: `hugo --minify --gc` renders the updated page cleanly.
core/auth/cred.go carried a stale `// TODO: Move this to are shared binaryprimitives` comment that directly contradicted the file's own design-rationale paragraph above it (which already claimed hex coding "stays hand-rolled below regardless") -- and the TODO was right: this was real, not just theoretical, duplication. core/csnet/ macparse_tinygo.go had a byte-for-byte identical hexNibble function (confirmed before touching anything). - New core/binaryprimitives/hex.go: EncodeHex/DecodeHex/HexNibble, test-first (hex_test.go), matching the existing PutBE16-style package conventions. - core/auth/cred.go: SaltHex/HashHex/ParseCredential now call bp.EncodeHex/DecodeHex; deleted the local encodeHex/decodeHex/ hexNibble/hexDigits and the stale TODO. Updated the design-rationale comment to match. cred_test.go's TestHexRoundTrip (now redundant with core/binaryprimitives' own tests) removed; TestPBKDF2SHA256Vector repointed to bp.EncodeHex. - core/csnet/macparse_tinygo.go: ParseMAC's decode loop now calls bp.HexNibble instead of its own copy. - core/csnet/mac.go's FormatMAC keeps its own inline hex encoding -- genuinely different shape (upper-case, colon-separated), not a duplicate of EncodeHex's plain lower-case form, so left as-is rather than forced into the same helper. - Fixed the identical stale "hex coding stays hand-rolled... matching core/binaryprimitives' style" comment in cmd/cs-tinygo/main.go's M8a note, found while touching this. core/csnet/macparse_tinygo.go is tinygo-only (a normal `go build` never compiles it); verified with a real `GOOS=linux GOARCH=amd64 tinygo build ./cmd/cs-tinygo` (which transitively reaches core/csnet per `go list -tags tinygo -deps`), exit 0. Full repo build/vet/test/archtest swept clean otherwise (one TestElectionWon flake in core/service/browser under full-suite load, confirmed pre-existing and unrelated -- passes 5/5 in isolation and clean on a full-suite rerun).
CLAUDE.md described a flat pre-restructuring layout (internal/app as the run-core, top-level appletalk/router/port/service/pkg/config directories, koanf+go-toml config parsing, [LToUdp]/[TashTalk]/ [Volumes.*] TOML sections) that predates the current five-ring core/adapter/compose/client/cmd structure ARCHITECTURE.md already documents accurately and keeps current -- contributors reading CLAUDE.md first were getting conflicting guidance. Replaced the stale "Core Data Flow"/"Key Packages" section with a short accurate summary of each ring plus a strong pointer to ARCHITECTURE.md as the authoritative source, rather than hand- maintaining a second package table that would just go stale again. Also fixed: the go test example path (./service/afp/... -> ./core/service/afp/...), the CNID-tracking claim (defaults to in-memory, not SQLite -- sqlite is an opt-in build tag), the config-parsing claim (koanf isn't even in go.mod anymore; go-toml/v2 only) and its TOML section names (checked against the real server.toml.example: [[afpvolumes]], [[smbshares]], [MacIP], etc., not the stale [AFP]/[Volumes.*]/[LToUdp] spellings), and the hardcoded Go version (now points at go.mod instead of duplicating a number that will drift again). Also removed an accidental duplicated "### AFP Architecture" heading from the edit. Verified every cited path/file exists, and that the two example commands (go build .../cmd/classicstack, go test .../core/service/afp/...) actually work. Docs-only change; full repo build/vet/test swept clean (one core/service/browser flake under full-suite load on the first pass, gone on rerun -- confirmed pre-existing/unrelated, this commit touches only CLAUDE.md). This closes out P5's items worth doing now: the P0 csconnect stale-comment note turned out already resolved by P0 itself, and the runport.go/ntcreate.go TODOs are deliberately-deferred design decisions the plan says to leave alone. The diagnostic-tool shared- flags helper remains open (cosmetic-only, no bug) if wanted later.
…s (P5) The 8 AppleTalk/IPX diagnostic probes (csecho, csgetzones, csnbp, cspap, csipxping, csncpinfo, csnetsend, csnetview) each hand-rolled their own flag.* calls for the same option groups, with drifting help text and inconsistent coverage: -v and -ifacetype existed on some tools but not others. New package cmd/internal/diagflags gives each group (Common: -v/-version; LLAPSource: -net/-src + range validation; and the per-flag Iface/IfaceType/MAC/ListIfaces registrars) a single definition that all 8 commands now call, instead of retyping flag definitions and validation logic by hand in each main.go. Consistency fixes made possible by unifying the registration: - -v/-version are now present on every probe, including csipxping and csncpinfo, which previously had neither; both gained real (not cosmetic) stderr trace lines on send/receive so the new flag isn't a no-op. - -net/-src help text and the src-node range check (0, or 1..254) are now identical across csecho/csgetzones/csnbp/cspap; cspap's slightly different validation expression was equivalent but is now literally shared code, not just equivalent code. - The AppleTalk transport flags (-transport/-iface/-device/-baud /-list-ifaces/-claim) stay owned by cmd/internal/atlink, which this package composes alongside rather than duplicates. No functional change to existing flag names/behavior for the 7 flags that already existed; -v is new for csipxping/csncpinfo, matching the plan's stated allowance to rename/extend options for consistency without preserving exact prior surface. Verified: go build/vet -tags "all pcap" ./..., go test -tags all -count=1 ./..., go test -tags all ./core/internal/archtest/..., tinygo build ./cmd/cs-tinygo (unaffected — cmd/ is outside the TinyGo blank-import set, checked per the session's verification bar anyway), plus a manual run of all 8 built binaries' -version/-h/ -list-ifaces and a bad -src validation error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
Author
Architecture review remediation (P0–P5)Ran a full architecture/code-quality review against this branch ahead of 1.0 and worked through the prioritized findings, test-first, one commit per fix:
Every change was verified with |
…parsing
localtalk_tinygo.go's openLToUDP/openTashTalk stubs still had the pre-claim
signatures (2 return values, no claim bool), while localtalk.go had grown a
claimed-node return value and claim parameter — broke the TinyGo/Pico build.
core/csnet.ParseMAC's doc comment promised bare-hex support ("001122AABBCC")
but delegated straight to net.ParseMAC, which has no separator-less form —
failing tests in every package that converges on csnet.ParseMAC (macipgw,
csconnect, core/port, adapter/control/finder). Added an explicit 12-hex-digit
fallback.
cmd/cs-tinygo (the amd64 gate target) never imports client/link, so it missed the localtalk.go/localtalk_tinygo.go signature drift that broke PR #20's separate "Build Embedded (TinyGo)" job. hardware/pico does pull in client/link, so building it here closes that gap for `make tinygo-gate`.
…rors golangci-lint's errorlint linter caps identical-message output at 3, so the CI Quality job only ever surfaced 3 of the 8 actual violations across these four test files, leaving the other 5 to fail on the next pass.
NJRoadfan
reviewed
Aug 29, 2026
There was a problem hiding this comment.
Dev builds of Wireshark 4.7 have full MacIP support with IP GP config packet dissection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This merges the
feature/refactorbranch: a ground-up rewrite of ClassicStack onto ahexagonal (
core/adapter/compose) architecture, plus everything built on top ofit since the rewrite landed. It replaces the old
internal/app,port/,protocol/,service/,router/,pkg/,netlog,capture/,config/tree entirely.253 commits, 2173 files changed (+393,924 / -64,367). Latest tag on
mainisv0.3.0;this is proposed as 1.0.0-RC.
.refactor/00-DESIGN.md,ARCHITECTURE.md):core/holdsprotocol-pure logic with zero I/O imports (enforced by an import-graph CI gate —
reflect,net,encoding/binary,encoding/jsonetc. are all forbidden incore/,which is what keeps a TinyGo/embedded build possible);
adapter/holds the concreteI/O (pcap, sqlite, http, uci, serial, dsi, smbtcp);
compose/wires componentstogether via a registry + supervisor with dependency-ordered start/stop.
strangler migration, milestones A–D, M1–M11, cutover) — see
.refactor/TODO.mdforthe full step-by-step log and design rationale for each seam. The cutover itself
(deleting the legacy runtime, repointing binaries at the new run-core) landed
2026-06-18 (
21f8d1b,511299a); everything since is feature work on the newarchitecture, not migration.
csmount/csfs)mounting AFP/SMB/NCP/EtherDFS shares via WinFsp/macFUSE/libfuse; a Finder-style web
admin UI (now a git submodule,
third_party/classicstack-web); macOS/Windows trayapp (
cmd/classicstack-tray); TashTalk serial and LToUDP LocalTalk transports;direct-hosted SMB-over-IPX and NetBIOS browser/messenger services; a Windows
installer (Inno Setup) built in CI; read-write ZIP filesystem backend.
Compatibility notes
[Bridge]is now theonly source for backend/device/MAC/frame mode (see
ARCHITECTURE.md). Existingserver.tomlfiles fromv0.3.0will need migration — there is no automatedupgrade path in this PR.
git submodule update --init --recursive(
third_party/classicstack-web). CI andREADME.mdare already updated for this.cmd/classicstacknow boots throughcmd/internal/cli→ the composeruntime instead of
internal/app. Flags/behavior should be equivalent per.refactor/TODO.mdM9/M10, but this is the highest-risk surface for regressionssince it's the main entry point everyone runs.
Known gaps / follow-ups (not blocking, but worth tracking post-merge)
ARCHITECTURE.mdstill describes the pre-refactor runtime topology almost verbatim(only one line changed vs.
main) — it doesn't yet describe the core/adapter/composerings, the registry/supervisor model, or the new cmd/internal/cli entry point. Worth
a follow-up doc pass.
.refactor/TODO.md, a handful of milestones are still open:M8a(share config →share.Managerwiring for AFP/SMB volumes),M8-spa(new-ring SPA, explicitlydeferred/held),
M11opener-dispatch follow-ons. None of these block a build, butthey're real scope not yet closed out.
scripts/ci/compute-release-metadata.sh's tag regex only accepts strictvMAJOR.MINOR.PATCH— av1.0.0-rc1tag will fail CI's release job. If you want anactual pre-release tag (not just merging to
main, which auto-cuts adev-<sha>prerelease), that script needs a pre-release-suffix case first.
CI
Refactor Harness CIis green on the current head (75db6b8, run32545567182).
Note this PR will run under
pr-ci.ymlonce opened againstmain, which hasn'texercised this tree before — worth watching the first run closely.
Heads-up: merging this triggers a release
release-main.ymlruns on every push tomainand publishes a GitHub Release(
dev-<sha>, marked prerelease) automatically — merging this PR will cut a releasebuild across all platform/variant matrix targets. Flagging this explicitly since it's
not something a normal PR merge does in most repos.