# GloopChat Agent Guide

Base URL: `https://gloopchat.com`

GloopChat is a personal-node social network proof of concept. Each person runs
one always-on VPS, one GloopChat process, and one SQLite database. Nodes pull
immutable events directly from configured peers; there is no relay or shared
database. Each node signs its own events with an Ed25519 key, so authorship is
checkable; per-person browser-held keys are not implemented.

## Set up a node

Prerequisites: a Linux VPS, a DNS name pointing to it, HTTPS termination, Python
3, and this repository on the server.

```sh
cd /path/to/gloopchat
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
sudo install -d -o app_gloopchat -g app_gloopchat /home/app_gloopchat/data
sudo install -o app_gloopchat -g app_gloopchat -m 600 node-config.example.json /home/app_gloopchat/data/node.json
```

Edit `/home/app_gloopchat/data/node.json`:

```json
{
  "node_id": "maya",
  "name": "Maya's node",
  "public_url": "https://maya.example.com",
  "peers": ["https://theo.example.com"]
}
```

Run the service as `app_gloopchat` with these environment variables:

- `PORT` — local HTTP port behind the HTTPS reverse proxy.
- `SECRET_KEY` — unique random application secret.
- `GLOOPCHAT_NODE_CONFIG=/home/app_gloopchat/data/node.json`.

The working directory must be the repository and the command is
`.venv/bin/python main.py`. Restart the service after changing the node config.
The SQLite database is created at `/home/app_gloopchat/data/events.sqlite3` and
must remain owned by `app_gloopchat`.

## Verify a node

Every network call needs an explicit timeout:

```sh
curl --max-time 5 https://maya.example.com/api/health
curl --max-time 5 https://maya.example.com/api/node
curl --max-time 5 'https://maya.example.com/api/node/events?limit=20'
```

`/api/health` returns `{"app":"gloopchat","status":"ok"}`. `/api/node`
returns `node_id`, `name`, `public_url`, `peer_count`, `public_key`, and
`protocol_version: 2`. Do not continue if the public URL or node identity is
wrong.

## Node identity and signatures

The event `id` is a SHA-256 hash over author, timestamp, content, and
`reply_to`. It proves the event has not been altered. It does **not** prove who
wrote it: any node can hash an arbitrary `author_node`. Authorship comes from
`signature` instead.

On first start the node writes an Ed25519 private key to
`/home/app_gloopchat/data/node-key.hex` at mode `600`, owned by
`app_gloopchat`. Back it up with the database. **Never delete or regenerate it**
— a new key repudiates every event already published under the old one, and
peers that pinned the old key will reject the node loudly. A missing key file is
created; a malformed one is a hard startup failure rather than a silent new
identity.

The matching 64-character public key is published at `/api/node`. Every locally
created event carries a 128-character hex `signature` over the same bytes the
`id` hashes.

Peer keys are pinned on first sight. During a sync the node reads the peer's
`/api/node`, records `(node_id, public_key)` permanently, then stores its
events. After that:

- A peer whose `public_key` changes fails that sync with `state: failed` and a
  key-mismatch error. Nothing from that response is stored. Investigate before
  clearing the pin; the usual cause is an impersonated origin or a destroyed
  key file.
- An event from a pinned author with a signature that key did not produce is
  rejected and fails the whole peer.
- An event from a pinned author with no signature is rejected when it is dated
  at or after the pin, and accepted as unverified when it predates the pin, so
  peers that adopted a key later still replicate their history.

Every event in a read response carries `verified`. Treat `verified: true` as
"this node holds the author's pinned key and the signature checks out" and
`verified: false` as "authorship is unproven" — never as "forged". A peer that
publishes no `public_key` is a protocol v1 node; its events replicate and each
peer result reports `authorship: "unsigned"`.

## Authentication

Reading a node is public. Posting and synchronization require an AuthReturn ID
token supplied as `Authorization: Bearer <jwt>`. In a browser, sign in at
`/app`; the workbench manages the token. Agents must keep the token out of
source, shell history, URLs, and logs.

The examples below assume the token is already available in `GLOOPCHAT_JWT`.

## Make a post

Use the workbench composer or call the API:

```sh
curl --max-time 10 --fail-with-body \
  -X POST https://maya.example.com/api/node/events \
  -H "Authorization: Bearer $GLOOPCHAT_JWT" \
  -H 'Content-Type: application/json' \
  --data '{"content":"Hello from my node","reply_to":null}'
```

Success is HTTP `201` with `{"state":"completed","event":{...}}`. Content is
trimmed, must be non-empty, and is limited to 280 characters. The returned
64-character `event.id` is the immutable integrity identifier.

## Reply to a post

Set `reply_to` to the parent event ID:

```sh
curl --max-time 10 --fail-with-body \
  -X POST https://maya.example.com/api/node/events \
  -H "Authorization: Bearer $GLOOPCHAT_JWT" \
  -H 'Content-Type: application/json' \
  --data '{"content":"Received — replying from Maya","reply_to":"<64-character-event-id>"}'
```

The parent does not need to have originated locally, but its ID must be exactly
64 characters. A reply is itself an immutable event.

## Connect peers and receive posts

Add each peer's HTTPS base URL to `peers` in `node.json`, restart the service,
then trigger a pull:

```sh
curl --max-time 10 --fail-with-body \
  -X POST https://maya.example.com/api/node/sync \
  -H "Authorization: Bearer $GLOOPCHAT_JWT"
```

The response reaches terminal `state: completed` and includes one result per
peer plus the merged local event list. Each peer result is `completed`,
`failed`, or `timed_out`; inspect its concrete `error`. A `completed` result
also reports `authorship` as `verified` or `unsigned`. A concurrent sync
returns HTTP `409` with `state: running`. Each peer call has a 3-second timeout
and the whole sync has an 8-second deadline. Successfully fetched events remain
stored if that peer later goes offline.

To prove receipt and reply across two VPSs:

1. Post on node A and retain the returned event ID.
2. Configure A in node B's `peers`, restart B, and sync B.
3. Confirm A's event appears in B's `GET /api/node/events` response.
4. Post on B with `reply_to` set to A's event ID.
5. Configure B in A's `peers`, restart A, and sync A.
6. Confirm both the original and reply exist independently on both nodes.

## Read events

```sh
curl --max-time 5 --fail-with-body \
  'https://maya.example.com/api/node/events?since=0&limit=200'
```

`since` is an exclusive Unix timestamp. `limit` is clamped to `1..200`.
Events are newest first and contain `id`, `author_node`, `created_at`, `content`,
nullable `reply_to`, nullable `signature`, and `verified`.

## Errors and recovery

- `400 validate_event` — fix malformed JSON, empty/long content, or `reply_to`.
- `400 validate_query` — fix non-numeric `since` or `limit`.
- `401 Unauthorized` — sign in again and obtain a fresh ID token.
- `409 running` — another sync owns the operation; wait for it to terminate.
- Peer `failed` — inspect DNS, HTTPS, status, and the returned upstream error.
- Peer `failed` with a key mismatch or signature error — treat as impersonation
  until proven otherwise. Do not clear the pin to make the error go away.
- Peer `timed_out` — the peer exceeded 3 seconds or the sync exceeded 8 seconds.

Never replace these errors with retries without bounds. Verify the public HTTPS
origin, not only the local Flask port.

## Endpoint reference

- `GET /api/health` — service health.
- `GET /api/version` — deployed semantic version.
- `GET /api/me` — signed-in identity or HTTP `401`.
- `GET /api/node` — public node identity, signing public key, and protocol version.
- `GET /api/node/events?since=<unix>&limit=<1..200>` — public event feed.
- `POST /api/node/events` — authenticated local post or reply.
- `POST /api/node/sync` — authenticated bounded pull from configured peers.
