memhouse 0.13.0 → 0.14.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
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
SCHEMA_VERSION, MIN_WRITER_SCHEMA, MIGRATIONS, META_TYPES, createStatement,
|
|
29
29
|
} = require(path.join(REPO_ROOT, 'memhouse', 'house', 'house'));
|
|
30
30
|
const envfile = require(path.join(REPO_ROOT, 'memhouse', 'envfile'));
|
|
31
|
+
const { consumeSecretChunk } = require(path.join(REPO_ROOT, 'memhouse', 'secret-input'));
|
|
31
32
|
|
|
32
33
|
const HOME_DIR = process.env.MEMHOUSE_HOME || path.join(os.homedir(), '.memhouse');
|
|
33
34
|
const ENV_FILE = path.join(HOME_DIR, 'env');
|
|
@@ -270,16 +271,20 @@ function askSecret(label, dflt = '') {
|
|
|
270
271
|
const wasRaw = stdin.isRaw;
|
|
271
272
|
stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf-8');
|
|
272
273
|
let buf = '';
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
if (
|
|
282
|
-
|
|
274
|
+
// One 'data' event carries a CHUNK, not a keystroke — see memhouse/secret-input.js
|
|
275
|
+
// for why that distinction is load-bearing and what it broke.
|
|
276
|
+
let done = false;
|
|
277
|
+
const onData = (chunk) => {
|
|
278
|
+
if (done) return;
|
|
279
|
+
const r = consumeSecretChunk(buf, chunk);
|
|
280
|
+
buf = r.buf;
|
|
281
|
+
if (r.interrupted) { process.stdout.write('\n'); process.exit(130); }
|
|
282
|
+
if (!r.done) return;
|
|
283
|
+
done = true;
|
|
284
|
+
stdin.removeListener('data', onData);
|
|
285
|
+
stdin.setRawMode(!!wasRaw); stdin.pause();
|
|
286
|
+
process.stdout.write('\n');
|
|
287
|
+
resolve(buf || dflt);
|
|
283
288
|
};
|
|
284
289
|
stdin.on('data', onData);
|
|
285
290
|
});
|
|
@@ -325,9 +330,11 @@ Setup onboard interactive wizard: discover → configure →
|
|
|
325
330
|
--env FILE installs from an invite file (see: invite)
|
|
326
331
|
invite <name> mint a member + house on the server and write the env
|
|
327
332
|
file their install needs (--url --admin-user
|
|
328
|
-
--admin-password
|
|
329
|
-
machine untouched. Refuses a house
|
|
330
|
-
someone's messages — --adopt if
|
|
333
|
+
[--admin-password, else prompted] [--db NAME]
|
|
334
|
+
[--out FILE]); local machine untouched. Refuses a house
|
|
335
|
+
that already holds someone's messages — --adopt if
|
|
336
|
+
sharing it is intended. Not an admin? --print-sql gives
|
|
337
|
+
the statements to hand to whoever is
|
|
331
338
|
passwd rotate this member's password + rewrite the env file
|
|
332
339
|
(admin-assisted: --admin-user --admin-password)
|
|
333
340
|
--print-sql print the SQL, run it yourself
|
|
@@ -2913,7 +2920,7 @@ async function cmdNightly() {
|
|
|
2913
2920
|
*/
|
|
2914
2921
|
async function cmdInvite() {
|
|
2915
2922
|
const name = positional[0];
|
|
2916
|
-
if (!name) { console.log('usage: memhouse invite <name> --url <house-url> --admin-user … --admin-password … [--db <house>] [--out <file>] [--adopt]'); return 2; }
|
|
2923
|
+
if (!name) { console.log('usage: memhouse invite <name> --url <house-url> [--admin-user … [--admin-password …]] [--db <house>] [--out <file>] [--adopt] [--print-sql]'); return 2; }
|
|
2917
2924
|
try { require(path.join(REPO_ROOT, 'memhouse', 'house', 'house')).assertUsableName(name, 'member'); }
|
|
2918
2925
|
catch (e) { console.log(bad(e.message)); return 1; }
|
|
2919
2926
|
const cfg = resolveConfig();
|
|
@@ -2936,27 +2943,106 @@ async function cmdInvite() {
|
|
|
2936
2943
|
try { require(path.join(REPO_ROOT, 'memhouse', 'house', 'house')).assertUsableName(db, 'house'); }
|
|
2937
2944
|
catch (e) { console.log(bad(e.message)); return 1; }
|
|
2938
2945
|
|
|
2946
|
+
// The way out for someone who is NOT the administrator. Being unable to invite is not a
|
|
2947
|
+
// bug to be worked around — a member holds no CREATE USER by design — but "ask your
|
|
2948
|
+
// admin" is only useful if you can hand them something exact. This prints the same
|
|
2949
|
+
// statements adminBootstrap would run, so the person with the credential runs four
|
|
2950
|
+
// lines instead of installing anything. Nothing is contacted and nothing is written.
|
|
2951
|
+
if (flags['print-sql'] === true) {
|
|
2952
|
+
const password = flags['member-password'] && flags['member-password'] !== true
|
|
2953
|
+
? String(flags['member-password']) : generatePassword();
|
|
2954
|
+
console.log(`-- memhouse: give '${name}' an account and their own house on this ClickHouse.`);
|
|
2955
|
+
console.log(`-- Run as a user with ACCESS MANAGEMENT (a stock 'default' will do).`);
|
|
2956
|
+
console.log(memberSql(db, name, password));
|
|
2957
|
+
console.log('-- Then send them these four lines — they are a credential, so use a channel');
|
|
2958
|
+
console.log('-- you trust (croc, a password manager), not chat:');
|
|
2959
|
+
console.log(`-- MEMHOUSE_URL='${url}'`);
|
|
2960
|
+
console.log(`-- MEMHOUSE_USER='${name}'`);
|
|
2961
|
+
console.log(`-- MEMHOUSE_PASSWORD='${password}'`);
|
|
2962
|
+
console.log(`-- MEMHOUSE_DB='${db}'`);
|
|
2963
|
+
console.log(`-- They install with: memhouse install --url ${url} --user ${name} --password '…' --db ${db}`);
|
|
2964
|
+
return 0;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2939
2967
|
// Whose credential mints the member. Explicit --admin-* wins; otherwise TRY THE
|
|
2940
|
-
// CONFIGURED ONE —
|
|
2941
|
-
//
|
|
2942
|
-
//
|
|
2943
|
-
// read system.users (the proxy for "can it provision").
|
|
2968
|
+
// CONFIGURED ONE — a `deploy --local` house makes its member the superuser
|
|
2969
|
+
// (CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1), so the person who stood the house up
|
|
2970
|
+
// invites with no extra flags.
|
|
2944
2971
|
let adminUser = flags['admin-user'];
|
|
2945
2972
|
let adminPass = flags['admin-password'];
|
|
2946
2973
|
if (!adminUser) {
|
|
2947
2974
|
const c = requireConfig(cfg, 'invite');
|
|
2948
2975
|
if (!c.user) return 1;
|
|
2976
|
+
// Ask what this credential may actually DO, not whether it can look at a table.
|
|
2977
|
+
// The old probe was `SELECT 1 FROM system.users` — but since 0.12.6 every member is
|
|
2978
|
+
// granted SHOW USERS, so every member passed it, was told "it can manage users on
|
|
2979
|
+
// this house", and then died at CREATE DATABASE with a raw ACCESS_DENIED. A probe
|
|
2980
|
+
// that says yes to everyone is worse than no probe: it turns "you are not an admin"
|
|
2981
|
+
// into a confusing failure three steps later.
|
|
2982
|
+
//
|
|
2983
|
+
// chRows appends FORMAT itself, so DO NOT pass one — a statement carrying FORMAT
|
|
2984
|
+
// twice is a syntax error, which the catch below turned into "no grants" and refused
|
|
2985
|
+
// EVERY credential, a real superuser included. Only a clean machine could show that:
|
|
2986
|
+
// this machine's credential is a member either way, so the bug was invisible here.
|
|
2987
|
+
// Reachability FIRST, and reported as itself. Folding it into the grants read makes an
|
|
2988
|
+
// unreachable host — a typo, a loopback-bound house, a tunnel that is down — come back
|
|
2989
|
+
// as "you are only a member", which sends the reader after a privilege they already
|
|
2990
|
+
// have. Caught on the testbed, where a wrong --url produced exactly that.
|
|
2949
2991
|
try {
|
|
2950
|
-
await ch({ ...c, url }, 'SELECT 1
|
|
2992
|
+
await ch({ ...c, url }, 'SELECT 1', { database: '' });
|
|
2993
|
+
} catch (e) {
|
|
2994
|
+
console.log(bad(`cannot reach ${url} as '${c.user}': ${netReason(e)}`));
|
|
2995
|
+
console.log(' --url is the address the INVITEE will use, and invite checks it from here first.');
|
|
2996
|
+
console.log(' A `deploy --local` house is bound to loopback on purpose, so no LAN address');
|
|
2997
|
+
console.log(' reaches it and no invitee could either — publish it (tunnel, reverse proxy)');
|
|
2998
|
+
console.log(' before inviting, or use --print-sql and let the invitee be told the address.');
|
|
2999
|
+
return 1;
|
|
3000
|
+
}
|
|
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) {
|
|
2951
3015
|
adminUser = c.user; adminPass = c.password;
|
|
2952
|
-
console.log(ok(`inviting as your own credential '${c.user}' (it can
|
|
2953
|
-
}
|
|
2954
|
-
console.log(bad('
|
|
2955
|
-
console.log('
|
|
3016
|
+
console.log(ok(`inviting as your own credential '${c.user}' (it can create users and houses here)`));
|
|
3017
|
+
} else {
|
|
3018
|
+
console.log(bad(`'${c.user}' is a MEMBER of this ClickHouse, not an administrator — it cannot create accounts.`));
|
|
3019
|
+
console.log(' Inviting mints a ClickHouse user and a database, which needs CREATE USER and');
|
|
3020
|
+
console.log(' CREATE DATABASE. A member deliberately holds neither: that is the boundary that');
|
|
3021
|
+
console.log(' keeps one housemate out of another house.');
|
|
3022
|
+
console.log('');
|
|
3023
|
+
console.log(' If you RUN this ClickHouse — use the admin credential you created it with');
|
|
3024
|
+
console.log(' (for a stock server that is `default`); memhouse never stores it, so pass it');
|
|
3025
|
+
console.log(' per invite and it is used for one connection and discarded:');
|
|
3026
|
+
console.log(` memhouse invite ${name} --url ${url} --admin-user default`);
|
|
3027
|
+
console.log(' (leave --admin-password off and it is prompted for, so it stays out of');
|
|
3028
|
+
console.log(' your shell history and the process list)');
|
|
3029
|
+
console.log('');
|
|
3030
|
+
console.log(' If SOMEONE ELSE runs it — you cannot invite, and no flag changes that.');
|
|
3031
|
+
console.log(' Print the statements and send them to whoever administers the server:');
|
|
3032
|
+
console.log(` memhouse invite ${name} --url ${url} --print-sql`);
|
|
2956
3033
|
return 1;
|
|
2957
3034
|
}
|
|
2958
3035
|
} else if (adminPass === undefined) {
|
|
2959
|
-
|
|
3036
|
+
// Prompt rather than refuse. A password given as --admin-password lands in the
|
|
3037
|
+
// process list for the life of the request and in shell history unless the caller
|
|
3038
|
+
// remembered a leading space; asking for it keeps it in this process only.
|
|
3039
|
+
// Non-TTY (an agent, CI) still gets the old refusal — there is nobody to ask.
|
|
3040
|
+
if (!process.stdin.isTTY) {
|
|
3041
|
+
console.log(bad('--admin-user given without --admin-password (no TTY to prompt on)'));
|
|
3042
|
+
return 1;
|
|
3043
|
+
}
|
|
3044
|
+
adminPass = await askSecret(` password for '${adminUser}'`);
|
|
3045
|
+
if (!adminPass) { console.log(bad('no password given')); return 1; }
|
|
2960
3046
|
}
|
|
2961
3047
|
|
|
2962
3048
|
// WHERE to connect to provision. --url is the address the INVITEE will use — it may be
|
|
@@ -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.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ramazan Polat"
|
|
7
7
|
},
|
|
@@ -42,14 +42,42 @@ substituted into a command — that burns the credential into the transcript on
|
|
|
42
42
|
If it prints `invite written: …`, go to step 4 — you are done, and no admin secret
|
|
43
43
|
ever touched this conversation.
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
Otherwise read WHICH refusal came back; they need different answers.
|
|
46
|
+
|
|
47
|
+
**"cannot reach `<url>` …"** — nothing to do with privileges. The address is wrong,
|
|
48
|
+
or the house is bound to loopback (`memhouse deploy --local` binds to 127.0.0.1 on
|
|
49
|
+
purpose, so no LAN address reaches it and no invitee could either). Ask the user for
|
|
50
|
+
the address the invitee will actually use; do not retry with a guess.
|
|
51
|
+
|
|
52
|
+
**"`<user>` is a MEMBER of this ClickHouse, not an administrator"** — inviting needs
|
|
53
|
+
CREATE USER and CREATE DATABASE, which a member deliberately does not hold. Two cases,
|
|
54
|
+
and you must ask which one applies rather than assuming:
|
|
55
|
+
|
|
56
|
+
- **They run the ClickHouse.** They have the admin credential; memhouse never stores
|
|
57
|
+
it. Do NOT take that password in chat — memhouse archives this transcript. Hand them
|
|
58
|
+
the line to run THEMSELVES, and leave `--admin-password` OFF so it is prompted for,
|
|
59
|
+
keeping it out of both the process list and shell history:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
memhouse invite <name> --url <url> [--db <shared-house>] --admin-user <admin>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**You cannot run that line yourself.** The prompt needs a TTY and your Bash tool has
|
|
66
|
+
none, so it refuses with "no TTY to prompt on" — and the fix is NOT to add
|
|
67
|
+
`--admin-password`, which would put the credential in this transcript. The user runs
|
|
68
|
+
it in their own terminal; you wait for them to say it is done.
|
|
69
|
+
|
|
70
|
+
- **Somebody else runs it.** They cannot invite, and no flag changes that. Print the
|
|
71
|
+
statements for whoever administers the server — this contacts nothing and needs no
|
|
72
|
+
credential:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
memhouse invite <name> --url <url> [--db <shared-house>] --print-sql
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The output ends with the four `MEMHOUSE_*` lines the invitee needs. Those lines are
|
|
79
|
+
a credential: tell the user to send them over a trusted channel, and **never print
|
|
80
|
+
them into the conversation yourself**.
|
|
53
81
|
|
|
54
82
|
**If it refuses with "house `<db>` already exists and holds N messages", STOP.** That
|
|
55
83
|
house is somebody's memory, and inviting into it would hand the invitee someone else's
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Consuming a hidden-input chunk, as pure logic so it can be tested without a terminal.
|
|
2
|
+
//
|
|
3
|
+
// The subtlety this exists for: a raw-mode stdin 'data' event carries a CHUNK, not a
|
|
4
|
+
// keystroke. While someone types, each chunk happens to be one character, so a handler
|
|
5
|
+
// that compares the whole chunk against '\r' looks correct for as long as anyone tests it
|
|
6
|
+
// by hand. The moment a password is PASTED — which is how a credential out of a password
|
|
7
|
+
// manager actually arrives — the chunk is 'hunter2\r', equal to no terminator, and the
|
|
8
|
+
// prompt hangs forever with the secret sitting in the buffer. Anything piping input hits
|
|
9
|
+
// the same wall.
|
|
10
|
+
//
|
|
11
|
+
// So: walk the chunk. Callers own the terminal; this owns the decision.
|
|
12
|
+
|
|
13
|
+
const ENTER = ['\r', '\n', '\u0004']; // CR, LF, EOT
|
|
14
|
+
const INTERRUPT = '\u0003'; // ctrl-C
|
|
15
|
+
const ERASE = ['\u007f', '\b']; // DEL, BS
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fold one chunk into the accumulated buffer.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} buf what has been read so far
|
|
21
|
+
* @param {string} chunk the raw 'data' payload (may hold any number of characters)
|
|
22
|
+
* @returns {{buf: string, done: boolean, interrupted: boolean}}
|
|
23
|
+
* `done` — a terminator was seen; `buf` is the final value and anything after the
|
|
24
|
+
* terminator in the same chunk is deliberately discarded (it belongs to whatever
|
|
25
|
+
* prompt comes next, not this one).
|
|
26
|
+
* `interrupted` — ctrl-C was seen; the caller decides how to exit.
|
|
27
|
+
*/
|
|
28
|
+
function consumeSecretChunk(buf, chunk) {
|
|
29
|
+
let out = String(buf == null ? '' : buf);
|
|
30
|
+
for (const ch of String(chunk == null ? '' : chunk)) {
|
|
31
|
+
if (ch === INTERRUPT) return { buf: out, done: false, interrupted: true };
|
|
32
|
+
if (ENTER.includes(ch)) return { buf: out, done: true, interrupted: false };
|
|
33
|
+
if (ERASE.includes(ch)) { out = out.slice(0, -1); continue; }
|
|
34
|
+
out += ch;
|
|
35
|
+
}
|
|
36
|
+
return { buf: out, done: false, interrupted: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { consumeSecretChunk };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memhouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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": {
|