memhouse 0.18.12 → 0.20.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/AGENTS.md +22 -151
- package/CHANGELOG.md +263 -29
- package/LICENSE +202 -22
- package/LICENSES/agentlytics-MIT.txt +21 -0
- package/LICENSES/memhouse-MIT-until-0.18.11.txt +22 -0
- package/LICENSES/models.dev-MIT.txt +22 -0
- package/NOTICE +28 -0
- package/README.md +42 -20
- package/SECURITY.md +21 -8
- package/bin/memhouse.js +2223 -311
- package/docs/README.md +11 -9
- package/docs/guides/admin-password.md +37 -10
- package/docs/guides/configuration.md +13 -6
- package/docs/guides/daemons.md +64 -3
- package/docs/guides/import-claude-ai.md +204 -0
- package/docs/guides/install.md +11 -6
- package/docs/guides/invite.md +21 -7
- package/docs/guides/operate.md +114 -7
- package/docs/guides/search-and-resume.md +72 -3
- package/docs/guides/share.md +91 -52
- package/docs/guides/storage.md +44 -4
- package/docs/guides/troubleshooting.md +121 -14
- package/docs/guides/uninstall.md +4 -3
- package/docs/guides/update.md +16 -0
- package/docs/guides/upgrade-to-0.20.md +188 -0
- package/docs/releases/README.md +2 -1
- package/docs/releases/v0.18.10.md +5 -5
- package/docs/releases/v0.18.11.md +3 -3
- package/docs/releases/v0.20.0.md +627 -0
- package/docs/releases/v0.20.1.md +114 -0
- package/docs/tutorials/01-first-house.md +10 -7
- package/docs/tutorials/02-join-a-house.md +24 -15
- package/docs/tutorials/03-share-your-memory.md +40 -31
- package/editors/antigravity.js +1 -80
- package/editors/base.js +463 -9
- package/editors/bridge.js +119 -0
- package/editors/claude.js +273 -105
- package/editors/codex.js +278 -52
- package/editors/copilot.js +1 -70
- package/editors/cursor.js +1 -75
- package/editors/derive-fingerprints.json +18 -0
- package/editors/harness-paths.js +69 -0
- package/editors/index.js +1 -20
- package/editors/normalise/agy.js +910 -0
- package/editors/normalise/copilot-cli.js +321 -0
- package/editors/normalise/cursor-agent.js +852 -0
- package/editors/normalise/cursor.js +474 -0
- package/editors/normalise/devin.js +575 -0
- package/editors/normalise/gemini-cli.js +602 -0
- package/editors/normalise/goose.js +673 -0
- package/editors/normalise/opencode.js +633 -0
- package/editors/normalise/vscode.js +549 -0
- package/editors/normalise/zed.js +473 -0
- package/editors/opencode-v4.js +119 -0
- package/editors/scope.js +14 -3
- package/editors/sessions-log.js +349 -0
- package/editors/sqlite.js +28 -1
- package/editors/vscode.js +1 -72
- package/editors/windsurf.js +1 -143
- package/examples/01-local-one-machine/README.md +24 -19
- package/examples/01-local-one-machine/run.sh +1 -1
- package/examples/02-existing-clickhouse/README.md +30 -22
- package/examples/02-existing-clickhouse/run.sh +5 -4
- package/examples/03-team-house/README.md +44 -37
- package/examples/03-team-house/run.sh +35 -18
- package/examples/04-self-hosted-team/README.md +29 -14
- package/examples/04-self-hosted-team/compose.yml +5 -2
- package/examples/04-self-hosted-team/smoke.sh +26 -12
- package/examples/README.md +8 -4
- package/examples/fixtures/README.md +4 -1
- package/examples/fixtures/make-sessions.js +8 -5
- package/memhouse/admin-secret.js +89 -24
- package/memhouse/argv-secrets.js +64 -0
- package/memhouse/channel.js +49 -9
- package/memhouse/codex-plugin.js +194 -0
- package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -1
- package/memhouse/delivery/AGENT-INSTALL.md +22 -11
- package/memhouse/delivery/INVITATION.md.tpl +44 -45
- package/memhouse/delivery/PROMPT.md +53 -5
- package/memhouse/delivery/UPGRADE.md.tpl +1 -1
- package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
- package/memhouse/delivery/codex/SKILL.md +50 -0
- package/memhouse/delivery/kernel-install.md +17 -9
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
- package/memhouse/delivery/plugin/reference/HOUSE.md +168 -41
- package/memhouse/delivery/plugin/skills/access/SKILL.md +13 -5
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +14 -6
- package/memhouse/delivery/plugin/skills/house/SKILL.md +25 -8
- package/memhouse/delivery/plugin/skills/recall/SKILL.md +66 -8
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +66 -7
- package/memhouse/deploy.js +1 -1
- package/memhouse/flags.js +35 -18
- package/memhouse/help.js +37 -0
- package/memhouse/host.js +82 -2
- package/memhouse/house/HOUSE.md +2 -2
- package/memhouse/house/convert-profile.js +638 -0
- package/memhouse/house/convert-v3.js +742 -0
- package/memhouse/house/doctor-v3.js +324 -0
- package/memhouse/house/host-alias.js +85 -0
- package/memhouse/house/house.js +3 -3
- package/memhouse/house/migrate.js +2 -2
- package/memhouse/house/profile-dedupe.js +183 -0
- package/memhouse/house/profile-edges.js +554 -0
- package/memhouse/house/profile-house.js +430 -0
- package/memhouse/house/profile-rollup.js +190 -0
- package/memhouse/house/q-cli.js +863 -0
- package/memhouse/house/queries-v3.js +1013 -0
- package/memhouse/house/reconcile-v3.js +192 -0
- package/memhouse/house/records-to-profile.js +174 -0
- package/memhouse/house/records-to-rows.js +169 -0
- package/memhouse/house/relocate.js +2 -1
- package/memhouse/house/schema-choice.js +121 -0
- package/memhouse/house/schema-profile.js +290 -0
- package/memhouse/house/schema-profile.sql.tpl +398 -0
- package/memhouse/house/schema-v3.js +267 -0
- package/memhouse/house/schema-v3.sql.tpl +213 -0
- package/memhouse/import/claude-ai.js +420 -0
- package/memhouse/import/import.js +372 -0
- package/memhouse/import/pyjson.js +206 -0
- package/memhouse/import/records.js +64 -0
- package/memhouse/import/writer-profile.js +192 -0
- package/memhouse/import/writer-v3.js +240 -0
- package/memhouse/import/zip.js +112 -0
- package/memhouse/invitation.js +102 -0
- package/memhouse/invitefile.js +5 -2
- package/memhouse/plugin-copies.js +176 -0
- package/memhouse/provision.js +7 -2
- package/memhouse/release-hint.js +24 -0
- package/memhouse/resume.js +4 -4
- package/memhouse/server/api-v3.js +632 -0
- package/memhouse/server/dashboard-auth.js +181 -0
- package/memhouse/server/dashboard-port.js +91 -0
- package/memhouse/server/queries.js +30 -0
- package/memhouse/server/server.js +118 -20
- package/memhouse/service.js +1 -1
- package/memhouse/share.js +687 -1
- package/memhouse/shipper/adapter-capture.js +756 -0
- package/memhouse/shipper/capture.js +2571 -0
- package/memhouse/shipper/dedupe-profile.js +237 -0
- package/memhouse/shipper/dedupe-v3.js +1027 -0
- package/memhouse/shipper/forks.js +233 -0
- package/memhouse/shipper/log-adapter.js +219 -0
- package/memhouse/shipper/maintenance.js +49 -0
- package/memhouse/shipper/normalise-run.js +148 -0
- package/memhouse/shipper/passlog.js +68 -5
- package/memhouse/shipper/profile-capture.js +903 -0
- package/memhouse/shipper/ship.js +91 -8
- package/memhouse/shipper/snapshots.js +193 -0
- package/memhouse/shipper/source-roots.js +111 -0
- package/memhouse/version-notice.js +66 -0
- package/package.json +14 -7
- package/public/THIRD_PARTY_NOTICES.txt +2 -2
- package/public/assets/index-CcNnhfM3.js +78 -0
- package/public/index.html +1 -1
- package/ui/package-lock.json +56 -37
- package/ui/package.json +1 -0
- package/ui/src/App.jsx +29 -14
- package/ui/src/components/ChatSidebar.jsx +69 -12
- package/ui/src/lib/api.js +76 -0
- package/ui/src/lib/dashboard-token.js +41 -0
- package/ui/src/main.jsx +22 -5
- package/ui/src/pages/Dashboard.jsx +22 -36
- package/ui/third-party-notices.js +119 -0
- package/ui/vite.config.js +5 -1
- package/docs/releases/v0.18.12.md +0 -59
- package/memhouse/COMPETITION.md +0 -221
- package/memhouse/DESIGN.md +0 -222
- package/memhouse/RESUME-AND-UPDATE.md +0 -182
- package/public/assets/index-CnWzrjF3.js +0 -78
|
@@ -40,40 +40,45 @@ alone.
|
|
|
40
40
|
|
|
41
41
|
## What you should see
|
|
42
42
|
|
|
43
|
-
Trimmed from a run on a
|
|
43
|
+
Trimmed from a run on a lab host (Linux, Docker 29), ClickHouse 26.7 and memhouse
|
|
44
|
+
0.20.0-dev.15, as root in a container, with `HOUSE_PORT=18129`:
|
|
44
45
|
|
|
45
46
|
```
|
|
46
47
|
== 1. a house: ClickHouse in a container, bound to loopback, plus the install
|
|
47
|
-
✓ ClickHouse starting via docker on http://localhost:
|
|
48
|
+
✓ ClickHouse starting via docker on http://localhost:18129 (loopback only)
|
|
48
49
|
✓ ClickHouse ready
|
|
49
|
-
✓ member '
|
|
50
|
-
✓
|
|
50
|
+
✓ member 'root' holds mem.root_* and nothing else; 'memhouse_root' administers the house
|
|
51
|
+
✓ schema 3: mem.root_turns, mem.root_calls, mem.root_sessions, mem_archive.root_records, mem_archive.root_files; capture tables granted to 'root'
|
|
51
52
|
✓ installed
|
|
52
|
-
✓ shipper started in the background (pid
|
|
53
|
+
✓ shipper started in the background (pid 281) — loading your history now
|
|
53
54
|
|
|
54
55
|
== 2. one shipping pass
|
|
55
|
-
[memhouse]
|
|
56
|
+
[memhouse] v3 capture: 2/2 files changed in 2 sessions → 2 files rows, 10 archive rows (0.0 MiB), 10 turns, 2 calls, 0 tombstones, 2 sessions [new 2]
|
|
56
57
|
|
|
57
58
|
== 3. what the house holds
|
|
58
|
-
✓ connected: http://localhost:
|
|
59
|
-
✓
|
|
59
|
+
✓ connected: http://localhost:18129 / mem as root
|
|
60
|
+
✓ schema 3 — tables for 'root': mem.root_turns, mem.root_calls, mem.root_sessions
|
|
61
|
+
✓ house: 2 sessions, 6 messages (freshest ingest 2026-10-22 15:34:18 UTC)
|
|
60
62
|
✓ fleet: 1 active writer(s)
|
|
61
|
-
|
|
62
|
-
✓ shipper: running — daemon (pid 617049)
|
|
63
|
+
✓ shipper: running — daemon (pid 281)
|
|
63
64
|
|
|
64
65
|
== 4. find a past conversation
|
|
65
|
-
claude-code:
|
|
66
|
-
The deploy fails with ClickHouse error ACCESS_DENIED on INSERT. Why?
|
|
66
|
+
claude-code:f7204114-477a-4fcc-8bd0-353ed6426ae8~WyJy… alpha 2026-10-22 13:34 (1 hit · The deploy fails with ClickHouse error ACCESS_DENIED on INSE)
|
|
67
|
+
user u1: The deploy fails with ClickHouse error ACCESS_DENIED on INSERT. Why?
|
|
68
|
+
|
|
69
|
+
read a hit with: memhouse q window <ref> --at <message_id>
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
tool call
|
|
72
|
+
Ten turns for six messages: a turn is one part of a message (its text, a thinking
|
|
73
|
+
block, a tool call or a tool result), and `memhouse status` counts messages. The ref
|
|
74
|
+
in step 4 names the session's store after the `~`; `memhouse q session <ref>` reads it.
|
|
71
75
|
|
|
72
|
-
To look around, start the dashboard in the example's home
|
|
73
|
-
|
|
76
|
+
To look around, start the dashboard in the example's home, then open it (when 4640 is taken
|
|
77
|
+
by a real install, the dashboard takes the next free port and says which):
|
|
74
78
|
|
|
75
79
|
```bash
|
|
76
|
-
MEMHOUSE_HOME=~/.memhouse-example-01
|
|
80
|
+
MEMHOUSE_HOME=~/.memhouse-example-01 memhouse start
|
|
81
|
+
MEMHOUSE_HOME=~/.memhouse-example-01 memhouse dashboard
|
|
77
82
|
```
|
|
78
83
|
|
|
79
84
|
## Teardown
|
|
@@ -95,5 +100,5 @@ memhouse deploy --local
|
|
|
95
100
|
```
|
|
96
101
|
|
|
97
102
|
Every editor memhouse knows is read (`memhouse discover` lists what it finds first). Then
|
|
98
|
-
`memhouse start` for the dashboard and `memhouse plugins install
|
|
99
|
-
`/mem:recall` and the other skills.
|
|
103
|
+
`memhouse start` and `memhouse dashboard` for the dashboard, and `memhouse plugins install
|
|
104
|
+
claude` to give your agents `/mem:recall` and the other skills.
|
|
@@ -57,5 +57,5 @@ echo "== 4. find a past conversation"
|
|
|
57
57
|
mh search ACCESS_DENIED
|
|
58
58
|
|
|
59
59
|
echo
|
|
60
|
-
echo "Done. Open the dashboard: MEMHOUSE_HOME=$MEMHOUSE_HOME $MH start
|
|
60
|
+
echo "Done. Open the dashboard: MEMHOUSE_HOME=$MEMHOUSE_HOME $MH start, then MEMHOUSE_HOME=$MEMHOUSE_HOME $MH dashboard"
|
|
61
61
|
echo "Remove everything: $0 --teardown"
|
|
@@ -10,12 +10,15 @@ and keeps only the member's credential on disk.
|
|
|
10
10
|
1. `memhouse whoami --admin` checks that the admin credential really can create users and
|
|
11
11
|
grant, before anything is changed.
|
|
12
12
|
2. `memhouse install --admin-user …` creates the member (default: your login name), grants
|
|
13
|
-
it `mem.<member>_
|
|
14
|
-
|
|
13
|
+
it `mem.<member>_*`, builds its schema-3 tables (the six conversation tables in `mem`,
|
|
14
|
+
`<member>_session_parts`, `_sessions` and four more, and its capture archive
|
|
15
|
+
`<member>_session_units` and `_session_sources` in `mem_archive`, granted to it and nothing
|
|
16
|
+
more), and verifies the install by connecting **as the
|
|
17
|
+
member**. The admin credential is used for this one command and not
|
|
15
18
|
saved (add `--keep-admin` if this machine should be able to invite people later).
|
|
16
|
-
3. `memhouse ship --ensure-schema`
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
3. `memhouse ship --ensure-schema` checks the member's five tables, as the member, and
|
|
20
|
+
creates what it may — safe to re-run — then `memhouse ship` ships two made-up
|
|
21
|
+
sessions.
|
|
19
22
|
4. `memhouse whoami` and `memhouse status` show the credential this machine now keeps and
|
|
20
23
|
what the house holds.
|
|
21
24
|
|
|
@@ -66,31 +69,34 @@ printf '%s' "$PASSWORD" | memhouse install --url "$MEMHOUSE_URL" --admin-user de
|
|
|
66
69
|
|
|
67
70
|
## What you should see
|
|
68
71
|
|
|
69
|
-
Trimmed from a run against ClickHouse 26.7.
|
|
72
|
+
Trimmed from a run against a lab ClickHouse 26.7.4.58 with memhouse 0.20.0-dev.15, member
|
|
73
|
+
`ex02`:
|
|
70
74
|
|
|
71
75
|
```
|
|
72
76
|
== 1. whoami --admin: is this credential really an administrator?
|
|
73
|
-
✓
|
|
77
|
+
✓ ex_admin at https://ch.example.com:8443 — administrator
|
|
74
78
|
credential from: MEMHOUSE_ADMIN_* environment
|
|
75
79
|
may create users: yes
|
|
76
80
|
|
|
77
|
-
== 2. install: build member '
|
|
78
|
-
✓ created ClickHouse user '
|
|
79
|
-
✓ granted '
|
|
80
|
-
✓
|
|
81
|
+
== 2. install: build member 'ex02' with the admin credential, then verify as the member
|
|
82
|
+
✓ created ClickHouse user 'ex02'
|
|
83
|
+
✓ granted 'ex02' mem.ex02_* — their tables (the capture archive is granted below); async_insert pinned
|
|
84
|
+
✓ schema 3: mem.ex02_turns, mem.ex02_calls, mem.ex02_sessions, mem_archive.ex02_records, mem_archive.ex02_files; capture tables granted to 'ex02'
|
|
85
|
+
✓ verified as 'ex02' with the member credential
|
|
81
86
|
✓ installed
|
|
82
87
|
the admin credential was used for this install and not saved. To invite later:
|
|
83
|
-
with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- memhouse invite <name> --url … --admin-user
|
|
88
|
+
with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- memhouse invite <name> --url … --admin-user ex_admin
|
|
84
89
|
|
|
85
|
-
== 3. make sure
|
|
86
|
-
[memhouse] schema ensured (
|
|
87
|
-
[memhouse]
|
|
90
|
+
== 3. make sure the member's tables exist, then ship once
|
|
91
|
+
[memhouse] schema ensured (v3: mem.ex02_turns, mem.ex02_calls, mem.ex02_sessions, mem_archive.ex02_records, mem_archive.ex02_files)
|
|
92
|
+
[memhouse] v3 capture: 2/2 files changed in 2 sessions → 2 files rows, 10 archive rows (0.0 MiB), 10 turns, 2 calls, 0 tombstones, 2 sessions [new 2]
|
|
88
93
|
|
|
89
94
|
== 4. whoami: the credential this machine keeps, and what it may do
|
|
90
|
-
✓
|
|
95
|
+
✓ ex02 at https://ch.example.com:8443 — member
|
|
91
96
|
may create users: no
|
|
92
97
|
|
|
93
98
|
== 5. status
|
|
99
|
+
✓ schema 3 — tables for 'ex02': mem.ex02_turns, mem.ex02_calls, mem.ex02_sessions
|
|
94
100
|
✓ house: 2 sessions, 6 messages
|
|
95
101
|
✓ fleet: 1 active writer(s)
|
|
96
102
|
```
|
|
@@ -104,18 +110,20 @@ Step 2 generates the member password and writes it only to the home's `env` file
|
|
|
104
110
|
./run.sh --teardown
|
|
105
111
|
```
|
|
106
112
|
|
|
107
|
-
Deletes the example's home. The member and its
|
|
113
|
+
Deletes the example's home. The member and its tables stay in the house: dropping them is
|
|
108
114
|
an administrator's decision. As the admin:
|
|
109
115
|
|
|
110
116
|
```sql
|
|
111
|
-
DROP USER
|
|
112
|
-
-- then each of mem.
|
|
113
|
-
--
|
|
117
|
+
DROP USER ex02;
|
|
118
|
+
-- then each of mem.ex02_session_parts, _session_usage, _session_links, _session_lifecycle,
|
|
119
|
+
-- _calls, _sessions, _meta, _events, and mem_archive.ex02_session_units, _session_sources
|
|
120
|
+
-- (docs/guides/operate.md, "Removing a member's tables", lists them by exact name)
|
|
114
121
|
```
|
|
115
122
|
|
|
116
123
|
## For real
|
|
117
124
|
|
|
118
125
|
Leave out the two fixture flags (`--editors claude --claude-roots …`) and the
|
|
119
126
|
`EXAMPLE_HOME`, and every editor on the machine ships into the house. Your other machines
|
|
120
|
-
join the same
|
|
121
|
-
… --user <member
|
|
127
|
+
join the same tables by installing with the same member credential (`memhouse install --url
|
|
128
|
+
… --user <member>` with the password in `MEMHOUSE_PASSWORD` or `--password-file`, or an
|
|
129
|
+
invite file — see [03](../03-team-house/)).
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Example 02 — point memhouse at a ClickHouse you already run.
|
|
3
3
|
#
|
|
4
|
-
# The admin credential builds a member account (
|
|
5
|
-
# those), the install is verified AS that member,
|
|
4
|
+
# The admin credential builds a member account (its tables mem.<member>_* and its capture
|
|
5
|
+
# archive in mem_archive, grants on exactly those), the install is verified AS that member,
|
|
6
|
+
# and the admin credential is then dropped:
|
|
6
7
|
# what stays on disk is the member's. The admin password never touches the command line.
|
|
7
8
|
#
|
|
8
9
|
# with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- ./run.sh
|
|
@@ -25,7 +26,7 @@ export MEMHOUSE_HOME=${EXAMPLE_HOME:-$HOME/.memhouse-example-02}
|
|
|
25
26
|
if [ "${1:-}" = "--teardown" ]; then
|
|
26
27
|
mh stop || true
|
|
27
28
|
rm -rf "$MEMHOUSE_HOME"
|
|
28
|
-
echo "example 02 home removed. The member and its
|
|
29
|
+
echo "example 02 home removed. The member and its tables stay in the house; dropping them is an admin's call."
|
|
29
30
|
exit 0
|
|
30
31
|
fi
|
|
31
32
|
|
|
@@ -56,7 +57,7 @@ mh install --url "$MEMHOUSE_URL" --admin-user "$MEMHOUSE_ADMIN_USER" --member "$
|
|
|
56
57
|
unset MEMHOUSE_ADMIN_USER MEMHOUSE_ADMIN_PASSWORD
|
|
57
58
|
|
|
58
59
|
echo
|
|
59
|
-
echo "== 3. make sure
|
|
60
|
+
echo "== 3. make sure the member's tables exist, then ship once"
|
|
60
61
|
# --ensure-schema only creates what is missing, as the member, and ships nothing; it is
|
|
61
62
|
# safe to re-run, and the thing to run after an upgrade that adds a table.
|
|
62
63
|
mh ship --ensure-schema
|
|
@@ -7,14 +7,16 @@ exactly who can see what. Finally she withdraws both shares.
|
|
|
7
7
|
|
|
8
8
|
## The model in four lines
|
|
9
9
|
|
|
10
|
-
- Every member owns `mem.<member>_
|
|
11
|
-
events) and
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- A **
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
- Every member owns `mem.<member>_*` (`session_parts`, `session_usage`, `session_links`,
|
|
11
|
+
`session_lifecycle`, `calls`, `sessions`, `meta`, `events`) and two tables in the capture
|
|
12
|
+
archive, `mem_archive.<member>_session_units` and `_session_sources`. Those grants are their
|
|
13
|
+
whole access.
|
|
14
|
+
- Housemates cannot read, list, or touch each other's tables.
|
|
15
|
+
- A **full share** (`memhouse share bob`) grants `SELECT` on exactly alice's six
|
|
16
|
+
conversation tables: every session, every project, every machine, and anything ever pasted
|
|
17
|
+
into one. Not the archive, not `meta` or `events`.
|
|
18
|
+
- A **scoped share** (`memhouse share carol --only project=alpha`) grants the same six
|
|
19
|
+
tables, each behind a row policy, written before the grant.
|
|
18
20
|
|
|
19
21
|
## What it does
|
|
20
22
|
|
|
@@ -31,7 +33,7 @@ exactly who can see what. Finally she withdraws both shares.
|
|
|
31
33
|
5. alice: `memhouse share bob --revoke`, `memhouse share carol --revoke`; the reads are
|
|
32
34
|
denied again.
|
|
33
35
|
|
|
34
|
-
At exit the script drops the three users and their
|
|
36
|
+
At exit the script drops the three users and their tables (`KEEP=1` keeps them).
|
|
35
37
|
|
|
36
38
|
## Needs
|
|
37
39
|
|
|
@@ -65,11 +67,11 @@ wrong, because their `localhost` is their own machine.
|
|
|
65
67
|
| `MEMHOUSE_ADMIN_USER` / `MEMHOUSE_ADMIN_PASSWORD` | (required) | the administrator |
|
|
66
68
|
| `MEMHOUSE_BIN` | `memhouse` | the command to run |
|
|
67
69
|
| `EXAMPLE_DIR` | a new temp dir | where the three homes live |
|
|
68
|
-
| `KEEP` | `0` | `1` keeps the members and their
|
|
70
|
+
| `KEEP` | `0` | `1` keeps the members and their tables |
|
|
69
71
|
|
|
70
72
|
## What you should see
|
|
71
73
|
|
|
72
|
-
From a run against ClickHouse 26.7.
|
|
74
|
+
From a run against a lab ClickHouse 26.7.4.58, memhouse 0.20.0-dev.15 (temp paths shortened):
|
|
73
75
|
|
|
74
76
|
```
|
|
75
77
|
== 1. the admin invites three members
|
|
@@ -81,38 +83,43 @@ From a run against ClickHouse 26.7.1.1315, memhouse 0.18.9 (temp paths shortened
|
|
|
81
83
|
alice: ✓ installed
|
|
82
84
|
alice: ✓ password rotated — this credential is now yours alone
|
|
83
85
|
alice: ✓ removed the spent invite file …/alice.env
|
|
84
|
-
alice: [memhouse]
|
|
86
|
+
alice: [memhouse] v3 capture: 2/2 files changed in 2 sessions → 2 files rows, 10 archive rows (0.0 MiB), 10 turns, 2 calls, 0 tombstones, 2 sessions [new 2]
|
|
85
87
|
(bob and carol the same)
|
|
86
88
|
|
|
87
89
|
== 3. alice shares: everything with bob, project alpha only with carol
|
|
88
|
-
✓ 'bob' can now read every session in
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
✓ 'bob' can now read every session in mem.alice_turns, mem.alice_calls, mem.alice_sessions
|
|
91
|
+
Those three tables only: not the capture archive, not meta or events.
|
|
92
|
+
✓ 'carol' can read your sessions where project=alpha
|
|
93
|
+
turns 5 of 10 rows
|
|
94
|
+
calls 1 of 2 rows
|
|
95
|
+
sessions 1 of 2 rows
|
|
96
|
+
who can read your sessions in 'mem' — memhouse's own record, not ClickHouse's grant table:
|
|
97
|
+
bob granted 2026-10-22 (every session: turns, calls, sessions)
|
|
98
|
+
carol granted 2026-10-22 scope=project=alpha
|
|
99
|
+
alice_calls project = 'alpha'
|
|
94
100
|
alice_sessions project = 'alpha'
|
|
95
|
-
|
|
101
|
+
alice_turns project = 'alpha'
|
|
96
102
|
|
|
97
103
|
== 4. who reads what — each with their own credential
|
|
98
|
-
alice, her own
|
|
99
|
-
bob (full share), alice's
|
|
100
|
-
carol (alpha only), alice's
|
|
101
|
-
bob, alice's
|
|
102
|
-
|
|
103
|
-
carol, alice's
|
|
104
|
-
carol, bob's
|
|
104
|
+
alice, her own turns by project alpha,beta 10
|
|
105
|
+
bob (full share), alice's turns alpha,beta 10
|
|
106
|
+
carol (alpha only), alice's turns alpha 5
|
|
107
|
+
bob, alice's meta denied
|
|
108
|
+
bob, alice's capture archive denied
|
|
109
|
+
carol, alice's sessions (alpha only) 1
|
|
110
|
+
carol, bob's turns (never shared) denied
|
|
105
111
|
|
|
106
112
|
== 5. alice withdraws both shares
|
|
107
|
-
✓ revoked 'bob' — SELECT withdrawn and
|
|
108
|
-
✓ revoked 'carol' — SELECT withdrawn and
|
|
109
|
-
bob, alice's
|
|
110
|
-
carol, alice's
|
|
111
|
-
removed alice, bob, carol, their
|
|
113
|
+
✓ revoked 'bob' — SELECT withdrawn from mem.alice_turns, mem.alice_calls, mem.alice_sessions and its row policies dropped
|
|
114
|
+
✓ revoked 'carol' — SELECT withdrawn from mem.alice_turns, mem.alice_calls, mem.alice_sessions and its row policies dropped
|
|
115
|
+
bob, alice's turns denied
|
|
116
|
+
carol, alice's turns denied
|
|
117
|
+
removed alice, bob, carol, their tables and their homes
|
|
112
118
|
```
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
On a house still on the 0.18 rooms the same script's reads would fail: they name the
|
|
121
|
+
schema-3 tables. An invite follows the house's layout, so a team house made with 0.20 is
|
|
122
|
+
schema 3 throughout.
|
|
116
123
|
|
|
117
124
|
## For real
|
|
118
125
|
|
|
@@ -123,7 +130,7 @@ a full share has no policies, so none existed to drop.
|
|
|
123
130
|
- **Each member** runs `memhouse install --env <name>.env` on their laptop, without the
|
|
124
131
|
fixture flags, then `memhouse start` (or `memhouse service install`) to keep shipping.
|
|
125
132
|
- **Sharing** is the member's own decision and needs no admin: `memhouse share <user>`,
|
|
126
|
-
`--only project=<p>` (also `session=`, `host=`, `
|
|
127
|
-
`until=`), `--revoke`, `--list`.
|
|
128
|
-
- A grantee reads the shared
|
|
129
|
-
|
|
133
|
+
`--only project=<p>` (also `session=`, `host=`, `harness=`, `folder=`, `since=`,
|
|
134
|
+
`until=`), `--revoke`, `--list`. See [Share and revoke](../../docs/guides/share.md).
|
|
135
|
+
- A grantee reads the shared tables with their own credential: `/mem:sql`, or SQL against
|
|
136
|
+
`mem.<owner>_session_parts` and `mem.<owner>_sessions FINAL`.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# if users named alice, bob or carol already exist.
|
|
12
12
|
#
|
|
13
13
|
# with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- ./run.sh
|
|
14
|
-
# KEEP=1 ... keep the three members and their
|
|
14
|
+
# KEEP=1 ... keep the three members and their tables afterwards (default: removed)
|
|
15
15
|
#
|
|
16
16
|
# Environment:
|
|
17
17
|
# MEMHOUSE_URL required the house's HTTP endpoint, as the members will reach it
|
|
@@ -46,28 +46,41 @@ as() {
|
|
|
46
46
|
| curl -sS -K - --data-binary "$sql" "$URL/" 2>&1 | head -1
|
|
47
47
|
}
|
|
48
48
|
denied() { case "$1" in *ACCESS_DENIED*|*"Not enough privileges"*) return 0;; *) return 1;; esac; }
|
|
49
|
+
# A "denied" means something only for a table that exists: a query on a missing table is refused
|
|
50
|
+
# too (UNKNOWN_TABLE, or ACCESS_DENIED on a server that checks grants first), and would read as
|
|
51
|
+
# a share that holds. So every table a check reads is first proved to exist, as the admin.
|
|
52
|
+
need() {
|
|
53
|
+
local db=${1%%.*} t=${1#*.} n
|
|
54
|
+
n=$(admin_q "SELECT count() FROM system.tables WHERE database = '$db' AND name = '$t' FORMAT TSV")
|
|
55
|
+
[ "$n" = "1" ] || { echo "MISSING TABLE $1: this check would read a refusal as a closed door. Nothing it says can be trusted; stopping." >&2; exit 1; }
|
|
56
|
+
}
|
|
57
|
+
# What a user gets from one table: the result, or "denied".
|
|
58
|
+
reads() { local r; r=$(as "$1" "SELECT count() FROM $2 FORMAT TSV"); if denied "$r"; then echo denied; else echo "$r rows"; fi; }
|
|
59
|
+
# One line of the table below. `need` runs here, in the script's own shell, so a missing table
|
|
60
|
+
# stops the script (inside $(…) its exit would end only the subshell).
|
|
61
|
+
show_reads() { need "$3"; show "$1" "$(reads "$2" "$3")"; }
|
|
49
62
|
|
|
50
63
|
cleanup() {
|
|
51
|
-
[ "${KEEP:-0}" = "1" ] && { echo "KEEP=1: members and
|
|
64
|
+
[ "${KEEP:-0}" = "1" ] && { echo "KEEP=1: members and tables left in place; homes in $WORK"; return; }
|
|
52
65
|
[ "${CREATED:-0}" = "1" ] || return 0
|
|
53
66
|
for p in $PEOPLE; do
|
|
54
67
|
admin_q "DROP USER IF EXISTS $p" >/dev/null 2>&1 || true
|
|
55
|
-
for t in $(admin_q "SELECT name FROM system.tables WHERE database
|
|
56
|
-
admin_q "DROP TABLE IF EXISTS
|
|
68
|
+
for t in $(admin_q "SELECT database || '.' || name FROM system.tables WHERE database IN ('mem', 'mem_archive') AND startsWith(name, '${p}_') FORMAT TSV" 2>/dev/null); do
|
|
69
|
+
admin_q "DROP TABLE IF EXISTS $t SYNC" >/dev/null 2>&1 || true
|
|
57
70
|
done
|
|
58
71
|
done
|
|
59
72
|
rm -rf "$WORK"
|
|
60
|
-
echo "removed alice, bob, carol, their
|
|
73
|
+
echo "removed alice, bob, carol, their tables and their homes"
|
|
61
74
|
}
|
|
62
75
|
trap cleanup EXIT
|
|
63
76
|
|
|
64
|
-
# Refuse anything this run did not make: the users, and any table under their
|
|
65
|
-
# (orphaned
|
|
77
|
+
# Refuse anything this run did not make: the users, and any table under their prefixes
|
|
78
|
+
# (orphaned tables can outlive their users). The cleanup below drops by those prefixes, so it
|
|
66
79
|
# may only run when this check proved them empty first.
|
|
67
80
|
existing=$(admin_q "SELECT count() FROM system.users WHERE name IN ('alice','bob','carol') FORMAT TSV")
|
|
68
81
|
[ "$existing" = "0" ] || { echo "refusing: users alice/bob/carol already exist on this server — use a throwaway ClickHouse"; exit 1; }
|
|
69
|
-
|
|
70
|
-
[ "$
|
|
82
|
+
tables=$(admin_q "SELECT count() FROM system.tables WHERE database IN ('mem', 'mem_archive') AND (startsWith(name, 'alice_') OR startsWith(name, 'bob_') OR startsWith(name, 'carol_')) FORMAT TSV")
|
|
83
|
+
[ "$tables" = "0" ] || { echo "refusing: alice_*/bob_*/carol_* tables already exist ($tables) — use a throwaway ClickHouse"; exit 1; }
|
|
71
84
|
CREATED=1
|
|
72
85
|
|
|
73
86
|
# A loopback URL is right only through a tunnel; invite refuses one unless told so.
|
|
@@ -107,18 +120,22 @@ mh share --list
|
|
|
107
120
|
echo
|
|
108
121
|
echo "== 4. who reads what — each with their own credential"
|
|
109
122
|
show() { printf ' %-44s %s\n' "$1" "$2"; }
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
show "
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
parts() { as "$1" "SELECT arrayStringConcat(arraySort(groupUniqArray(project)), ',') || ' ' || toString(count()) FROM mem.$2_session_parts FINAL WHERE removed = 0 FORMAT TSV"; }
|
|
124
|
+
need mem.alice_session_parts
|
|
125
|
+
show "alice, her own message parts by project" "$(parts alice alice)"
|
|
126
|
+
show "bob (full share), alice's message parts" "$(parts bob alice)"
|
|
127
|
+
show "carol (alpha only), alice's message parts" "$(parts carol alice)"
|
|
128
|
+
# A share is SELECT on exactly alice's six conversation tables: full or scoped, nothing else.
|
|
129
|
+
show_reads "bob, alice's meta" bob mem.alice_meta
|
|
130
|
+
show_reads "bob, alice's capture archive" bob mem_archive.alice_session_units
|
|
131
|
+
need mem.alice_sessions
|
|
132
|
+
show "carol, alice's sessions (alpha only)" "$(as carol "SELECT count() FROM mem.alice_sessions FINAL FORMAT TSV")"
|
|
133
|
+
show_reads "carol, bob's message parts (never shared)" carol mem.bob_session_parts
|
|
117
134
|
|
|
118
135
|
echo
|
|
119
136
|
echo "== 5. alice withdraws both shares"
|
|
120
137
|
mh share bob --revoke --yes
|
|
121
138
|
mh share carol --revoke --yes
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
show_reads "bob, alice's message parts" bob mem.alice_session_parts
|
|
140
|
+
show_reads "carol, alice's message parts" carol mem.alice_session_parts
|
|
124
141
|
unset MEMHOUSE_HOME
|
|
@@ -15,7 +15,7 @@ with members shipping from their own laptops. One compose file holds all of it.
|
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| `clickhouse` | `clickhouse/clickhouse-server:26.7.1.1315` | the house. No published port; reachable only through Caddy. |
|
|
17
17
|
| `caddy` | `caddy:2.10` | TLS on 443: a Let's Encrypt certificate for your domain, or Caddy's own CA. Port 80 answers the ACME challenge and redirects. |
|
|
18
|
-
| `backup` | the ClickHouse image | at `BACKUP_AT` (UTC) writes the house **and its accounts** to the `backups` volume; keeps the newest `BACKUP_KEEP` (14). |
|
|
18
|
+
| `backup` | the ClickHouse image | at `BACKUP_AT` (UTC) writes the house, its capture archive **and its accounts** to the `backups` volume; keeps the newest `BACKUP_KEEP` (14). |
|
|
19
19
|
|
|
20
20
|
ClickHouse is configured inline (`configs:` in [compose.yml](compose.yml)): the `backups`
|
|
21
21
|
disk, and `users_without_row_policies_can_read_rows` set explicitly to `true`, which
|
|
@@ -81,7 +81,7 @@ memhouse plugins install claude # /mem:recall and the other agent ski
|
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
`memhouse service status` shows the unit; `memhouse status` shows freshness and every
|
|
84
|
-
machine writing to her
|
|
84
|
+
machine writing to her tables. Without a service, `memhouse start` runs the shipper and the
|
|
85
85
|
dashboard as background processes until logout.
|
|
86
86
|
|
|
87
87
|
## Smoke test
|
|
@@ -89,11 +89,12 @@ dashboard as background processes until logout.
|
|
|
89
89
|
`./smoke.sh` brings the stack up with `.env`, then, through the proxy only: checks the
|
|
90
90
|
certificate, that the `default` user is gone, invites a member `smoke`, installs it from
|
|
91
91
|
the invite file, ships two made-up sessions, reads them back as the member, backs up and
|
|
92
|
-
restores the member's
|
|
93
|
-
its
|
|
92
|
+
restores the member's message parts and archive units, and checks the nightly job. At exit it drops `smoke` and
|
|
93
|
+
its tables (`KEEP=1` keeps them). `SKIP_UP=1` tests a stack that is already running; see
|
|
94
94
|
the top of the script.
|
|
95
95
|
|
|
96
|
-
From a run with `MEMHOUSE_TLS=internal` on a Linux
|
|
96
|
+
From a run with `MEMHOUSE_TLS=internal` on a lab host (Linux, Docker 29, Compose 5.3),
|
|
97
|
+
memhouse 0.20.0-dev.15:
|
|
97
98
|
|
|
98
99
|
```
|
|
99
100
|
smoke test against https://localhost:18443
|
|
@@ -102,28 +103,36 @@ smoke test against https://localhost:18443
|
|
|
102
103
|
ok admin reaches ClickHouse 26.7.1.1315
|
|
103
104
|
ok admin invited member 'smoke'
|
|
104
105
|
ok the member installed from the invite file, over https
|
|
105
|
-
ok shipped:
|
|
106
|
-
ok the member reads its
|
|
106
|
+
ok shipped: v3 capture: 2/2 files changed in 2 sessions → 2 files rows, 10 archive rows (0.0 MiB), 10 turns, 2 calls, 0 tombstones, 2 sessions [new 2]
|
|
107
|
+
ok the member reads its 2 sessions back through the proxy
|
|
107
108
|
ok the member cannot create tables outside mem.smoke_*
|
|
108
109
|
ok BACKUP to the backups disk
|
|
109
110
|
ok RESTORE from it
|
|
110
|
-
ok the restored copy holds the same
|
|
111
|
-
|
|
111
|
+
ok the restored copy holds the same 10 turns
|
|
112
|
+
info no nightly backup yet this server lifetime (BACKUP_ON_START=1 runs one at start)
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
11 passed, 0 failed
|
|
114
115
|
```
|
|
115
116
|
|
|
117
|
+
The nightly job was then run at once (`BACKUP_ON_START=1 docker compose up -d
|
|
118
|
+
--force-recreate backup`): `backup written: mem-2026-10-22-15_38.zip`, and `mem_archive`
|
|
119
|
+
restored from that file under another name held the member's 10 capture records, as
|
|
120
|
+
the live one did.
|
|
121
|
+
|
|
116
122
|
## Backups and restore
|
|
117
123
|
|
|
118
124
|
Every night the `backup` service runs
|
|
119
125
|
|
|
120
126
|
```sql
|
|
121
|
-
BACKUP DATABASE mem,
|
|
127
|
+
BACKUP DATABASE mem, DATABASE mem_archive,
|
|
128
|
+
TABLE system.users, TABLE system.roles, TABLE system.row_policies,
|
|
122
129
|
TABLE system.settings_profiles, TABLE system.quotas
|
|
123
130
|
TO Disk('backups', 'mem-YYYY-MM-DD-HH_MM.zip')
|
|
124
131
|
```
|
|
125
132
|
|
|
126
|
-
and deletes all but the newest `BACKUP_KEEP`.
|
|
133
|
+
and deletes all but the newest `BACKUP_KEEP`. `mem_archive` is the capture archive of a
|
|
134
|
+
0.20 house: every transcript byte as captured, which the sessions in `mem` are derived
|
|
135
|
+
from. It is left out while it does not exist (a house on the 0.18 rooms). The accounts are in the same file on
|
|
127
136
|
purpose: users, their password hashes, their grants and the row policies of scoped shares
|
|
128
137
|
live outside the database, and a restored house without them is one nobody can log in to.
|
|
129
138
|
`docker compose logs backup` shows each run.
|
|
@@ -149,11 +158,14 @@ docker compose exec clickhouse chown clickhouse:clickhouse /backups/mem-2026-10-
|
|
|
149
158
|
then, as the admin:
|
|
150
159
|
|
|
151
160
|
```sql
|
|
152
|
-
RESTORE DATABASE mem,
|
|
161
|
+
RESTORE DATABASE mem, DATABASE mem_archive,
|
|
162
|
+
TABLE system.users, TABLE system.roles, TABLE system.row_policies,
|
|
153
163
|
TABLE system.settings_profiles, TABLE system.quotas
|
|
154
164
|
FROM Disk('backups', 'mem-2026-10-07-21_17.zip')
|
|
155
165
|
```
|
|
156
166
|
|
|
167
|
+
(Leave out `DATABASE mem_archive` for a file written before the house had one.)
|
|
168
|
+
|
|
157
169
|
Members come back with their own passwords and grants, so their machines keep shipping
|
|
158
170
|
once the DNS name points at the new server. Restoring accounts fails if a user of the same
|
|
159
171
|
name already exists, which is why this is a restore onto a fresh stack. To look inside a
|
|
@@ -161,9 +173,12 @@ backup without touching the live house, restore just the data under another name
|
|
|
161
173
|
`RESTORE DATABASE mem AS mem_check FROM Disk('backups', '<file>')`, then
|
|
162
174
|
`DROP DATABASE mem_check`.
|
|
163
175
|
|
|
164
|
-
Tested on ClickHouse 26.7.1.1315: a nightly file copied off the server with the command
|
|
176
|
+
Tested on ClickHouse 26.7.1.1315 with memhouse 0.18.9 (no archive yet): a nightly file copied off the server with the command
|
|
165
177
|
above, a fresh stack (`down -v`, `up`), the copy put back and restored — the data came
|
|
166
178
|
back, and the member logged in with its original password.
|
|
179
|
+
With 0.20 the archive's part was checked on its own: `mem_archive` restored from a nightly
|
|
180
|
+
file under another name held the same records as the live one. The full fresh-stack
|
|
181
|
+
restore has not been repeated on 0.20.
|
|
167
182
|
|
|
168
183
|
## Upgrading ClickHouse
|
|
169
184
|
|
|
@@ -122,7 +122,8 @@ configs:
|
|
|
122
122
|
content: |
|
|
123
123
|
#!/bin/bash
|
|
124
124
|
# Nightly backup at BACKUP_AT (UTC), keeping the newest BACKUP_KEEP. One file holds
|
|
125
|
-
# the house
|
|
125
|
+
# the house, its capture archive (<db>_archive, schema 3: every transcript byte as
|
|
126
|
+
# captured) AND the accounts: users, their grants and the row policies of scoped
|
|
126
127
|
# shares live outside the database, and a restore without them is a house nobody
|
|
127
128
|
# can log in to. README.md has the restore.
|
|
128
129
|
set -uo pipefail
|
|
@@ -133,8 +134,10 @@ configs:
|
|
|
133
134
|
return
|
|
134
135
|
fi
|
|
135
136
|
name="$$MEMHOUSE_DB-$$(date -u +%Y-%m-%d-%H_%M).zip"
|
|
137
|
+
DBS="DATABASE $$MEMHOUSE_DB"
|
|
138
|
+
[ "$$(clickhouse-client --host clickhouse --query "EXISTS DATABASE $${MEMHOUSE_DB}_archive")" = "1" ] && DBS="$$DBS, DATABASE $${MEMHOUSE_DB}_archive"
|
|
136
139
|
if clickhouse-client --host clickhouse --query \
|
|
137
|
-
"BACKUP
|
|
140
|
+
"BACKUP $$DBS, $$ACCESS TO Disk('backups', '$$name')" >/dev/null; then
|
|
138
141
|
echo "$$(date -u +%FT%TZ) backup written: $$name"
|
|
139
142
|
else
|
|
140
143
|
echo "$$(date -u +%FT%TZ) backup FAILED: $$name" >&2
|