Skip to content

ghost.js8 0.1.0: native JS8Call via KiwiSDR, GhostNet autopilot, ops console UI, amd64 + arm64 - #1

Merged
d3mocide merged 34 commits into
mainfrom
claude/wizardly-rubin-uuz2ia
Oct 8, 2026
Merged

d3mocide merged 34 commits into
mainfrom
claude/wizardly-rubin-uuz2ia

Conversation

@d3mocide

@d3mocide d3mocide commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Everything for the 0.1.0 release: the receive-only MVP, the GhostNet autopilot, the redesigned UI, 64-bit ARM support, a multi-arch release pipeline and pre-release security hardening. Design and decisions are in docs/architecture.md, which also records where the build deviates from the plan.

What's in it

  • Decoder container:
    • Base and JS8Call: Ubuntu 24.04 pinned by digest, with native JS8Call 3.0.3. The AppImage is chosen per architecture (x86_64 / aarch64), each pinned by its own SHA-256, and is unpacked with unsquashfs without ever being executed.
    • Runtime: Xvfb, a PulseAudio null sink → module-remap-source → JS8Call, and a discard sink.
    • Supervision: a FATAL process exits the container with code 70. The healthcheck requires a live capture stream.
  • decoder-agent: writes PCM into PulseAudio and owns the JS8Call UDP API, sending read-only requests only. It polls call activity and reports health facts.
  • KiwiSDR adapter:
    • SND and W/F streams are paired under one session ID.
    • Big-endian PCM is normalized, honouring the little-endian flag.
    • Typed errors cover rejections and bad formats.
    • Only per-channel settings are ever sent.
  • Bridge:
    • v1 contract (Pydantic → JSON Schema → TypeScript, with a CI drift check) and an evidence-based health state machine.
    • Binary audio/waterfall fan-out, a receiver directory, SQLite history, reconnect policy and JSON logs.
  • GhostNet autopilot (opt-in, docs/ghostnet.md):
    • Follows the GhostNet v1.5 JS8 windows for one region on the nearest suitable KiwiSDR.
    • Parks on 7.107 MHz between windows.
    • Records traffic, a waterfall PNG and FLAC audio for each window.
    • Steps aside when a viewer takes control.
  • Web, the "JS8 ops console":
    • Svelte 5, frosted glass over a live ASCII "signal" backdrop. Radio rings carry the callsign of each new decode.
    • Stencil ghost mark with a Share Tech Mono wordmark, and an RX mode chip (capability, not brand).
    • Waterfall, traffic, stations, map, audio monitor and health; a GhostNet panel, net log viewer and @GSTFLASH alert.
    • Responsive and accessible. No transmit control exists.
  • Deploy:
    • docker-compose.yml: only web (nginx) publishes a port, on 127.0.0.1 by default. The decoder runs on an internal network with no egress.
    • Every container drops all capabilities and sets no-new-privileges.
    • Compose can pull published images (GHOSTJS8_IMAGE_PREFIX, GHOSTJS8_TAG).
  • Release pipeline: a vX.Y.Z tag builds natively on amd64 and arm64 runners. Images are pushed by digest and merged into multi-arch manifests on GHCR (ghostjs8-{web,bridge,decoder}), with provenance and an SBOM.

Security review (pre-0.1.0)

A dedicated review found no high-severity issues. Every medium and low finding is fixed and tested:

  • Receiver addresses:
    • Receivers chosen by viewers or the autopilot are resolved at connect time, and every address is checked. The bridge then dials the checked IP; the URL keeps the name for Host/SNI.
    • That closes 127.1/0x7f000001 tricks, internal DNS names and DNS rebinding. HTTP redirects are refused.
    • Private receivers are opt-in. The operator-configured boot receiver stays trusted.
  • Error details: viewers see generic connection errors, so the bridge can't be used as a port scanner. Address refusals are terminal.
  • Politeness: a station-wide floor of 5 s between receiver connects; a burst of re-selects connects only the latest choice.
  • WebSocket origin: same-origin by default and port-aware. nginx now forwards $http_host; this was found by end-to-end testing through nginx.
  • Kiwi password: may not contain whitespace or control characters, because it is sent inside SET auth.
  • nginx: security headers apply on every location, plus a body-size limit.
  • IPv6: receiver literals are bracketed correctly in URLs.

Findings along the way

  • JS8Call emits RX.SPOT only when PSKReporter spotting is on, so the agent polls RX.GET_CALL_ACTIVITY instead.
  • RX.DIRECTED omits the sender and appends ♢. PSKReporter and APRS spotting default to on; both are forced off.
  • nginx serves .mjs as octet-stream, which breaks MapLibre's worker under nosniff. Fixed in the template.
  • Upstream publishes an aarch64 AppImage but no checksums. Both architectures are pinned on first download, and the GitHub build attestation is documented.

Verification

  • make check: ruff, mypy --strict, 248 pytest, eslint, prettier, svelte-check and 53 Vitest.
  • make acceptance:
    • The real upstream recording goes through fake-Kiwi framing, the bridge, the agent, PulseAudio and native JS8Call; K0OG: KN4CRD SNR +02 reaches the browser WebSocket.
    • It passes on amd64 with the hardened compose settings, and on arm64 under full emulation.
    • CI now also runs it on native arm64 runners.
  • make e2e: 20 Playwright tests (desktop and phone), including the axe scan, the no-transmit assertion, GhostNet, and the backdrop toggle.
  • Production stack checked by hand:
    • all containers healthy with capabilities dropped;
    • security headers on /, assets, .mjs and /api;
    • same-origin WebSocket allowed and cross-site origins refused;
    • 127.1, 0x7f000001, decoder and private IPs refused as receivers.

Not done yet / follow-ups

  • Live validation: against a real public KiwiSDR, and a live GhostNet window (checklist in docs/operations.md).
  • Bridge features: a pool of decoder slots in one bridge, so several receivers can be decoded at once; a hold-tuning lock.
  • Per-IP client limits: these need trusted X-Forwarded-For from the reverse proxy.
  • First release: after merge, tag v0.1.0 to publish images.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5

claude added 22 commits October 8, 2026 01:47
…3.0 license

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…and pytest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…int, prettier and vitest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…Renovate config

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…ap-source and discard sink

Ubuntu 24.04 by digest; AppImage downloaded, SHA-256 verified and extracted at
build time. Xvfb, PulseAudio and JS8Call run under supervisord; a FATAL
critical process exits the container with code 70. JS8Call.ini is rendered from
the environment on every start: stable Pulse device IDs, monitoring on, UDP API
to localhost, decode filter 100-3000 Hz, and spotting/transmit forced off.
Healthcheck requires JS8Call to hold an uncorked capture stream on ghost_rx_in.

Verified locally: the upstream A_1_4.wav fixture played into ghost_rx decodes
'K0OG: KN4CRD SNR +02' over the UDP API on both replays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…nd fake Kiwi

Receiver interface with typed errors (rejections, unreachable, timeout,
pairing, malformed frames, unsupported audio formats) and an injectable clock.
Pure framing functions parse SND/W/F/MSG, normalize big-endian PCM to s16le,
honor the little-endian flag without double-swapping, and reject compressed,
stereo, IQ and non-12 kHz audio. KiwiReceiver opens both streams under one
session id, sends only per-channel settings, and keeps alive.

ghostjs8.sim.fake_kiwi serves real SND/W/F framing with UTC-aligned WAV
playback, synthetic waterfall rows, pairing enforcement and fault injection.
64 tests (unit + localhost integration).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
The agent runs inside the decoder container: it accepts one bridge WebSocket,
pipes PCM into the ghost_rx null sink through a bounded pacat writer, owns
JS8Call's localhost UDP API (learning the reply port from incoming datagrams),
forwards RX.ACTIVITY/RX.DIRECTED/RX.CALL_ACTIVITY verbatim, polls call
activity (RX.SPOT is suppressed while spotting is off), and reports health
facts including whether JS8Call holds an uncorked capture stream. Only
read-only requests can be sent; TX.* is impossible by construction.

The bridge-side adapter normalizes JS8Call messages (directed traffic shown as
'FROM: ...' with the EOM marker stripped) into decode and spot events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
Pydantic contract v0 (hello, decode) with protocol version; bounded fan-out
hub with per-session client limit; station wiring receiver -> decoder ->
browsers with jittered exponential backoff on both sides; FastAPI /ws with
Origin allowlist and /healthz; env-based settings with optional boot-time
receiver for persistent listening; python -m ghostjs8 entry point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
Fetches A_1_4.wav from JS8Call-improved at 4c592bd9 and verifies its SHA-256,
serves it from fake-kiwi with real SND framing aligned to UTC 15 s boundaries
(twice), runs bridge + decoder (now with the supervised decoder-agent) on an
isolated network, and asserts 'K0OG: KN4CRD SNR +02' arrives on the
browser-facing WebSocket. Passes locally. Bridge image on python:3.12-slim by
digest with uv-locked hashed dependencies; CI gains an acceptance job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…cript

Pydantic server/client unions (hello, history, session, health, decode,
station, receiver_status, error, pong; hello, subscribe, tune,
select_receiver, disconnect_receiver, ping) plus the receiver directory HTTP
type. make contract generates contract/schema.json and
web/src/lib/protocol/generated.ts via json-schema-to-typescript; CI fails on
drift. Binary audio/waterfall layouts in contract/binary.py and
docs/protocol.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
Clock-injected tracker for bridge, decoder process/API/capture, receiver,
audio and waterfall freshness, with since/last_seen per component and a
last-decode time set only by real decodes. Covers decoder crash while the
bridge stays up, agent link loss, stale reports and heartbeats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
Directory parses the public KiwiSDR list (rx.linkfanel.net), caches 15 min,
serves stale on error without hammering. Grids are normalized and only valid
4/6/8-character locators are ever passed on. SQLite (WAL) keeps decodes and
heard stations with retention pruning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
… logs

Station drives the receiver lifecycle with a reconnect policy (no retry after
auth/redirect or unsupported formats; at least 60 s after full/busy), handles
tune/select/disconnect from browsers, fans out audio and rate-capped
waterfall frames only to subscribed clients, debounces ADC overload, persists
decodes/stations, sends history on join, and can release the receiver when
nobody is watching. The API validates receiver addresses (no loopback,
link-local or service names), rate-limits control messages, and exposes
/readyz and /api/receivers. Logs are structured JSON with session ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
Compose's bake builder refuses to read /dev/null as a build secret without an
extra --allow grant, which failed the acceptance job in CI. Default the
extra_ca secret to an empty placeholder inside the project and build without
bake so local and CI builds behave the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
Binary frame decoding, a reconnecting bridge client that validates server
messages and releases its socket on stop, a bounded PCM ring buffer with
drop/resync semantics, waterfall colormaps (spectre default, viridis, inferno,
grey) and peak-preserving row blitting, Maidenhead validation mirroring the
bridge, JS8Call's default band presets, a pure state reducer that collapses
activity+directed records of one transmission, and a situation model that
tells band-quiet / receiver-full / ADC-overload / decoder-offline apart.
42 Vitest tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
Glass panels with redaction-bar headers, a classification strip with UTC
clock and link state, and design tokens as the single source for colour,
blur, radii, spacing, type and motion (honours reduced motion and reduced
transparency, solid fallback without backdrop-filter). Self-hosted JetBrains
Mono and Space Grotesk, SVG mark and favicon.

Panels: receiver directory search + manual host:port, JS8 band presets and
passband tuning, AudioWorklet monitor with bounded buffering, DPR-aware
scrolling waterfall with passband overlay, decoded-traffic timeline with a
throttled live region, heard stations, and a MapLibre map of stations with
valid grids over bundled Natural Earth outlines (no tile requests).
Responsive: three columns, two on tablets, tabs on phones. No transmit
control exists anywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
fake-agent speaks the real agent protocol and emits scripted JS8 traffic;
fake-kiwi now honours the requested waterfall view and paints JS8-like tones.
tools/dev-stack.sh runs fake Kiwi + fake agent + real bridge without Docker.
Playwright (desktop + phone) checks link, traffic, stations, waterfall pixels,
map grids, audio start/stop, an axe scan, and that no transmit control
exists in any view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
web: Vite build served by nginx-unprivileged (digest-pinned) with a strict
CSP, security headers, a same-origin /ws proxy with upgrade headers and long
timeouts, and an explicit JavaScript MIME type for .mjs module workers.
docker-compose.yml publishes only the web port (bound to 127.0.0.1 by
default), keeps the decoder on an internal network with no egress, and
persists the bridge's SQLite store on a volume. CI builds the web image and
runs the Playwright suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
operations.md covers deployment behind Caddy / Nginx Proxy Manager, the
health model, what each headline means, troubleshooting, KiwiSDR etiquette,
the manual real-receiver validation checklist, and the transmit extension
point. SECURITY-UPDATES.md lists every pin, the Renovate setup, the JS8Call
update procedure (gated on make acceptance) and hardening in place.
architecture.md records where 0.1.0 deviates from the plan (basemap, one
station per bridge, no hold lock yet, RX.CALL_ACTIVITY as station source).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
@d3mocide d3mocide changed the title M1: repo skeleton, tooling, agent notes and CI ghost.js8 0.1.0 MVP: native JS8Call via KiwiSDR, truthful health, listening-post UI Oct 8, 2026
claude added 4 commits October 8, 2026 04:14
Encodes the GhostNet v1.5 JS8Call plan (Thursday nets on 7.107 MHz, Saturday
data bridges on 14.107 / 3.575 / 7.107 MHz) per region. When enabled, the
autopilot tunes into each window with a 10 minute pre-roll on the nearest
free public KiwiSDR to the operator's home grid (benching receivers that
reject or fail to connect), records the window, and parks on 7.107 MHz
between nets. A viewer taking manual control pauses it until the window ends.

Each net is recorded as its own traffic log, a FLAC of the receiver audio
(soundfile/libsndfile) and a waterfall PNG (one row per second, -500..3500 Hz,
spectre palette, stdlib encoder), with its own retention. New ghostnet status
message, /api/nets endpoints with strict id validation, opt-in settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
… output in lint

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
- GhostNet panel: autopilot mode, current/next window with countdown
  (UTC + local), recording indicator, receiver choice reason, recorded nets
- Net log view (#/net/<id>): waterfall image with time/offset axes, FLAC
  playback with click-to-seek, stations and traffic tagged FLASH/GN/DIR
- Live @GSTFLASH banner (role=alert) with acknowledge
- seed-net command and dev-stack GHOSTNET=1/SEED_NET=1 for UI work and e2e
- Timeline list is keyboard-focusable when it scrolls (axe)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…notes

- docs/ghostnet.md: schedule, configuration, receiver choice, override
  semantics, recording layout and API, UI, dev workflow
- .env.example, README, operations, CHANGELOG, AGENTS gotchas
- Net log: don't clip the +0m time tick

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
@d3mocide d3mocide changed the title ghost.js8 0.1.0 MVP: native JS8Call via KiwiSDR, truthful health, listening-post UI ghost.js8 0.1.0 MVP: native JS8Call via KiwiSDR, truthful health, listening-post UI, GhostNet autopilot Oct 8, 2026
claude added 2 commits October 8, 2026 04:45
… layout

- Tokens: ink base, aurora backdrop colours, aqua→violet accent gradient,
  mint signal, softer warn/alert; larger radii; lighter, more translucent glass
- Slow-drifting aurora behind the glass (off for reduced motion/transparency);
  film grain kept, scanlines dropped
- Panels: frosted card with lit gradient edge, sentence-case titles
- Sticky glass top bar replaces the classification banner + masthead
- Hero strip: gradient frequency readout, situation, decodes/stations/last decode
- Pill buttons with gradient primary/pressed states; glass inputs; chips
- Traffic rows: category rail, sender callsign highlighted, chip tags
- Waterfall: inset canvas, retuned "spectre" aurora colormap (bridge PNG
  palette mirrored), restyled map, GhostNet panel, net log and flash alert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
… aurora

- Canvas ASCII field behind the glass: drifting ionospheric static with
  per-cell flicker, radio-wave rings ((( ~ ))) that ripple out with the
  sender's callsign on each new decode, a soft pulse on every UTC-aligned
  15 s JS8 slot, and an occasional ASCII ghost
- Deep solid gradient base (ink -> midnight indigo -> plum); aurora removed
- Top-bar toggle for a still gradient (remembered per viewer); reduced
  motion draws a single frame, reduced transparency hides the canvas;
  paused while the tab is hidden; capped at 16 fps
- Stronger frost so backdrop glyphs never compete with panel text
- Unit tests for the engine's helpers; e2e for the toggle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
claude added 6 commits October 8, 2026 05:11
- Logo: line-art sheet ghost in military-stencil style (bridges cut into the
  outline), radio-wave hem, angled slit eyes; favicon to match
- Wordmark "ghost.js8_" in Share Tech Mono with a blinking cursor (static
  under reduced motion); tagline in the same face
- --g-font-display token: panel titles (uppercase, tracked), the hero
  frequency readout and the net-log heading use it
- @fontsource/share-tech-mono pinned at an exact version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…sets

- docs/assets: banner (stencil ghost + wordmark over the signal backdrop),
  dashboard screenshot, standalone mark
- README trimmed to the essentials: what it does, quick start incl.
  GhostNet, docs index, develop, license
- Backdrop callsign labels slightly fainter behind glass

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…chip

- Tagline "JS8 ops console" in the top bar, banner, README and meta; the
  footer and hero labels drop "receive-only"/"listening post"
- The "Receive only" badge becomes an RX mode chip: capability, not brand,
  tied to the contract's hello.receive_only; a future transmit extension
  changes the chip, not the name
- README: RX mode badge and an "RX today, built to grow" note; the
  receive-only invariant and its test are unchanged
- Refreshed banner and dashboard screenshot

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
… in CI

- Decoder picks the JS8Call AppImage by TARGETARCH (x86_64 | aarch64), each
  pinned by its own SHA-256; the AppImage is unpacked with unsquashfs at the
  offset after its ELF runtime and never executed during the build
- CI: decoder build+boot and the real-recording acceptance test run on
  ubuntu-24.04 and native ubuntu-24.04-arm; web and bridge images build on
  both (bridge also proves FLAC writing in a read-only container)
- release.yml: vX.Y.Z tag -> native per-arch builds pushed by digest, merged
  into multi-arch manifests on GHCR (ghostjs8-{web,bridge,decoder}:X.Y.Z,
  :latest) with provenance and SBOM; actions pinned by SHA

Verified locally: amd64 acceptance passes; arm64 acceptance passes under
qemu emulation (aarch64 JS8Call decodes the upstream recording).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
…t floor, hardening

From a pre-0.1.0 security review:
- Receivers chosen by viewers or the GhostNet autopilot are resolved at
  connect time, every address is checked, and the checked IP is dialed
  (URL keeps the name for Host/SNI). Closes 127.1 / 0x7f000001 tricks,
  internal DNS names and rebinding. Redirects are refused. The operator's
  boot receiver stays trusted.
- Private receivers are opt-in (GHOSTJS8_ALLOW_PRIVATE_RECEIVERS=false by
  default); loopback only for the simulated stack.
- Viewers see generic "unreachable"/"timed out" details, not raw errors
  (no port-scanning oracle); address refusals are terminal.
- Station-wide 5 s floor between receiver connects; a burst of re-selects
  connects only the latest choice (invariant 10).
- WebSocket Origin: same-origin by default when no allow-list is set,
  port-aware; nginx forwards $http_host so ports survive.
- Kiwi password may not contain whitespace/control characters (it is sent
  inside "SET auth"); contract regenerated.
- nginx security headers included on every location; 16k body limit.
- All containers: cap_drop ALL + no-new-privileges (prod and test compose).
- IPv6 receiver literals are bracketed in URLs.
- Compose image refs take GHOSTJS8_IMAGE_PREFIX / GHOSTJS8_TAG for
  published images; docs for ARM, LAN receivers and hardening.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQHMvquKtGGkMTjMwxGrZ5
@d3mocide d3mocide changed the title ghost.js8 0.1.0 MVP: native JS8Call via KiwiSDR, truthful health, listening-post UI, GhostNet autopilot ghost.js8 0.1.0: native JS8Call via KiwiSDR, GhostNet autopilot, ops console UI, amd64 + arm64 Oct 8, 2026
@d3mocide
d3mocide marked this pull request as ready for review October 8, 2026 14:26
@d3mocide
d3mocide merged commit 2c597cb into main Oct 8, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants