@topy-ai/maggie 0.7.58 → 0.7.60

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-zh-TW.md CHANGED
@@ -143,7 +143,7 @@ structure drift 或重複 treatment product 時 fail closed。
143
143
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
144
144
  publish 與 production deployment 需要明確確認。
145
145
 
146
- 目前 npm 版本為 `0.7.58`,包含 fix-plan、public-check、Maggie Security、batch 31 排程 agent routines、報告收件匣與無人值守 agent 限制、PHP runtime 盤點與主機鎖、pull 網站改寫、query 點子與社群佇列檢查,batch 30 MaggieDash 先裝核心、release privacy gate、feedback 備援、社群連結與圖片檢查、大量頁面產生與 app 檢查,batch 29 預約規則、部署、app QA、助理與社群功能,以及 batch 28 產品 app、assistant tools、mobile、product release、skills catalog 與 bridge host 功能、batch 26、27 feedback 修正與 idempotent `maggie update`與 Booking email-template 合約/CLI validator 同步。本版完成 2026-09-24 booking feedback batch:Calendar grid/free-slot entry 與 host modal focus-restoration evidence。本版新增 Starter marketplace provisioning plan;本版完成 feedback batches 22、23:加入 declarative browser interaction evidence、design markup/style/content-key gate、compiler source anchoring、entity double-escape protection、`update --help` read-only、noindex hypothetical indexability、可過期 baseline acknowledgement、DOM-only link extraction regression,以及 MaggieDash 0.2.10 的 content override、短期 browser session、CDN-safe public-session cache contracts。authentication 和 isolated dev server 仍由 host 負責。`maggie-booking` workflow、傳統 email/password、Stripe、Resend、public booking flow 及既有安全邊界維持不變;provider-bound video generation 與 transactional delivery 仍記錄在 issue #39,前一批延後功能仍在 issue #38,不會直接成為 active memory。0.7.33 記錄並審查一筆更正回饋;確認 0.7.32 已正確處理 batch index,沒有修改已驗證的 shared behavior。0.7.32 加入 feedback 一基索引支援、MaggieDash panel 的 source/freshness/error evidence、Consent Mode 與實際 write/no-op reconciliation、SEO baseline recapture、sitemap origin rebasing、content-diff scope、migration ledger reconciliation、deployment credential preflight、release provenance/runtime preflight、icon release gate、opt-in blog auto-publish gate,以及 least-privilege VPS deployer。也修復 top-level `maggie feedback` dispatcher parity。0.7.31 加入 update 差異摘要、atomic installer copy、subset install manifest merge,以及 inventory drift 時 doctor non-zero。0.7.30 補上 feedback tracking issue closeout gate 與 batch 14/15 audit ledger。0.7.29 修正 npm package landing page 使用英文 README;0.7.28 加入 versioned Gemini model policy、明確 fallback 與 provenance、bundled-first CLI
146
+ 目前 npm 版本為 `0.7.60`,包含 fix-plan、public-check、Maggie Security、batch 31 排程 agent routines、報告收件匣與無人值守 agent 限制、PHP runtime 盤點與主機鎖、pull 網站改寫、query 點子與社群佇列檢查,batch 30 MaggieDash 先裝核心、release privacy gate、feedback 備援、社群連結與圖片檢查、大量頁面產生與 app 檢查,batch 29 預約規則、部署、app QA、助理與社群功能,以及 batch 28 產品 app、assistant tools、mobile、product release、skills catalog 與 bridge host 功能、batch 26、27 feedback 修正與 idempotent `maggie update`與 Booking email-template 合約/CLI validator 同步。本版完成 2026-09-24 booking feedback batch:Calendar grid/free-slot entry 與 host modal focus-restoration evidence。本版新增 Starter marketplace provisioning plan;本版完成 feedback batches 22、23:加入 declarative browser interaction evidence、design markup/style/content-key gate、compiler source anchoring、entity double-escape protection、`update --help` read-only、noindex hypothetical indexability、可過期 baseline acknowledgement、DOM-only link extraction regression,以及 MaggieDash 0.2.10 的 content override、短期 browser session、CDN-safe public-session cache contracts。authentication 和 isolated dev server 仍由 host 負責。`maggie-booking` workflow、傳統 email/password、Stripe、Resend、public booking flow 及既有安全邊界維持不變;provider-bound video generation 與 transactional delivery 仍記錄在 issue #39,前一批延後功能仍在 issue #38,不會直接成為 active memory。0.7.33 記錄並審查一筆更正回饋;確認 0.7.32 已正確處理 batch index,沒有修改已驗證的 shared behavior。0.7.32 加入 feedback 一基索引支援、MaggieDash panel 的 source/freshness/error evidence、Consent Mode 與實際 write/no-op reconciliation、SEO baseline recapture、sitemap origin rebasing、content-diff scope、migration ledger reconciliation、deployment credential preflight、release provenance/runtime preflight、icon release gate、opt-in blog auto-publish gate,以及 least-privilege VPS deployer。也修復 top-level `maggie feedback` dispatcher parity。0.7.31 加入 update 差異摘要、atomic installer copy、subset install manifest merge,以及 inventory drift 時 doctor non-zero。0.7.30 補上 feedback tracking issue closeout gate 與 batch 14/15 audit ledger。0.7.29 修正 npm package landing page 使用英文 README;0.7.28 加入 versioned Gemini model policy、明確 fallback 與 provenance、bundled-first CLI
147
147
  dispatch、`maggie --version`、feedback batch review 聚合與重複偵測、marketplace enrichment evidence、booking
148
148
  worker/resolver evidence,以及 read-only migration preflight。它也包含 host-owned mobile app surface contract、signed-in
149
149
  camera-state QA、直接 Astro route resolution、correlated feedback batch,以及
package/README.md CHANGED
@@ -89,7 +89,14 @@ Maggie keeps the existing project foundation and asks for decisions before
89
89
  shared routes, analytics, or publishing boundaries change. The current
90
90
  package ships 23 installable skills and a local-first MaggieDash foundation.
91
91
 
92
- This package is `0.7.58`. It adds `maggie security fix-plan` — a
92
+ This package is `0.7.60`. `maggie security fix-plan` now emits **runnable** nginx fixes: http-context directives (TLS protocols, `server_tokens`) go into a global `conf.d` drop-in, and server-context ones (deny dotfiles, `proxy_hide_header`) into a snippet file applied with a backed-up, `nginx -t`-validated, auto-reverting `include`.
93
+
94
+ The 0.7.59 release made `maggie security host-audit` (the deep
95
+ lynis audit over SSH) robust on hosts where a storage test can stall: lynis runs
96
+ in its own process group under a watchdog, and the bundled profile skips the ZFS
97
+ storage/filesystem tests that hang, so the audit completes instead of timing out.
98
+
99
+ The 0.7.58 release added `maggie security fix-plan` — a
93
100
  reviewed remediation runbook built from the open findings (file permissions,
94
101
  firewall, package/apt updates, intrusion prevention, web-server config, end of
95
102
  life). It writes a shell script you run yourself, under a host lock, after a
@@ -851,8 +858,8 @@ artifact schemas.
851
858
  Recommended upgrade sequence for the current release:
852
859
 
853
860
  ```bash
854
- npx @topy-ai/maggie@0.7.58 update --scope project --project . --force
855
- npx @topy-ai/maggie@0.7.58 cleanup --scope project --project .
861
+ npx @topy-ai/maggie@0.7.60 update --scope project --project . --force
862
+ npx @topy-ai/maggie@0.7.60 cleanup --scope project --project .
856
863
  ```
857
864
 
858
865
  Maintainers should pass npm credentials through the repository helper, never
@@ -266,18 +266,38 @@ def lynis_findings(ctx: core.Context, data: dict, host: str) -> list[dict]:
266
266
 
267
267
 
268
268
  LYNIS_PROFILE = """# Maggie lynis profile: no upload, skip tests that do not apply to web servers
269
+ # and skip the storage/filesystem enumeration that hangs on ZFS hosts
270
+ # (the remote watchdog below is the backstop if anything else stalls).
269
271
  upload=no
270
272
  skip-test=PRNT-2307
271
273
  skip-test=PRNT-2308
272
274
  skip-test=STRG-1840
273
275
  skip-test=STRG-1842
276
+ skip-test=STRG-1846
277
+ skip-test=STRG-1902
274
278
  skip-test=NAME-4402
275
279
  skip-test=NAME-4404
276
280
  skip-test=KRB-5108
277
281
  skip-test=LDAP-2219
278
- skip-test=STRG-1902
282
+ skip-test=FILE-6310
283
+ skip-test=FILE-6311
284
+ skip-test=FILE-6329
285
+ skip-test=FILE-6330
286
+ skip-test=FILE-6332
287
+ skip-test=FILE-6336
288
+ skip-test=FILE-6354
289
+ skip-test=FILE-6362
290
+ skip-test=FILE-6368
291
+ skip-test=FILE-6374
292
+ skip-test=FILE-6376
279
293
  """
280
294
 
295
+ # A ZFS host can wedge a lynis storage test for a very long time, and killing the
296
+ # ssh client leaves the stuck test running and holding the channel. So we run
297
+ # lynis in its own process group under a remote watchdog that SIGKILLs the whole
298
+ # group; the command always returns and flushes, finished or not.
299
+ LYNIS_WATCHDOG_SECONDS = 600
300
+
281
301
 
282
302
  def host_audit(ctx: core.Context):
283
303
  args = ctx.args
@@ -329,15 +349,21 @@ def remote_lynis(ctx: core.Context, alias: str, target: str) -> dict:
329
349
  subprocess.run(["ssh", "-o", "BatchMode=yes", target, f"umask 077 && mkdir -p {remote_dir}"], check=True, timeout=60)
330
350
  subprocess.run(["scp", "-q", "-o", "BatchMode=yes", str(tarball), f"{target}:{remote_dir}/lynis.tar.gz"], check=True, timeout=300)
331
351
  profile = shlex.quote(LYNIS_PROFILE)
332
- # Run lynis with sudo when available (a full audit needs root); read the
333
- # report back with the SAME sudo, because a root run writes report.dat
334
- # root-owned and 0600 — a plain `cat` as the login user reads nothing.
352
+ # Run lynis with sudo when available (a full audit needs root). Run it in
353
+ # its own session (setsid) under a watchdog that SIGKILLs the whole process
354
+ # group if it overruns, so a stuck storage test cannot hang the session or
355
+ # leave an orphan holding the ssh channel. Read report.dat back with the
356
+ # same sudo (a root run writes it 0600, so a plain cat reads nothing).
357
+ inner = (f"setsid ./lynis audit system --quick --no-colors --no-log "
358
+ f"--profile {remote_dir}/maggie.prf --report-file {remote_dir}/report.dat --logfile /dev/null >/dev/null 2>&1 & "
359
+ f"LP=$!; ( sleep {LYNIS_WATCHDOG_SECONDS}; kill -9 -$LP 2>/dev/null ) & WD=$!; "
360
+ f"wait $LP 2>/dev/null; kill $WD 2>/dev/null")
335
361
  script = (f"SUDO=$( [ $(id -u) != 0 ] && sudo -n true 2>/dev/null && echo 'sudo -n' ); "
336
362
  f"cd {remote_dir} && tar xzf lynis.tar.gz && printf %s {profile} > maggie.prf && cd lynis && "
337
- f"$SUDO ./lynis audit system --quick --no-colors --no-log "
338
- f"--profile {remote_dir}/maggie.prf --report-file {remote_dir}/report.dat --logfile /dev/null >/dev/null 2>&1; "
339
- f"$SUDO cat {remote_dir}/report.dat")
340
- proc = subprocess.run(["ssh", "-o", "BatchMode=yes", target, script], capture_output=True, text=True, timeout=1800, check=False)
363
+ f"$SUDO sh -c {shlex.quote(inner)} </dev/null >/dev/null 2>&1; "
364
+ f"$SUDO cat {remote_dir}/report.dat 2>/dev/null")
365
+ proc = subprocess.run(["ssh", "-o", "BatchMode=yes", target, script], input="", capture_output=True, text=True,
366
+ timeout=LYNIS_WATCHDOG_SECONDS + 180, check=False)
341
367
  report = proc.stdout
342
368
  finally:
343
369
  subprocess.run(["ssh", "-o", "BatchMode=yes", target, f"rm -rf {remote_dir}"], timeout=60, check=False)
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
  import argparse
16
16
  import json
17
17
  import re
18
+ import shlex
18
19
  from pathlib import Path
19
20
 
20
21
  import maggie_security_core as core
@@ -557,6 +558,71 @@ def fix_commands(action: dict) -> tuple[list[str], list[tuple[str, str]], list[s
557
558
  return commands, snippets, todos
558
559
 
559
560
 
561
+ # nginx directives valid in the http{} context can be dropped into conf.d; the
562
+ # rest (location/proxy/add_header) must live in a server{} block.
563
+ NGINX_HTTP_DIRECTIVES = {"ssl_protocols", "ssl_ciphers", "ssl_prefer_server_ciphers", "ssl_ecdh_curve",
564
+ "ssl_session_tickets", "ssl_session_timeout", "ssl_dhparam", "server_tokens"}
565
+ NGINX_FILE_RE = re.compile(r"(/etc/nginx/[\w./-]+\.conf)")
566
+
567
+
568
+ def nginx_fix_block(actions: list[dict]) -> tuple[list[str], list[str]]:
569
+ """Runnable nginx fixes: a global drop-in for http-context directives, and a
570
+ snippet file + a guarded, reversible `include` for server-context ones."""
571
+ http_dirs: list[tuple[str, dict]] = []
572
+ server_by_file: dict[str, list[str]] = {}
573
+ manual: list[dict] = []
574
+ for action in actions:
575
+ rem = action["lead"].get("remediation") or {}
576
+ snippets = [s.get("text", "").strip() for s in (rem.get("snippets") or []) if s.get("text")]
577
+ if not snippets:
578
+ manual.append(action)
579
+ continue
580
+ tp = (action["lead"].get("target") or {}).get("path") or ""
581
+ match = NGINX_FILE_RE.search(tp) or NGINX_FILE_RE.search(action["lead"].get("title", ""))
582
+ vfile = match.group(1) if match else None
583
+ for text in snippets:
584
+ name = text.split()[0] if text.split() else ""
585
+ if name in NGINX_HTTP_DIRECTIVES:
586
+ http_dirs.append((text, action))
587
+ elif vfile:
588
+ server_by_file.setdefault(vfile, [])
589
+ if text not in server_by_file[vfile]:
590
+ server_by_file[vfile].append(text)
591
+ else:
592
+ manual.append(action)
593
+ sh: list[str] = []
594
+ md: list[str] = []
595
+ if http_dirs:
596
+ directives = list(dict.fromkeys(d for d, _ in http_dirs))
597
+ fixes = "; ".join(dict.fromkeys(a["title"] for _, a in http_dirs))
598
+ body = "".join(d + "\n" for d in directives)
599
+ sh += [f"# Global TLS/hardening — fixes: {fixes}",
600
+ f"printf %s {shlex.quote(body)} | sudo tee /etc/nginx/conf.d/00-maggie-hardening.conf >/dev/null",
601
+ "sudo nginx -t && sudo systemctl reload nginx", ""]
602
+ md.append("- **nginx global hardening** → drop-in `/etc/nginx/conf.d/00-maggie-hardening.conf`: "
603
+ + ", ".join(f"`{d}`" for d in directives))
604
+ for vfile, directives in server_by_file.items():
605
+ stem = re.sub(r"[^\w.-]", "_", re.sub(r"\.conf$", "", vfile.rsplit("/", 1)[-1]))
606
+ snip = f"/etc/nginx/snippets/maggie-{stem}.conf"
607
+ heredoc = "\n".join(directives)
608
+ sh += [f"# Harden {vfile} (backs up, validates, auto-reverts on error)",
609
+ f"sudo tee {snip} >/dev/null <<'MAGGIE_SNIP'",
610
+ heredoc,
611
+ "MAGGIE_SNIP",
612
+ f"sudo cp -a {shlex.quote(vfile)} {shlex.quote(vfile + '.maggie.bak')}",
613
+ f"sudo sed -i -E '/^[[:space:]]*server[[:space:]]*\\{{[[:space:]]*$/a\\ include {snip};' {shlex.quote(vfile)}",
614
+ f"sudo nginx -t && sudo systemctl reload nginx || {{ echo 'nginx test failed — reverting'; "
615
+ f"sudo cp -a {shlex.quote(vfile + '.maggie.bak')} {shlex.quote(vfile)}; sudo systemctl reload nginx; }}", ""]
616
+ md.append(f"- **nginx vhost hardening** (`{vfile}` via `{snip}` + guarded include): "
617
+ + ", ".join(f"`{d}`" for d in directives))
618
+ for action in manual:
619
+ rem = action["lead"].get("remediation") or {}
620
+ sh += [f"# [{action['priority']}] {action['title']} — {action['why']}",
621
+ f"# TODO (manual): {rem.get('summary') or action['how']}", ""]
622
+ md.append(f"- **{action['title']}** — {rem.get('summary') or action['how']} _(manual)_")
623
+ return sh, md
624
+
625
+
560
626
  def fix_plan(ctx: core.Context) -> dict:
561
627
  args = ctx.args
562
628
  since = resolve_since(ctx, args.since)
@@ -597,6 +663,12 @@ def fix_plan(ctx: core.Context) -> dict:
597
663
  continue
598
664
  sh += [f"# =================== {label} ===================", f"# {note}", ""]
599
665
  md += [f"## {label}", f"_{note}_", ""]
666
+ if key == "web-server":
667
+ block_sh, block_md = nginx_fix_block(items)
668
+ sh += block_sh
669
+ md += block_md
670
+ total += len(items)
671
+ continue
600
672
  for action in items:
601
673
  total += 1
602
674
  sh.append(f"# [{action['priority']}] {action['title']} — {action['why']}")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.58",
3
+ "version": "0.7.60",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",