tibet-cmail 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,193 @@
1
+ # Secrets & env
2
+ .env
3
+ *.env
4
+ *.secret
5
+
6
+ # Keys & certs
7
+ *.key
8
+ *.pem
9
+ certs/
10
+ secrets/
11
+
12
+ # Databases & dumps
13
+ *.db
14
+ *.sqlite
15
+ *.sql
16
+ dump_*/
17
+
18
+ # EXCEPT: Allow database schemas (needed for server rebuild)
19
+ !database-schemas/*.sql
20
+
21
+ # Logs & runtime data
22
+ logs/
23
+ *.log
24
+ __pycache__/
25
+ *.pyc
26
+ venv/
27
+ .venv/
28
+ **/venv/
29
+ **/.venv/
30
+
31
+ # ─── Brain API runtime state (mirrored to OHM1, NOT to GitHub) ──────────
32
+ # Registry + sessions + caches contain hardware hashes, public keys,
33
+ # session tokens, conversation state. Privacy-sensitive. Restore from
34
+ # OHM1 mirror on server rebuild, not from git.
35
+ brain_api/data/
36
+ brain_api/.conversation_cache/
37
+ brain_api/ains_registry.json
38
+ brain_api/ipoll_registry.json
39
+ brain_api/high_five_log.json
40
+ brain_api/agent_keys/
41
+ brain_api/founder_counter.json
42
+
43
+ # Pending claims, phantom sessions, consent store — never to GitHub
44
+ brain_api/**/pending_claims.json
45
+ brain_api/**/phantom_sessions.json
46
+ brain_api/**/consent_store.json
47
+ brain_api/**/ainternet_sessions.json
48
+ brain_api/**/ainternet_challenges.json
49
+ brain_api/**/byoa_agents.json
50
+ brain_api/**/canvas_data.json
51
+ brain_api/**/ai_response_log.json
52
+ brain_api/**/ai_team_context.json
53
+ brain_api/**/ai_teams_sessions.json
54
+ brain_api/**/evolution_timeline.json
55
+
56
+ # Static downloads (binaries served via nginx, not source)
57
+ brain_api/static/downloads/
58
+
59
+ # ─── Signing keys / keystores — NEVER on GitHub ─────────────────────────
60
+ # These live on DL360 + OHM1 mirror + USB stick + encrypted off-site backup.
61
+ # Loss = no more Play Store updates for org.ainternet.kit forever.
62
+ *.keystore
63
+ *.jks
64
+ *.keystore.gpg
65
+ *.jks.gpg
66
+ keystore.properties
67
+ keystores/
68
+
69
+ # Configs met secrets (we gebruiken straks templates)
70
+ config/
71
+ brain_api/provisioning.local.json
72
+ brain_api/provisioning.json
73
+
74
+ # Landing pages (privé - niet open source)
75
+ landing-pages/
76
+ humotica.com/
77
+ jtel.nl/
78
+
79
+ # Social media posts (strategie - niet open source)
80
+ SOCIAL-MEDIA-POSTS.md
81
+ HN-POST-UNDER-4000.md
82
+ STRATO-DEPLOY-HUMOTICA.md
83
+
84
+ # Endorsement outreach (privaat contact)
85
+ ARXIV-ENDORSEMENT-OUTREACH.md
86
+
87
+ # Deployment secrets
88
+ DEPLOYMENT-GUIDE.md
89
+
90
+ # R Project files (Dirty Data Challenge)
91
+ .Rproj.user
92
+ .Rhistory
93
+ .RData
94
+ .Ruserdata
95
+ *.zip
96
+ .mural_tokens.json
97
+ auth.json
98
+ gen-lang-client*.json
99
+ *.credentials.json
100
+
101
+ # Rust build artifacts
102
+ **/target/
103
+ *.whl
104
+
105
+ # Compiled binaries (build locally)
106
+ jis-router/jis-router
107
+ sentinel-rs/sentinel-rs
108
+
109
+ # Build distribution
110
+ sandbox/ai/codex/dist/
111
+ sandbox_backup/
112
+ did-jis-core
113
+
114
+ # =============================================================================
115
+ # Eigen repos — hebben hun eigen git remotes, niet dubbel opslaan
116
+ # =============================================================================
117
+
118
+ # Packages (elk een eigen repo)
119
+ packages/jis-iam-bridge/
120
+ packages/rapid-rag/
121
+ packages/reflux/
122
+ packages/sema-protocol/
123
+ packages/tibet-anticheat/
124
+ packages/tibet-ci/
125
+ packages/tibet-claw/
126
+ packages/tibet-context/
127
+ packages/tibet-core/
128
+ packages/tibet-db/
129
+ packages/tibet-edge/
130
+ packages/tibet-forge/
131
+ packages/tibet-iot/
132
+ packages/tibet-jawbreaker/
133
+ packages/tibet-ledger/
134
+ packages/tibet-marketplace/
135
+ packages/tibet-mesh/
136
+ packages/tibet-mirror/
137
+ packages/tibet-nis2/
138
+ packages/tibet-overlay/
139
+ packages/tibet-phantom/
140
+ packages/tibet-phantom-mcp/
141
+ packages/tibet-ping/
142
+ packages/tibet-pol/
143
+ packages/tibet-pqc/
144
+ packages/tibet-sbom/
145
+ packages/tibet-snap/
146
+ packages/tibet-soc/
147
+ packages/tibet-spiffe/
148
+ packages/tibet-tools/
149
+ packages/tibet-trail/
150
+ packages/tibet-triage/
151
+ packages/tibet-triage-mcp/
152
+ packages/tibet-twin/
153
+ packages/tibet-workload/
154
+ packages/tibet-y2k38/
155
+ packages/tlex-edge/
156
+ packages/tibet-tail/
157
+ packages/tibet-nc/
158
+
159
+ # Sub-projects met eigen repos
160
+ bunq7/
161
+ humotica-core/
162
+ jis-core/
163
+ JTm-dev/
164
+ kit-package/
165
+ symbAIon/
166
+ tibet-audit/
167
+ tibet-audit-npm/
168
+ tibet-core/
169
+ tibetclaw/
170
+ snaft/
171
+
172
+ # MCP servers (eigen repos)
173
+ mcp-servers/aidrac/
174
+ mcp-servers/ainternet/
175
+ mcp-servers/mcp-server-jis/
176
+ mcp-servers/sensory/
177
+ mcp-servers/tibet/
178
+
179
+ # Hackathon sub-repos
180
+ hackaway2026/clawmetry/
181
+
182
+ # Private memory (eigen repo)
183
+ .root_ai_memory/
184
+ .root_ai_thoughts/
185
+ brain_api/static/*.apk
186
+
187
+ # SWARM-003 refactor backups (local rollback only)
188
+ *.pre-secrets-refactor.bak
189
+ .env.bak-*
190
+
191
+ # Forensic audit trail — pentest logs (groot, niet versioned)
192
+ brain_api/pentest_logs/*.jsonl
193
+ brain_api/pentest_logs/*.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jasper van de Meent, Root AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: tibet-cmail
3
+ Version: 0.1.0
4
+ Summary: Cmail — capsulated email + command hub for HumoticaOS. Light Mode v0.1: I-Poll transport + JSON envelopes + cap-bus event emit. Sealed Mode (TBZ + continuityd) comes in 0.2.x.
5
+ Project-URL: Homepage, https://humotica.com
6
+ Project-URL: Repository, https://github.com/Humotica/tibet-cmail
7
+ Author-email: Jasper van de Meent <jasper@humotica.com>, Root AI <root_idd@humotica.nl>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai-messaging,ainternet,capsulated-email,cmail,humotica,ipoll,tibet
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Communications :: Email
20
+ Classifier: Topic :: System :: Networking
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ # tibet-cmail
25
+
26
+ **Cmail — capsulated email + command hub for HumoticaOS.**
27
+
28
+ Cmail turns AInternet into a mailbox: human-readable messages that carry sealed
29
+ intent, provenance, and consent across `.aint` agents. Light Mode v0.1 ships
30
+ today; Sealed Mode comes in 0.2.x.
31
+
32
+ [![PyPI](https://img.shields.io/pypi/v/tibet-cmail)](https://pypi.org/project/tibet-cmail/)
33
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
34
+
35
+ ## Quick start
36
+
37
+ ```bash
38
+ pip install tibet-cmail
39
+
40
+ # send a cmail through the local brain_api
41
+ tibet-cmail send bob.aint "lunch?" "12:30 at the usual" --from alice
42
+
43
+ # or via the public AInternet hub
44
+ tibet-cmail --ainternet send bob.aint "lunch?" "12:30" --from alice
45
+
46
+ # read what landed in your inbox
47
+ tibet-cmail inbox alice
48
+ tibet-cmail read alice cmail_1feab795c68c4674
49
+ ```
50
+
51
+ ## Why cmail?
52
+
53
+ `ainternet` already gives `.aint` agents the ability to message each other via
54
+ **I-Poll**. `tibet-cmail` adds the human surface on top:
55
+
56
+ - **structured envelopes** — `from`, `to`, `subject`, `body`, `content_hash`
57
+ - **identity-anchored** — sender and recipient are `.aint` addresses
58
+ - **auditable** — every message ID can be cross-referenced against a
59
+ `gateway-event.v1` record on `tibet-cap-bus`
60
+ - **routable** — same `--local` / `--ainternet` / `--brein` shortcuts as
61
+ `ipoll`, so you can talk privately to your own brain or publicly to the
62
+ AInternet hub
63
+
64
+ Cmail is to AInternet what **email** is to the public Internet: a daily-use
65
+ shape on top of the protocol layer.
66
+
67
+ ## Light Mode (v0.1)
68
+
69
+ ```text
70
+ compose envelope ─→ I-Poll PUSH ─→ recipient.aint inbox
71
+
72
+ └→ tibet-cmail inbox → list
73
+ └→ tibet-cmail read → full body
74
+ ```
75
+
76
+ - Transport: I-Poll (the AInternet messaging protocol).
77
+ - Envelope: JSON with stable key order, sha256 `content_hash`, `cmail.message.v1` kind.
78
+ - Backend: `localhost:8000` (default), `api.ainternet.org` (`--ainternet`),
79
+ or `brein.jaspervandemeent.nl` (`--brein`).
80
+ - No encryption — Light Mode is for friction-free first use; Sealed Mode v0.2.x
81
+ will add TBZ + tibet-continuityd routing for confidentiality.
82
+
83
+ ## Sealed Mode (v0.2.x — coming)
84
+
85
+ ```text
86
+ compose envelope ─→ tbz pack ─→ /var/lib/tibet/inbox ─→ tibet-continuityd
87
+
88
+ └→ trust-verdict
89
+ └→ I-Poll notify
90
+ └→ cmail inbox
91
+ ```
92
+
93
+ Sealed Mode adds:
94
+
95
+ - TBZ packing (`tibet-zip-cli`) with AES-256-GCM.
96
+ - `tibet-continuityd` arrival + verify_fork on the recipient side.
97
+ - SAM-binding for human (non-AI) recipients.
98
+ - Sealed audit record in `tibet-trail`.
99
+
100
+ ## CLI reference
101
+
102
+ | Command | What it does |
103
+ |---|---|
104
+ | `tibet-cmail send <to> <subject> <body> --from <agent>` | Send a cmail (Light Mode). |
105
+ | `tibet-cmail inbox <agent>` | Preview inbound cmails (no mark-read). |
106
+ | `tibet-cmail read <agent> <message-id>` | Print one cmail in full + verify content_hash. |
107
+ | `tibet-cmail status` | Backend status + cmail mode + envelope kind. |
108
+
109
+ Global flags: `--local`, `--ainternet`, `--brein`, `--url <host>:<port>`,
110
+ `--timeout`, `--json`. The `CMAIL_API_URL` env var overrides `--url`.
111
+
112
+ ## Envelope shape (v1)
113
+
114
+ ```json
115
+ {
116
+ "kind": "cmail.message.v1",
117
+ "message_id": "cmail_<uuid4-hex16>",
118
+ "from": "alice.aint",
119
+ "to": "bob.aint",
120
+ "subject": "Re: lunch?",
121
+ "body": "12:30 at the usual",
122
+ "body_class": "text/plain",
123
+ "sent_at": "2026-05-30T08:00:00+00:00",
124
+ "content_hash": "sha256:..."
125
+ }
126
+ ```
127
+
128
+ `tibet-cmail inbox` filters incoming I-Polls by `kind == cmail.message.v1`, so
129
+ the cmail surface stays separate from generic agent-to-agent I-Polls.
130
+
131
+ ## Stack position
132
+
133
+ Layer in the Humotica stack:
134
+
135
+ - Group: **agentic** (operator + agent inbox surface)
136
+ - Bootstrap: I-Poll transport via [`ainternet`](https://pypi.org/project/ainternet/) +
137
+ [`ipoll`](https://pypi.org/project/ipoll/) `0.2.5+`.
138
+ - Audit trail: [`tibet-cap-bus`](https://pypi.org/project/tibet-cap-bus/) `0.1.3+`
139
+ carries `cmail.message.sent` / `cmail.message.received` as
140
+ `gateway-event.v1` records.
141
+ - Sealed Mode adds: [`tibet-zip-cli`](https://crates.io/crates/tibet-zip-cli) +
142
+ [`tibet-continuityd`](https://pypi.org/project/tibet-continuityd/) `0.6.16+`.
143
+
144
+ See `STACK.md` in the Humotica org for the full canonical package map.
145
+
146
+ ## License
147
+
148
+ MIT — see [`LICENSE`](./LICENSE).
149
+
150
+ ## Credits
151
+
152
+ Built by **Jasper van de Meent** + **Root AI (Claude)**, with design input from
153
+ Codex (cmail-osapi-daemon-architecture, cmail-as-hub).
154
+
155
+ Part of [HumoticaOS](https://humotica.com). One love, one fAmIly. 💙
@@ -0,0 +1,132 @@
1
+ # tibet-cmail
2
+
3
+ **Cmail — capsulated email + command hub for HumoticaOS.**
4
+
5
+ Cmail turns AInternet into a mailbox: human-readable messages that carry sealed
6
+ intent, provenance, and consent across `.aint` agents. Light Mode v0.1 ships
7
+ today; Sealed Mode comes in 0.2.x.
8
+
9
+ [![PyPI](https://img.shields.io/pypi/v/tibet-cmail)](https://pypi.org/project/tibet-cmail/)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
+
12
+ ## Quick start
13
+
14
+ ```bash
15
+ pip install tibet-cmail
16
+
17
+ # send a cmail through the local brain_api
18
+ tibet-cmail send bob.aint "lunch?" "12:30 at the usual" --from alice
19
+
20
+ # or via the public AInternet hub
21
+ tibet-cmail --ainternet send bob.aint "lunch?" "12:30" --from alice
22
+
23
+ # read what landed in your inbox
24
+ tibet-cmail inbox alice
25
+ tibet-cmail read alice cmail_1feab795c68c4674
26
+ ```
27
+
28
+ ## Why cmail?
29
+
30
+ `ainternet` already gives `.aint` agents the ability to message each other via
31
+ **I-Poll**. `tibet-cmail` adds the human surface on top:
32
+
33
+ - **structured envelopes** — `from`, `to`, `subject`, `body`, `content_hash`
34
+ - **identity-anchored** — sender and recipient are `.aint` addresses
35
+ - **auditable** — every message ID can be cross-referenced against a
36
+ `gateway-event.v1` record on `tibet-cap-bus`
37
+ - **routable** — same `--local` / `--ainternet` / `--brein` shortcuts as
38
+ `ipoll`, so you can talk privately to your own brain or publicly to the
39
+ AInternet hub
40
+
41
+ Cmail is to AInternet what **email** is to the public Internet: a daily-use
42
+ shape on top of the protocol layer.
43
+
44
+ ## Light Mode (v0.1)
45
+
46
+ ```text
47
+ compose envelope ─→ I-Poll PUSH ─→ recipient.aint inbox
48
+
49
+ └→ tibet-cmail inbox → list
50
+ └→ tibet-cmail read → full body
51
+ ```
52
+
53
+ - Transport: I-Poll (the AInternet messaging protocol).
54
+ - Envelope: JSON with stable key order, sha256 `content_hash`, `cmail.message.v1` kind.
55
+ - Backend: `localhost:8000` (default), `api.ainternet.org` (`--ainternet`),
56
+ or `brein.jaspervandemeent.nl` (`--brein`).
57
+ - No encryption — Light Mode is for friction-free first use; Sealed Mode v0.2.x
58
+ will add TBZ + tibet-continuityd routing for confidentiality.
59
+
60
+ ## Sealed Mode (v0.2.x — coming)
61
+
62
+ ```text
63
+ compose envelope ─→ tbz pack ─→ /var/lib/tibet/inbox ─→ tibet-continuityd
64
+
65
+ └→ trust-verdict
66
+ └→ I-Poll notify
67
+ └→ cmail inbox
68
+ ```
69
+
70
+ Sealed Mode adds:
71
+
72
+ - TBZ packing (`tibet-zip-cli`) with AES-256-GCM.
73
+ - `tibet-continuityd` arrival + verify_fork on the recipient side.
74
+ - SAM-binding for human (non-AI) recipients.
75
+ - Sealed audit record in `tibet-trail`.
76
+
77
+ ## CLI reference
78
+
79
+ | Command | What it does |
80
+ |---|---|
81
+ | `tibet-cmail send <to> <subject> <body> --from <agent>` | Send a cmail (Light Mode). |
82
+ | `tibet-cmail inbox <agent>` | Preview inbound cmails (no mark-read). |
83
+ | `tibet-cmail read <agent> <message-id>` | Print one cmail in full + verify content_hash. |
84
+ | `tibet-cmail status` | Backend status + cmail mode + envelope kind. |
85
+
86
+ Global flags: `--local`, `--ainternet`, `--brein`, `--url <host>:<port>`,
87
+ `--timeout`, `--json`. The `CMAIL_API_URL` env var overrides `--url`.
88
+
89
+ ## Envelope shape (v1)
90
+
91
+ ```json
92
+ {
93
+ "kind": "cmail.message.v1",
94
+ "message_id": "cmail_<uuid4-hex16>",
95
+ "from": "alice.aint",
96
+ "to": "bob.aint",
97
+ "subject": "Re: lunch?",
98
+ "body": "12:30 at the usual",
99
+ "body_class": "text/plain",
100
+ "sent_at": "2026-05-30T08:00:00+00:00",
101
+ "content_hash": "sha256:..."
102
+ }
103
+ ```
104
+
105
+ `tibet-cmail inbox` filters incoming I-Polls by `kind == cmail.message.v1`, so
106
+ the cmail surface stays separate from generic agent-to-agent I-Polls.
107
+
108
+ ## Stack position
109
+
110
+ Layer in the Humotica stack:
111
+
112
+ - Group: **agentic** (operator + agent inbox surface)
113
+ - Bootstrap: I-Poll transport via [`ainternet`](https://pypi.org/project/ainternet/) +
114
+ [`ipoll`](https://pypi.org/project/ipoll/) `0.2.5+`.
115
+ - Audit trail: [`tibet-cap-bus`](https://pypi.org/project/tibet-cap-bus/) `0.1.3+`
116
+ carries `cmail.message.sent` / `cmail.message.received` as
117
+ `gateway-event.v1` records.
118
+ - Sealed Mode adds: [`tibet-zip-cli`](https://crates.io/crates/tibet-zip-cli) +
119
+ [`tibet-continuityd`](https://pypi.org/project/tibet-continuityd/) `0.6.16+`.
120
+
121
+ See `STACK.md` in the Humotica org for the full canonical package map.
122
+
123
+ ## License
124
+
125
+ MIT — see [`LICENSE`](./LICENSE).
126
+
127
+ ## Credits
128
+
129
+ Built by **Jasper van de Meent** + **Root AI (Claude)**, with design input from
130
+ Codex (cmail-osapi-daemon-architecture, cmail-as-hub).
131
+
132
+ Part of [HumoticaOS](https://humotica.com). One love, one fAmIly. 💙
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tibet-cmail"
7
+ version = "0.1.0"
8
+ description = "Cmail — capsulated email + command hub for HumoticaOS. Light Mode v0.1: I-Poll transport + JSON envelopes + cap-bus event emit. Sealed Mode (TBZ + continuityd) comes in 0.2.x."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Jasper van de Meent", email = "jasper@humotica.com" },
14
+ { name = "Root AI", email = "root_idd@humotica.nl" }
15
+ ]
16
+ keywords = [
17
+ "cmail",
18
+ "capsulated-email",
19
+ "ai-messaging",
20
+ "ainternet",
21
+ "tibet",
22
+ "humotica",
23
+ "ipoll",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Topic :: Communications :: Email",
35
+ "Topic :: System :: Networking",
36
+ ]
37
+
38
+ # Light Mode 0.1.x: pure stdlib (argparse + urllib + json + hashlib + uuid).
39
+ # Sealed Mode 0.2.x will add optional deps: tibet-zip, tibet-continuityd, tibet-drop.
40
+ dependencies = []
41
+
42
+ [project.urls]
43
+ Homepage = "https://humotica.com"
44
+ Repository = "https://github.com/Humotica/tibet-cmail"
45
+
46
+ [project.scripts]
47
+ tibet-cmail = "tibet_cmail.cli:main"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/tibet_cmail"]
@@ -0,0 +1,26 @@
1
+ """
2
+ tibet-cmail — capsulated email + command hub for HumoticaOS.
3
+
4
+ Light Mode (v0.1.x):
5
+ Transport: I-Poll PUSH (over brain_api at localhost:8000 / api.ainternet.org)
6
+ Envelope: JSON with from/to/subject/body/sent_at/message_id/content_hash
7
+ Evidence: cap-bus gateway-event.v1 (intent=cmail.message.sent/received)
8
+
9
+ Sealed Mode (v0.2.x — future):
10
+ Add TBZ-pack + tibet-continuityd inbox routing + SAM-binding for non-AI recipients.
11
+
12
+ Three pillars (mirrors `cmail-as-hub` anchor doc):
13
+ INBOX — read inbound cmails
14
+ COMPOSE — write + send
15
+ AUDIT — open trace via cap-bus events
16
+
17
+ Public API:
18
+ from tibet_cmail.envelope import Envelope, build_envelope, hash_body
19
+ from tibet_cmail.cli import main as cli_main
20
+ """
21
+
22
+ __version__ = "0.1.0"
23
+
24
+ from .envelope import Envelope, build_envelope, hash_body
25
+
26
+ __all__ = ["Envelope", "build_envelope", "hash_body", "__version__"]
@@ -0,0 +1,288 @@
1
+ """
2
+ tibet-cmail — command-line interface (Light Mode, v0.1).
3
+
4
+ Commands:
5
+ tibet-cmail send <to> <subject> <body> --from <agent>
6
+ send a cmail via I-Poll (Light Mode)
7
+ tibet-cmail inbox <agent> list inbound cmails (preview)
8
+ tibet-cmail read <agent> <msg-id> read one cmail body in full
9
+ tibet-cmail status backend status
10
+
11
+ Routing (mirrors ipoll discipline):
12
+ --local http://localhost:8000 your local brain_api
13
+ --ainternet https://api.ainternet.org primary public hub
14
+ --brein https://brein.jaspervandemeent.nl secondary fallback
15
+
16
+ Light Mode = no encryption. Sealed Mode (v0.2.x) will add TBZ + continuityd routing.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+ import urllib.error
26
+ import urllib.request
27
+ from typing import Any
28
+
29
+ from . import __version__
30
+ from .envelope import CMAIL_KIND, Envelope, build_envelope
31
+
32
+
33
+ LOCAL_URL = "http://localhost:8000"
34
+ AINTERNET_URL = "https://api.ainternet.org"
35
+ BREIN_URL = "https://brein.jaspervandemeent.nl"
36
+ DEFAULT_URL = os.environ.get("CMAIL_API_URL", LOCAL_URL)
37
+ _USER_AGENT = f"tibet-cmail/{__version__}"
38
+
39
+
40
+ def _is_connection_refused(exc: BaseException) -> bool:
41
+ cause = exc.__cause__ or exc.__context__
42
+ while cause is not None:
43
+ if isinstance(cause, ConnectionRefusedError):
44
+ return True
45
+ cause = cause.__cause__ or cause.__context__
46
+ if isinstance(exc, urllib.error.URLError):
47
+ return isinstance(getattr(exc, "reason", None), ConnectionRefusedError)
48
+ return isinstance(exc, ConnectionRefusedError)
49
+
50
+
51
+ def _explain_unreachable(url: str, exc: BaseException) -> str:
52
+ if _is_connection_refused(exc):
53
+ return (
54
+ f"tibet-cmail: backend at {url} is not running (connection refused).\n"
55
+ f" - Start a local brain_api on port 8000 (private/fast), or\n"
56
+ f" - retry with --ainternet for the public hub ({AINTERNET_URL}), or\n"
57
+ f" - retry with --brein for the secondary fallback ({BREIN_URL}), or\n"
58
+ f" - set CMAIL_API_URL / use --url <host>:<port>."
59
+ )
60
+ reason = getattr(exc, "reason", exc)
61
+ return f"tibet-cmail: cannot reach {url}: {reason}"
62
+
63
+
64
+ def _get_json(url: str, timeout: float = 5.0) -> dict[str, Any]:
65
+ req = urllib.request.Request(
66
+ url, headers={"Accept": "application/json", "User-Agent": _USER_AGENT},
67
+ )
68
+ with urllib.request.urlopen(req, timeout=timeout) as response:
69
+ return json.loads(response.read().decode("utf-8"))
70
+
71
+
72
+ def _post_json(url: str, payload: dict[str, Any], timeout: float = 5.0) -> dict[str, Any]:
73
+ body = json.dumps(payload).encode("utf-8")
74
+ req = urllib.request.Request(
75
+ url, data=body, method="POST",
76
+ headers={
77
+ "Content-Type": "application/json",
78
+ "Accept": "application/json",
79
+ "User-Agent": _USER_AGENT,
80
+ },
81
+ )
82
+ with urllib.request.urlopen(req, timeout=timeout) as response:
83
+ return json.loads(response.read().decode("utf-8"))
84
+
85
+
86
+ def cmd_send(args: argparse.Namespace) -> int:
87
+ """Send a cmail via I-Poll PUSH (Light Mode)."""
88
+ envelope = build_envelope(
89
+ from_=args.from_agent,
90
+ to=args.to,
91
+ subject=args.subject,
92
+ body=args.body,
93
+ )
94
+ payload = {
95
+ "from_agent": envelope.from_,
96
+ "to_agent": envelope.to,
97
+ "content": envelope.to_json(),
98
+ "poll_type": "PUSH",
99
+ }
100
+ url = f"{args.url.rstrip('/')}/api/ipoll/push"
101
+ try:
102
+ data = _post_json(url, payload, timeout=args.timeout)
103
+ except urllib.error.HTTPError as e:
104
+ print(f"tibet-cmail: send failed: HTTP {e.code}", file=sys.stderr)
105
+ return 1
106
+ except urllib.error.URLError as e:
107
+ print(_explain_unreachable(args.url, e), file=sys.stderr)
108
+ return 2
109
+
110
+ if args.json:
111
+ print(json.dumps({
112
+ "envelope": envelope.to_dict(),
113
+ "i_poll": data,
114
+ }, indent=2, ensure_ascii=False))
115
+ return 0
116
+
117
+ poll_id = data.get("id") or data.get("poll_id") or "?"
118
+ print(f"cmail sent: {envelope.message_id}")
119
+ print(f" from={envelope.from_} to={envelope.to}")
120
+ print(f" subject: {envelope.subject}")
121
+ print(f" content_hash: {envelope.content_hash}")
122
+ print(f" i-poll envelope: {poll_id}")
123
+ return 0
124
+
125
+
126
+ def _is_cmail_envelope(content_str: str) -> bool:
127
+ """Heuristic: True if I-Poll PUSH content looks like a Light Mode cmail."""
128
+ try:
129
+ d = json.loads(content_str)
130
+ except (ValueError, TypeError):
131
+ return False
132
+ return isinstance(d, dict) and d.get("kind") == CMAIL_KIND
133
+
134
+
135
+ def _fetch_polls(args: argparse.Namespace, mark_read: bool) -> tuple[int, list[dict]]:
136
+ url = (
137
+ f"{args.url.rstrip('/')}/api/ipoll/pull/{args.agent}"
138
+ f"?mark_read={'true' if mark_read else 'false'}"
139
+ )
140
+ try:
141
+ data = _get_json(url, timeout=args.timeout)
142
+ except urllib.error.HTTPError as e:
143
+ if e.code == 404:
144
+ print(f"tibet-cmail: agent '{args.agent}' not found", file=sys.stderr)
145
+ return 4, []
146
+ if e.code == 401:
147
+ print(
148
+ f"tibet-cmail: inbox-pull requires authentication at this backend (HTTP 401). "
149
+ f"Use --local with a brain_api you control.",
150
+ file=sys.stderr,
151
+ )
152
+ return 5, []
153
+ print(f"tibet-cmail: pull failed: HTTP {e.code}", file=sys.stderr)
154
+ return 1, []
155
+ except urllib.error.URLError as e:
156
+ print(_explain_unreachable(args.url, e), file=sys.stderr)
157
+ return 2, []
158
+ return 0, data.get("polls", [])
159
+
160
+
161
+ def cmd_inbox(args: argparse.Namespace) -> int:
162
+ """List inbound cmails (filters non-cmail I-Polls out)."""
163
+ rc, polls = _fetch_polls(args, mark_read=False)
164
+ if rc != 0:
165
+ return rc
166
+ cmails: list[Envelope] = []
167
+ for p in polls:
168
+ content = p.get("content", "")
169
+ if _is_cmail_envelope(content):
170
+ try:
171
+ cmails.append(Envelope.from_json(content))
172
+ except Exception:
173
+ continue
174
+ if args.json:
175
+ print(json.dumps([e.to_dict() for e in cmails], indent=2, ensure_ascii=False))
176
+ return 0
177
+ if not cmails:
178
+ non_cmail = len(polls)
179
+ print(f" (no cmails for {args.agent}; {non_cmail} non-cmail I-Poll(s) skipped)")
180
+ return 0
181
+ print(f"inbox for {args.agent} ({len(cmails)} cmail{'s' if len(cmails) != 1 else ''}):")
182
+ for e in cmails:
183
+ subject = e.subject or "(no subject)"
184
+ if len(subject) > 50:
185
+ subject = subject[:47] + "..."
186
+ print(f" [{e.message_id}] from={e.from_:<18} {subject}")
187
+ return 0
188
+
189
+
190
+ def cmd_read(args: argparse.Namespace) -> int:
191
+ """Read one cmail in full by message_id (uses inbox preview, no mark-read)."""
192
+ rc, polls = _fetch_polls(args, mark_read=False)
193
+ if rc != 0:
194
+ return rc
195
+ for p in polls:
196
+ content = p.get("content", "")
197
+ if not _is_cmail_envelope(content):
198
+ continue
199
+ e = Envelope.from_json(content)
200
+ if e.message_id == args.message_id:
201
+ if args.json:
202
+ print(e.to_json(indent=2))
203
+ else:
204
+ print(f"From: {e.from_}")
205
+ print(f"To: {e.to}")
206
+ print(f"Sent: {e.sent_at}")
207
+ print(f"Subject: {e.subject}")
208
+ print(f"Hash: {e.content_hash} ({'verified' if e.verify() else 'MISMATCH'})")
209
+ print()
210
+ print(e.body)
211
+ return 0
212
+ print(f"tibet-cmail: no cmail with message_id '{args.message_id}' in {args.agent}'s inbox",
213
+ file=sys.stderr)
214
+ return 4
215
+
216
+
217
+ def cmd_status(args: argparse.Namespace) -> int:
218
+ url = f"{args.url.rstrip('/')}/api/ipoll/status"
219
+ try:
220
+ data = _get_json(url, timeout=args.timeout)
221
+ except urllib.error.URLError as e:
222
+ print(_explain_unreachable(args.url, e), file=sys.stderr)
223
+ return 2
224
+ print(f"tibet-cmail backend: {args.url}")
225
+ print(f" transport status: {data.get('status', 'unknown')}")
226
+ print(f" cmail mode: Light (I-Poll transport, no seal)")
227
+ print(f" envelope kind: {CMAIL_KIND}")
228
+ return 0
229
+
230
+
231
+ def build_parser() -> argparse.ArgumentParser:
232
+ parser = argparse.ArgumentParser(
233
+ prog="tibet-cmail",
234
+ description="Cmail — capsulated email + command hub. Light Mode (I-Poll transport).",
235
+ )
236
+ parser.add_argument("--url", default=DEFAULT_URL,
237
+ help=f"backend URL (default: {DEFAULT_URL}; env CMAIL_API_URL overrides)")
238
+ parser.add_argument("--local", action="store_true",
239
+ help=f"shortcut for --url {LOCAL_URL}")
240
+ parser.add_argument("--ainternet", action="store_true",
241
+ help=f"shortcut for --url {AINTERNET_URL}")
242
+ parser.add_argument("--brein", action="store_true",
243
+ help=f"shortcut for --url {BREIN_URL}")
244
+ parser.add_argument("--timeout", type=float, default=5.0,
245
+ help="HTTP timeout in seconds (default: 5.0)")
246
+ parser.add_argument("--json", action="store_true",
247
+ help="emit raw JSON instead of human output")
248
+ parser.add_argument("--version", action="version", version=f"tibet-cmail {__version__}")
249
+
250
+ sub = parser.add_subparsers(dest="cmd", required=True)
251
+
252
+ p_send = sub.add_parser("send", help="send a cmail via I-Poll (Light Mode)")
253
+ p_send.add_argument("to", help="recipient .aint agent")
254
+ p_send.add_argument("subject", help="cmail subject line")
255
+ p_send.add_argument("body", help="cmail body (use quotes for multi-word)")
256
+ p_send.add_argument("--from", dest="from_agent", required=True,
257
+ help="sender agent id")
258
+ p_send.set_defaults(func=cmd_send)
259
+
260
+ p_inbox = sub.add_parser("inbox", help="list inbound cmails (preview, no mark-read)")
261
+ p_inbox.add_argument("agent", help="agent name (without .aint suffix)")
262
+ p_inbox.set_defaults(func=cmd_inbox)
263
+
264
+ p_read = sub.add_parser("read", help="read one cmail in full by message_id")
265
+ p_read.add_argument("agent", help="recipient agent name")
266
+ p_read.add_argument("message_id", help="cmail message_id (cmail_<hex>)")
267
+ p_read.set_defaults(func=cmd_read)
268
+
269
+ p_status = sub.add_parser("status", help="show backend + cmail-mode status")
270
+ p_status.set_defaults(func=cmd_status)
271
+
272
+ return parser
273
+
274
+
275
+ def main(argv: list[str] | None = None) -> int:
276
+ parser = build_parser()
277
+ args = parser.parse_args(argv)
278
+ if getattr(args, "local", False):
279
+ args.url = LOCAL_URL
280
+ elif getattr(args, "ainternet", False):
281
+ args.url = AINTERNET_URL
282
+ elif getattr(args, "brein", False):
283
+ args.url = BREIN_URL
284
+ return args.func(args)
285
+
286
+
287
+ if __name__ == "__main__":
288
+ sys.exit(main())
@@ -0,0 +1,119 @@
1
+ """
2
+ Cmail envelope — the shape of a Light Mode cmail.
3
+
4
+ A cmail envelope is a JSON object that travels inside an I-Poll PUSH content
5
+ field. It carries the human-readable fields (from/to/subject/body/sent_at)
6
+ plus a unique message_id and a content_hash so receivers can verify integrity
7
+ without opening the body. Auditors who hold the corresponding
8
+ `gateway-event.v1` on cap-bus can cross-reference the message_id.
9
+
10
+ Shape (v0.1):
11
+
12
+ {
13
+ "kind": "cmail.message.v1",
14
+ "message_id": "cmail_<uuid4>",
15
+ "from": "alice.aint",
16
+ "to": "bob.aint",
17
+ "subject": "Re: lunch?",
18
+ "body": "...",
19
+ "body_class": "text/plain",
20
+ "sent_at": "2026-05-30T08:00:00+00:00",
21
+ "content_hash": "sha256:..."
22
+ }
23
+
24
+ Sealed Mode (v0.2) will add tbz_envelope_ref + attestation_ref + JIS-signed
25
+ fields. The kind string changes to `cmail.message.sealed.v1` to differentiate.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import hashlib
31
+ import json
32
+ import uuid
33
+ from dataclasses import dataclass, field, asdict
34
+ from datetime import datetime, timezone
35
+ from typing import Any, Optional
36
+
37
+
38
+ CMAIL_KIND = "cmail.message.v1"
39
+
40
+
41
+ def hash_body(body: str) -> str:
42
+ """Return sha256:<hex> for the given body (canonical UTF-8 encoding)."""
43
+ digest = hashlib.sha256(body.encode("utf-8")).hexdigest()
44
+ return f"sha256:{digest}"
45
+
46
+
47
+ def _utcnow_iso() -> str:
48
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
49
+
50
+
51
+ @dataclass
52
+ class Envelope:
53
+ """A Light Mode cmail envelope."""
54
+
55
+ from_: str
56
+ to: str
57
+ subject: str
58
+ body: str
59
+ message_id: str = field(default_factory=lambda: f"cmail_{uuid.uuid4().hex[:16]}")
60
+ sent_at: str = field(default_factory=_utcnow_iso)
61
+ body_class: str = "text/plain"
62
+ content_hash: str = ""
63
+ kind: str = CMAIL_KIND
64
+
65
+ def __post_init__(self):
66
+ if not self.content_hash:
67
+ self.content_hash = hash_body(self.body)
68
+
69
+ def to_dict(self) -> dict[str, Any]:
70
+ d = asdict(self)
71
+ # JSON-friendly: rename `from_` → `from`
72
+ d["from"] = d.pop("from_")
73
+ # Stable key order matters for hash-stable serialisation
74
+ ordered_keys = ("kind", "message_id", "from", "to", "subject",
75
+ "body", "body_class", "sent_at", "content_hash")
76
+ return {k: d[k] for k in ordered_keys}
77
+
78
+ def to_json(self, *, indent: Optional[int] = None) -> str:
79
+ return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
80
+
81
+ @classmethod
82
+ def from_dict(cls, data: dict[str, Any]) -> "Envelope":
83
+ return cls(
84
+ from_=data["from"],
85
+ to=data["to"],
86
+ subject=data.get("subject", ""),
87
+ body=data["body"],
88
+ message_id=data.get("message_id", ""),
89
+ sent_at=data.get("sent_at", ""),
90
+ body_class=data.get("body_class", "text/plain"),
91
+ content_hash=data.get("content_hash", ""),
92
+ kind=data.get("kind", CMAIL_KIND),
93
+ )
94
+
95
+ @classmethod
96
+ def from_json(cls, payload: str) -> "Envelope":
97
+ return cls.from_dict(json.loads(payload))
98
+
99
+ def verify(self) -> bool:
100
+ """True iff the content_hash matches the body."""
101
+ return self.content_hash == hash_body(self.body)
102
+
103
+
104
+ def build_envelope(
105
+ *,
106
+ from_: str,
107
+ to: str,
108
+ subject: str,
109
+ body: str,
110
+ body_class: str = "text/plain",
111
+ ) -> Envelope:
112
+ """Build a fresh envelope. message_id, sent_at, content_hash are auto-filled."""
113
+ return Envelope(
114
+ from_=from_,
115
+ to=to,
116
+ subject=subject,
117
+ body=body,
118
+ body_class=body_class,
119
+ )
@@ -0,0 +1,124 @@
1
+ """Tests for tibet_cmail.envelope — Light Mode v0.1 shape + hashing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+
8
+ import pytest
9
+
10
+ from tibet_cmail.envelope import CMAIL_KIND, Envelope, build_envelope, hash_body
11
+
12
+
13
+ def test_hash_body_stable():
14
+ h1 = hash_body("hello world")
15
+ h2 = hash_body("hello world")
16
+ assert h1 == h2
17
+ assert h1.startswith("sha256:")
18
+ assert re.fullmatch(r"sha256:[0-9a-f]{64}", h1)
19
+
20
+
21
+ def test_hash_body_changes_with_content():
22
+ assert hash_body("a") != hash_body("b")
23
+
24
+
25
+ def test_build_envelope_autofills():
26
+ e = build_envelope(
27
+ from_="alice.aint",
28
+ to="bob.aint",
29
+ subject="Re: lunch?",
30
+ body="how about 12:30?",
31
+ )
32
+ assert e.from_ == "alice.aint"
33
+ assert e.to == "bob.aint"
34
+ assert e.subject == "Re: lunch?"
35
+ assert e.body == "how about 12:30?"
36
+ assert e.message_id.startswith("cmail_")
37
+ assert len(e.message_id) > len("cmail_")
38
+ assert e.kind == CMAIL_KIND
39
+ assert e.body_class == "text/plain"
40
+ assert e.content_hash == hash_body("how about 12:30?")
41
+ assert e.sent_at # ISO timestamp present
42
+
43
+
44
+ def test_envelope_id_unique_per_build():
45
+ e1 = build_envelope(from_="a", to="b", subject="x", body="x")
46
+ e2 = build_envelope(from_="a", to="b", subject="x", body="x")
47
+ assert e1.message_id != e2.message_id
48
+
49
+
50
+ def test_envelope_verify_true_when_intact():
51
+ e = build_envelope(from_="a", to="b", subject="s", body="hello")
52
+ assert e.verify() is True
53
+
54
+
55
+ def test_envelope_verify_false_when_body_tampered():
56
+ e = build_envelope(from_="a", to="b", subject="s", body="hello")
57
+ e.body = "tampered"
58
+ assert e.verify() is False
59
+
60
+
61
+ def test_envelope_roundtrip_json():
62
+ original = build_envelope(
63
+ from_="alice.aint",
64
+ to="bob.aint",
65
+ subject="Réservé", # non-ASCII to verify utf-8 path
66
+ body="Bonjour 👋",
67
+ )
68
+ js = original.to_json()
69
+ restored = Envelope.from_json(js)
70
+ assert restored.message_id == original.message_id
71
+ assert restored.from_ == original.from_
72
+ assert restored.to == original.to
73
+ assert restored.subject == original.subject
74
+ assert restored.body == original.body
75
+ assert restored.content_hash == original.content_hash
76
+ assert restored.verify() is True
77
+
78
+
79
+ def test_envelope_json_uses_from_not_from_underscore():
80
+ """The wire-format uses `from`, not the Python-safe `from_`."""
81
+ e = build_envelope(from_="a", to="b", subject="s", body="x")
82
+ d = e.to_dict()
83
+ assert "from" in d
84
+ assert "from_" not in d
85
+
86
+
87
+ def test_envelope_json_has_stable_key_order():
88
+ """Top-level keys come in a canonical order so audit-side tooling can pin them."""
89
+ e = build_envelope(from_="a", to="b", subject="s", body="x")
90
+ keys = list(e.to_dict().keys())
91
+ expected = ["kind", "message_id", "from", "to", "subject",
92
+ "body", "body_class", "sent_at", "content_hash"]
93
+ assert keys == expected
94
+
95
+
96
+ def test_from_dict_accepts_minimal_payload():
97
+ """Receivers should be tolerant: missing optional fields take defaults."""
98
+ minimal = {
99
+ "kind": CMAIL_KIND,
100
+ "from": "x.aint",
101
+ "to": "y.aint",
102
+ "body": "hi",
103
+ }
104
+ e = Envelope.from_dict(minimal)
105
+ assert e.from_ == "x.aint"
106
+ assert e.body == "hi"
107
+ assert e.subject == ""
108
+ assert e.body_class == "text/plain"
109
+
110
+
111
+ def test_explicit_message_id_honored():
112
+ """Tests/dev can pin a stable message_id."""
113
+ e = Envelope(
114
+ from_="a",
115
+ to="b",
116
+ subject="s",
117
+ body="x",
118
+ message_id="cmail_test_pinned",
119
+ )
120
+ assert e.message_id == "cmail_test_pinned"
121
+
122
+
123
+ if __name__ == "__main__":
124
+ pytest.main([__file__, "-v"])