mop-agent 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,8 @@ through MOP-FLOW. It stores project memory, performs semantic recall and
5
5
  consolidation, serves grounded chat, and can request approved actions from a
6
6
  linked FLOW node.
7
7
 
8
- > **Release status:** npm package `mop-agent@0.1.0` is prepared but may not have
9
- > been published yet. After publication, the canonical installation command is
8
+ > **Release status:** npm package `mop-agent@0.1.1` contains the root/VPS
9
+ > installer fix. After publishing 0.1.1, the canonical installation command is
10
10
  > exactly `npx mop-agent`.
11
11
 
12
12
  ## Current status
@@ -44,7 +44,7 @@ Prerequisites:
44
44
  - A domain with an `A`/`AAAA` record pointing to the server
45
45
  - Inbound ports 80 and 443 allowed by the firewall/security group
46
46
 
47
- Run as your normal user:
47
+ Run as either your normal sudo user or directly as root on a VPS:
48
48
 
49
49
  ```bash
50
50
  npx mop-agent
@@ -56,9 +56,26 @@ The first run copies the npm-packaged runtime from the temporary npx cache into
56
56
  SQLite database, HTTPS, and systemd service. The menu remains open between
57
57
  steps.
58
58
 
59
- The installer requests `sudo` only when it needs to write under `/opt` or
60
- `/etc`, install OS packages, or control nginx/systemd. Do not run the entire
61
- npm/npx process with `sudo`.
59
+ During `setup`, choose one deployment mode:
60
+
61
+ - `public` enter a public domain and optionally obtain a Let's Encrypt HTTPS
62
+ certificate. Use this for an internet-facing server with public IP/DNS.
63
+ - `local` — use a LAN hostname such as `mop-agent.local`; the installer uses
64
+ HTTP and does not invoke Certbot.
65
+
66
+ For a LAN-only test, map the selected hostname to the server IP in your router
67
+ DNS or client `/etc/hosts`. Let's Encrypt public mode requires a real public
68
+ domain and reachable ports 80/443.
69
+
70
+ When launched by a normal user, the installer requests `sudo` only when it
71
+ needs to write under `/opt` or `/etc`, install OS packages, or control
72
+ nginx/systemd. When launched as root, it creates a locked-down `mop-agent`
73
+ system account and runs the web service under that account—not as root.
74
+
75
+ During the `install` step, MOP-AGENT checks the installed npm version. If a
76
+ newer npm is available it displays the version and Node.js requirement, then
77
+ asks before running the global npm update. Set `MOP_AGENT_SKIP_NPM_UPDATE=1` to
78
+ skip this check.
62
79
 
63
80
  Subsequent operations use the same command:
64
81
 
@@ -83,6 +100,7 @@ so it uses `/opt/mop-agent` rather than `/var/www` for application code.
83
100
  | systemd unit | `/etc/systemd/system/mop-agent.service` | same |
84
101
  | TLS certificates | `/etc/letsencrypt/live/<domain>/` | same |
85
102
  | Service logs | `journalctl -u mop-agent -f` | same |
103
+ | Root-install service account | `mop-agent` | `mop-agent` |
86
104
 
87
105
  `MOP_AGENT_DIR` can override `/opt/mop-agent`. Updates preserve
88
106
  `apps/web/.env` and `data/`; uninstall preserves SQLite brain data unless the
@@ -1,11 +1,13 @@
1
1
  # MOP-AGENT web — copy to .env and fill in.
2
2
  PORT=3000
3
+ MOP_AGENT_DEPLOY_MODE=public # public | local
3
4
 
4
5
  # --- Fasa 2 (auth + db + secrets) ---
5
6
  # BETTER_AUTH_SECRET=
6
7
  # BETTER_AUTH_URL=http://localhost:3000
7
8
  # MOP_AGENT_SECRET= # AES-GCM key for provider keys at rest (32 bytes hex)
8
9
  # MOP_AGENT_DATA_DIR= # defaults to OS data dir if unset
10
+ # MOP_AGENT_MODEL_CACHE= # local MiniLM model cache; installer puts it under data/models
9
11
 
10
12
  # --- providers (Fasa 2/3) ---
11
13
  # ANTHROPIC_API_KEY=
@@ -15,6 +15,9 @@ export function localEmbedder(model = "Xenova/all-MiniLM-L6-v2"): Embedder {
15
15
  _extractor = (async () => {
16
16
  const t = await import("@xenova/transformers");
17
17
  t.env.allowLocalModels = false;
18
+ if (process.env.MOP_AGENT_MODEL_CACHE) {
19
+ t.env.cacheDir = process.env.MOP_AGENT_MODEL_CACHE;
20
+ }
18
21
  return t.pipeline("feature-extraction", model);
19
22
  })();
20
23
  }
@@ -12,6 +12,7 @@ import {
12
12
  constants,
13
13
  cpSync,
14
14
  existsSync,
15
+ mkdirSync,
15
16
  readFileSync,
16
17
  writeFileSync,
17
18
  } from "node:fs";
@@ -53,9 +54,10 @@ Usage:
53
54
  Environment:
54
55
  MOP_AGENT_DIR Durable app directory (default: ${DEFAULT_DIR})
55
56
 
56
- Run this command as your normal user. MOP-AGENT asks for sudo only when an OS
57
- operation requires it. Native Windows/macOS production installation is not yet
58
- supported; use WSL2 Ubuntu on Windows or a Linux host.
57
+ Run this command as either a normal sudo user or root. When launched by a normal
58
+ user, MOP-AGENT asks for sudo only when an OS operation requires it. The web
59
+ service itself never runs as root. Native Windows/macOS production installation
60
+ is not yet supported; use WSL2 Ubuntu on Windows or a Linux host.
59
61
  `);
60
62
  }
61
63
 
@@ -86,7 +88,10 @@ function writable(path) {
86
88
 
87
89
  function ensureDestination() {
88
90
  if (existsSync(APP_DIR) && writable(APP_DIR)) return;
89
- if (isRoot) fail("Do not run `npx mop-agent` with sudo/root. Run it as your normal user.");
91
+ if (isRoot) {
92
+ mkdirSync(APP_DIR, { recursive: true });
93
+ return;
94
+ }
90
95
  const uid = String(process.getuid());
91
96
  const gid = String(process.getgid());
92
97
  console.log(`\n▸ Preparing ${APP_DIR} (sudo is required once for this system directory)`);
@@ -151,7 +156,6 @@ if (argv.includes("--help") || argv.includes("-h")) {
151
156
  } else {
152
157
  assertPlatform();
153
158
  assertSafeDestination();
154
- if (isRoot) fail("Do not run `npx mop-agent` with sudo/root. Run it as your normal user.");
155
159
  ensureDestination();
156
160
  deployPackage();
157
161
  run(process.execPath, [resolve(APP_DIR, "installer/mop-agent.mjs"), ...argv], {
@@ -105,10 +105,11 @@ function q(value) {
105
105
 
106
106
  // ---- commands ----------------------------------------------------------
107
107
 
108
- function cmdInstall() {
108
+ async function cmdInstall() {
109
109
  banner();
110
110
  const os = detectOS();
111
111
  if (!printInstallLocations(os)) return;
112
+ await maybeUpdateNpm();
112
113
  console.log(c("bold", "Installing system dependencies (nginx and Certbot)…\n"));
113
114
  runSteps(planInstallDeps(os), { privileged: true });
114
115
  console.log(c("green", "\n✓ dependencies step complete. Next: mop-agent setup\n"));
@@ -121,10 +122,22 @@ async function cmdSetup() {
121
122
  const rl = createInterface({ input, output });
122
123
  const ask = async (q, def) => (await rl.question(c("cyan", ` ${q}${def ? c("gray", ` [${def}]`) : ""}: `))).trim() || def || "";
123
124
 
124
- const domain = await ask("Domain (e.g. agent.mydomain.com)");
125
+ const deployMode = (await ask("Deployment mode (public/local)", "public")).toLowerCase();
126
+ if (!new Set(["public", "local"]).has(deployMode)) {
127
+ rl.close();
128
+ throw new Error(`Invalid deployment mode: ${deployMode}. Use public or local.`);
129
+ }
130
+ const domain = await ask(
131
+ deployMode === "public" ? "Public domain (e.g. agent.mydomain.com)" : "LAN hostname",
132
+ deployMode === "local" ? "mop-agent.local" : "",
133
+ );
125
134
  const port = await ask("App port", "3000");
126
- const email = await ask("Email for Let's Encrypt", domain ? `admin@${domain.split(".").slice(-2).join(".")}` : "");
127
- const wantSsl = (await ask("Obtain HTTPS cert now? (y/n)", "y")).toLowerCase().startsWith("y");
135
+ const wantSsl = deployMode === "public"
136
+ ? (await ask("Obtain HTTPS cert now? (y/n)", "y")).toLowerCase().startsWith("y")
137
+ : false;
138
+ const email = wantSsl
139
+ ? await ask("Email for Let's Encrypt", domain ? `admin@${domain.split(".").slice(-2).join(".")}` : "")
140
+ : "";
128
141
  rl.close();
129
142
 
130
143
  if (!isValidDomain(domain)) throw new Error(`Invalid domain: ${domain || "(empty)"}`);
@@ -133,12 +146,15 @@ async function cmdSetup() {
133
146
 
134
147
  // 1) .env
135
148
  const secret = (n) => randomToken(n);
149
+ const protocol = wantSsl ? "https" : "http";
136
150
  const env = [
137
151
  `PORT=${port}`,
138
- `BETTER_AUTH_URL=https://${domain}`,
152
+ `MOP_AGENT_DEPLOY_MODE=${deployMode}`,
153
+ `BETTER_AUTH_URL=${protocol}://${domain}`,
139
154
  `BETTER_AUTH_SECRET=${secret(48)}`,
140
155
  `MOP_AGENT_SECRET=${secret(64).replace(/[^0-9a-f]/g, "").padEnd(64, "0").slice(0, 64)}`,
141
156
  `MOP_AGENT_DATA_DIR=${APP_DIR}/data`,
157
+ `MOP_AGENT_MODEL_CACHE=${APP_DIR}/data/models`,
142
158
  `MOP_AGENT_CONSOLIDATE_CRON=0 3 * * *`,
143
159
  ].join("\n") + "\n";
144
160
  console.log(c("cyan", "\n▸ Write apps/web/.env"));
@@ -172,8 +188,13 @@ async function cmdSetup() {
172
188
  { label: "Reload nginx", cmd: "systemctl reload nginx" },
173
189
  ], { privileged: true });
174
190
 
175
- // 4) systemd — run the app as the invoking user, never as root by default.
176
- const serviceUser = isRoot ? process.env.SUDO_USER || "root" : process.env.USER || String(process.getuid?.() ?? "root");
191
+ // 4) systemd — root installs get a dedicated locked-down service account.
192
+ const serviceUser = isRoot
193
+ ? ensureRootServiceUser(os)
194
+ : process.env.USER || String(process.getuid?.() ?? "root");
195
+ if (isRoot) {
196
+ run(`mkdir -p ${q(`${APP_DIR}/data`)} && chown -R ${serviceUser}:${serviceUser} ${q(`${APP_DIR}/data`)} ${q(`${APP_DIR}/apps/web/.env`)}`);
197
+ }
177
198
  const unit = renderSystemdUnit({ appDir: APP_DIR, port, user: serviceUser });
178
199
  console.log(c("cyan", "▸ systemd service (auto-restart on boot)"));
179
200
  writeConf("/etc/systemd/system/mop-agent.service", unit, { privileged: true });
@@ -200,7 +221,7 @@ async function cmdSetup() {
200
221
  }
201
222
  }
202
223
 
203
- console.log(c("green", `\n✓ Setup complete. Visit ${domain ? `https://${domain}` : `http://localhost:${port}`}/setup to create the owner.\n`));
224
+ console.log(c("green", `\n✓ ${deployMode} setup complete. Visit ${protocol}://${domain}/setup to create the owner.\n`));
204
225
  printInstallLocations(os);
205
226
  }
206
227
 
@@ -282,6 +303,37 @@ function randomToken(n) {
282
303
  return randomBytes(Math.ceil(n / 2)).toString("hex").slice(0, n);
283
304
  }
284
305
 
306
+ async function maybeUpdateNpm() {
307
+ if (args["skip-npm-update"] || process.env.MOP_AGENT_SKIP_NPM_UPDATE === "1") return;
308
+ const current = run("npm --version", { capture: true }).stdout.trim();
309
+ const latestResult = run("npm view npm version", { capture: true, allowFailure: true });
310
+ const latest = latestResult.stdout.trim();
311
+ if (!latest || latest === current) {
312
+ console.log(c("green", `✓ npm ${current || "unknown"} is current\n`));
313
+ return;
314
+ }
315
+ const engineResult = run("npm view npm@latest engines.node", { capture: true, allowFailure: true });
316
+ const engine = engineResult.stdout.trim();
317
+ const rl = createInterface({ input, output });
318
+ const answer = (await rl.question(c("cyan", ` Update npm ${current} → ${latest}${engine ? ` (Node ${engine})` : ""}? [Y/n]: `))).trim().toLowerCase();
319
+ rl.close();
320
+ if (answer && !answer.startsWith("y")) {
321
+ console.log(c("gray", " npm update skipped.\n"));
322
+ return;
323
+ }
324
+ run("npm install -g npm@latest", { privileged: true });
325
+ console.log(c("green", `✓ npm updated to ${latest}\n`));
326
+ }
327
+
328
+ function ensureRootServiceUser(os) {
329
+ const user = "mop-agent";
330
+ const create = os.family === "alpine"
331
+ ? `id -u ${user} >/dev/null 2>&1 || adduser -S -H -h ${q(APP_DIR)} -s /sbin/nologin ${user}`
332
+ : `id -u ${user} >/dev/null 2>&1 || useradd --system --home-dir ${q(APP_DIR)} --shell /usr/sbin/nologin ${user}`;
333
+ run(create);
334
+ return user;
335
+ }
336
+
285
337
  async function tui() {
286
338
  banner();
287
339
  if (!supportedLinux()) return;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "mop-agent",
9
- "version": "0.1.0",
9
+ "version": "0.1.1",
10
10
  "license": "UNLICENSED",
11
11
  "workspaces": [
12
12
  "packages/*",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Self-hosted AI brain and control plane for MOP-FLOW projects, installed with npx mop-agent.",
5
5
  "author": "BURHANDEV ENTERPRISE",
6
6
  "license": "UNLICENSED",