Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ MANDIR ?= $(PREFIX)/share/man
SYSTEMD_UNIT_DIR ?= $(PREFIX)/lib/systemd/system
CI_TEST_PORT ?= $(if $(PORT),$(PORT),2222)

.PHONY: all clean install install-systemd uninstall uninstall-systemd debug release release-check release-check-strict package-publish-check debian-source-package asan valgrind check test test-advisory ci-test unit-test script-test integration-test anonymous-access-test connection-limit-test security-test stress-test soak-test slow-client-test user-lifecycle-test info
.PHONY: all clean install install-systemd uninstall uninstall-systemd debug release release-check release-check-strict package-publish-check debian-source-package asan valgrind check test test-advisory ci-test unit-test script-test integration-test module-runtime-test anonymous-access-test connection-limit-test security-test stress-test soak-test slow-client-test user-lifecycle-test info

all: $(TARGETS)

Expand Down Expand Up @@ -123,6 +123,7 @@ test-advisory: all unit-test
@cd tests && PORT=$${PORT:-2222} ./test_basic.sh || echo "(basic integration tests are advisory)"
@cd tests && PORT=$$(($${PORT:-2222} + 1)) ./test_exec_mode.sh || echo "(exec mode tests are advisory)"
@cd tests && PORT=$$(($${PORT:-2222} + 2)) ./test_interactive_input.sh || echo "(interactive input tests are advisory)"
@cd tests && PORT=$$(($${PORT:-2222} + 6)) ./test_module_runtime.sh || echo "(module runtime tests are advisory)"

unit-test:
@echo "Running unit tests..."
Expand All @@ -145,8 +146,13 @@ integration-test: all
@cd tests && PORT=$$(($${PORT:-2222} + 3)) ./test_user_lifecycle.sh
@cd tests && PORT=$$(($${PORT:-2222} + 4)) ./test_mute_joins_view.sh
@cd tests && PORT=$$(($${PORT:-2222} + 5)) ./test_empty_view.sh
@cd tests && PORT=$$(($${PORT:-2222} + 6)) ./test_module_runtime.sh
@cd tests && ./test_tntctl_cli.sh

module-runtime-test: all
@echo "Running module runtime tests..."
@cd tests && PORT=$${PORT:-2222} ./test_module_runtime.sh

anonymous-access-test: all
@echo "Running anonymous access tests..."
@cd tests && PORT=$${PORT:-2222} ./test_anonymous_access.sh
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ TNT/
│ ├── bootstrap.c # SSH authentication and session bootstrap
│ ├── chat_room.c # chat room logic
│ ├── message.c # message persistence
│ ├── module_protocol.c # external module JSONL protocol helpers
│ ├── module_runtime.c # optional external module supervisor
│ ├── json_text.c # small JSON string helpers
│ ├── input_buffer.c # validated terminal input buffer helpers
│ ├── history_view.c # message viewport and scroll state
│ ├── help_text.c # full-screen key reference content
│ ├── manual.c # concise manual panel rendering
Expand Down Expand Up @@ -428,7 +432,11 @@ tnt.service - systemd service unit
```

The persisted chat-history format is documented in
[docs/MESSAGE_LOG.md](docs/MESSAGE_LOG.md).
[docs/MESSAGE_LOG.md](docs/MESSAGE_LOG.md). Experimental community modules
should follow the external-process protocol in
[docs/MODULE_PROTOCOL.md](docs/MODULE_PROTOCOL.md). Module-generated content
must always include a plain-text fallback so TNT can keep working on basic
terminal clients and preserve the stable `messages.log` v1 history contract.

### MOTD (Message of the Day)

Expand All @@ -450,6 +458,7 @@ Delete `motd.txt` to disable the MOTD.
- [Quick Setup](docs/EASY_SETUP.md) - 5-minute deployment guide
- [Roadmap](docs/ROADMAP.md) - Long-term Unix/GNU direction and next stages
- [Interface Contract](docs/INTERFACE.md) - Scriptable commands, exit statuses, and JSON fields
- [Module Protocol](docs/MODULE_PROTOCOL.md) - External-process module contract
- [Security Reference](docs/SECURITY_QUICKREF.md) - Security config quick reference
- [Contributing](docs/CONTRIBUTING.md) - How to contribute
- [Changelog](docs/CHANGELOG.md) - Version history
Expand Down
31 changes: 31 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,37 @@ Recommended interpretation:
- `TNT_MAX_CONN_RATE_PER_IP`: new connection attempts allowed per IP per 60 seconds
- `TNT_RATE_LIMIT=0`: disables rate-based blocking and auth-failure IP blocking, but not the explicit capacity limits

## Edge Module Production Profile

Some deployments intentionally track the newest TNT builds and newest module
integrations to exercise the full product surface. Treat these as edge
production environments: user-facing, but optimized for fast integration and
fast rollback.

For that profile:

- Deploy TNT and modules as separate artifacts so a module can be disabled
without replacing the core server.
- Keep module permissions explicit and minimal. Do not grant private-message
access unless the module exists for that purpose.
- Keep a known-good TNT binary and module manifest set on disk for immediate
rollback.
- Log module startup failures, invalid JSONL, protocol errors, and timeouts
separately from chat history.
- Prefer plain-text fallbacks for every module-created message, even when the
module also targets richer terminal renderers.
- Before promoting a module, test its manifest and JSONL handshake against the
protocol in `docs/MODULE_PROTOCOL.md`.

Enable modules explicitly with `TNT_MODULE_PATHS`, using a colon-separated
list of module directories:

```bash
TNT_MODULE_PATHS=/opt/tnt-modules/echo-module:/opt/tnt-modules/other-module
```

Unset `TNT_MODULE_PATHS` and restart TNT to return to the plain core server.

## MOTD (Message of the Day)

Place a `motd.txt` file in the state directory. TNT displays it to each user on connect; they press any key to enter the chat.
Expand Down
8 changes: 8 additions & 0 deletions docs/Development-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ src/
├── command_catalog.c - COMMAND-mode names, aliases, and help summaries
├── exec_catalog.c - SSH exec command matching and help metadata
├── exec.c - SSH exec command dispatch
├── json_text.c - JSON string escaping and top-level string extraction
├── input_buffer.c - Validated INSERT/COMMAND/paste buffer helpers
├── module_protocol.c - External module JSONL protocol helpers
├── module_runtime.c - Optional external module supervisor
├── tntctl.c - Local wrapper around the SSH exec interface
├── tntctl_text.c - tntctl local help and diagnostics
├── chat_room.c - Chat room state, message ring, and update sequence
Expand Down Expand Up @@ -112,6 +116,10 @@ include/
├── message_log_tool.h - Offline log check/recover interface
├── command_catalog.h - COMMAND-mode command metadata interface
├── exec_catalog.h - SSH exec command metadata interface
├── json_text.h - JSON text helper interface
├── input_buffer.h - Terminal input buffer helper interface
├── module_protocol.h - External module protocol helper interface
├── module_runtime.h - External module supervisor interface
├── cli_text.h - Server CLI text interface
├── tntctl_text.h - tntctl text interface
├── history_view.h - Scroll-state helpers
Expand Down
143 changes: 143 additions & 0 deletions docs/MODULE_PROTOCOL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# TNT Module Protocol

This document defines the compatibility contract for external TNT modules.
The first implementation target is external-process modules that exchange
JSON Lines with TNT over stdin/stdout. Keeping modules out of the server
address space makes the community extension surface easier to audit, restart,
rate-limit, and disable.

The protocol is intentionally separate from `messages.log` v1. TNT 1.x keeps
the persisted public history format stable. Module-generated content must
always provide a plain-text fallback that can be stored and rendered by older
or less capable clients.

TNT core should stay conservative: text-first, terminal-compatible, and easy
to deploy over plain SSH. Modules are the extension surface for personalized
workflow features, rich rendering, terminal-specific visuals, and other
experience experiments. Integrating a module with TNT must not make plain
terminal users lose the basic chat path.

## Compatibility

- Protocol version: `tnt.module.v1`
- Transport: UTF-8 JSON Lines
- Framing: one complete JSON object per line
- Direction: TNT sends events to module stdin; modules write responses to
stdout
- Error stream: modules should write diagnostics to stderr
- License: module protocol examples and official community modules should use
the same license as TNT unless a module states stricter terms

Modules are disabled unless `TNT_MODULE_PATHS` is set. The value is a
colon-separated list of module directories, each containing `tnt-module.json`
and the declared executable entrypoint.

TNT may add optional fields to existing messages. Modules must ignore unknown
fields. TNT must ignore unknown response fields unless the response type
explicitly requires them.

## Manifest

Each module directory should include `tnt-module.json`:

```json
{
"protocol": "tnt.module.v1",
"name": "echo",
"version": "0.1.0",
"description": "Echoes public messages for testing",
"entrypoint": "./echo-module.sh",
"permissions": ["message:read", "message:create"],
"events": ["message.created"]
}
```

Required fields:

- `protocol`: protocol compatibility string
- `name`: stable module id, lowercase ASCII, `a-z`, `0-9`, and `-`
- `version`: module version
- `entrypoint`: executable path relative to the manifest directory
- `permissions`: explicit capabilities requested by the module
- `events`: event names the module wants to receive

## Handshake

TNT starts a module process and writes a handshake event:

```json
{"type":"handshake","protocol":"tnt.module.v1","server":{"name":"tnt","version":"1.0.1"}}
```

The module should answer:

```json
{"type":"handshake.ok","protocol":"tnt.module.v1","module":{"name":"echo","version":"0.1.0"}}
```

If the module cannot run, it should answer:

```json
{"type":"error","code":"unsupported_protocol","message":"requires tnt.module.v2"}
```

## Events

Message-created event:

```json
{
"type": "message.created",
"message": {
"id": "local-00000001",
"timestamp": "2026-06-04T12:00:00Z",
"sender": "alice",
"kind": "text",
"plain_text": "hello",
"metadata": {}
}
}
```

The `plain_text` field is mandatory for every user-visible message. Future
rich content, images, and terminal-specific render hints must be represented
as optional metadata or attachment records with a plain-text fallback.

## Responses

Create a public message:

```json
{"type":"message.create","plain_text":"echo: hello"}
```

No-op acknowledgement:

```json
{"type":"event.ok"}
```

Module error:

```json
{"type":"error","code":"bad_request","message":"missing plain_text"}
```

## Security Rules

- Modules are untrusted external processes.
- TNT should enforce per-module permissions before delivering events or
accepting responses.
- TNT should cap stdout line length, startup time, event handling time, and
total queued output.
- TNT should disable a module after repeated invalid JSON, protocol errors, or
timeout failures.
- Modules must never receive private messages unless they request and are
granted an explicit private-message permission.

## Rendering Rules

Every module-created message must be renderable as plain text. Terminal image
protocols such as Kitty graphics or Sixel are optional renderer capabilities,
not message requirements. A module may provide attachment metadata later, but
TNT must be able to fall back to a link, filename, digest, or short label.
4 changes: 4 additions & 0 deletions docs/QUICKREF.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,13 @@ STRUCTURE
src/commands.c COMMAND-mode command dispatch
src/exec_catalog.c SSH exec command matching, usage, argument shape
src/exec.c SSH exec command dispatch
src/json_text.c JSON string escape/extract helpers
src/input_buffer.c validated INSERT/COMMAND/paste buffer helpers
src/message.c persistence, search
src/message_log.c messages.log v1 parsing and formatting
src/message_log_tool.c offline messages.log check/recover CLI
src/module_protocol.c external module JSONL protocol helpers
src/module_runtime.c optional external module supervisor
src/history_view.c message viewport / scroll state
src/help_text.c full-screen key reference text
src/manual.c concise manual panel rendering
Expand Down
20 changes: 20 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ Goal: keep the interface efficient for terminal users without sacrificing simpli
- ✅ improve discoverability of NORMAL and COMMAND mode actions
- ✅ make status lines and help output concise enough for small terminals

## Stage 4.5: Module Foundation

Goal: let community features plug into TNT without coupling every user request
to the core server binary.

- keep TNT core basic and broadly compatible; route personalized workflows,
rich visuals, and terminal-specific experience upgrades through modules
- define the external-process module protocol before loading any third-party
code into production rooms
- keep module messages compatible with plain terminal clients by requiring
plain-text fallbacks for rich content and attachments
- treat terminal image protocols as optional renderer capabilities, not as the
core message format
- prefer JSON Lines over stdin/stdout for early modules so TNT can supervise,
restart, rate-limit, and disable modules independently
- keep module permissions explicit: message read/create, command registration,
private-message access, and future attachment access must be separate grants
- publish official examples in a companion community repository that tracks
TNT protocol versions and license terms

## Stage 5: Operations and Security

Goal: make public deployment manageable.
Expand Down
35 changes: 35 additions & 0 deletions include/input_buffer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#ifndef INPUT_BUFFER_H
#define INPUT_BUFFER_H

#include "common.h"

typedef enum {
TNT_INPUT_APPEND_OK = 0,
TNT_INPUT_APPEND_IGNORED = 1 << 0,
TNT_INPUT_APPEND_OVERFLOW = 1 << 1,
TNT_INPUT_APPEND_INVALID_UTF8 = 1 << 2
} tnt_input_append_status_t;

typedef struct {
char bytes[4];
int len;
int expected_len;
} tnt_input_utf8_state_t;

void tnt_input_utf8_state_reset(tnt_input_utf8_state_t *state);

int tnt_input_append_ascii(char *input, size_t input_size, unsigned char b);
int tnt_input_append_utf8_sequence(char *input, size_t input_size,
const char *bytes, int len);

/* Append one byte from a terminal stream, validating UTF-8 across calls.
* In paste mode CR/LF/TAB are normalized to spaces so existing TNT 1.x
* single-line message semantics are preserved. */
int tnt_input_append_stream_byte(char *input, size_t input_size,
tnt_input_utf8_state_t *state,
unsigned char b, bool paste_mode);

/* Returns TNT_INPUT_APPEND_INVALID_UTF8 when the stream ended mid-codepoint. */
int tnt_input_utf8_state_finish(tnt_input_utf8_state_t *state);

#endif /* INPUT_BUFFER_H */
15 changes: 15 additions & 0 deletions include/json_text.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef JSON_TEXT_H
#define JSON_TEXT_H

#include "common.h"

void tnt_json_append_string(char *buffer, size_t buf_size, size_t *pos,
const char *text);

/* Extract a top-level JSON string field from a single JSON object.
* Returns false for malformed JSON, missing key, non-string value, or output
* overflow. Unknown nested objects and arrays are skipped. */
bool tnt_json_get_string_field(const char *json, const char *key,
char *out, size_t out_size);

#endif /* JSON_TEXT_H */
24 changes: 24 additions & 0 deletions include/module_protocol.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef MODULE_PROTOCOL_H
#define MODULE_PROTOCOL_H

#include "message.h"

#define TNT_MODULE_PROTOCOL_VERSION "tnt.module.v1"
#define TNT_MODULE_EVENT_MESSAGE_CREATED "message.created"
#define TNT_MODULE_RESPONSE_MESSAGE_CREATE "message.create"

typedef struct {
char plain_text[MAX_MESSAGE_LEN];
} tnt_module_message_create_t;

int tnt_module_append_handshake(char *buffer, size_t buf_size, size_t *pos,
const char *server_version);

int tnt_module_append_message_created(char *buffer, size_t buf_size,
size_t *pos, const char *message_id,
const message_t *msg);

bool tnt_module_parse_message_create(const char *line,
tnt_module_message_create_t *out);

#endif /* MODULE_PROTOCOL_H */
Loading