clavarium 0.0.2 → 0.1.0-next.1

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -6
  3. package/config/tools.json +172 -0
  4. package/deploy/appliance.sh +83 -0
  5. package/deploy/backup.sh +97 -0
  6. package/deploy/clavarium-host +720 -0
  7. package/deploy/clavarium.mjs +30 -0
  8. package/deploy/git-read +85 -0
  9. package/deploy/install.sh +653 -0
  10. package/deploy/move-to-mnt.sh +142 -0
  11. package/deploy/parley-archive.mjs +47 -0
  12. package/deploy/relay.sh +156 -0
  13. package/deploy/restore.sh +147 -0
  14. package/deploy/updater.mjs +600 -0
  15. package/npm-shrinkwrap.json +1951 -0
  16. package/package.json +53 -8
  17. package/server/alerts.mjs +83 -0
  18. package/server/answerer.mjs +326 -0
  19. package/server/api.mjs +458 -0
  20. package/server/bridge.mjs +271 -0
  21. package/server/capacity.mjs +220 -0
  22. package/server/conversations.mjs +895 -0
  23. package/server/dataformat.mjs +59 -0
  24. package/server/devices.mjs +257 -0
  25. package/server/door.mjs +235 -0
  26. package/server/files.mjs +455 -0
  27. package/server/gate.mjs +54 -0
  28. package/server/gitaccess.mjs +295 -0
  29. package/server/gitchecks.mjs +126 -0
  30. package/server/gitkeeper.mjs +719 -0
  31. package/server/gitpush.mjs +585 -0
  32. package/server/gitrunner.mjs +540 -0
  33. package/server/grants.mjs +110 -0
  34. package/server/guide.mjs +278 -0
  35. package/server/health.mjs +207 -0
  36. package/server/imap.mjs +75 -0
  37. package/server/mailbox.mjs +165 -0
  38. package/server/main.mjs +420 -0
  39. package/server/names.mjs +10 -0
  40. package/server/network.mjs +85 -0
  41. package/server/parley.mjs +62 -0
  42. package/server/resources.mjs +396 -0
  43. package/server/seats.mjs +24 -0
  44. package/server/secrets.mjs +118 -0
  45. package/server/setup.mjs +768 -0
  46. package/server/shelf.mjs +260 -0
  47. package/server/shortcuts.mjs +62 -0
  48. package/server/signin.mjs +185 -0
  49. package/server/smoke.mjs +26 -0
  50. package/server/store.mjs +48 -0
  51. package/server/talk.mjs +514 -0
  52. package/server/tmux.mjs +54 -0
  53. package/server/toolbox.mjs +168 -0
  54. package/server/tools.mjs +109 -0
  55. package/server/updates.mjs +116 -0
  56. package/server/vault.mjs +37 -0
  57. package/server/vm.mjs +139 -0
  58. package/server/voice.mjs +90 -0
  59. package/server/wakeups.mjs +320 -0
  60. package/web/app.js +4779 -0
  61. package/web/device.html +149 -0
  62. package/web/device.js +152 -0
  63. package/web/icon-192.png +0 -0
  64. package/web/icon-512.png +0 -0
  65. package/web/icon.svg +1 -0
  66. package/web/icons.js +9 -0
  67. package/web/index.html +784 -0
  68. package/web/links.js +995 -0
  69. package/web/manifest.webmanifest +30 -0
  70. package/web/prism-manual.js +3 -0
  71. package/web/style.css +1006 -0
  72. package/web/sw.js +26 -0
  73. package/web/voice-core.js +107 -0
  74. package/web/voice.js +56 -0
  75. package/keep.mjs +0 -8
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Clavarium authors
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.
package/README.md CHANGED
@@ -1,10 +1,269 @@
1
- # clavarium
1
+ # Clavarium
2
2
 
3
- Keeps the name until the Clavarium server is published under it.
3
+ A conversation is a VM with a terminal in it. Everything else is a folder.
4
4
 
5
- Clavarium: each AI agent in its own machine, with only what you attach. A terminal and a chat in a web page.
5
+ - **Conversation**: one Multipass VM and one command running in tmux. The
6
+ command is a setting: any command-line assistant, or a shell.
7
+ - **Screen**: the raw terminal of that command, in the browser. No chat layer.
8
+ - **Resource**: a folder mounted in the VM. A directory, a git repository, a
9
+ mailbox and a secret all arrive as files.
6
10
 
7
- - Site: https://clavarium.github.io/clavarium/
8
- - Source: https://github.com/clavarium/clavarium
11
+ ## Where it stands
9
12
 
10
- Running `npx clavarium` prints where it is and exits. It runs nothing else and reaches no network.
13
+ Built slice by slice; the plan is in `blueprint/architecture.html`, tab
14
+ "Plan". Done so far:
15
+
16
+ - **0 - repository and rules**: hooks, checks, conventions.
17
+ - **1 - the terminal**: a browser page drives a command-line tool running alone
18
+ in tmux; a reload finds the same session; the newest screen takes over.
19
+ - **2 - a conversation is a VM**: created from the page with a tool chosen in
20
+ `config/tools.json`; it keeps running when the page is closed, stops a
21
+ minute after the tool has exited, sleeps and wakes on request, is archived.
22
+
23
+ - **3 - folders**: a folder kept by the server is attached to a conversation
24
+ read-only or read-write and appears under `/resources/` in its VM; the same
25
+ folder can be shared by several conversations; read-only is enforced on the
26
+ host; a Files panel lists, downloads and imports.
27
+
28
+ - **4 - the edge**: with `--operators`, an authenticating proxy must stand in
29
+ front; pages, API and terminal refuse anyone who is not a declared operator.
30
+
31
+ - **5 - hands**: one-click shortcuts (sent, or left to complete), and
32
+ dictation: the microphone stays open and each phrase is typed into the
33
+ terminal; "auto" presses Enter once the person has stopped. Dictation needs
34
+ `CLAVARIUM_TRANSCRIPTION_URL` (and `_KEY`) in the environment.
35
+
36
+ - **6 - the app and sharing**: the page is installable; a conversation is
37
+ shared with a one-use QR code; the device that scans it makes its own key,
38
+ sees that one conversation under `/c/` and nothing else, and can be revoked.
39
+
40
+ - **7 - secrets, network, sign-in**: a secret is sealed at rest and handed to
41
+ a tool as a file in memory and a variable; a conversation can be switched
42
+ offline, and conversations never reach each other, both enforced on the
43
+ host; a mailbox dedicated to a tool's account is watched, read-only, and
44
+ its sign-in link shown to the operator next to the terminal.
45
+
46
+ - **8 - the answerer**: while the owner is away, several models read a
47
+ question the tool asks and answer in the owner's place, only when they all
48
+ give the same answer; one doubt and the owner is asked. Off by default,
49
+ per conversation, and never on without objectives and a charter. A limit of
50
+ answers in a row (0: none), and a log of everything. It is the one place
51
+ where the server reads what a tool prints, and the screen of that
52
+ conversation is sent to the models. Needs `CLAVARIUM_ANSWERER_URL` (a
53
+ "chat completions" address), `CLAVARIUM_ANSWERER_MODELS` (names, separated
54
+ by commas; three of different makers is the idea) and `_KEY` in the
55
+ environment.
56
+
57
+ - **9 - wake-ups**: a tool is told to get to work without its owner: when
58
+ something arrives in a folder, a repository or a mailbox attached to its
59
+ conversation (a file, a mail, a commit), every so many minutes, every day
60
+ at a time, on a cron line, or when an address of its own is called from
61
+ outside (a scheduler, a workflow tool, a script). A line the owner wrote is typed to it. This is how two tools
62
+ work together: one leaves a request in a folder they share, the other is
63
+ woken. A cap of wake-ups an hour (0: none); a conversation asleep is not
64
+ woken.
65
+
66
+ - **10 - alerts**: when a tool stops and waits for its owner, the owner's
67
+ devices are told, through the browser's own push service: nothing of ours
68
+ runs outside the house and no port is opened. Asked for on each device, in
69
+ Settings; the message is encrypted for that device alone.
70
+
71
+ - **11 - the simple screen**: each conversation can be shown without its
72
+ terminal, for whoever does not read one: a box to say what is wanted,
73
+ what is ready to read, and what the tool says now as plain text, read by
74
+ the page from its own screen. What is written is typed into the same
75
+ terminal; the server reads nothing more.
76
+
77
+ - **12 - talk through the app**: a branching on Clavarium Parley, a project
78
+ of its own (an npm dependency at an exact version, `clavarium-parley`). Every machine has a
79
+ read-only folder of tools at hand, `/tools`, with an index; Parley's
80
+ archive is the first. Asked once, in its terminal, the tool installs it
81
+ (`npm install -g /tools/...`) and talks through six commands: `parley
82
+ tell`, `ask`, `show`, `read`, `status`, `doing`, in the person's language;
83
+ it is also told to check out its branches in the repository's main folder
84
+ or under /work, never in /tmp. Parley
85
+ announces, this server decides: when the person wrote, one line is typed
86
+ in its terminal ("You have a new message: run "parley read"."), once
87
+ until it has read, never while somebody types there, never into a machine
88
+ that does not run (the message waits). Opening the conversation shows
89
+ its chat first, wherever it is opened (the list, a notification, a device
90
+ given its chat, which may write there too), its terminal one click
91
+ away where it was given; turned off, nothing changes. The chat, in two views: simple
92
+ (bubbles, the one moving line of what it does now, gone with its next
93
+ message, the steps before it folded into that message, times on hover or
94
+ a long press, the day and long silences said); detailed (every step
95
+ unfolded with its time and state, where it is, files with their paths,
96
+ its terminal); in the
97
+ device's language or one the owner chooses, with the owner's own words if
98
+ he gives any; a file outside the shared folders is said so; the owner's
99
+ phone is alerted when the tool asks or waits. Files travel in the chat both
100
+ ways: what the tool shows, and what the person attaches to a message (put
101
+ under /work/.parley/files/, the tool given its path). What is wrong with its
102
+ machine, and its restarts, are grey lines in the thread, with what can be
103
+ done; after a restart the tool is told once to read the whole thread again
104
+ (`parley read --all`). Off by default, per conversation; what the tool
105
+ prints in its terminal is never read.
106
+
107
+ - **13 - the machine**: each conversation's machine is looked at after a
108
+ start, a wake-up, a restart, and every two minutes while it runs (never one
109
+ that sleeps): its state, whether it answers, its memory, its disk, and
110
+ whether each folder that should be in it really is. A folder that came
111
+ loose is unmounted and mounted again. What is wrong is said over its
112
+ screen (the terminal or the chat), with "More memory..." where memory is
113
+ short, "Restart..." (asked first; a clean stop, then by force) and "See the
114
+ terminal"; no restart is offered while a machine merely starts. A terminal
115
+ that does not come though its machine runs is repaired by the server
116
+ itself (its channel opened again, its tmux made again if it does not
117
+ answer), and only then is a restart offered. A tool that can take up its
118
+ last session starts on it (`resume` in config/tools.json: `claude
119
+ --continue`...), unless its owner chose "Start fresh".
120
+ Its memory and disk are set in its settings (a disk only grows); the record
121
+ holds what the VM manager says it has (drawn in `blueprint/machine.html`).
122
+ The page's text has one size per
123
+ device, the terminal's included; on a phone the terminal has the room.
124
+
125
+ - **14 - who gets what, and flows**: one screen draws every device,
126
+ conversation and asset, and each right between them in the colour of what
127
+ it gives (terminal, chat, read, write, use). At rest each box says how many
128
+ links it has; a box touched shows its own, greys the rest and says them in
129
+ words, through a conversation too. A link is made by dragging from a dot
130
+ or touching one box then the other, changed or taken away by the knob on
131
+ it: the same requests and confirmations as Access, Sharing and Assets. On
132
+ a phone, a list, then one box and its links as rows. Apart, "Flows":
133
+ the trace of one box (what reaches it, what it reaches, step by step),
134
+ every group that touches nothing else, named after its most linked
135
+ conversation, and a whole setup as one file (`server/setup.mjs`): its
136
+ configuration, never its data, no secret, no key, no password, no time in
137
+ it. A file is shown whole before it is imported (what is new, what is
138
+ already here, what is missing, every text a tool will be typed); nothing
139
+ in it runs on the server; a tool not in `config/tools.json` is refused.
140
+ What a file cannot bring (a secret's value, a password, a device's
141
+ pairing) waits, drawn dashed, and is done by itself once added. Drawn in
142
+ `blueprint/flows.html`.
143
+
144
+ Not yet: mailbox and git resources, delivery.
145
+
146
+ Where the tool works (`/work`) is a folder kept by the server: what the tool
147
+ makes is seen in Files and outlives its machine. It is a disk of a fixed size
148
+ (10 GB; `WORK_MB=` in `/etc/clavarium/host.conf` changes it), so a tool that
149
+ runs away fills its own disk and not the host's. A conversation can be created
150
+ to work on its machine's own disk instead, and moved from one to the other
151
+ later (its tool is stopped for the move; a gigabyte must stay free). A kept
152
+ folder refuses symbolic links that point upwards or outside -- what installed
153
+ packages are full of -- so a code project belongs on the machine's disk.
154
+ Attached folders have no such limit yet.
155
+
156
+ Who may do what with what is one table (`server/grants.mjs`): a subject (a
157
+ conversation, or a device's pairing), an asset (conversation, folder, mailbox,
158
+ git repository, secret) and rights -- read, write, use. Attaching a folder to
159
+ a conversation and sharing with a device are lines of the same table. A share
160
+ is a chosen list in one code; a secret is never shared with a device. A
161
+ conversation is shared in two parts, ticked one by one: its terminal ("use")
162
+ and its chat ("read" and "write"); its chat is proposed where it talks through
163
+ the app, its terminal otherwise, and either is changed later in its Sharing
164
+ chapter. The server refuses what was not given: no terminal, keys, shortcuts
165
+ or dictation into it without the terminal; none of the chat without the chat.
166
+ A device given the chat alone has the files of the chat, and nothing more: no
167
+ file browser, no import, only what the tool showed and what was attached to a
168
+ message.
169
+
170
+ A git repository can be added (Folders, "Add a git repository"): empty, or
171
+ cloned from an https address. It is then a folder like any other. Git is run
172
+ by the server once, at creation, and never in a repository a tool has written.
173
+ What the tool committed is shown to the owner before it is sent ("Review &
174
+ send"): the server fetches the commits into a copy of its own, shows them, and
175
+ pushes exactly what was shown with a key it keeps sealed (`server/gitpush.mjs`).
176
+ The tool never holds that key.
177
+
178
+ A repository may also be cloned from a private remote (`server/gitaccess.mjs`),
179
+ over SSH (`user@host:path`, `ssh://...`) or HTTPS. The safest way in comes
180
+ first: an SSH key made by the server, whose private half is sealed in the vault
181
+ and never leaves the server -- only its public half is shown, to be added as a
182
+ deploy key; one key may open several repositories. An HTTPS access token, or a
183
+ user name and password (not recommended), are sealed too. Whatever opens the
184
+ remote is used by the server only, for the clone, for looking at what would
185
+ be sent, and for Send; a token only ever goes back to the origin it was given
186
+ for. For one git command at a time the SSH key is a 0600 file in a directory
187
+ of the server's own, removed right after; ssh trusts a host on first contact
188
+ and refuses it if its key changes later.
189
+
190
+ A mailbox can be added as a folder (Folders, "Add a mailbox as a folder"): its
191
+ recent messages become files that a conversation reads and never writes; the
192
+ mailbox itself is never changed. For a mail server of your own whose
193
+ certificate authority is private, `CLAVARIUM_MAIL_CA` names its file.
194
+
195
+ The host also needs `nft` (the network rules).
196
+
197
+ The host needs `tmux` and `bindfs` (the read-only views).
198
+
199
+ npm install
200
+
201
+ # the product: conversations are Multipass VMs. The server has no rights
202
+ # of its own: it reaches root only through deploy/clavarium-host, which
203
+ # reads its data directory from /etc/clavarium/host.conf (DATA=/path/to/data)
204
+ node server/main.mjs --host "sudo -n /usr/local/sbin/clavarium-host" --data /path/to/data
205
+ # then open http://127.0.0.1:7717/
206
+
207
+ # or, on a fresh Ubuntu Server (24.04, 26.04), everything above in one command:
208
+ # an account with no rights, the one program it may run as root, a service.
209
+ # and, in front, TLS and a sign-in that knows the operators (pinned releases).
210
+ # It prints each operator's password once. --core-only leaves the front out.
211
+ sudo deploy/install.sh --domain clav.example.com --operator you@example.com
212
+
213
+ # to try it on a machine of your own, with no domain: this machine only, no sign-in
214
+ sudo deploy/install.sh --local
215
+
216
+ # from npm, an exact version, checked (integrity, provenance, signatures);
217
+ # it then updates itself (Settings, Updates; --updates auto|asap|ask|off,
218
+ # --update-window 02:00-05:00, --channel latest|next) and keeps the last
219
+ # two versions to go back to (deploy/updater.mjs)
220
+ sudo npx clavarium@VERSION install --domain clav.example.com --operator you@example.com
221
+
222
+ # or all of it as one VM on any computer that has Multipass (the computer is left alone)
223
+ deploy/appliance.sh --domain clav.example.com --operator you@example.com --relay relay.example.net
224
+
225
+ # a relay for a machine at home (run on the relay; install.sh --relay prints the line)
226
+ sudo deploy/relay.sh --key "ssh-ed25519 AAAA... clavarium-tunnel"
227
+
228
+ # a backup is two files -- the archive, and the key of the vault apart
229
+ sudo deploy/backup.sh --to /path/to/a/folder
230
+ sudo deploy/restore.sh clavarium-DATE.tgz --vault-key clavarium-DATE.vault-key
231
+
232
+ # development, no VM: one tool on this machine
233
+ node server/main.mjs --command "your-tool" --cwd /path/to/work
234
+ # then open http://127.0.0.1:7717/?c=demo
235
+
236
+ It listens on the loopback interface only.
237
+
238
+ The agreed design is in `blueprint/`: `architecture.html` (architecture,
239
+ security layers, resources, app and sharing, rules, decisions, plan) and
240
+ `app-designs.html` (three first design ideas for the installable app). Open
241
+ either file in a browser. The public site is `docs/`: plain files, no build.
242
+
243
+ ## Tests
244
+
245
+ npm test # unit and integration tests: no VM, no browser
246
+ npm run test:e2e # end to end: the real server, a real browser, real terminals
247
+ npm run check # everything a push runs
248
+
249
+ The end-to-end suite drives the real pages against the real server and real
250
+ tmux sessions; only the privileged host program is a stand-in
251
+ (`e2e/fake-clavarium-host.mjs`, which plays each VM as a directory). It needs a browser:
252
+ the `playwright` package, or `CLAVARIUM_E2E_PLAYWRIGHT` pointing at one.
253
+ Without a browser its tests are skipped, and say so.
254
+
255
+ ## Working here
256
+
257
+ Read `CONVENTIONS.md` first. Then:
258
+
259
+ npm install # also wires the hooks (npm run hooks does only that)
260
+
261
+ Put your leak guard next to the clone, as `../.clavarium-guard/guard.sh`. The
262
+ hooks find it there by themselves; `hooks/guard.sh` lists the other ways.
263
+ Without a guard, nothing commits. That is on purpose.
264
+
265
+ npm run check # what a push runs
266
+
267
+ ## Licence
268
+
269
+ MIT. See `LICENSE`.
@@ -0,0 +1,172 @@
1
+ [
2
+ {
3
+ "id": "claude-code",
4
+ "instructions": ".claude/CLAUDE.md",
5
+ "label": "Claude Code",
6
+ "command": [
7
+ "claude"
8
+ ],
9
+ "resume": [
10
+ "claude",
11
+ "--continue"
12
+ ],
13
+ "memoryMb": 2048,
14
+ "rights": {
15
+ "full": [
16
+ "--dangerously-skip-permissions",
17
+ "--settings",
18
+ "{\"skipDangerousModePermissionPrompt\":true,\"permissions\":{\"allow\":[\"Bash(parley:*)\"]}}"
19
+ ],
20
+ "ask": [
21
+ "--settings",
22
+ "{\"permissions\":{\"allow\":[\"Bash(parley:*)\"]}}"
23
+ ]
24
+ },
25
+ "install": [
26
+ "node",
27
+ [
28
+ "sudo",
29
+ "-n",
30
+ "npm",
31
+ "install",
32
+ "-g",
33
+ "--no-fund",
34
+ "--no-audit",
35
+ "@anthropic-ai/claude-code"
36
+ ],
37
+ "npm-writable",
38
+ {
39
+ "argv": [
40
+ "install",
41
+ "-D",
42
+ "-m",
43
+ "0600",
44
+ "/dev/stdin",
45
+ ".claude/settings.json"
46
+ ],
47
+ "input": "{\n \"theme\": \"dark-ansi\",\n \"permissions\": {\n \"allow\": [\n \"Bash(parley:*)\"\n ]\n }\n}\n"
48
+ }
49
+ ],
50
+ "signin": {
51
+ "senders": [
52
+ "anthropic.com",
53
+ "claude.com",
54
+ "claude.ai"
55
+ ],
56
+ "links": [
57
+ "claude.ai",
58
+ "claude.com",
59
+ "anthropic.com"
60
+ ]
61
+ }
62
+ },
63
+ {
64
+ "id": "opencode",
65
+ "instructions": ".config/opencode/AGENTS.md",
66
+ "label": "OpenCode",
67
+ "command": [
68
+ "opencode"
69
+ ],
70
+ "resume": [
71
+ "opencode",
72
+ "--continue"
73
+ ],
74
+ "memoryMb": 2048,
75
+ "rights": {
76
+ "full": [
77
+ "--auto"
78
+ ]
79
+ },
80
+ "install": [
81
+ "node",
82
+ [
83
+ "sudo",
84
+ "-n",
85
+ "npm",
86
+ "install",
87
+ "-g",
88
+ "--no-fund",
89
+ "--no-audit",
90
+ "opencode-ai"
91
+ ],
92
+ "npm-writable"
93
+ ]
94
+ },
95
+ {
96
+ "id": "codex",
97
+ "instructions": ".codex/AGENTS.md",
98
+ "label": "Codex CLI",
99
+ "command": [
100
+ "codex"
101
+ ],
102
+ "resume": [
103
+ "codex",
104
+ "resume",
105
+ "--last"
106
+ ],
107
+ "memoryMb": 2048,
108
+ "rights": {
109
+ "full": [
110
+ "--dangerously-bypass-approvals-and-sandbox"
111
+ ]
112
+ },
113
+ "install": [
114
+ "node",
115
+ [
116
+ "sudo",
117
+ "-n",
118
+ "npm",
119
+ "install",
120
+ "-g",
121
+ "--no-fund",
122
+ "--no-audit",
123
+ "@openai/codex"
124
+ ],
125
+ "npm-writable"
126
+ ]
127
+ },
128
+ {
129
+ "id": "gemini",
130
+ "instructions": ".gemini/GEMINI.md",
131
+ "label": "Gemini CLI",
132
+ "command": [
133
+ "gemini"
134
+ ],
135
+ "resume": [
136
+ "gemini",
137
+ "--resume",
138
+ "latest"
139
+ ],
140
+ "memoryMb": 2048,
141
+ "rights": {
142
+ "full": [
143
+ "--yolo",
144
+ "--skip-trust"
145
+ ]
146
+ },
147
+ "install": [
148
+ "node",
149
+ [
150
+ "sudo",
151
+ "-n",
152
+ "npm",
153
+ "install",
154
+ "-g",
155
+ "--no-fund",
156
+ "--no-audit",
157
+ "@google/gemini-cli"
158
+ ],
159
+ "npm-writable"
160
+ ]
161
+ },
162
+ {
163
+ "id": "shell",
164
+ "label": "Shell",
165
+ "command": [
166
+ "bash",
167
+ "-l"
168
+ ],
169
+ "memoryMb": 768,
170
+ "install": []
171
+ }
172
+ ]
@@ -0,0 +1,83 @@
1
+ #!/bin/bash
2
+ # ---------------------------------------------------------------------------
3
+ # CLAVARIUM AS ONE VIRTUAL MACHINE, on the computer you run this on. One
4
+ # command, parameters only: it starts an Ubuntu VM with Multipass and installs
5
+ # Clavarium inside it (install.sh). The computer itself is left alone.
6
+ #
7
+ # deploy/appliance.sh --domain clav.example.com --operator you@example.com --relay relay.example.net
8
+ #
9
+ # --domain, --operator, --relay, --relay-port, --port as for install.sh
10
+ # --name NAME the VM's name (default clavarium)
11
+ # --cpus N --memory NG --disk NG its size (default 4, 8G, 60G): every
12
+ # conversation is a VM INSIDE this one, and they share it
13
+ # --release X the Ubuntu release (default 24.04)
14
+ # --sudo reach Multipass through sudo
15
+ # --dry-run check the parameters, say what would be done, do nothing
16
+ #
17
+ # A VM on a computer has no public address: without --relay, the site is only
18
+ # reachable from this computer.
19
+ #
20
+ # Exit codes: 0 done; 64 bad parameters; 78 refused.
21
+ # ---------------------------------------------------------------------------
22
+ set -euo pipefail
23
+
24
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
25
+ bad() { echo "appliance: $*" >&2; echo "usage: appliance.sh --domain NAME --operator EMAIL [--relay HOST] [--name NAME] [--cpus N] [--memory NG] [--disk NG] [--release X] [--sudo] [--dry-run]" >&2; exit 64; }
26
+ refuse() { echo "appliance: REFUSED: $*" >&2; exit 78; }
27
+ step() { echo "== $*"; }
28
+
29
+ NAME="clavarium"; CPUS="4"; MEMORY="8G"; DISK="60G"; RELEASE="24.04"; SUDO="no"; DRY="no"
30
+ pass=() # what goes to install.sh, untouched: it checks its own parameters
31
+ while [ $# -gt 0 ]; do
32
+ case "$1" in
33
+ --name) NAME="${2:-}"; shift 2 || bad "--name needs a value" ;;
34
+ --cpus) CPUS="${2:-}"; shift 2 || bad "--cpus needs a value" ;;
35
+ --memory) MEMORY="${2:-}"; shift 2 || bad "--memory needs a value" ;;
36
+ --disk) DISK="${2:-}"; shift 2 || bad "--disk needs a value" ;;
37
+ --release) RELEASE="${2:-}"; shift 2 || bad "--release needs a value" ;;
38
+ --sudo) SUDO="yes"; shift ;;
39
+ --dry-run) DRY="yes"; shift ;;
40
+ --domain|--operator|--relay|--relay-port|--port) [ $# -ge 2 ] || bad "$1 needs a value"; pass+=("$1" "$2"); shift 2 ;;
41
+ *) bad "unknown parameter: $1" ;;
42
+ esac
43
+ done
44
+ [[ "$NAME" =~ ^[a-z]([a-z0-9-]{0,30}[a-z0-9])?$ ]] || bad "--name is lower-case letters, digits and hyphens"
45
+ case "$NAME" in clv-*) bad "--name cannot start with clv-: those names are for conversations" ;; esac
46
+ [[ "$CPUS" =~ ^[0-9]{1,2}$ ]] && [ "$CPUS" -ge 2 ] || bad "--cpus is a number, 2 at least"
47
+ [[ "$MEMORY" =~ ^[0-9]{1,3}G$ ]] && [ "${MEMORY%G}" -ge 4 ] || bad "--memory is like 8G, 4G at least"
48
+ [[ "$DISK" =~ ^[0-9]{2,4}G$ ]] && [ "${DISK%G}" -ge 20 ] || bad "--disk is like 60G, 20G at least"
49
+ [[ "$RELEASE" =~ ^[0-9]{2}\.[0-9]{2}$ ]] || bad "--release is like 24.04"
50
+ # The rest is checked by the installer itself, here, before any VM exists.
51
+ plan="$(bash "$HERE/deploy/install.sh" "${pass[@]}" --dry-run)" || exit 64
52
+
53
+ if [ "$DRY" = "yes" ]; then
54
+ echo "would start a VM named $NAME (Ubuntu $RELEASE, $CPUS processors, $MEMORY of memory, $DISK of disk), and inside it:"
55
+ echo "$plan"
56
+ exit 0
57
+ fi
58
+
59
+ mp=(multipass); [ "$SUDO" = "no" ] || mp=(sudo multipass)
60
+ command -v multipass >/dev/null 2>&1 || refuse "Multipass is not installed on this computer (https://multipass.run)"
61
+ if "${mp[@]}" info "$NAME" >/dev/null 2>&1; then refuse "a VM named $NAME exists already; choose another --name, or delete it first"; fi
62
+
63
+ step "starting the VM (a few minutes)"
64
+ "${mp[@]}" launch "$RELEASE" --name "$NAME" --cpus "$CPUS" --memory "$MEMORY" --disk "$DISK" --timeout 1200
65
+
66
+ step "sending Clavarium"
67
+ # Multipass only reads files in the home directory, and not hidden ones.
68
+ package="$HOME/clavarium-appliance-$$.tgz"
69
+ trap 'rm -f -- "$package"' EXIT
70
+ tar -czf "$package" -C "$HERE" server web config deploy package.json package-lock.json
71
+ [ "$SUDO" = "no" ] || chmod 644 "$package"
72
+ "${mp[@]}" transfer "$package" "$NAME:/tmp/clavarium.tgz"
73
+ "${mp[@]}" exec "$NAME" -- bash -c 'mkdir -p ~/clavarium && tar -xzf /tmp/clavarium.tgz -C ~/clavarium && rm -f /tmp/clavarium.tgz'
74
+
75
+ step "installing inside the VM"
76
+ # shellcheck disable=SC2016 # expanded inside the VM, where ~ is its own home
77
+ "${mp[@]}" exec "$NAME" -- bash -c 'sudo bash ~/clavarium/deploy/install.sh "$@"' install "${pass[@]}"
78
+
79
+ address="$("${mp[@]}" info "$NAME" --format csv | tail -1 | cut -d, -f3)"
80
+ echo
81
+ echo "The VM is named $NAME; its address on this computer is $address."
82
+ echo "To look inside: ${mp[*]} shell $NAME"
83
+ echo "To stop it: ${mp[*]} stop $NAME (and start: ${mp[*]} start $NAME)"
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ # ---------------------------------------------------------------------------
3
+ # COPIES WHAT CLAVARIUM REMEMBERS INTO ONE ARCHIVE, and the key of the vault
4
+ # into a SECOND file. One command, parameters only.
5
+ #
6
+ # sudo deploy/backup.sh --to /path/to/a/folder
7
+ #
8
+ # TWO FILES, ON PURPOSE:
9
+ # clavarium-<date>.tgz the list of conversations, the folders and
10
+ # their files, devices, shortcuts, the secrets
11
+ # (sealed), and the settings of the edge
12
+ # clavarium-<date>.vault-key what opens the sealed secrets
13
+ # Keep them in two different places: whoever finds the archive alone cannot
14
+ # read a secret. The archive still holds the operators' password hashes and
15
+ # this machine's own keys: it is written readable by root only.
16
+ #
17
+ # WHAT IS NOT IN IT: the machines themselves. A conversation's VM -- what the
18
+ # tool installed there, what sits in /work -- is not copied. What must outlive
19
+ # a machine belongs in a folder attached to the conversation.
20
+ #
21
+ # --server-only the server's own data and settings only (home/clavarium,
22
+ # without the folders given to machines; etc/clavarium): what
23
+ # an update may change. Taken by the updater before each one.
24
+ # --root DIR look for everything under DIR instead of /. For tests.
25
+ #
26
+ # Exit codes: 0 done; 64 bad parameters; 78 refused.
27
+ # ---------------------------------------------------------------------------
28
+ set -euo pipefail
29
+
30
+ bad() { echo "backup: $*" >&2; echo "usage: backup.sh --to FOLDER" >&2; exit 64; }
31
+ refuse() { echo "backup: REFUSED: $*" >&2; exit 78; }
32
+
33
+ TO=""; ROOT=""; SERVER_ONLY="no"
34
+ while [ $# -gt 0 ]; do
35
+ case "$1" in
36
+ --to) TO="${2:-}"; shift 2 || bad "--to needs a value" ;;
37
+ --root) ROOT="${2:-}"; shift 2 || bad "--root needs a value" ;;
38
+ --server-only) SERVER_ONLY="yes"; shift ;;
39
+ *) bad "unknown parameter: $1" ;;
40
+ esac
41
+ done
42
+ [ -n "$TO" ] || bad "--to says where the two files go"
43
+ [ -d "$TO" ] || bad "--to must be a folder that exists: $TO"
44
+ if [ -z "$ROOT" ]; then
45
+ [ "$(id -u)" -eq 0 ] || refuse "run it as root (sudo)"
46
+ ROOT="/"
47
+ fi
48
+ [ -d "$ROOT/home/clavarium" ] || refuse "there is no Clavarium data here ($ROOT/home/clavarium)"
49
+ KEY="$ROOT/var/lib/clavarium-vault/vault.key"
50
+
51
+ # Everything that would be needed on a new machine, if it exists here.
52
+ # (home/clavarium-mnt holds what the machines are given, on a chain root owns:
53
+ # see clavarium-host. The read-only views are not content: they are mirrors,
54
+ # made again at start. Work folders are copied as their files; the disks that
55
+ # hold them are made again. An older house kept all that under home/clavarium:
56
+ # the same rule there.)
57
+ parts=(); skip=()
58
+ all="home/clavarium home/clavarium-mnt etc/clavarium etc/dex etc/oauth2-proxy etc/caddy var/lib/caddy var/lib/dex var/lib/clavarium-tunnel"
59
+ if [ "$SERVER_ONLY" = "yes" ]; then
60
+ all="home/clavarium etc/clavarium"
61
+ # An older house kept the machines' folders in its data directory: not these.
62
+ skip=(--exclude='home/clavarium/resources' --exclude='home/clavarium/work' --exclude='home/clavarium/removed' --exclude='home/clavarium/toolbox')
63
+ fi
64
+ for part in $all; do
65
+ [ -e "$ROOT/$part" ] && parts+=("$part")
66
+ done
67
+
68
+ stamp="$(date -u +%Y%m%dT%H%M%SZ)"
69
+ archive="$TO/clavarium-$stamp.tgz"
70
+ note="$(mktemp -d)"
71
+ trap 'rm -rf -- "$note"' EXIT
72
+ {
73
+ echo "Clavarium backup, made $stamp"
74
+ echo "Conversations (their machines are NOT in this backup):"
75
+ sed -n 's/^ *"name": "\([a-z0-9-]*\)",\{0,1\}$/ \1/p' "$ROOT/home/clavarium/conversations.json" 2>/dev/null || true
76
+ } > "$note/BACKUP.txt"
77
+
78
+ # The identity provider keeps a small database: it is still while it is copied.
79
+ dex_was_up="no"
80
+ if [ "$ROOT" = "/" ] && [ "$SERVER_ONLY" = "no" ] && systemctl is-active --quiet dex.service 2>/dev/null; then dex_was_up="yes"; systemctl stop dex.service; fi
81
+ umask 077
82
+ status=0
83
+ tar -czf "$archive" --exclude='home/clavarium/views' --exclude='home/clavarium/work-disks' --exclude='home/clavarium-mnt/views' --exclude='home/clavarium-mnt/work-disks' --exclude='*.tmp' "${skip[@]}" -C "$note" BACKUP.txt -C "$(cd "$ROOT" && pwd)" "${parts[@]}" || status=$?
84
+ [ "$dex_was_up" = "no" ] || systemctl start dex.service
85
+ # (1: a file changed while it was read -- the copy is still whole.)
86
+ [ "$status" -le 1 ] || { rm -f -- "$archive"; refuse "the archive could not be written"; }
87
+ tar -tzf "$archive" >/dev/null || { rm -f -- "$archive"; refuse "the archive cannot be read back"; }
88
+
89
+ echo "archive $archive ($(du -h "$archive" | cut -f1))"
90
+ if [ -s "$KEY" ]; then
91
+ install -m 0600 "$KEY" "$TO/clavarium-$stamp.vault-key"
92
+ echo "vault key $TO/clavarium-$stamp.vault-key"
93
+ echo "Keep these two files in two different places."
94
+ else
95
+ echo "vault key none yet (no secret was ever stored)"
96
+ fi
97
+ echo "Not in the backup: the conversations' machines. What must last belongs in an attached folder."