memhouse 0.14.0 → 0.15.0

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/bin/memhouse.js CHANGED
@@ -29,6 +29,7 @@ const {
29
29
  } = require(path.join(REPO_ROOT, 'memhouse', 'house', 'house'));
30
30
  const envfile = require(path.join(REPO_ROOT, 'memhouse', 'envfile'));
31
31
  const { consumeSecretChunk } = require(path.join(REPO_ROOT, 'memhouse', 'secret-input'));
32
+ const { capabilitiesFrom } = require(path.join(REPO_ROOT, 'memhouse', 'capabilities'));
32
33
 
33
34
  const HOME_DIR = process.env.MEMHOUSE_HOME || path.join(os.homedir(), '.memhouse');
34
35
  const ENV_FILE = path.join(HOME_DIR, 'env');
@@ -162,6 +163,24 @@ async function ch(cfg, sql, { database = cfg.db, settings = null, timeout = 3000
162
163
  if (!res.ok) throw new Error(text.trim().split('\n')[0]);
163
164
  return text.trim();
164
165
  }
166
+ /**
167
+ * The grant lines ClickHouse reports for a user, as strings.
168
+ *
169
+ * Never pass a FORMAT here — chRows appends its own, and a statement carrying two is a
170
+ * syntax error. That mistake once read as "no grants at all", which refused every
171
+ * credential including a genuine superuser, and it was invisible on any machine whose
172
+ * own credential is an ordinary member.
173
+ *
174
+ * Unreadable grants come back as [] rather than throwing: a credential that cannot read
175
+ * its own grants is, for every decision made from this, simply not an administrator.
176
+ */
177
+ async function readGrants(cfg, user) {
178
+ try {
179
+ const rows = await chRows(cfg, `SHOW GRANTS FOR ${user}`, { database: '' });
180
+ return rows.map((r) => Object.values(r).join(' '));
181
+ } catch { return []; }
182
+ }
183
+
165
184
  async function chRows(cfg, sql, opts) {
166
185
  const text = await ch(cfg, sql + ' FORMAT JSONEachRow', opts);
167
186
  return text ? text.split('\n').map((l) => JSON.parse(l)) : [];
@@ -363,6 +382,8 @@ Data ship one incremental pass (--full | --loop [sec])
363
382
  resume <session-id> print the command that reopens that session in its editor
364
383
  sessions-query print the session rollup SQL for this credential
365
384
  start | stop | shipper loop + dashboard as background daemons
385
+ whoami which credential is in play, and what it may do
386
+ (--json; --admin reads MEMHOUSE_ADMIN_*)
366
387
  status daemons, connection, counts, freshness (--json)
367
388
  doctor diagnose the whole pipeline
368
389
 
@@ -2918,6 +2939,74 @@ async function cmdNightly() {
2918
2939
  * transfer, and rotation (memhouse passwd) is printed because the inviter knows this
2919
2940
  * password until the invitee changes it.
2920
2941
  */
2942
+ /**
2943
+ * `memhouse whoami [--json] [--admin]`
2944
+ *
2945
+ * Which credential is in play here, and what may it actually do. Exists so nothing has
2946
+ * to hand-roll `SHOW GRANTS` and grep the result — a skill that reasons about privileges
2947
+ * in prose gets it subtly wrong, and the wrong answer is either "you cannot" to an
2948
+ * administrator or "go ahead" to a member who is about to hit ACCESS_DENIED.
2949
+ *
2950
+ * `--admin` resolves MEMHOUSE_ADMIN_USER / MEMHOUSE_ADMIN_PASSWORD first, so an agent
2951
+ * can ask "is there an admin credential in this environment?" without inventing a place
2952
+ * to store one. Nothing here prints or persists a password.
2953
+ */
2954
+ async function cmdWhoami() {
2955
+ const cfg = resolveConfig();
2956
+ const wantAdmin = flags.admin === true;
2957
+ const au = process.env.MEMHOUSE_ADMIN_USER;
2958
+ const ap = process.env.MEMHOUSE_ADMIN_PASSWORD;
2959
+ const usingAdminEnv = wantAdmin && au;
2960
+ const who = usingAdminEnv ? { ...cfg, user: au, password: ap || '' } : cfg;
2961
+
2962
+ if (!who.url || !who.user) {
2963
+ const msg = 'no credential configured';
2964
+ if (JSON_OUT) console.log(JSON.stringify({ ok: false, reason: msg }, null, 2));
2965
+ else {
2966
+ console.log(bad(`${msg} — nothing in ${short(ENV_FILE)} and no MEMHOUSE_URL/MEMHOUSE_USER set.`));
2967
+ if (wantAdmin) console.log(' For --admin, export MEMHOUSE_ADMIN_USER and MEMHOUSE_ADMIN_PASSWORD.');
2968
+ }
2969
+ return 1;
2970
+ }
2971
+
2972
+ let server = null;
2973
+ try { server = (await chRows(who, 'SELECT currentUser() AS u', { database: '' }))[0]?.u; }
2974
+ catch (e) {
2975
+ const reason = netReason(e);
2976
+ if (JSON_OUT) console.log(JSON.stringify({ ok: false, url: who.url, user: who.user, reason }, null, 2));
2977
+ else console.log(bad(`cannot reach ${who.url} as '${who.user}': ${reason}`));
2978
+ return 1;
2979
+ }
2980
+
2981
+ const grants = await readGrants(who, server);
2982
+ const caps = capabilitiesFrom(grants);
2983
+ const source = usingAdminEnv ? 'MEMHOUSE_ADMIN_* environment'
2984
+ : (process.env.MEMHOUSE_USER ? 'MEMHOUSE_* environment' : short(ENV_FILE));
2985
+ const role = caps.isSuperuser ? 'administrator'
2986
+ : caps.canProvision ? 'can provision (users and houses)'
2987
+ : 'member';
2988
+
2989
+ if (JSON_OUT) {
2990
+ console.log(JSON.stringify({
2991
+ ok: true, url: who.url, user: server, db: who.db, source, role, ...caps,
2992
+ grants_readable: grants.length > 0,
2993
+ }, null, 2));
2994
+ return 0;
2995
+ }
2996
+ console.log(ok(`${server} at ${who.url} — ${role}`));
2997
+ console.log(` credential from: ${source}`);
2998
+ console.log(` house: ${who.db || '(none set)'}`);
2999
+ console.log(` may create users: ${caps.canMintUsers ? 'yes' : 'no'}`);
3000
+ console.log(` may create houses: ${caps.canMintHouses ? 'yes' : 'no'}`);
3001
+ console.log(` may read any house: ${caps.canReadEveryHouse ? 'yes' : 'no'}`);
3002
+ if (!grants.length) console.log(warn('could not read its own grants — treating it as a member'));
3003
+ if (!caps.canProvision) {
3004
+ console.log(' Inviting and server-wide administration need an administrator; this is not one.');
3005
+ console.log(' Supply one for this shell: export MEMHOUSE_ADMIN_USER=… MEMHOUSE_ADMIN_PASSWORD=…');
3006
+ }
3007
+ return 0;
3008
+ }
3009
+
2921
3010
  async function cmdInvite() {
2922
3011
  const name = positional[0];
2923
3012
  if (!name) { console.log('usage: memhouse invite <name> --url <house-url> [--admin-user … [--admin-password …]] [--db <house>] [--out <file>] [--adopt] [--print-sql]'); return 2; }
@@ -2998,20 +3087,11 @@ async function cmdInvite() {
2998
3087
  console.log(' before inviting, or use --print-sql and let the invitee be told the address.');
2999
3088
  return 1;
3000
3089
  }
3001
- let grants = '';
3002
- try {
3003
- const g = await chRows({ ...c, url }, `SHOW GRANTS FOR ${c.user}`, { database: '' });
3004
- grants = g.map((r) => Object.values(r).join(' ')).join('\n');
3005
- } catch { /* reachable but grants unreadable — treated as "not an admin" below */ }
3006
- // Two capabilities, and a superuser states them in more than one shape: CREATE USER
3007
- // may arrive as itself or under ACCESS MANAGEMENT, and the database half may be an
3008
- // explicit CREATE DATABASE or the umbrella CREATE on *.* — which is what
3009
- // `deploy --local` grants. Match the meaning, not one spelling.
3010
- const canMintUsers = /\bACCESS MANAGEMENT\b|\bCREATE USER\b/.test(grants);
3011
- const canMintHouses = /\bCREATE DATABASE\b/.test(grants)
3012
- || grants.split('\n').some((l) => /\bCREATE\b/.test(l) && /\bON \*\.\*/.test(l));
3013
- const canProvision = canMintUsers && canMintHouses;
3014
- if (canProvision) {
3090
+ // What this credential may do, judged from its own grants and SCOPE-AWARE — a member
3091
+ // holds CREATE DATABASE inside `ON <their-db>.*`, which mints no new house at all.
3092
+ // Shared with `whoami` so the two can never disagree about who is an administrator.
3093
+ const caps = capabilitiesFrom(await readGrants({ ...c, url }, c.user));
3094
+ if (caps.canProvision) {
3015
3095
  adminUser = c.user; adminPass = c.password;
3016
3096
  console.log(ok(`inviting as your own credential '${c.user}' (it can create users and houses here)`));
3017
3097
  } else {
@@ -3444,6 +3524,7 @@ async function cmdUninstall() {
3444
3524
  }
3445
3525
  break;
3446
3526
  case 'reset': process.exitCode = await cmdReset(); break;
3527
+ case 'whoami': process.exitCode = await cmdWhoami(); break;
3447
3528
  case 'invite': process.exitCode = await cmdInvite(); break;
3448
3529
  case 'passwd': process.exitCode = await cmdPasswd(); break;
3449
3530
  case 'nightly': process.exitCode = await cmdNightly(); break;
@@ -0,0 +1,75 @@
1
+ // What a ClickHouse credential may actually DO, read from its own grants.
2
+ //
3
+ // This exists because asking the wrong question here is expensive and quiet. `invite`
4
+ // used to probe with `SELECT 1 FROM system.users`: since 0.12.6 every member holds
5
+ // SHOW USERS, so every member passed, was told it could manage users, and then died at
6
+ // CREATE DATABASE with a raw ACCESS_DENIED. The replacement must be exact, and it must
7
+ // live in one place — invite and /mem:admin disagreeing about who is an administrator is
8
+ // the same class of bug wearing a different hat.
9
+ //
10
+ // SCOPE IS THE WHOLE POINT. A grant is `GRANT <privileges> ON <scope> TO <user>`, and a
11
+ // privilege only means what its scope allows: an ordinary member holds CREATE DATABASE
12
+ // inside `ON polat.*`, which lets them do nothing whatsoever about creating a NEW
13
+ // database. Matching the privilege name anywhere in the text — which an earlier draft of
14
+ // this did — reads that member as a server administrator. So parse the line, keep the
15
+ // scope, and only count a privilege that is granted server-wide.
16
+
17
+ /** `GRANT a, b, c ON scope TO user [WITH GRANT OPTION]` -> {privs:[…], scope} | null */
18
+ function parseGrantLine(line) {
19
+ const m = /^\s*GRANT\s+(.+?)\s+ON\s+(\S+)\s+TO\s+/i.exec(String(line || ''));
20
+ if (!m) return null;
21
+ const privs = m[1].split(',').map((p) => p.trim().toUpperCase()).filter(Boolean);
22
+ return { privs, scope: m[2] };
23
+ }
24
+
25
+ /** Server-wide scope: `*.*` (tables everywhere) or `*` (the access/user namespace). */
26
+ function isServerWide(scope) {
27
+ return scope === '*.*' || scope === '*';
28
+ }
29
+
30
+ function hasPriv(privs, name) {
31
+ return privs.includes(name);
32
+ }
33
+
34
+ /**
35
+ * Derive capabilities from the lines `SHOW GRANTS FOR <user>` returned.
36
+ *
37
+ * @param {string[]|string} grants one string per grant line, or the whole blob
38
+ * @returns {{canMintUsers:boolean, canMintHouses:boolean, canProvision:boolean,
39
+ * canReadEveryHouse:boolean, canSeeUsers:boolean, isSuperuser:boolean}}
40
+ */
41
+ function capabilitiesFrom(grants) {
42
+ const lines = Array.isArray(grants) ? grants : String(grants || '').split('\n');
43
+ let canMintUsers = false;
44
+ let canMintHouses = false;
45
+ let canReadEveryHouse = false;
46
+ let canSeeUsers = false;
47
+
48
+ for (const raw of lines) {
49
+ const g = parseGrantLine(raw);
50
+ if (!g) continue;
51
+ const wide = isServerWide(g.scope);
52
+ // SHOW USERS is the one every member has — never evidence of anything.
53
+ if (hasPriv(g.privs, 'SHOW USERS')) canSeeUsers = true;
54
+ if (!wide) continue;
55
+ // Minting accounts. ACCESS MANAGEMENT is the umbrella; CREATE USER the specific.
56
+ if (hasPriv(g.privs, 'ACCESS MANAGEMENT') || hasPriv(g.privs, 'CREATE USER')) canMintUsers = true;
57
+ // Minting houses. `CREATE DATABASE` is the specific; bare `CREATE` is the umbrella
58
+ // ClickHouse hands a superuser (what `deploy --local` grants), and `ALL` covers both.
59
+ if (hasPriv(g.privs, 'CREATE DATABASE') || hasPriv(g.privs, 'CREATE') || hasPriv(g.privs, 'ALL')) canMintHouses = true;
60
+ // Reading anyone's rooms — what separates an administrator's view from a member's.
61
+ if (hasPriv(g.privs, 'SELECT') || hasPriv(g.privs, 'ALL')) canReadEveryHouse = true;
62
+ }
63
+
64
+ const canProvision = canMintUsers && canMintHouses;
65
+ return {
66
+ canMintUsers,
67
+ canMintHouses,
68
+ canProvision,
69
+ canReadEveryHouse,
70
+ canSeeUsers,
71
+ isSuperuser: canProvision && canReadEveryHouse,
72
+ };
73
+ }
74
+
75
+ module.exports = { capabilitiesFrom, parseGrantLine, isServerWide };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mem",
3
3
  "description": "Conversation memory for AI agents over the memhouse typed ClickHouse store: ask, search, browse, analyze, check, share, and invite across all 17 supported editors and machines, plus administration of the ClickHouse behind it. Skills install as /mem:hello, /mem:ask, /mem:search, /mem:sessions, /mem:share, /mem:invite, /mem:sql, /mem:status, /mem:users, and /mem:admin.",
4
- "version": "0.14.0",
4
+ "version": "0.15.0",
5
5
  "author": {
6
6
  "name": "Ramazan Polat"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: admin
3
- description: Operate the memhouse ClickHouse as its administrator — list and inspect every user and house, provision or remove members, grant and revoke access, and read server-wide health (disk, parts, mutations, running queries). Use when the user asks to "do X as admin", "manage users", "drop a house", "see disk usage", "who is on this server", or when a member-scoped skill refused for want of privileges. Requires an admin credential; says so plainly when there is not one.
3
+ description: Operate the memhouse ClickHouse as its administrator — list and inspect every user and house, provision or remove members, grant and revoke access, and read server-wide health (disk, parts, mutations, running queries). Use when the user asks to "do X as admin", "manage users", "drop a house", "see disk usage", "who is on this server", or when a member-scoped skill refused for want of privileges. Requires an administrator credential in the environment; says so plainly, and how to supply one, when there is not.
4
4
  user-invocable: true
5
5
  argument-hint: "<what you want done>"
6
6
  allowed-tools: Bash
@@ -12,61 +12,61 @@ A member owns one database. An **administrator** owns the ClickHouse: every hous
12
12
  account, every grant. This skill is the second one, and it is the only skill allowed to
13
13
  act outside the caller's own house.
14
14
 
15
- Everything here runs through the same rule as `/mem:invite`: **an admin password must
16
- never enter this conversation.** memhouse ships this transcript into the house you are
17
- administering, so a password pasted in chat is a password published to the archive.
18
- Resolve it from the environment or a file, never from a prompt, and never echo it.
15
+ **An admin password must never enter this conversation.** memhouse ships this transcript
16
+ into the house you are administering, so a password pasted here is a password published
17
+ to the archive — and unlike a file you can delete, the archive is insert-only. Resolve
18
+ the credential from the environment or refuse. Never ask for one in chat, never echo one,
19
+ never write one to a file.
19
20
 
20
- ## 1. Find an admin credential — never ask for one in chat
21
-
22
- Try these in order and stop at the first that works. **Never print any of these files or
23
- the variables in them** — no `cat`, no `env | grep`, no `set -x`.
24
-
25
- ```bash
26
- MH_HOME="${MEMHOUSE_HOME:-$HOME/.memhouse}"
27
- # a) an explicit admin credential in the environment
28
- ADM_U=${MEMHOUSE_ADMIN_USER-}; ADM_P=${MEMHOUSE_ADMIN_PASSWORD-}
29
- # b) an admin env file kept beside the member config
30
- if [ -z "$ADM_U" ] && [ -f "$MH_HOME/admin.env" ]; then
31
- set -a; . "$MH_HOME/admin.env"; set +a
32
- ADM_U=${MEMHOUSE_ADMIN_USER:-${MEMHOUSE_USER-}}; ADM_P=${MEMHOUSE_ADMIN_PASSWORD:-${MEMHOUSE_PASSWORD-}}
33
- fi
34
- # c) the ordinary member credential — on a house the pilot stood up themselves
35
- # (memhouse deploy --local) the member IS the superuser
36
- set -a; [ -f "$MH_HOME/env" ] && . "$MH_HOME/env"; set +a
37
- : "${ADM_U:=${MEMHOUSE_USER-}}"; : "${ADM_P:=${MEMHOUSE_PASSWORD-}}"
38
- ```
39
-
40
- Then **prove** it is actually an admin rather than assuming — a member can often read
41
- `system.users` while holding nothing else:
21
+ ## 1. Ask the CLI what you are holding — do not work it out yourself
42
22
 
43
23
  ```bash
44
- curl -sS --fail-with-body --user "$ADM_U:$ADM_P" \
45
- --data-binary "SHOW GRANTS FOR $ADM_U FORMAT TSV" "$MEMHOUSE_URL/"
24
+ memhouse whoami --admin --json
46
25
  ```
47
26
 
48
- Admin means the grants carry `ACCESS MANAGEMENT`, or `CREATE USER`/`CREATE DATABASE` at
49
- `*.*` — not merely `SHOW USERS`, which every member has. If none of a/b/c qualifies,
50
- **stop and say so**:
51
-
52
- > This needs administrator access to the ClickHouse at `<url>`, and the credential
53
- > configured here is the member account `<user>` — it owns its own house and nothing
54
- > else. To use `/mem:admin`, put an admin credential in `~/.memhouse/admin.env`
55
- > (`MEMHOUSE_ADMIN_USER=` / `MEMHOUSE_ADMIN_PASSWORD=`, mode 600) or export
56
- > `MEMHOUSE_ADMIN_USER` / `MEMHOUSE_ADMIN_PASSWORD`, then ask again. Whoever operates
57
- > the server has it; on a house from `memhouse deploy --local` it is the account already
58
- > in `~/.memhouse/env`.
59
-
60
- Do not offer to carry on with reduced powers, do not suggest the user paste a password
61
- into the chat, and do not improvise around the refusal. Say which of a/b/c you tried.
62
-
63
- ## 2. Run the request
64
-
65
- Use the resolved credential for every query. `--fail-with-body` so ClickHouse's own error
66
- is visible; add `&readonly=1` on anything that only reads.
27
+ That resolves `MEMHOUSE_ADMIN_USER` / `MEMHOUSE_ADMIN_PASSWORD` when they are set and
28
+ falls back to the configured credential, then reports what it may actually do. Read
29
+ `role` and the capability flags; never infer privileges from a grant string yourself.
30
+ The command prints no password.
31
+
32
+ - **`"canProvision": true`** (or `"role": "administrator"`) — you have an administrator.
33
+ Go to §2.
34
+ - **`"canProvision": false`** — the credential is a member. **Stop.** Do not retry, do
35
+ not look for a stored password, and do not ask for one. Tell the user:
36
+
37
+ > `/mem:admin` needs an administrator credential for `<url>`, and what is configured
38
+ > here is the member account `<user>` — it owns its own house and nothing else. If you
39
+ > administer that ClickHouse, put the credential in this shell's environment and ask
40
+ > again:
41
+ >
42
+ > ```
43
+ > export MEMHOUSE_ADMIN_USER=default
44
+ > read -rs MEMHOUSE_ADMIN_PASSWORD && export MEMHOUSE_ADMIN_PASSWORD
45
+ > ```
46
+ >
47
+ > `read -rs` keeps it off the screen and out of your shell history. It lives only in
48
+ > that shell — nothing is written to disk, and memhouse never stores it.
49
+ >
50
+ > If somebody else administers the server, this is theirs to run, not yours.
51
+
52
+ - **`"ok": false`** — a connection problem, not a privilege one. Report `reason` as
53
+ given; do not translate it into "you are not an admin".
54
+
55
+ **Never suggest storing the credential** — not in `~/.memhouse/env`, not in an
56
+ `admin.env`, not anywhere on disk. An admin credential owns the whole server, and every
57
+ process running as this user can read a file. The environment of one shell is the whole
58
+ supported surface. (A house from `memhouse deploy --local` is the exception that needs
59
+ nothing: its member *is* the superuser, and `whoami` will already say so.)
60
+
61
+ ## 2. Do the work
62
+
63
+ Use the same environment for every query. `--fail-with-body` so ClickHouse's own error is
64
+ visible; `&readonly=1` on anything that only reads.
67
65
 
68
66
  ```bash
69
- adm() { curl -sS --fail-with-body --user "$ADM_U:$ADM_P" --data-binary "$1" "$MEMHOUSE_URL/${2-}"; }
67
+ adm() { curl -sS --fail-with-body \
68
+ --user "$MEMHOUSE_ADMIN_USER:$MEMHOUSE_ADMIN_PASSWORD" \
69
+ --data-binary "$1" "$MEMHOUSE_URL/${2-}"; }
70
70
  adm "SELECT … FORMAT PrettyCompact" "?readonly=1"
71
71
  ```
72
72
 
@@ -74,21 +74,19 @@ Reads worth knowing — reach for these before inventing SQL:
74
74
 
75
75
  | Question | Query |
76
76
  |---|---|
77
- | who exists | `SELECT name FROM system.users ORDER BY name` |
78
- | what can someone do | `SHOW GRANTS FOR <user>` |
77
+ | who exists | `SELECT name, storage FROM system.users ORDER BY name` |
78
+ | what someone may do | `SHOW GRANTS FOR <user>` |
79
79
  | every house and its size | `SELECT database, formatReadableSize(sum(bytes_on_disk)) AS disk, sum(rows) AS rows FROM system.parts WHERE active GROUP BY database ORDER BY sum(bytes_on_disk) DESC` |
80
80
  | a house's rooms | `SELECT table, formatReadableSize(sum(bytes_on_disk)) AS disk, sum(rows) AS rows FROM system.parts WHERE active AND database = '<db>' GROUP BY table` |
81
81
  | who writes where | `SELECT user_id, host, count() FROM <db>.messages GROUP BY user_id, host` |
82
82
  | running queries | `SELECT query_id, user, elapsed, formatReadableSize(memory_usage) AS mem, substring(query,1,120) AS q FROM system.processes` |
83
83
  | unfinished mutations | `SELECT database, table, mutation_id, command, parts_to_do, latest_fail_reason FROM system.mutations WHERE NOT is_done` |
84
- | merge backlog | `SELECT database, table, elapsed, progress FROM system.merges` |
85
84
 
86
- Writes follow the shapes memhouse itself uses, so an admin-made member is identical to an
87
- invited one — a member gets `GRANT ALL ON <db>.* … WITH GRANT OPTION`, plus `SHOW USERS`,
88
- self-scoped `ALTER USER`, `REMOTE`, and the async-insert pin. **Prefer the CLI over raw
89
- SQL when one exists**: `memhouse invite <name> --url …` provisions a member correctly
90
- (and refuses a house that already holds someone's messages); this skill is for the things
91
- it does not cover.
85
+ **Prefer a CLI verb over raw SQL wherever one exists.** `memhouse invite <name> --url …`
86
+ provisions a member correctly — right grants, right pin, refuses a house that already
87
+ holds someone's messages — and it takes `--admin-user`/`--admin-password`, so it works
88
+ from this environment. Hand-written `CREATE USER` skips every one of those guards. This
89
+ skill is for what no verb covers.
92
90
 
93
91
  ## 3. Destructive work — look first, then confirm
94
92
 
@@ -99,30 +97,33 @@ editors that produced them.
99
97
 
100
98
  Before any of them:
101
99
 
102
- 1. **Look at the target and report what is actually there** — row counts per room, who
100
+ 1. **Look at the target and report what is really there** — row counts per room, who
103
101
  wrote them, the date span, which machines. A house you expected to be empty may not
104
- be.
102
+ be: one `ege` house looked disposable and held 3,733 messages belonging to somebody
103
+ else entirely.
105
104
  2. **Name the collateral.** Dropping a user stops that account's shipper on every machine
106
- it runs on. Revoking a grant closes a share the other person may be mid-query on.
107
- Rotating a password breaks every machine shipping under it.
108
- 3. **Get an explicit yes for that specific object**, quoting the counts you just read.
109
- A general "yes do admin things" is not consent to drop a populated house.
110
- 4. **Verify afterwards** and say what is now true.
105
+ it runs on. Revoking a grant closes a share someone may be mid-query on. Rotating a
106
+ password breaks every machine shipping under it — and grants are per-user while the
107
+ fleet is per-`user@host`, so there is no way to revoke one machine.
108
+ 3. **Get an explicit yes for that specific object**, quoting the counts you just read. A
109
+ general "yes, do admin things" is not consent to drop a populated house.
110
+ 4. **Verify afterwards** and say what is now true, including what was destroyed.
111
111
 
112
112
  If the user has already told you exactly what to destroy and why, and the counts match
113
- their description, proceed — do not re-litigate a decision they have made. Report the
114
- outcome plainly, including the row counts destroyed.
113
+ their description, proceed — do not re-litigate a decision they have made.
115
114
 
116
115
  ## Never
117
116
 
118
117
  Never accept an admin password typed into the conversation, and never print one — not in
119
- a command, not in an echo, not in an error. If a command needs it, expand a shell
120
- variable (`--admin-password "$ADM_P"`), so the value never appears in the transcript.
118
+ a command, not in an echo, not in an error. Commands expand `"$MEMHOUSE_ADMIN_PASSWORD"`;
119
+ the value never appears as literal text.
120
+
121
+ Never recommend writing an admin credential to disk.
121
122
 
122
- Never widen a member's grants beyond what `memhouse invite` issues without the user
123
- asking for that specific privilege and being told what it allows. Never grant
124
- `ACCESS MANAGEMENT`, `CREATE USER`, or anything at `*.*` casually — those make a second
123
+ Never widen a member's grants beyond what `memhouse invite` issues unless the user asks
124
+ for that specific privilege and has been told what it allows. Never grant `ACCESS
125
+ MANAGEMENT`, `CREATE USER`, or anything at `*.*` casually — those mint a second
125
126
  administrator.
126
127
 
127
- Never touch a house that is not the subject of the request. Never run a destructive
128
+ Never touch a house that is not the subject of the request, and never run a destructive
128
129
  statement whose predicate you have not first run as a `SELECT`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memhouse",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "memhouse \u2014 agent conversation memory across 17 editors: parse-on-client shippers, a typed ClickHouse store with one set of rooms per member, kernel-agency install, and the agentlytics dashboard",
5
5
  "main": "bin/memhouse.js",
6
6
  "bin": {