memhouse 0.18.8 → 0.18.10
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 +194 -0
- package/CHANGELOG.md +970 -0
- package/README.md +54 -423
- package/SECURITY.md +68 -0
- package/bin/memhouse.js +202 -92
- package/docs/README.md +46 -0
- package/docs/guides/admin-password.md +95 -0
- package/docs/guides/configuration.md +109 -0
- package/docs/guides/daemons.md +53 -0
- package/docs/guides/install.md +128 -0
- package/docs/guides/invite.md +87 -0
- package/docs/guides/operate.md +112 -0
- package/docs/guides/search-and-resume.md +95 -0
- package/docs/guides/share.md +102 -0
- package/docs/guides/storage.md +90 -0
- package/docs/guides/troubleshooting.md +78 -0
- package/docs/guides/uninstall.md +35 -0
- package/docs/guides/update.md +68 -0
- package/docs/releases/README.md +10 -0
- package/docs/releases/v0.18.10.md +226 -0
- package/docs/tutorials/01-first-house.md +132 -0
- package/docs/tutorials/02-join-a-house.md +91 -0
- package/docs/tutorials/03-share-your-memory.md +96 -0
- package/examples/01-local-one-machine/README.md +99 -0
- package/examples/01-local-one-machine/run.sh +61 -0
- package/examples/02-existing-clickhouse/README.md +121 -0
- package/examples/02-existing-clickhouse/run.sh +71 -0
- package/examples/03-team-house/README.md +129 -0
- package/examples/03-team-house/run.sh +124 -0
- package/examples/04-self-hosted-team/.env.example +22 -0
- package/examples/04-self-hosted-team/README.md +187 -0
- package/examples/04-self-hosted-team/compose.yml +158 -0
- package/examples/04-self-hosted-team/smoke.sh +113 -0
- package/examples/README.md +42 -0
- package/examples/fixtures/README.md +24 -0
- package/examples/fixtures/make-sessions.js +63 -0
- package/memhouse/admin-secret.js +81 -0
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +1 -1
- package/memhouse/deploy.js +4 -2
- package/memhouse/flags.js +4 -4
- package/memhouse/provision.js +12 -3
- package/memhouse/server/queries.js +18 -4
- package/memhouse/server/server.js +9 -1
- package/memhouse/service.js +5 -0
- package/package.json +10 -2
package/bin/memhouse.js
CHANGED
|
@@ -147,7 +147,7 @@ function requireConfig(cfg, what) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
function childEnv(cfg, override = {}) {
|
|
150
|
-
|
|
150
|
+
const env = {
|
|
151
151
|
...process.env,
|
|
152
152
|
// Set only by the install path, which prints its own refusal for this case.
|
|
153
153
|
...(cfg._quietDenied ? { MEMHOUSE_QUIET_DENIED: '1' } : {}),
|
|
@@ -159,6 +159,11 @@ function childEnv(cfg, override = {}) {
|
|
|
159
159
|
// environment would write into another member's rooms.
|
|
160
160
|
...override,
|
|
161
161
|
};
|
|
162
|
+
// A daemon never needs the admin credential. Inherited from an exported shell variable
|
|
163
|
+
// it would sit in the shipper's and dashboard's environment for their whole lifetime.
|
|
164
|
+
delete env.MEMHOUSE_ADMIN_USER;
|
|
165
|
+
delete env.MEMHOUSE_ADMIN_PASSWORD;
|
|
166
|
+
return env;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
function writeEnvFile(cfg) {
|
|
@@ -349,6 +354,34 @@ function netReason(e) {
|
|
|
349
354
|
// Read without echoing. `onboard` and `setup` both prompt for the house password and
|
|
350
355
|
// printed it back on the terminal — in a wizard whose whole audience is someone typing a
|
|
351
356
|
// credential in front of whoever is in the room.
|
|
357
|
+
// The admin password for `adminUser`: --admin-password-file (or - for stdin), then
|
|
358
|
+
// --admin-password (warned), then MEMHOUSE_ADMIN_PASSWORD, then the stored credential
|
|
359
|
+
// of that SAME admin, then a TTY prompt. null = none, and the caller has been told how
|
|
360
|
+
// to give one without putting it on the command line. See memhouse/admin-secret.js.
|
|
361
|
+
async function adminPasswordFor(adminUser) {
|
|
362
|
+
const as = require(path.join(REPO_ROOT, 'memhouse', 'admin-secret'));
|
|
363
|
+
const file = readEnvFile();
|
|
364
|
+
let r;
|
|
365
|
+
try {
|
|
366
|
+
r = as.resolveAdminPassword({
|
|
367
|
+
adminUser, flags, stored: { user: file.MEMHOUSE_ADMIN_USER, password: file.MEMHOUSE_ADMIN_PASSWORD },
|
|
368
|
+
});
|
|
369
|
+
} catch (e) { console.log(bad(e.message)); return null; }
|
|
370
|
+
// stderr: `members --json` and friends must emit nothing but JSON on stdout.
|
|
371
|
+
if (r.source === 'flag') console.error(warn(as.ARGV_WARNING));
|
|
372
|
+
if (r.password !== undefined) return r.password;
|
|
373
|
+
if (process.stdin.isTTY) {
|
|
374
|
+
const p = await askSecret(` password for '${adminUser}'`);
|
|
375
|
+
if (p) return p;
|
|
376
|
+
console.log(bad('no password given'));
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
const lines = as.missingAdminPasswordHelp(adminUser);
|
|
380
|
+
console.log(bad(lines[0]));
|
|
381
|
+
for (const l of lines.slice(1)) console.log(l);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
352
385
|
function askSecret(label, dflt = '') {
|
|
353
386
|
if (!process.stdin.isTTY) return ask(label, dflt);
|
|
354
387
|
return new Promise((resolve) => {
|
|
@@ -414,22 +447,23 @@ memhouse ${PKG.version} — agent conversation memory across 17 editors
|
|
|
414
447
|
Setup onboard interactive wizard: discover → configure → ship → start
|
|
415
448
|
install scriptable setup (--url --user --password --db [--yes] [--no-ship])
|
|
416
449
|
--print-sql print the SQL, run it yourself
|
|
417
|
-
with admin: --admin-user
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
file (
|
|
421
|
-
|
|
450
|
+
with admin: --admin-user NAME [--member NAME] builds the
|
|
451
|
+
member + grants, then verifies as the member. The admin
|
|
452
|
+
password comes from MEMHOUSE_ADMIN_PASSWORD (with-secret),
|
|
453
|
+
--admin-password-file FILE|- (stdin), or a prompt; plain
|
|
454
|
+
--admin-password works but shows in ps/history. It is NOT
|
|
455
|
+
saved unless --keep-admin (then invite needs no flags).
|
|
422
456
|
--env FILE installs from an invite file (see: invite)
|
|
423
457
|
invite <name> mint a member and write the env file their install needs
|
|
424
|
-
(--url [--db NAME] [--out FILE]; --admin-user
|
|
425
|
-
|
|
458
|
+
(--url [--db NAME] [--out FILE]; --admin-user — password
|
|
459
|
+
as for install — only if this install keeps no admin
|
|
426
460
|
credential). They get rooms named for them, <db>.<name>_*,
|
|
427
461
|
and one grant on exactly those. Refuses a name whose rooms
|
|
428
462
|
already hold messages; --adopt takes them over (same
|
|
429
463
|
person, new credential). Not an admin? --print-sql prints
|
|
430
464
|
the plan to hand to whoever is.
|
|
431
465
|
passwd rotate this member's password + rewrite the env file
|
|
432
|
-
(admin-assisted: --admin-user
|
|
466
|
+
(admin-assisted: --admin-user, password as for install)
|
|
433
467
|
setup (re)write the connection config only (--yes = no prompts)
|
|
434
468
|
discover read-only preflight: editors, sessions, reachable ClickHouses
|
|
435
469
|
uninstall stop daemons + service, clear runtime state (asks; --yes).
|
|
@@ -752,10 +786,33 @@ function generatePassword() {
|
|
|
752
786
|
* hand produces exactly what `--admin-user` would have produced. It is rendered from the
|
|
753
787
|
* same templates provision.js applies, so the two cannot drift into different houses.
|
|
754
788
|
*/
|
|
755
|
-
function memberSql(db, member
|
|
756
|
-
// The SAME plan adminBootstrap executes, rendered. Not a second description of it
|
|
789
|
+
function memberSql(db, member) {
|
|
790
|
+
// The SAME plan adminBootstrap executes, rendered. Not a second description of it — except
|
|
791
|
+
// the password: a printed plan carries a placeholder, never a real one (G23). Printed SQL
|
|
792
|
+
// lands in terminals, scrollback and agent transcripts, and memhouse ships transcripts.
|
|
757
793
|
const { plan, render } = require(path.join(REPO_ROOT, 'memhouse', 'provision'));
|
|
758
|
-
return render(plan({ db, member,
|
|
794
|
+
return render(plan({ db, member, placeholder: true }), { db, member });
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// The handoff lines that follow a printed plan: an env file for the member, with the
|
|
798
|
+
// password left as the placeholder the plan used. `invite` marks it MEMHOUSE_INVITE=1, so
|
|
799
|
+
// the member's `install --env` offers to rotate it to one only they know.
|
|
800
|
+
function handoffLines({ url, db, member, invite }) {
|
|
801
|
+
const { PASSWORD_PLACEHOLDER } = require(path.join(REPO_ROOT, 'memhouse', 'provision'));
|
|
802
|
+
const lines = [
|
|
803
|
+
`-- Then put these lines in a file (mode 600), with the same password where ${PASSWORD_PLACEHOLDER}`,
|
|
804
|
+
'-- stands, and hand it over a channel you trust (croc, a password manager) — not chat:',
|
|
805
|
+
`-- MEMHOUSE_URL='${url}'`,
|
|
806
|
+
`-- MEMHOUSE_USER='${member}'`,
|
|
807
|
+
`-- MEMHOUSE_PASSWORD='${PASSWORD_PLACEHOLDER}'`,
|
|
808
|
+
`-- MEMHOUSE_DB='${db}'`,
|
|
809
|
+
];
|
|
810
|
+
if (invite) lines.push('-- MEMHOUSE_INVITE=1');
|
|
811
|
+
lines.push('-- They install with: memhouse install --env <that file> (it deletes the file afterwards)');
|
|
812
|
+
if (flags['member-password'] !== undefined) {
|
|
813
|
+
lines.push('-- The --member-password you gave is not printed: use it where the placeholder stands.');
|
|
814
|
+
}
|
|
815
|
+
return lines.join('\n');
|
|
759
816
|
}
|
|
760
817
|
|
|
761
818
|
/**
|
|
@@ -813,17 +870,13 @@ async function adminBootstrap(cfg, admin) {
|
|
|
813
870
|
} else {
|
|
814
871
|
if (!password) {
|
|
815
872
|
password = generatePassword();
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
console.log(` Shown once. It goes into ${ENV_FILE}; to change it later:`);
|
|
824
|
-
console.log(` ALTER USER ${admin.member} IDENTIFIED BY '…' then: memhouse setup --password …`);
|
|
825
|
-
console.log('');
|
|
826
|
-
}
|
|
873
|
+
// Never printed, on any path (G23). It used to be shown once on an install, "the
|
|
874
|
+
// user's only copy" — but install writes it to the env file, and an invite to the
|
|
875
|
+
// credential file, so the terminal never needed it. What the terminal gets ends up in
|
|
876
|
+
// scrollback and, via /mem:access and /mem:admin, in a transcript memhouse itself
|
|
877
|
+
// ships into the house. A run that fails before the password is written down drops
|
|
878
|
+
// the user it created (below), so no account is left behind with a password nobody
|
|
879
|
+
// holds.
|
|
827
880
|
}
|
|
828
881
|
try { await q(`CREATE USER ${admin.member} IDENTIFIED BY '${password.replace(/'/g, "\\'")}'`, { database: '' }); }
|
|
829
882
|
catch (e) {
|
|
@@ -869,9 +922,15 @@ async function adminBootstrap(cfg, admin) {
|
|
|
869
922
|
} catch (e) {
|
|
870
923
|
console.log(bad(`could not grant the house to '${admin.member}': ${e.message}`));
|
|
871
924
|
if (createdUser) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
925
|
+
// This run made the user and nobody holds its password (it is never printed), so the
|
|
926
|
+
// only useful state is "not there": drop it, and the same command can simply be re-run.
|
|
927
|
+
try {
|
|
928
|
+
await q(`DROP USER IF EXISTS ${createdUser}`, { database: '' });
|
|
929
|
+
console.log(` the ClickHouse user '${createdUser}' this run created was dropped — fix the cause and re-run`);
|
|
930
|
+
} catch (e2) {
|
|
931
|
+
console.log(` ClickHouse user '${createdUser}' WAS created and could not be dropped (${String(e2.message).split('\n')[0]}).`);
|
|
932
|
+
console.log(` Undo it as the admin: DROP USER ${createdUser} — then re-run.`);
|
|
933
|
+
}
|
|
875
934
|
} else {
|
|
876
935
|
console.log(' nothing was written');
|
|
877
936
|
}
|
|
@@ -1029,6 +1088,7 @@ async function cmdInstall({ interactive }) {
|
|
|
1029
1088
|
}
|
|
1030
1089
|
let cfg = resolveConfig();
|
|
1031
1090
|
const adminUser = flags['admin-user'];
|
|
1091
|
+
if (adminUser === true) { console.log(bad('--admin-user needs a name, e.g. --admin-user default')); return 1; }
|
|
1032
1092
|
|
|
1033
1093
|
// The house name is spliced unquoted into CREATE DATABASE and GRANT ALL ON <db>.* —
|
|
1034
1094
|
// validate it HERE, on every install mode, not only where deploy happens to choose it.
|
|
@@ -1046,17 +1106,11 @@ async function cmdInstall({ interactive }) {
|
|
|
1046
1106
|
if (flags['print-sql'] === true) {
|
|
1047
1107
|
const member = resolveMemberHandle();
|
|
1048
1108
|
if (!member) return 1;
|
|
1049
|
-
|
|
1050
|
-
console.log(memberSql(cfg.db, member, password));
|
|
1051
|
-
console.log(`-- Then, once that has run:`);
|
|
1109
|
+
console.log(memberSql(cfg.db, member));
|
|
1052
1110
|
// Only echo a URL that was actually STATED. --print-sql is the path where the house is
|
|
1053
1111
|
// typically someone else's, run by someone else, and cfg.url falls back to
|
|
1054
1112
|
// http://localhost:8123 — the one address the rest of the product refuses to guess.
|
|
1055
|
-
|
|
1056
|
-
// leaving a placeholder.
|
|
1057
|
-
console.log(cfg.stated
|
|
1058
|
-
? `-- memhouse install --url ${cfg.url} --db ${cfg.db} --user ${member} --password '${password}'`
|
|
1059
|
-
: `-- memhouse install --url <the house this SQL was run on> --db ${cfg.db} --user ${member} --password '${password}'`);
|
|
1113
|
+
console.log(handoffLines({ url: cfg.stated ? cfg.url : '<the house this SQL was run on>', db: cfg.db, member, invite: false }));
|
|
1060
1114
|
return 0;
|
|
1061
1115
|
}
|
|
1062
1116
|
|
|
@@ -1064,11 +1118,22 @@ async function cmdInstall({ interactive }) {
|
|
|
1064
1118
|
if (adminUser) {
|
|
1065
1119
|
const member = resolveMemberHandle();
|
|
1066
1120
|
if (!member) return 1;
|
|
1121
|
+
// From a file, stdin, the environment or a prompt — never REQUIRED on argv.
|
|
1122
|
+
const adminPassword = await adminPasswordFor(String(adminUser));
|
|
1123
|
+
if (adminPassword === null) return 1;
|
|
1067
1124
|
const built = await adminBootstrap(cfg, {
|
|
1068
|
-
user: adminUser, password:
|
|
1125
|
+
user: adminUser, password: adminPassword, member,
|
|
1069
1126
|
});
|
|
1070
1127
|
if (!built) return 1;
|
|
1071
|
-
|
|
1128
|
+
// The admin credential is used for this install and NOT saved, unless asked
|
|
1129
|
+
// (--keep-admin) or this env file already keeps that same admin (an operator seat).
|
|
1130
|
+
// It used to be saved every time, which the help said and every agent-facing text
|
|
1131
|
+
// (the install prompt, invite's own advice, /mem:admin) contradicted — and a saved
|
|
1132
|
+
// admin password ends up wherever the env file goes.
|
|
1133
|
+
const keepAdmin = flags['keep-admin'] === true || readEnvFile().MEMHOUSE_ADMIN_USER === String(adminUser);
|
|
1134
|
+
cfg = keepAdmin
|
|
1135
|
+
? { ...built, adminUser: String(adminUser), adminPassword }
|
|
1136
|
+
: { ...built, adminUser: '', adminPassword: '' };
|
|
1072
1137
|
// BEFORE the env file and BEFORE the ship. INSTALL.md's contract is that the config is
|
|
1073
1138
|
// written last, "after everything above has proved out", because a config left by a
|
|
1074
1139
|
// failed install is read as truth by the next command. Placed after the write, a
|
|
@@ -1099,8 +1164,13 @@ async function cmdInstall({ interactive }) {
|
|
|
1099
1164
|
console.log(` every machine you install as '${cfg.user}' writes into the same rooms; this is what tells them apart`);
|
|
1100
1165
|
}
|
|
1101
1166
|
console.log(ok('installed'));
|
|
1102
|
-
|
|
1103
|
-
|
|
1167
|
+
if (cfg.adminUser) {
|
|
1168
|
+
console.log(' adding a housemate later — the admin credential is kept in your env file, so no flags:');
|
|
1169
|
+
console.log(` memhouse invite <name> --url ${cfg.url}`);
|
|
1170
|
+
} else {
|
|
1171
|
+
console.log(` the admin credential was used for this install and not saved. To invite later:`);
|
|
1172
|
+
console.log(` with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- memhouse invite <name> --url ${cfg.url} --admin-user ${adminUser}`);
|
|
1173
|
+
}
|
|
1104
1174
|
printGettingStarted(cfg);
|
|
1105
1175
|
await finishInvite(cfg);
|
|
1106
1176
|
// The first ship loads the whole backlog; do it in the background so install returns
|
|
@@ -1114,7 +1184,9 @@ async function cmdInstall({ interactive }) {
|
|
|
1114
1184
|
// authenticating as memhouse_root instead: the three flags did nothing, --yes satisfied
|
|
1115
1185
|
// haveAll so no prompt asked who you were, and resolveConfig's default was used AS A
|
|
1116
1186
|
// CREDENTIAL. Refuse rather than do something else silently.
|
|
1117
|
-
|
|
1187
|
+
// Every flag that only means something to the admin bootstrap. Left out, it was silently
|
|
1188
|
+
// ignored on a member install (`--keep-admin` was — Codex P2 on #14).
|
|
1189
|
+
const adminOnly = ['member', 'member-password', 'keep-admin', 'admin-password-file', 'admin-password'].filter((k) => flags[k] !== undefined);
|
|
1118
1190
|
if (!adminUser && adminOnly.length) {
|
|
1119
1191
|
console.log(bad(`--${adminOnly.join(', --')} ${adminOnly.length > 1 ? 'are' : 'is'} only read with --admin-user`));
|
|
1120
1192
|
console.log(' Those flags provision a member, which needs house admin. Without them this');
|
|
@@ -3168,9 +3240,16 @@ async function cmdShare() {
|
|
|
3168
3240
|
// today (the member has no grant option on the database) but with a raw ACCESS_DENIED
|
|
3169
3241
|
// naming a privilege nobody can explain. Grant the rooms this member actually owns.
|
|
3170
3242
|
const mine = roomNames(cfg.user);
|
|
3171
|
-
//
|
|
3172
|
-
// (SELECT only). It reaches nothing a housemate wrote
|
|
3173
|
-
|
|
3243
|
+
// A FULL share is one grant on the member's own pattern: the shape the member holds, one
|
|
3244
|
+
// level down (SELECT only). It reaches nothing a housemate wrote — and, by design,
|
|
3245
|
+
// everything of this member's, including the stat tables and any room added later.
|
|
3246
|
+
const wildcard = `${db}.${mine.pattern}`;
|
|
3247
|
+
// A SCOPED share is granted table by table: exactly the rooms its row policies filter,
|
|
3248
|
+
// and nothing else. It used to take the wildcard too, and the policies covered only
|
|
3249
|
+
// these three, so a grantee scoped to one project read every other project's opening
|
|
3250
|
+
// prompt, folder and token totals straight out of <member>_session_stats, plus meta,
|
|
3251
|
+
// events and whatever table the pattern would match next.
|
|
3252
|
+
const scopedTargets = ROOM_TYPES.map((room) => `${db}.${mine.physical[room]}`);
|
|
3174
3253
|
|
|
3175
3254
|
// Statements, not reads: this is the one command in the read family that writes, so it
|
|
3176
3255
|
// does NOT go through the readonly connection the skills use.
|
|
@@ -3196,13 +3275,19 @@ async function cmdShare() {
|
|
|
3196
3275
|
// re-share silently inherits the old scope. Measured — a re-granted user saw 2,121 rows
|
|
3197
3276
|
// instead of the whole house, with nothing on any surface explaining the filter.
|
|
3198
3277
|
if (flags.revoke === true) {
|
|
3278
|
+
// Withdraw the grant FIRST, then the filters. The other order failed open: a REVOKE
|
|
3279
|
+
// refused after the policies were dropped left the grantee with an unfiltered share.
|
|
3280
|
+
try { for (const tgt of [wildcard, ...scopedTargets]) await run(`REVOKE SELECT ON ${tgt} FROM ${user}`); }
|
|
3281
|
+
catch (e) {
|
|
3282
|
+
console.log(bad(`could not revoke: ${netReason(e)}`));
|
|
3283
|
+
console.log(` Nothing else was changed: any row policies for '${user}' are still in place.`);
|
|
3284
|
+
return 1;
|
|
3285
|
+
}
|
|
3199
3286
|
let dropped = 0;
|
|
3200
3287
|
for (const room of ROOM_TYPES) {
|
|
3201
3288
|
try { await run(`DROP ROW POLICY IF EXISTS ${share.policyName(user, room)} ON ${db}.${mine.physical[room]}`); dropped++; }
|
|
3202
3289
|
catch (e) { console.log(warn(`could not drop the policy on ${room}: ${e.message.split('\n')[0]}`)); }
|
|
3203
3290
|
}
|
|
3204
|
-
try { for (const tgt of shareTargets) await run(`REVOKE SELECT ON ${tgt} FROM ${user}`); }
|
|
3205
|
-
catch (e) { console.log(bad(`could not revoke: ${netReason(e)}`)); return 1; }
|
|
3206
3291
|
await houseMeta(cfg, `share:${user}`, `revoked ${new Date().toISOString().slice(0, 10)}`);
|
|
3207
3292
|
console.log(ok(`revoked '${user}' — SELECT withdrawn and ${dropped} row polic${dropped === 1 ? 'y' : 'ies'} dropped`));
|
|
3208
3293
|
console.log(' Dropping the grant alone would have left the policies, and a later re-share');
|
|
@@ -3211,9 +3296,12 @@ async function cmdShare() {
|
|
|
3211
3296
|
}
|
|
3212
3297
|
|
|
3213
3298
|
// ── grant ─────────────────────────────────────────────────────────────────────
|
|
3214
|
-
const scopeRaw = flags.only !== undefined && flags.only !== true ? String(flags.only) : null;
|
|
3299
|
+
const scopeRaw = flags.only !== undefined && flags.only !== true ? String(flags.only).trim() : null;
|
|
3215
3300
|
let scope = null;
|
|
3216
|
-
|
|
3301
|
+
// An EMPTY scope is a mistake, not a request for everything. `--only "$SCOPE"` with an
|
|
3302
|
+
// unset variable used to fall through to a full share — the widest grant this command
|
|
3303
|
+
// makes, from the flag whose whole purpose is to narrow it.
|
|
3304
|
+
if (flags.only === true || scopeRaw === '') { console.log(bad('--only needs a scope, e.g. --only project=memhouse or --only session=<id>')); return 1; }
|
|
3217
3305
|
if (scopeRaw) {
|
|
3218
3306
|
try { scope = share.parseScope(scopeRaw); } catch (e) { console.log(bad(e.message)); return 1; }
|
|
3219
3307
|
}
|
|
@@ -3251,24 +3339,47 @@ async function cmdShare() {
|
|
|
3251
3339
|
made.push(room);
|
|
3252
3340
|
} catch (e) {
|
|
3253
3341
|
console.log(bad(`could not scope ${room}: ${e.message.split('\n')[0]}`));
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
console.log(
|
|
3342
|
+
// Do NOT drop what this attempt wrote. `OR REPLACE` may have just overwritten the
|
|
3343
|
+
// filter of an EXISTING scoped share, and dropping it would leave that grantee's
|
|
3344
|
+
// table grant unfiltered — failing open. A filter without a grant is inert, and the
|
|
3345
|
+
// full-share and --revoke paths both clear leftovers.
|
|
3346
|
+
console.log(` NOTHING WAS GRANTED — '${user}' can read no more than before.`);
|
|
3347
|
+
if (made.length) console.log(` The filters written for ${made.join(', ')} are left in place; \`memhouse share ${user} --revoke\` clears them.`);
|
|
3259
3348
|
return 1;
|
|
3260
3349
|
}
|
|
3261
3350
|
}
|
|
3262
3351
|
}
|
|
3263
3352
|
|
|
3264
|
-
|
|
3265
|
-
|
|
3353
|
+
// Full: the wildcard. Scoped: first take back any wildcard an earlier share left — it
|
|
3354
|
+
// would reach straight past the filters — then grant the filtered rooms one by one.
|
|
3355
|
+
// A failure part-way leaves the grantee with LESS than before, never more.
|
|
3356
|
+
try {
|
|
3357
|
+
if (scope) {
|
|
3358
|
+
await run(`REVOKE SELECT ON ${wildcard} FROM ${user}`);
|
|
3359
|
+
for (const tgt of scopedTargets) await run(`GRANT SELECT ON ${tgt} TO ${user}`);
|
|
3360
|
+
} else {
|
|
3361
|
+
await run(`GRANT SELECT ON ${wildcard} TO ${user}`);
|
|
3362
|
+
}
|
|
3363
|
+
} catch (e) {
|
|
3266
3364
|
console.log(bad(`could not grant: ${netReason(e)}`));
|
|
3267
|
-
// Filters without a grant are inert, but leaving them behind would make a later full
|
|
3268
|
-
// share silently scoped — the case the widening path below exists to catch.
|
|
3269
3365
|
if (scope) {
|
|
3270
|
-
|
|
3271
|
-
|
|
3366
|
+
// Withdraw the grant, and drop the filters ONLY once that is confirmed. If any REVOKE
|
|
3367
|
+
// is refused — the owner lost the grant option, a transient error — the grantee may
|
|
3368
|
+
// still hold a table-level SELECT from an earlier scoped share, and dropping its
|
|
3369
|
+
// filter would expose every row. Leaving a filter behind costs nothing: without a
|
|
3370
|
+
// grant it is inert, and the full-share and --revoke paths both clear it.
|
|
3371
|
+
let withdrawn = true;
|
|
3372
|
+
for (const tgt of [wildcard, ...scopedTargets]) {
|
|
3373
|
+
try { await run(`REVOKE SELECT ON ${tgt} FROM ${user}`); } catch { withdrawn = false; }
|
|
3374
|
+
}
|
|
3375
|
+
if (withdrawn) {
|
|
3376
|
+
for (const room of ROOM_TYPES) {
|
|
3377
|
+
try { await run(`DROP ROW POLICY IF EXISTS ${share.policyName(user, room)} ON ${db}.${mine.physical[room]}`); } catch { /* inert without a grant */ }
|
|
3378
|
+
}
|
|
3379
|
+
console.log(` '${user}' was left with no access to your rooms — nothing wider than before.`);
|
|
3380
|
+
} else {
|
|
3381
|
+
console.log(` Could not confirm '${user}' lost every grant, so the filters stay in place — whatever`);
|
|
3382
|
+
console.log(` '${user}' can still reach is filtered. Retry, or: memhouse share ${user} --revoke`);
|
|
3272
3383
|
}
|
|
3273
3384
|
}
|
|
3274
3385
|
return 1;
|
|
@@ -3306,6 +3417,8 @@ async function cmdShare() {
|
|
|
3306
3417
|
console.log(` ${room.padEnd(11)} ${r[0].n} of ${all[0].n} rows`);
|
|
3307
3418
|
} catch { /* counting is a courtesy */ }
|
|
3308
3419
|
}
|
|
3420
|
+
console.log(' Not shared under a scope: the stat tables, meta and events — they carry no');
|
|
3421
|
+
console.log(' per-session filter, so a scoped reader gets none of them.');
|
|
3309
3422
|
console.log(` Widen to everything: memhouse share ${user}`);
|
|
3310
3423
|
console.log(` Withdraw: memhouse share ${user} --revoke`);
|
|
3311
3424
|
return 0;
|
|
@@ -3321,7 +3434,10 @@ async function cmdShare() {
|
|
|
3321
3434
|
* true = permissive, false = restrictive, null = could not tell.
|
|
3322
3435
|
*/
|
|
3323
3436
|
async function probePermissive(cfg, db) {
|
|
3324
|
-
|
|
3437
|
+
// Inside the member's own pattern: the one place the one-layout grant lets them create a
|
|
3438
|
+
// table and put a policy on it. A bare `_mh_probe_<pid>` sat outside it, was refused on
|
|
3439
|
+
// every server, and turned this check into a permanent "could not verify".
|
|
3440
|
+
const t = `${cfg.user}__mh_probe_${process.pid}`;
|
|
3325
3441
|
const run = (sql) => ch(cfg, sql, { database: db });
|
|
3326
3442
|
try {
|
|
3327
3443
|
await run(`CREATE TABLE IF NOT EXISTS ${t} (x UInt8) ENGINE = MergeTree ORDER BY x`);
|
|
@@ -3576,19 +3692,11 @@ async function cmdInvite() {
|
|
|
3576
3692
|
// statements adminBootstrap would run, so the person with the credential runs four
|
|
3577
3693
|
// lines instead of installing anything. Nothing is contacted and nothing is written.
|
|
3578
3694
|
if (flags['print-sql'] === true) {
|
|
3579
|
-
const password = flags['member-password'] && flags['member-password'] !== true
|
|
3580
|
-
? String(flags['member-password']) : generatePassword();
|
|
3581
3695
|
// memberSql prints its own header naming the house and the credential it needs; a
|
|
3582
3696
|
// second one above it said the same thing twice, and in a shared house said the wrong
|
|
3583
3697
|
// thing ("their own house" — they get rooms in a shared one).
|
|
3584
|
-
console.log(memberSql(db, name
|
|
3585
|
-
console.log(
|
|
3586
|
-
console.log('-- you trust (croc, a password manager), not chat:');
|
|
3587
|
-
console.log(`-- MEMHOUSE_URL='${url}'`);
|
|
3588
|
-
console.log(`-- MEMHOUSE_USER='${name}'`);
|
|
3589
|
-
console.log(`-- MEMHOUSE_PASSWORD='${password}'`);
|
|
3590
|
-
console.log(`-- MEMHOUSE_DB='${db}'`);
|
|
3591
|
-
console.log(`-- They install with: memhouse install --url ${url} --user ${name} --password '…' --db ${db}`);
|
|
3698
|
+
console.log(memberSql(db, name));
|
|
3699
|
+
console.log(handoffLines({ url, db, member: name, invite: true }));
|
|
3592
3700
|
return 0;
|
|
3593
3701
|
}
|
|
3594
3702
|
|
|
@@ -3599,8 +3707,16 @@ async function cmdInvite() {
|
|
|
3599
3707
|
// --admin-* wins. Otherwise the admin credential this install keeps beside its member
|
|
3600
3708
|
// credential (a house you deployed or administer). Failing both, the member credential
|
|
3601
3709
|
// itself is tried below — which works for whoever set the house up as its superuser.
|
|
3602
|
-
|
|
3603
|
-
let
|
|
3710
|
+
if (flags['admin-user'] === true) { console.log(bad('--admin-user needs a name, e.g. --admin-user default')); return 1; }
|
|
3711
|
+
let adminUser = (flags['admin-user'] ? String(flags['admin-user']) : '') || cfg.adminUser || undefined;
|
|
3712
|
+
let adminPass;
|
|
3713
|
+
if (adminUser) {
|
|
3714
|
+
// --admin-password-file / --admin-password / MEMHOUSE_ADMIN_PASSWORD / the stored one
|
|
3715
|
+
// for this same admin / a prompt. `--admin-user` as a flag used to switch the
|
|
3716
|
+
// environment off, so the only scriptable path put the password on argv.
|
|
3717
|
+
adminPass = await adminPasswordFor(adminUser);
|
|
3718
|
+
if (adminPass === null) return 1;
|
|
3719
|
+
}
|
|
3604
3720
|
if (!adminUser) {
|
|
3605
3721
|
const c = requireConfig(cfg, 'invite');
|
|
3606
3722
|
if (!c.user) return 1;
|
|
@@ -3643,28 +3759,16 @@ async function cmdInvite() {
|
|
|
3643
3759
|
console.log(' keeps one housemate out of another house.');
|
|
3644
3760
|
console.log('');
|
|
3645
3761
|
console.log(' If you RUN this ClickHouse — use the admin credential you created it with');
|
|
3646
|
-
console.log(' (for a stock server that is `default`); memhouse
|
|
3647
|
-
console.log(' per invite
|
|
3648
|
-
console.log(` memhouse invite ${name} --url ${url} --admin-user default`);
|
|
3649
|
-
console.log(
|
|
3650
|
-
console.log(' your shell history and the process list)');
|
|
3762
|
+
console.log(' (for a stock server that is `default`); memhouse does not store it, so give it');
|
|
3763
|
+
console.log(' per invite — used for one connection and discarded, never on the command line:');
|
|
3764
|
+
console.log(` with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- memhouse invite ${name} --url ${url} --admin-user default`);
|
|
3765
|
+
console.log(` memhouse invite ${name} --url ${url} --admin-user default (prompts on a terminal)`);
|
|
3651
3766
|
console.log('');
|
|
3652
3767
|
console.log(' If SOMEONE ELSE runs it — you cannot invite, and no flag changes that.');
|
|
3653
3768
|
console.log(' Print the statements and send them to whoever administers the server:');
|
|
3654
3769
|
console.log(` memhouse invite ${name} --url ${url} --print-sql`);
|
|
3655
3770
|
return 1;
|
|
3656
3771
|
}
|
|
3657
|
-
} else if (adminPass === undefined) {
|
|
3658
|
-
// Prompt rather than refuse. A password given as --admin-password lands in the
|
|
3659
|
-
// process list for the life of the request and in shell history unless the caller
|
|
3660
|
-
// remembered a leading space; asking for it keeps it in this process only.
|
|
3661
|
-
// Non-TTY (an agent, CI) still gets the old refusal — there is nobody to ask.
|
|
3662
|
-
if (!process.stdin.isTTY) {
|
|
3663
|
-
console.log(bad('--admin-user given without --admin-password (no TTY to prompt on)'));
|
|
3664
|
-
return 1;
|
|
3665
|
-
}
|
|
3666
|
-
adminPass = await askSecret(` password for '${adminUser}'`);
|
|
3667
|
-
if (!adminPass) { console.log(bad('no password given')); return 1; }
|
|
3668
3772
|
}
|
|
3669
3773
|
|
|
3670
3774
|
// WHERE to connect to provision. --url is the address the INVITEE will use — it may be
|
|
@@ -3844,12 +3948,15 @@ async function cmdPasswd({ quiet = false, forNext = null } = {}) {
|
|
|
3844
3948
|
}
|
|
3845
3949
|
}
|
|
3846
3950
|
}
|
|
3847
|
-
if (!adminUser ||
|
|
3951
|
+
if (!adminUser || adminUser === true) {
|
|
3848
3952
|
console.log(bad(`'${cfg.user}' cannot change its own password on this house (pre-0.11 member, or no access management).`));
|
|
3849
|
-
console.log(' Rotate with an admin credential
|
|
3953
|
+
console.log(' Rotate with an admin credential (password from MEMHOUSE_ADMIN_PASSWORD, --admin-password-file, or a prompt):');
|
|
3954
|
+
console.log(' memhouse passwd --admin-user <a>');
|
|
3850
3955
|
return 1;
|
|
3851
3956
|
}
|
|
3852
|
-
const
|
|
3957
|
+
const adminPassword = await adminPasswordFor(String(adminUser));
|
|
3958
|
+
if (adminPassword === null) return 1;
|
|
3959
|
+
const via = { ...cfg, user: adminUser, password: adminPassword };
|
|
3853
3960
|
try { await ch(via, `ALTER USER \`${cfg.user}\` IDENTIFIED BY '${escPw}'`, { database: '' }); }
|
|
3854
3961
|
catch (e) { console.log(bad(`could not rotate: ${e.message}`)); return 1; }
|
|
3855
3962
|
}
|
|
@@ -3866,7 +3973,8 @@ async function cmdPasswd({ quiet = false, forNext = null } = {}) {
|
|
|
3866
3973
|
console.log(ok(`password rotated for '${cfg.user}' and ${ENV_FILE.replace(os.homedir(), '~')} updated`));
|
|
3867
3974
|
console.log(' restart anything that inlines the credential here: memhouse update --no-install');
|
|
3868
3975
|
console.log(warn('this rotated the ONE server credential — every OTHER machine you ship as'));
|
|
3869
|
-
console.log(` '${cfg.user}' now fails auth until
|
|
3976
|
+
console.log(` '${cfg.user}' now fails auth until its env file gets the new password: copy the`);
|
|
3977
|
+
console.log(` MEMHOUSE_PASSWORD line from ${ENV_FILE.replace(os.homedir(), '~')} over a channel you trust.`);
|
|
3870
3978
|
return 0;
|
|
3871
3979
|
}
|
|
3872
3980
|
|
|
@@ -4100,7 +4208,8 @@ async function cmdUninstall() {
|
|
|
4100
4208
|
const convert = require(path.join(REPO_ROOT, 'memhouse', 'convert'));
|
|
4101
4209
|
const db = flags.db && flags.db !== true ? String(flags.db) : 'mem';
|
|
4102
4210
|
const au = flags['admin-user'] && flags['admin-user'] !== true ? String(flags['admin-user']) : (cfg.adminUser || null);
|
|
4103
|
-
const ap =
|
|
4211
|
+
const ap = au ? await adminPasswordFor(au) : '';
|
|
4212
|
+
if (ap === null) { process.exitCode = 1; break; }
|
|
4104
4213
|
if (!au) { console.log(bad('convert needs an admin credential: --admin-user/--admin-password (or the one this instance keeps)')); process.exitCode = 1; break; }
|
|
4105
4214
|
const who = { ...cfg, user: au, password: ap };
|
|
4106
4215
|
const only = flags.member && flags.member !== true ? String(flags.member) : null;
|
|
@@ -4188,7 +4297,8 @@ async function cmdUninstall() {
|
|
|
4188
4297
|
// --admin-* wins; otherwise the admin credential this install keeps (a house you
|
|
4189
4298
|
// deployed or administer). A member alone cannot read other accounts' grants.
|
|
4190
4299
|
const au = flags['admin-user'] && flags['admin-user'] !== true ? String(flags['admin-user']) : (cfg.adminUser || null);
|
|
4191
|
-
const ap =
|
|
4300
|
+
const ap = au ? await adminPasswordFor(au) : '';
|
|
4301
|
+
if (ap === null) { process.exitCode = 1; break; }
|
|
4192
4302
|
// Reading ANOTHER account's grants needs privilege; without it ClickHouse simply
|
|
4193
4303
|
// returns fewer rows. Say so rather than presenting a short list as the whole truth.
|
|
4194
4304
|
const who = au ? { ...cfg, user: au, password: ap } : cfg;
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# memhouse documentation
|
|
2
|
+
|
|
3
|
+
Start with a tutorial if memhouse is new to you. Use a guide when you know what you want
|
|
4
|
+
to do. [`examples/`](../examples/README.md) holds runnable setups.
|
|
5
|
+
|
|
6
|
+
## Tutorials
|
|
7
|
+
|
|
8
|
+
Each one starts from nothing and ends with something working, in about ten minutes.
|
|
9
|
+
|
|
10
|
+
| | |
|
|
11
|
+
|---|---|
|
|
12
|
+
| [1. Your first house](tutorials/01-first-house.md) | one machine: a local ClickHouse, your sessions shipped, the dashboard, search, `/mem:recall` |
|
|
13
|
+
| [2. Join a team's house](tutorials/02-join-a-house.md) | someone invited you: install from the invite file, make the password yours, ship |
|
|
14
|
+
| [3. Share your memory](tutorials/03-share-your-memory.md) | let a teammate read your house, all of it or one project, and take it back |
|
|
15
|
+
|
|
16
|
+
## Guides
|
|
17
|
+
|
|
18
|
+
One common operation each.
|
|
19
|
+
|
|
20
|
+
| Guide | When you want to |
|
|
21
|
+
|---|---|
|
|
22
|
+
| [Install and onboard](guides/install.md) | install against a local, existing, or invited house; fix `EACCES` |
|
|
23
|
+
| [Configuration](guides/configuration.md) | choose which editors and directories ship; run several instances; follow a channel |
|
|
24
|
+
| [Update](guides/update.md) | stay current, and upgrade every machine of a house |
|
|
25
|
+
| [Daemons and services](guides/daemons.md) | keep the shipper and dashboard running, across reboots |
|
|
26
|
+
| [Invite members](guides/invite.md) | bring a teammate into a house you administer |
|
|
27
|
+
| [Share and revoke](guides/share.md) | let someone read your rooms, scoped or whole |
|
|
28
|
+
| [Admin password](guides/admin-password.md) | run an admin command without the password reaching `ps`, history, or a transcript |
|
|
29
|
+
| [Search, resume and skills](guides/search-and-resume.md) | find an old session, reopen it, let an agent answer from history |
|
|
30
|
+
| [How memory is stored](guides/storage.md) | houses, rooms, epochs, machine identity, what is never deleted |
|
|
31
|
+
| [Operate a house](guides/operate.md) | members, migrations, moving a house, backups |
|
|
32
|
+
| [Troubleshooting](guides/troubleshooting.md) | an editor ships nothing, daemons stop, a dashboard looks empty |
|
|
33
|
+
| [Uninstall](guides/uninstall.md) | stop, forget the house, or remove everything |
|
|
34
|
+
|
|
35
|
+
## Reference
|
|
36
|
+
|
|
37
|
+
- `memhouse <command> --help`: every command's own usage. It always prints usage and
|
|
38
|
+
never runs the command.
|
|
39
|
+
- [Release notes](releases/): what changed in each version, and what to do about it.
|
|
40
|
+
- [`../memhouse/house/HOUSE.md`](../memhouse/house/HOUSE.md): the layout and the schema,
|
|
41
|
+
column by column.
|
|
42
|
+
- [`../memhouse/DESIGN.md`](../memhouse/DESIGN.md): the design, and why it is shaped this
|
|
43
|
+
way.
|
|
44
|
+
- [`../SECURITY.md`](../SECURITY.md): what holds, and what does not.
|
|
45
|
+
- [`docs/design/`](https://github.com/agent-realm/memhouse/tree/main/docs/design) (in the
|
|
46
|
+
repository): designs captured before they are built.
|