Commit graph

337 commits

Author SHA1 Message Date
Stephen Dennis
4e94bb75aa docs: the browser client connects DIRECTLY to netmux — correct #2215's README
The README merged in #2216 claimed browsers cannot reach netmux and
that both transports require Hydra.  Wrong on the central point:
netmux serves WebSocket natively on its ordinary game ports
(mux/src/websocket.cpp, RFC 6455) with first-byte protocol detection
sharing each port between telnet and WebSocket (#1074/#2193,
proto_detect_window), and the handshake accepts both /wsclient and /
— which is exactly the path js/connection.js dials.  The minimal
browser deployment is netmux plus static files, no extra process;
Hydra is the OPTIONAL layer for session resume, multi-game links,
stored credentials, and gRPC-Web.

The error came from concluding absence out of a truncated grep: the
file listing was piped through head and mux/proxy's matches filled the
window before mux/src/websocket.cpp appeared.  A truncated listing is
not a complete listing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 22:12:06 -06:00
Stephen Dennis
6e6dc6d611 docs(#2215): README for the browser client
client/web was a complete HTML5 client with nothing self-describing in
it — and players have started asking publicly whether TinyMUX has
browser support, with no page to point at.  Covers what it is, why
both transports go through Hydra (browsers cannot open raw TCP), which
transport is proxy-agnostic, deployment (static hosting + hydra.conf,
the mixed-content and cors_origin rules), the test_web.js harness and
its keep-modules-dependency-free convention, and the 2.13-vs-2.14
status.

Facts checked against the sources: transport split per
connection.js/hydra_connection.js (including the /h-command list and
gRPC-Web fallback), listener types and CORS default from
hydra.conf.example, hydra's standalone make from mux/proxy/Makefile,
localStorage persistence from settings.js, and `node test_web.js`
run green before writing (all PASS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 22:05:50 -06:00
Stephen Dennis
df1d10d6e0 fix(#2211): win32gui defines CredStore::SetFilePath so it links again
#1891 added a CredStore::SetFilePath() call to the shared
client/console/src/world.cpp and implemented it in console's
credential_store.  win32gui compiles that same world.cpp but has its own
Credential Manager backend, which had no such function, so win32gui.exe
has failed to link with LNK2019 since 2026-07-31.

Define it here as a documented no-op.  Credential Manager is keyed by
target name, not backed by a file, so there is genuinely no path to set.

Deliberately NOT consolidating onto console's credential_store.cpp, which
is cross-platform and a strict superset on Windows: the two backends use
different Credential Manager target prefixes -- console "HydraConsole:"
versus win32gui "Titan:".  Switching would leave every password the GUI
client has already saved stranded under the old target name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:42:33 -06:00
Stephen Dennis
677276cf81
Merge pull request #2209 from brazilofmux/fix/2205-console-nodelay
console/win32gui set TCP_NODELAY on connect completion (#2205).
2026-08-07 13:36:20 -06:00
Stephen Dennis
79d9ccf5cf
Merge pull request #2210 from brazilofmux/fix/2206-android-nodelay
Titan Android sets tcpNoDelay on the raw socket (#2206).
2026-08-07 13:36:06 -06:00
Stephen Dennis
63ae742168 fix(#2206): Titan Android sets tcpNoDelay on the raw socket
Java's Socket defaults TCP_NODELAY to false and MudConnection never
touched it, so the platform enabled Nagle and nothing overrode it —
every send after the first in a burst (a trigger, a macro) waited on
the ACK of its predecessor, worst on exactly the high-RTT radio links
an Android client lives on.  Android half of #2204.

Set on the RAW socket before connect and before the TLS wrap:
sslContext.socketFactory.createSocket(raw, ...) layers over raw and
inherits its options.

NOT build-verified: no Android SDK/gradle/kotlinc on this box (that
tooling lives on Hatsuhara).  The edit is the one-property fix #2206
specifies at the site it names.  Wants a gradle build before the issue
closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:58:48 -06:00
Stephen Dennis
3b69fa7eeb fix(#2205): console/win32gui set TCP_NODELAY on connect completion
The IOCP session socket ran with Nagle enabled — a multi-write burst
(trigger, macro, keybinding) serialized at the peer's delayed-ACK
cadence, up to ~40ms per write after the first.  win32gui compiles the
same connection.cpp, so one fix covers both clients.  Windows half of
#2204.

Placed on the ConnectEx COMPLETION, immediately after
SO_UPDATE_CONNECT_CONTEXT — before that call the socket is not fully
associated and option calls do not behave (mirrors the server IOCP
engine's setSocketOptions ordering).  Non-fatal by design: a latency
hint failing is no reason to drop a working connection.  The server's
IOCP engine treats the same failure as fatal only because its call
site also configures the listener.

NOT build-verified: MSVC/IOCP code, no Windows toolchain on this box.
The edit is byte-for-byte the fix specified in #2205 at the site it
names, and TCP_NODELAY/IPPROTO_TCP come from the winsock2.h/ws2tcpip.h
pair connection.h already includes.  Wants a Windows build and ideally
the #2196-style option-readback confirmation before the issue closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:58:26 -06:00
Stephen Dennis
5ba023e419 fix(#2207): Titan iOS sets TCP noDelay on both parameter arms
NWProtocolTCP.Options defaults noDelay to false, and MudConnection
built its NWParameters without touching TCP options on either arm —
the plain path took bare NWParameters.tcp defaults and the TLS path
configured TLS options only.  So the platform enabled Nagle and
nothing overrode it, on exactly the high-RTT radio links where a
multi-write burst (trigger, macro, script) serializing at delayed-ACK
cadence is most visible.  iOS half of #2204.

One NWProtocolTCP.Options with noDelay = true, passed to both arms via
NWParameters(tls:tcp:).

Verified on macOS/Xcode 26.6: Titan app target builds; a scratch
Network.framework probe confirms defaultProtocolStack.transportProtocol
carries noDelay=true through BOTH constructions and that both OLD
constructions read back false (the defect, demonstrated); TitanCore's
45 SPM tests pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:58:04 -06:00
Stephen Dennis
619f735cb2 fix(tf): honour IAC GA / IAC EOR so prompts display immediately (#2195)
client/tf's telnet FSM had no handling for IAC GA (249) or IAC EOR (239)
anywhere.  Both fell into the IAC state's `default` arm and were silently
discarded, so a prompt terminated by GA and no newline stayed in
line_buf_ until check_prompt()'s 250ms partial-line timer guessed at it.

TinyMUX's @program prompt is exactly that shape --
queue_write_LEN(d, T(">\377\371"), 3), i.e. ">" IAC GA -- so the
interactive flow where prompt responsiveness matters most was the one
paying the delay.  The timer is the right safety net for servers that send
bare unterminated prompts; GA exists so a client that gets one does not
have to guess.

Both commands now set a flag that check_prompt() consumes, so the prompt
still reaches its single consumer (Terminal::set_prompt + Hook::PROMPT) by
exactly the path it did before -- just without the quarter second.

Three details worth stating:

- The flag is consumed unconditionally at the top of check_prompt(), even
  when there is nothing to deliver.  Left set, it would make some later
  unrelated partial line fire instantly and attribute a prompt boundary to
  a server that never claimed one.

- The GA path deliberately skips the last_prompt_ dedup.  The timed path
  needs it, because it is re-evaluated every main-loop pass while the same
  partial line sits in the buffer.  GA arrives once per prompt, and a
  server that sends the same prompt text twice has genuinely prompted
  twice.

- line_buf_ is not cleared, matching what the timed path already does, so
  current_prompt() still works on a world switch.

EOR is handled alongside GA for correctness if it is ever negotiated;
today tf answers DONT to TELOPT_EOR, so servers use GA.

Measured with tf under a pty against a server that sends a prompt
terminated by IAC GA:

    before:  prompt displayed 251 ms after the server sent it
    after:   prompt displayed   0 ms after the server sent it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:04:52 -06:00
Stephen Dennis
2f1399c80d fix(tf): set TCP_NODELAY on the session socket (#2196)
client/tf never set TCP_NODELAY, so the client side of every session ran
with Nagle enabled.  Client companion to #2194, which was the same gap in
the server's POSIX engines.

A single typed command is immune either way: send_line() assembles command
+ CRLF into one buffer and issues one write().  But anything that sends
several lines in one event-loop pass -- a trigger firing off a match, a
macro or keybinding bound to multiple commands, a scripted burst, a
speedwalk -- produces back-to-back small writes, and Nagle makes each one
after the first wait for the ACK of its predecessor, which the peer's
delayed-ACK timer can hold for up to ~40ms.  Against a remote server the
burst then leaves the client at ACK cadence instead of departing together.

Invisible on loopback, which is why local testing never showed it.

Set after the connect loop rather than at either `break`, so both paths
(immediate connect and EINPROGRESS + poll) are covered by one call.
Non-fatal: a latency hint failing is no reason to refuse a connection that
otherwise works.

Verified by strace'ing tf under a pty against a scratch netmux:

    setsockopt(4, SOL_TCP, TCP_NODELAY, [1], 4) = 0

Catch-verified: with the change reverted, tf makes no setsockopt call at
all on the connect path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:04:12 -06:00
Stephen Dennis
0a934a7d78 build(ragel): guard date_scan and strip #line from the remaining outputs (#2029)
Two remainders from #2025.

## date_scan.cpp was unguarded

hooks/pre-commit knew about four Ragel outputs; mux/lib/date_scan.cpp was
absent from GENERATED_GENS entirely, so it could be hand-edited and
committed without its .rl and nothing objected -- while being listed in
docs/generated-files.md and shipped via unix/TOC.patchable. Added to all
three parallel arrays, which now hold 27 each.

Verified by alignment rather than by count: equal lengths prove nothing
about correspondence, so the check confirms every ragel triple's basenames
match and its rule lives in the same directory as its output. Then
exercised: staging date_scan.cpp alone is now blocked, naming date_scan.rl
as its source.

## Six more outputs carried #line

#2025's scope was right for what the mux build regenerates and dounix.sh
ships, but these have live rules of their own and could still dirty a tree:

  testcases/tools/unformat.c    28    testcases/tools/Makefile  (%.c: %.rl)
  testcases/tools/reformat.c    23    same
  ragel/trigger_match.c         16    ragel/Makefile
  tools/ansify/ansify.c         19    tools/ansify/Makefile  (make regen)
  client/tf/src/script_lex.cpp  67    client/tf/CMakeLists.txt
  client/tf/src/input_lex.cpp   62    same

The issue reported "no rule found" for trigger_match.c and ansify.c. Both
were wrong: ragel/Makefile:25 generates trigger_match.c on dependency, and
tools/ansify/Makefile has an explicit `regen` target. Neither is inert --
ansify's is opt-in rather than implicit, which is why it reads as absent.

Each regenerated through its OWN rule rather than by running sed by hand,
so the rule is what is under test. All six are idempotent across a second
regeneration, and the diffs are 215 deletions, every one a #line, nothing
added.

ragel/color_ops.c is generated from the same ../mux/lib/color_ops.rl as the
shipped copy but is not tracked, so it could not dirty anything; its rule
gets the strip anyway, so the two generations of one source cannot disagree
if it is ever checked in.

client/tf could not be built here -- src/regex_utils.h needs pcre2.h, which
is not on that target's include path on macOS -- but that is a pre-existing
gap unrelated to this change. The Ragel custom commands themselves DID run
(cmake configure succeeds once NCURSESW_LIB is pointed at a macOS ncurses),
and both outputs came back stripped, so the CMake edit is exercised rather
than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:59:11 -06:00
Stephen Dennis
5a42ea2837 fix(color): honour no_flash on live co_render_* paths (#1935)
g_no_flash was only checked inside convert_color/ColorTransitionANSI,
which no longer sits on the output path. Live rendering uses
co_render_ansi{16,256}/truecolor, which always emitted SGR 5 for blink.

Thread a bNoFlash argument through those renderers (same shape as
bNoBleed). net.cpp passes g_no_flash; other callers pass 0. Regenerate
color_ops.c from color_ops.rl.

Unit tests: blink emits SGR 5 with bNoFlash=0 and not with bNoFlash=1.
Dead convert_color left for a separate cleanup pass.
2026-08-01 19:41:21 -06:00
Stephen Dennis
10efa3eda7 fix(android): never persist Hydra secrets without EncryptedSharedPreferences (#1892)
On Keystore/EncryptedSharedPreferences failure, fall back only for world
metadata and strip hydraPass/hydraSession on load, save, and disk scrub.
Surface isSecureStorageAvailable with a Worlds dialog warning. Unit tests
cover worldsForPersistence stripping.
2026-07-31 09:36:45 -06:00
Stephen Dennis
b4466c4e7c
fix(mobile/mcp): cap multiline reassembly pending size (#1893) (#1900)
fix(mobile/mcp): cap multiline reassembly pending size (#1893)
2026-07-31 09:35:43 -06:00
Stephen Dennis
858825f69f fix(console): keep Hydra passwords out of worlds.txt (#1891)
Store Hydra account passwords in a platform secret store (Windows
Credential Manager target HydraConsole:<world>, Unix worlds.cred mode
0600) instead of the plaintext world definition line.

On load, migrate legacy "hydra … user pass game" lines into the store.
Save writes "hydra name host port user game" only.  worlds.txt is
created with restrictive permissions; on Unix, group/other-readable
files are refused unless HYDRA_ALLOW_INSECURE_WORLDS=1.

Standalone test_world_cred covers migration, absence of password on
disk, mode 0600, and the insecure-file refuse path.
2026-07-31 09:32:48 -06:00
Stephen Dennis
ff124db46d fix(mobile/mcp): cap multiline reassembly pending size (#1893)
Android and iOS McpParser kept every unterminated #$#* tag without
bound. Mirror the web client limits from #1889: 32 pending messages,
256 KiB per message, 1 MiB total pending; evict oldest or drop the
overflowing tag with an optional diagnostic callback.

iOS unit tests cover tag flood, fat continuations, and happy-path
reassembly. Android gains a matching JUnit suite (JVM unit tests).
2026-07-31 09:31:45 -06:00
Stephen Dennis
17f74f1a94 fix(web/mcp): cap multiline reassembly pending size (#1889)
McpParser stored every unterminated #$#* tag without bound. Cap pending
messages (32), per-message bytes (256 KiB), and total pending (1 MiB);
evict oldest or drop the overflowing tag with a diagnostic. Regression
covers flood of unique tags, fat continuations, and happy-path reassembly.
2026-07-31 09:27:38 -06:00
Stephen Dennis
c5fab97969 fix(client): tf cap must admit newline; console SB reassembly cap
Review follow-ups to #1789/#1790 (the #1788 buffer-cap family):

tf: the kMaxLine gate dropped every byte at the cap, including '\n'.
Line extraction only fires on a newline found inside line_buf_, and
nothing else clears the buffer, so one 64 KiB run without a newline
wedged the buffer at the cap and silently discarded all server output
for the rest of the connection.  Admit '\n' through the gate so the
line resets as the #1788 comment promised.

console (and win32gui, which compiles the same connection.cpp): the
telnet SB reassembly buffer had no cap at all — a hostile server
holding the parser in SB_DATA grows sb_buf_ without bound.  Cap it at
4 KiB, matching the tf client's kMaxSb; both the plain-byte and the
escaped-IAC appends are gated.

tf compile-verified (connection.cpp.o builds); console is the Windows
client, hand-verified only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 06:59:24 -06:00
Stephen Dennis
123563522e test(web): pin oversized telnet SB discard (#1788)
Rebase onto master after #1789; add node unit for 5k GMCP SB then ok line.
2026-07-29 13:21:35 -06:00
Stephen Dennis
d0cdcce123 fix(client): extend #1788 buffer caps to web, tf, android, win32gui
Same unbounded line/SB/Hydra reassembly as console/iOS. Cap text at 64 KiB
and telnet SB at 4 KiB; cap web grpc-web frame reassembly. Stamp J1–J6 on
the audit map.
2026-07-29 13:21:35 -06:00
Stephen Dennis
703a0a2f33 fix(client): cap line and telnet SB reassembly buffers (#1788)
Hostile servers could grow Titan iOS and console heap without bound via
streams without newlines or oversized IAC SB. Cap text lines at 64 KiB
and telnet SB at 4 KiB (server-aligned); discard overflowed SB on SE.
Stamp J1/J5 Pass 14 on the audit map.
2026-07-29 13:10:28 -06:00
Stephen Dennis
278a33e619 build(win32): add /utf-8 to the three vcxproj #1506 does not cover (#1499)
#1506 puts /utf-8 on eleven project files.  Three others in the tree do not
have it:

    mux/ganl/tests/ganl_tests.vcxproj
    client/console/console.vcxproj
    client/win32gui/win32gui.vcxproj

Nothing is broken today -- none of the three compiles a source file containing
a single non-ASCII byte, so none can raise C4819 as things stand.  This is
about step 2 of #1499, which converts \xE2\x80\x99-style escapes to characters
across the tree.  The first prose string that reaches anything these projects
build would fail to compile on a DBCS box, and for ganl_tests that failure
would surface inside a test harness rather than in the server, which is a
confusing place to go looking for an encoding problem.  ganl_tests is also the
documented Windows path for the GANL harness (CLAUDE.md), so it is the one that
would be missed longest.

Deliberately a separate branch rather than a push to #1506: the file sets are
disjoint -- every project touched here has no /utf-8 in #1506, and every project
touched there has none here -- so the two merge cleanly in either order and
#1506's Windows verification stays valid as tested.

Same form as #1506 throughout: /utf-8 first in each <ClCompile>, with
%(AdditionalOptions) preserved.  None of the three had an AdditionalOptions
element before, and all six ItemDefinitionGroup configurations are covered
(2 per project), so there is no configuration that builds without the flag.

Verified: all three parse as well-formed XML, and none gained a BOM or a CRLF.
Together with #1506 this brings all fourteen vcxproj in the tree to /utf-8.

Not built -- no MSVC here.  The change is additive and behaviour-neutral on a
tree that is currently all-ASCII in these projects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:31:55 -06:00
Stephen Dennis
941dec87b8 feat(client/android): persist and resume the Hydra session id (#762)
The Console client (and Win32GUI, which shares hydra_connection.cpp)
persists its Hydra session id and resumes via GetSession(saved_id) before
falling back to Authenticate. The Android client did neither. Bring it to
parity:

- World: add `hydraSession`, serialized with the other Hydra fields. It
  lands in EncryptedSharedPreferences alongside the password, so the token
  is encrypted at rest (the Console keeps its copy in plaintext
  worlds.txt).
- HydraConnection: accept a `resumeSessionId` and, when non-empty, probe
  it with GetSession before authenticating. A successful probe (session
  exists and the username matches) skips password auth entirely; any
  failure — expired, unknown, or transport error — clears the stale token
  and falls through to the normal Authenticate path. Expose
  `currentSessionId` so the caller can persist it. The Connect(game_name)
  call after auth is left unconditional, matching the Console.
- TitanApp: pass `world.hydraSession` when connecting a saved world, and
  on connect write the new-or-resumed id back via WorldRepository (only
  when it changed, and only for saved worlds — ad-hoc /hconnect sessions
  are not persisted).

Also fixes a pre-existing compile break that made the Android client
unbuildable: WorldTab.idleSeconds() returns Int but ConditionContext
expects Long (TitanApp.kt:218).

Verification: builds clean in a real Android environment (Android Studio
JBR 21 + SDK 35, Gradle 9.3.1) — `assembleDebug` produces app-debug.apk.
Not runtime-tested: the Hydra server lives in a separate repo and this box
has no AVD or instrumentation test source set, so resume behaviour has not
been exercised against a live server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:18:43 -06:00
OrbisAI Security
d5a6bb87be
fix(console): clip input line and cursor to console width in redraw_input() (#895)
Clip the console input line and cursor to the window width in redraw_input(), matching the existing guard in redraw_status(). Prevents long input from wrapping onto the status bar and keeps the cursor on-screen on narrow terminals. Cosmetic/UI fix; no security change.
2026-07-12 20:40:29 -06:00
Stephen Dennis
11355aa5bc feat(tf): interactive keyboard read for read()/tfread() (#758, #759, #760)
read() was a stub returning "" and tfread() only read file handles, so
macros could not pause for user input. Add a modal keyboard-read
primitive and wire the scripting functions to it.

modal_read_line() (main.cpp) runs a nested input pump: it saves the
in-progress input line and prompt, drains keyboard input into the editor
until Enter (SUBMIT) or EOF, then restores. It deliberately pumps ONLY
keyboard input — not network sockets, shell pipes, or timers — so it
cannot recursively fire triggers/hooks while an outer script is
mid-evaluation (the reentrancy class behind UAF bugs elsewhere here);
incoming MUD output simply waits until the read completes. It uses its
own InputLexer so it never disturbs the outer loop's in-flight event
iteration. The pump is installed on App::read_line_fn by run().

- read([prompt]) (#759): pauses the macro, returns the typed line; ""
  on EOF/interrupt or when no interactive read is available.
- tfread() (#758): now accepts 1 or 2 args. tfread("var") or
  tfread("tfin"|"-", "var") read one line from the keyboard into the
  named variable (classic TF tfin handle); tfread(handle, "var") keeps
  the existing file-handle behavior. Returns 1, or 0 at EOF/error.
- @read status field (#760): status_read_depth is now incremented for
  the duration of a read, so the previously inert @read indicator shows
  the nesting depth. Ctrl-D on an empty line is treated as EOF rather
  than the editor's "/quit".

Verified end-to-end over a pty: read() and all three tfread keyboard
forms capture the line; @read shows 1 during the read and clears after;
Ctrl-D yields EOF (read()="" / tfread=0) without quitting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:45:09 -06:00
Stephen Dennis
a01fe82be2 perf(tf): cache tokenized status format-var expressions (#761)
status_int_*/status_var_* format variables are re-evaluated on every
status-bar redraw, re-tokenizing the same expression text each time.
Tokenizing is a pure function of the expression text — variable
references resolve at parse time against the live ScriptEnv and are
never baked into tokens — so the token stream can be cached and reused
across redraws while still reflecting current values.

eval_expr() now memoizes the lexer output in a bounded map keyed by the
exact expression text. Eviction is gated on g_eval_depth == 0 so a
reentrant eval() (which calls eval_expr while an outer Parser still holds
a reference into the cache) can never dangle that reference;
unordered_map already guarantees element references survive insertion and
rehash, so only erase/clear needs guarding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:55:00 -06:00
Stephen Dennis
0f46b9c8e8 docs: add client/tf/README.md explaining what TitanFugue is
The migrated "TitanFugue: ..." GitHub issues referenced the name with no
public context (it appeared only incidentally in-tree), prompting "What is
TitanFugue?" on #758.  Document it: the terminal MU* client in client/tf, a
modernized TinyFugue 5.0b8 (ICU branch, GPLv2) fork with full Unicode/UTF-8,
TLS/SSL, and 24-bit truecolor, part of the "Titan" client family under
client/.  Includes lineage/license, build steps, and the issue-tracker note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 09:00:26 -05:00
Stephen Dennis
6288c4da37 Retire ISSUES.md trackers; migrate open items to GitHub issues
The 22 per-tracker ISSUES.md files carried both their open items and a
full FIXED / FALSE ALARM / NOT A BUG audit history. The 57 still-open
items have been migrated to GitHub issues #706-#762 with a 2.14-aligned
label taxonomy (area:* / type:* / priority:* / topic:*), so open work
now lives in the issue tracker instead of in-tree Markdown.

The closed/audit history of every tracker is preserved here in git
history (this commit's parent); nothing is lost.

Open items migrated by tracker:
  mux/src/         (1)   -> #706
  mux/lib/         (10)  -> #707-#716
  mux/modules/engine/ (18) -> #717-#734
  mux/ganl/        (10)  -> #735-#744
  mux/modules/sqlslave/ (9) -> #745-#753
  testcases/       (4)   -> #754-#757
  client/tf/ + client/ (5) -> #758-#762

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:14:00 -05:00
Stephen Dennis
b5bdf36684 Wire grpc-swift v2 into Titan and add macOS target
Hydra was previously gated behind canImport(GRPC) but the Xcode project
had no gRPC package, so the entire transport silently compiled to nothing.
This wires up grpc-swift v2 so HydraConnection actually builds, and adds
a macOS application target alongside the iOS one.

- Port HydraConnection.swift from grpc-swift v1 to v2: GRPCCore +
  HTTP2ClientTransport.Posix, closure-style bidi streaming, GRPCClient
  lifecycle via runConnections() / beginGracefulShutdown().
- Generate Hydra_*.pb.swift + Hydra_*.grpc.swift from mux/proxy/hydra.proto
  using protoc-gen-swift 1.37.0 and protoc-gen-grpc-swift 2.3.0; commit
  the output so the project builds without local protoc.
- regen-grpc.sh: helper script to regenerate the stubs from hydra.proto.
- project.yml: bump iOS to 18.0 (v2 requires it via @available), add
  TitanMac target at macOS 15.0, depend on grpc-swift-2,
  grpc-swift-protobuf, and grpc-swift-nio-transport.
- Rename canImport(GRPC) gates to canImport(GRPCCore) in WorldTab.swift
  and ContentView.swift.
- Wrap iOS-only SwiftUI modifiers (navigationBarTitleDisplayMode,
  textInputAutocapitalization, keyboardType) in #if os(iOS) so the
  shared view sources also compile on macOS.

Both Titan (iOS Simulator) and TitanMac build with zero warnings, both
binaries launch without crashing, and the 41 SwiftPM headless tests
still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:31:58 -06:00
Stephen Dennis
bad833cbfb Get Titan launching on iOS Simulator
Two more blind-code build errors surfaced once the iOS Simulator
runtime was installed and the linker stage actually ran:

- ContentView.updateTerminalSize: `state.activeTab?.hydraConnection?`
  call wasn't gated by `#if canImport(GRPC)` even though the property
  itself is. Wrapped the call site to match.
- ContentView.inputBar: `.onKeyPress(characters:modifiers:)` is not a
  real SwiftUI signature — modifier filtering needs the closure form
  that takes a `KeyPress`. Combined the Cmd+F and Cmd+L bindings into
  a single `.onKeyPress { keyPress in ... }` block that switches on
  `keyPress.key` after checking `.command` is held.

Titan now boots on iPhone 17 Pro Simulator (org.tinymux.titan, iOS
26.4) and renders the System tab with the full toolbar. First time
this codebase has ever run on Apple hardware.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:26:58 -06:00
Stephen Dennis
91e6a4aa46 Add XcodeGen project.yml and fix four blind-code build errors
First attempt at building Titan as an actual iOS app surfaced four
compile errors that the earlier headless SPM scaffold could not see
(it didn't include the View/ tree or the Hydra-conditional call sites):

- WorldTab.sendLine: `else if` clause inside `#if canImport(GRPC)`
  orphaned the bare `else` when GRPC was absent. Restructured as an
  early `return` after the primary connection branch.
- ContentView.statusText: same `#if`-around-`else if` bug. Replaced
  with an explicit `tab.connection == nil` guard on the Hydra branch.
- TelnetParser.send / MudConnection extension: `private func send` in
  TelnetParser was redeclared by a same-signature extension method in
  MudConnection. Promoted TelnetParser's send to internal and dropped
  the now-redundant extension.
- EditTriggerView: `@State private var body` shadowed SwiftUI's `var
  body: some View`. Renamed to `commandBody`.

XcodeGen config (`project.yml`) builds an iOS-only target for now;
multiplatform macOS will be a follow-up. `.gitignore` excludes the
generated `Titan.xcodeproj` and `Info.plist` so the manifest stays
the source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:13:22 -06:00
Stephen Dennis
8774a7e10b Add Variables, TimerEngine, TriggerEngine tests and fix two bugs
27 new tests across the three modules surfaced two latent bugs that
the test cases catch:

- TimerEngine: MudTimer is a struct, so the timer the firing closure
  decremented was a separate copy from the one stored in the dict.
  list() always returned the original shotsRemaining. Fixed by making
  the dict the source of truth and mutating the stored entry.

- Variables: $var expansion regex required at least 2 chars after $
  (so $x was unreachable) and was greedy across trailing dots.
  Switched to a segmented pattern that matches one identifier
  optionally followed by .identifier groups.

TriggerEngine tests pass clean — gag, hilite, substitute, body $0
expansion, disabled/zero-shot skipping, line classification, and
ANDed/ORed composite conditions all work.

41 tests, 0 failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:01:02 -06:00
Stephen Dennis
ca49ab2625 Add McpParser tests for init, negotiation, multiline, and edit set
Nine tests covering the core MCP-2.1 protocol surface: prefix
detection, version negotiation, package recording, auth-key
enforcement, multiline message assembly via continuations, and
the simpleedit-set send path. No bugs surfaced — McpParser was
already correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:55:11 -06:00
Stephen Dennis
8785cd53e4 Add headless SPM package for Titan iOS parsers and models
First time the iOS Titan codebase has been compiled on hardware. The
package pulls Foundation-only files from Titan/ via SPM source paths
so they can be exercised by `swift test` without an Xcode project.

Two latent compile errors surfaced and were fixed:
- Condition.swift: TriggerCondition was indirectly recursive through
  Box<TriggerCondition> (a struct, not a class) and needed `indirect`.
- Variables.swift: bare-slash regex literal didn't parse; switched to
  the unambiguous extended literal #/.../#.

TelnetParserTests covers IAC escaping, WILL/DO negotiation, GA prompt
flush, and NAWS subnegotiation — 5 tests, all green on macOS arm64.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:51:27 -06:00
Stephen Dennis
228586ae41 Persist Hydra session_id across Console restart
Add session resume so a restarted Console client can skip password
entry when its last-known session is still alive server-side.

worlds.txt format: `hydra` lines gain an optional trailing
`session=<id>` token. Parser accepts `notls` and `session=` in any
order after the required fields; save() writes both when present.

HydraConnection takes a new defaulted `resume_session_id` parameter
and seeds `sessionId_` with it. connect() probes via GetSession
with the saved id before Authenticate — a successful probe where
the returned SessionInfo.username matches the configured username
means the server still has our session, its links, and its GMCP
subscriptions, so we skip the full password auth path. On any
failure (expired, unknown, transport error, username mismatch) the
token is discarded and the normal Authenticate flow runs.

cmd_connect persists the resulting session_id back to the World
and rewrites worlds.txt after each successful connect, so a
fresh auth and a resumed connect both leave the same recovery
token on disk.

The new constructor parameter is defaulted, so win32gui's
mainframe.cpp call site compiles unchanged; the GUI can pick up
session persistence in a small follow-up. Android remains
pending — that half needs an Android-capable environment.

Verified: Release x64 builds of console.exe and win32gui.exe are
clean with only pre-existing protobuf inline-attribute warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:48:05 -06:00
Stephen Dennis
b4f8114709 Add structured Hydra vitals hooks across clients 2026-04-10 19:53:43 -06:00
Stephen Dennis
06e9b1772e Add web regression harness and Win32 project validator 2026-04-10 19:46:20 -06:00
Stephen Dennis
1f2782fa72 Move web world passwords out of localStorage 2026-04-10 19:40:56 -06:00
Stephen Dennis
2268322273 Fix console macro validation and add DB reopen coverage 2026-04-10 19:38:34 -06:00
Stephen Dennis
26b1aef528 Document newly found project issues 2026-04-10 19:24:35 -06:00
Stephen Dennis
1e5fc8b2d3 Fix Windows build: regen stale proto files, add missing vcxproj entries
Regenerate hydra.pb.cc/h for console and win32gui clients from the
canonical hydra.proto — the checked-in copies were missing the
end_of_record field added to GameOutput.

Add credential_store.cpp/h to win32gui.vcxproj and secure_util.h to
console.vcxproj (omitted in c1e63dee).

Parenthesize std::min calls in jit_compiler.cpp to avoid the Windows
min/max macro conflict.  Add conditional <windows.h> include to
secure_util.h so SecureZeroMemory resolves regardless of include order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:08:48 -06:00
Stephen Dennis
c1e63deee3 Harden credential storage across console and Win32 GUI clients
Console (Unix): worlds.txt saved with mode 0600, warns on load if
permissions are loose. Password zeroed from memory via secure_zero()
after Hydra authentication succeeds.

Win32 GUI: Hydra passwords moved from plaintext worlds.json to
Windows Credential Manager (CredWriteW/CredReadW). Transparent
migration from existing JSON on first load; passwords stripped from
JSON on next save. Username and non-secret fields remain in JSON.

iOS (Keychain) and Android (EncryptedSharedPreferences) were already
using platform credential stores — no changes needed.

Web client localStorage exposure deferred as lower priority (different
threat model, not the primary deployment target).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:56:47 -06:00
Stephen Dennis
037922e3ff Use live view geometry for Hydra terminal sizing on iOS
Replace the UIScreen.main.bounds estimation with a GeometryReader on
the output pane so terminal columns/rows are derived from actual view
dimensions.  Add HydraConnection.updateTerminalSize() which resends
SetPreferences on geometry changes (rotation, split-screen, etc.),
suppressing no-ops when dimensions haven't changed.

Closes both remaining iOS client issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:46:20 -06:00
Stephen Dennis
176e50a591 Bounds-check ANSI 256/truecolor parser in web client
renderAnsiLine() in client/web/js/terminal.js parsed `\e[38;5;Nm`
(256-color) and `\e[38;2;R;G;Bm` (truecolor) by directly reading
codes[j+2..j+4] with no check that the codes array was that long.
Malformed short sequences from the server leaked `undefined` into
`XTERM_COLORS[undefined]` and into `rgb(r,g,b)` CSS strings, producing
wrong fallback colors or literal `NaN` in styles.

Add an `isByte(v)` guard that requires a finite integer in [0, 255]
for every palette index and RGB component on both the fg (38) and bg
(48) paths. Short or out-of-range sequences now fall through to plain
text instead of emitting broken CSS.

Verified with `node --check` and a 15-case harness (well-formed,
short, out-of-range, bg variants, bold mix, plain, empty).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:56:37 -06:00
Stephen Dennis
1cd7297ef4 Pin console Connection across pending overlapped I/O
The console client used to allocate a heap IoContext per WSASend and
let its "eventual IOCP completion" delete it. That model broke down
the moment a disconnect closed the socket: WSA_OPERATION_ABORTED
completions would fire with the Connection pointer as the IOCP key,
and if app.connections had already dropped the connection, the
main-loop dispatch would do `conn->on_completion(...)` on freed
memory. Read and connect contexts are embedded directly in the
Connection, so their pending overlapped addresses would also dangle.

Switch the map to std::shared_ptr<IConnection> and make Connection
inherit std::enable_shared_from_this<Connection>. Each heap write
IoContext now carries an `owner` shared_ptr populated via
shared_from_this() in send_raw(); the ctx delete in on_completion
drops the reference naturally. Read and connect operations pin the
Connection through pending_read_self_ / pending_connect_self_
members set before WSARecv/ConnectEx and cleared on the matching
completion (or on the synchronous error path). on_completion() also
takes a local keepalive at entry so that releasing those refs from
inside the method cannot destroy *this mid-call.

Value-initialize IoContext instead of memset'ing the whole struct
(memset would corrupt the new shared_ptr field's control block).
The two remaining memsets only zero the embedded OVERLAPPED
sub-field and are harmless.

Build verification is deferred to a Windows host — client/console
pulls in <windows.h>, <winsock2.h>, <mswsock.h>, and <schannel.h>
and does not compile on the Linux dev environment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:39:58 -06:00
Stephen Dennis
e4fffbf29e Harden console CHARSET parsing 2026-04-04 17:06:51 -06:00
Stephen Dennis
1ec186ff44 Remove redundant TF log overrides 2026-04-04 16:43:39 -06:00
Stephen Dennis
ef7ba14518 Implement TF nlog() counter 2026-04-04 16:41:04 -06:00
Stephen Dennis
853d2875e1 Improve TF Hydra error diagnostics 2026-04-04 16:26:53 -06:00
Stephen Dennis
a209ecabbc Fix TF Hydra reconnect and update command 2026-04-04 16:18:45 -06:00