@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.
|
|
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.
|
|
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.
|
|
855
|
-
npx @topy-ai/maggie@0.7.
|
|
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=
|
|
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)
|
|
333
|
-
#
|
|
334
|
-
#
|
|
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
|
|
338
|
-
f"
|
|
339
|
-
|
|
340
|
-
|
|
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']}")
|