clavarium 0.0.1 → 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.
- package/README.md +266 -4
- package/config/tools.json +172 -0
- package/deploy/appliance.sh +83 -0
- package/deploy/backup.sh +97 -0
- package/deploy/clavarium-host +720 -0
- package/deploy/clavarium.mjs +30 -0
- package/deploy/git-read +85 -0
- package/deploy/install.sh +653 -0
- package/deploy/move-to-mnt.sh +142 -0
- package/deploy/parley-archive.mjs +47 -0
- package/deploy/relay.sh +156 -0
- package/deploy/restore.sh +147 -0
- package/deploy/updater.mjs +600 -0
- package/npm-shrinkwrap.json +1951 -0
- package/package.json +53 -6
- package/server/alerts.mjs +83 -0
- package/server/answerer.mjs +326 -0
- package/server/api.mjs +458 -0
- package/server/bridge.mjs +271 -0
- package/server/capacity.mjs +220 -0
- package/server/conversations.mjs +895 -0
- package/server/dataformat.mjs +59 -0
- package/server/devices.mjs +257 -0
- package/server/door.mjs +235 -0
- package/server/files.mjs +455 -0
- package/server/gate.mjs +54 -0
- package/server/gitaccess.mjs +295 -0
- package/server/gitchecks.mjs +126 -0
- package/server/gitkeeper.mjs +719 -0
- package/server/gitpush.mjs +585 -0
- package/server/gitrunner.mjs +540 -0
- package/server/grants.mjs +110 -0
- package/server/guide.mjs +278 -0
- package/server/health.mjs +207 -0
- package/server/imap.mjs +75 -0
- package/server/mailbox.mjs +165 -0
- package/server/main.mjs +420 -0
- package/server/names.mjs +10 -0
- package/server/network.mjs +85 -0
- package/server/parley.mjs +62 -0
- package/server/resources.mjs +396 -0
- package/server/seats.mjs +24 -0
- package/server/secrets.mjs +118 -0
- package/server/setup.mjs +768 -0
- package/server/shelf.mjs +260 -0
- package/server/shortcuts.mjs +62 -0
- package/server/signin.mjs +185 -0
- package/server/smoke.mjs +26 -0
- package/server/store.mjs +48 -0
- package/server/talk.mjs +514 -0
- package/server/tmux.mjs +54 -0
- package/server/toolbox.mjs +168 -0
- package/server/tools.mjs +109 -0
- package/server/updates.mjs +116 -0
- package/server/vault.mjs +37 -0
- package/server/vm.mjs +139 -0
- package/server/voice.mjs +90 -0
- package/server/wakeups.mjs +320 -0
- package/web/app.js +4779 -0
- package/web/device.html +149 -0
- package/web/device.js +152 -0
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/icon.svg +1 -0
- package/web/icons.js +9 -0
- package/web/index.html +784 -0
- package/web/links.js +995 -0
- package/web/manifest.webmanifest +30 -0
- package/web/prism-manual.js +3 -0
- package/web/style.css +1006 -0
- package/web/sw.js +26 -0
- package/web/voice-core.js +107 -0
- package/web/voice.js +56 -0
- package/keep.mjs +0 -4
package/README.md
CHANGED
|
@@ -1,7 +1,269 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Clavarium
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A conversation is a VM with a terminal in it. Everything else is a folder.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
11
|
+
## Where it stands
|
|
12
|
+
|
|
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)"
|
package/deploy/backup.sh
ADDED
|
@@ -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."
|