@rubytech/create-sitedesk-code 0.1.518 → 0.1.520
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/dist/__tests__/edge-bind-template-wiring.test.js +37 -0
- package/dist/__tests__/samba-provision.test.js +103 -1
- package/dist/index.js +19 -4
- package/dist/samba-provision.js +72 -0
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +65 -0
- package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +155 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +69 -19
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/bin/portal-assemble.mjs +187 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +46 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +19 -7
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +154 -38
- package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +100 -29
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-assemble.test.ts +143 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +12 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +15 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +105 -11
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +15 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +9 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +47 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +26 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +12 -4
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +23 -15
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +23 -15
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +8 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +23 -6
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +36 -20
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +85 -22
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +47 -7
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +27 -18
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +10 -3
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +27 -11
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +18 -9
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +16 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +16 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +26 -11
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +14 -10
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +13 -9
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +42 -22
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +17 -4
- package/payload/platform/plugins/docs/references/admin-ui.md +43 -5
- package/payload/platform/plugins/docs/references/samba.md +2 -0
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +20 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js +142 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js +153 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js +203 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +69 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js +5 -7
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +42 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +76 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +38 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js +67 -2
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts +10 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js +73 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts +18 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js +38 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +14 -2
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +10 -0
- package/payload/platform/plugins/email/skills/email-signature/SKILL.md +61 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +8 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +26 -15
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js +48 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +24 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +33 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +22 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js +30 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js +43 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js +87 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js +48 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js +14 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +4 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +39 -2
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts +4 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js +50 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +43 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +7 -3
- package/payload/platform/scripts/check-no-esm-require.mjs +5 -3
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js +5 -3
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.js +13 -10
- package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts +6 -2
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js +66 -32
- package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +11 -4
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.js +30 -14
- package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.js +11 -5
- package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -3
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +9 -5
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +6 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.js +8 -2
- package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/platform/templates/systemd/edge.service.template +1 -1
- package/payload/server/chunk-PXNY2TAP.js +1980 -0
- package/payload/server/chunk-TIIWMF5G.js +4837 -0
- package/payload/server/{chunk-S6HYTIU3.js → chunk-UX2FRNO7.js} +72 -2031
- package/payload/server/manager-B6OFOXSW.js +61 -0
- package/payload/server/maxy-edge.js +5 -3
- package/payload/server/package.json +2 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js +1 -0
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-Djg6ndne.js +2 -0
- package/payload/server/public/assets/AdminShell-Djg6ndne.js.br +0 -0
- package/payload/server/public/assets/AdminShell-Djg6ndne.js.gz +0 -0
- package/payload/server/public/assets/{activity-BE9gv5xL.js → activity-sqbyX0pX.js} +1 -1
- package/payload/server/public/assets/activity-sqbyX0pX.js.br +0 -0
- package/payload/server/public/assets/activity-sqbyX0pX.js.gz +0 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js +1 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js.br +0 -0
- package/payload/server/public/assets/admin-D9Bt47GY.js.gz +0 -0
- package/payload/server/public/assets/{agents-B-x0Ai2v.js → agents-BFYM5EDA.js} +1 -1
- package/payload/server/public/assets/agents-BFYM5EDA.js.br +0 -0
- package/payload/server/public/assets/agents-BFYM5EDA.js.gz +0 -0
- package/payload/server/public/assets/{browser-ZEQUkPfs.js → browser-DP5GZe4G.js} +1 -1
- package/payload/server/public/assets/browser-DP5GZe4G.js.br +0 -0
- package/payload/server/public/assets/browser-DP5GZe4G.js.gz +0 -0
- package/payload/server/public/assets/calendar-BKaemLDG.js +1 -0
- package/payload/server/public/assets/calendar-BKaemLDG.js.br +0 -0
- package/payload/server/public/assets/calendar-BKaemLDG.js.gz +0 -0
- package/payload/server/public/assets/chat-DPhT6urD.js +1 -0
- package/payload/server/public/assets/chat-DPhT6urD.js.br +1 -0
- package/payload/server/public/assets/chat-DPhT6urD.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-BSdXvJcH.js +1 -0
- package/payload/server/public/assets/chevron-right-DI7vI3IB.js +1 -0
- package/payload/server/public/assets/chevron-right-DI7vI3IB.js.br +0 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js +1 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js.br +0 -0
- package/payload/server/public/assets/clock-YUTPXZH5.js.gz +0 -0
- package/payload/server/public/assets/data-CfjttTvR.js +1 -0
- package/payload/server/public/assets/data-CfjttTvR.js.br +2 -0
- package/payload/server/public/assets/data-CfjttTvR.js.gz +0 -0
- package/payload/server/public/assets/{file-text-BTV6EdBv.js → file-text-BKK7R-k_.js} +1 -1
- package/payload/server/public/assets/file-text-BKK7R-k_.js.br +0 -0
- package/payload/server/public/assets/file-text-BKK7R-k_.js.gz +0 -0
- package/payload/server/public/assets/{graph-hD_gPbbL.js → graph-FuxYySjK.js} +3 -3
- package/payload/server/public/assets/graph-FuxYySjK.js.br +0 -0
- package/payload/server/public/assets/graph-FuxYySjK.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-CY1T19PY.js → graph-labels-Cip6fmKK.js} +1 -1
- package/payload/server/public/assets/graph-labels-Cip6fmKK.js.br +0 -0
- package/payload/server/public/assets/graph-labels-Cip6fmKK.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-BFV9VKHO.js → maximize-2-D79AhVDo.js} +1 -1
- package/payload/server/public/assets/maximize-2-D79AhVDo.js.br +0 -0
- package/payload/server/public/assets/maximize-2-D79AhVDo.js.gz +0 -0
- package/payload/server/public/assets/operator-BPAfgFhP.js +1 -0
- package/payload/server/public/assets/operator-BPAfgFhP.js.br +0 -0
- package/payload/server/public/assets/operator-BPAfgFhP.js.gz +0 -0
- package/payload/server/public/assets/page--78CbdFs.js +32 -0
- package/payload/server/public/assets/page--78CbdFs.js.br +0 -0
- package/payload/server/public/assets/page--78CbdFs.js.gz +0 -0
- package/payload/server/public/assets/page-BqEyS0W4.js +1 -0
- package/payload/server/public/assets/page-BqEyS0W4.js.br +0 -0
- package/payload/server/public/assets/page-BqEyS0W4.js.gz +0 -0
- package/payload/server/public/assets/{public-D6Tnj5LU.js → public-D1xs3FzD.js} +1 -1
- package/payload/server/public/assets/public-D1xs3FzD.js.br +0 -0
- package/payload/server/public/assets/public-D1xs3FzD.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-C-RfQUza.js → rotate-ccw-CcU_XD8B.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.gz +0 -0
- package/payload/server/public/assets/{routines-NIMElUzV.js → routines-CA7D0myr.js} +1 -1
- package/payload/server/public/assets/routines-CA7D0myr.js.br +0 -0
- package/payload/server/public/assets/routines-CA7D0myr.js.gz +0 -0
- package/payload/server/public/assets/{skills-N0ZlHosx.js → skills-CfVPPSxO.js} +1 -1
- package/payload/server/public/assets/skills-CfVPPSxO.js.br +0 -0
- package/payload/server/public/assets/skills-CfVPPSxO.js.gz +0 -0
- package/payload/server/public/assets/tasks-DgieyZqz.js +1 -0
- package/payload/server/public/assets/tasks-DgieyZqz.js.br +0 -0
- package/payload/server/public/assets/tasks-DgieyZqz.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-jQxlRPoI.js → time-entry-format-IFqinQJj.js} +1 -1
- package/payload/server/public/assets/time-entry-format-IFqinQJj.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-IFqinQJj.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-wUS4rpzv.js → triangle-alert-C5vH__sq.js} +1 -1
- package/payload/server/public/assets/triangle-alert-C5vH__sq.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-C5vH__sq.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-BRix_wIl.js → useCopyFeedback-BeL2gPl6.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js +14 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js +2 -0
- package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.gz +0 -0
- package/payload/server/public/assets/{wrench-sDivAjhf.js → wrench-BBEup3hd.js} +1 -1
- package/payload/server/public/assets/wrench-BBEup3hd.js.br +0 -0
- package/payload/server/public/assets/wrench-BBEup3hd.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +8176 -11914
- package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js +0 -1
- package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-TSa8KgID.js +0 -2
- package/payload/server/public/assets/AdminShell-TSa8KgID.js.br +0 -0
- package/payload/server/public/assets/AdminShell-TSa8KgID.js.gz +0 -0
- package/payload/server/public/assets/activity-BE9gv5xL.js.br +0 -0
- package/payload/server/public/assets/activity-BE9gv5xL.js.gz +0 -0
- package/payload/server/public/assets/admin-Cb0BZriv.js +0 -1
- package/payload/server/public/assets/admin-Cb0BZriv.js.br +0 -0
- package/payload/server/public/assets/admin-Cb0BZriv.js.gz +0 -0
- package/payload/server/public/assets/agents-B-x0Ai2v.js.br +0 -0
- package/payload/server/public/assets/agents-B-x0Ai2v.js.gz +0 -0
- package/payload/server/public/assets/browser-ZEQUkPfs.js.br +0 -0
- package/payload/server/public/assets/browser-ZEQUkPfs.js.gz +0 -0
- package/payload/server/public/assets/calendar-DsRxrW0G.js +0 -1
- package/payload/server/public/assets/calendar-DsRxrW0G.js.br +0 -0
- package/payload/server/public/assets/calendar-DsRxrW0G.js.gz +0 -0
- package/payload/server/public/assets/chat-BS9B9lDo.js +0 -1
- package/payload/server/public/assets/chat-BS9B9lDo.js.br +0 -1
- package/payload/server/public/assets/chat-BS9B9lDo.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DLtGuH6K.js +0 -1
- package/payload/server/public/assets/chevron-right-CY_trg5L.js +0 -1
- package/payload/server/public/assets/chevron-right-CY_trg5L.js.br +0 -0
- package/payload/server/public/assets/clock-oM5driJm.js +0 -1
- package/payload/server/public/assets/clock-oM5driJm.js.br +0 -0
- package/payload/server/public/assets/clock-oM5driJm.js.gz +0 -0
- package/payload/server/public/assets/data-Cio6PO0F.js +0 -1
- package/payload/server/public/assets/data-Cio6PO0F.js.br +0 -2
- package/payload/server/public/assets/data-Cio6PO0F.js.gz +0 -0
- package/payload/server/public/assets/file-text-BTV6EdBv.js.br +0 -0
- package/payload/server/public/assets/file-text-BTV6EdBv.js.gz +0 -0
- package/payload/server/public/assets/graph-hD_gPbbL.js.br +0 -0
- package/payload/server/public/assets/graph-hD_gPbbL.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-CY1T19PY.js.br +0 -0
- package/payload/server/public/assets/graph-labels-CY1T19PY.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-BFV9VKHO.js.br +0 -0
- package/payload/server/public/assets/maximize-2-BFV9VKHO.js.gz +0 -0
- package/payload/server/public/assets/operator-Dxze8CwV.js +0 -1
- package/payload/server/public/assets/operator-Dxze8CwV.js.br +0 -0
- package/payload/server/public/assets/operator-Dxze8CwV.js.gz +0 -0
- package/payload/server/public/assets/page-Bi86QF6_.js +0 -32
- package/payload/server/public/assets/page-Bi86QF6_.js.br +0 -0
- package/payload/server/public/assets/page-Bi86QF6_.js.gz +0 -0
- package/payload/server/public/assets/page-BnXly9aN.js +0 -1
- package/payload/server/public/assets/page-BnXly9aN.js.br +0 -0
- package/payload/server/public/assets/page-BnXly9aN.js.gz +0 -0
- package/payload/server/public/assets/public-D6Tnj5LU.js.br +0 -0
- package/payload/server/public/assets/public-D6Tnj5LU.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.gz +0 -0
- package/payload/server/public/assets/routines-NIMElUzV.js.br +0 -0
- package/payload/server/public/assets/routines-NIMElUzV.js.gz +0 -0
- package/payload/server/public/assets/skills-N0ZlHosx.js.br +0 -0
- package/payload/server/public/assets/skills-N0ZlHosx.js.gz +0 -0
- package/payload/server/public/assets/tasks-BsB_0duv.js +0 -1
- package/payload/server/public/assets/tasks-BsB_0duv.js.br +0 -0
- package/payload/server/public/assets/tasks-BsB_0duv.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js +0 -14
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js +0 -2
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.gz +0 -0
- package/payload/server/public/assets/wrench-sDivAjhf.js.br +0 -0
- package/payload/server/public/assets/wrench-sDivAjhf.js.gz +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Nothing else in the repo ties the edge unit template's placeholders to the
|
|
2
|
+
// substitutions in index.ts, so either half can be edited alone and the whole
|
|
3
|
+
// suite stays green. Two silent failures this closes:
|
|
4
|
+
//
|
|
5
|
+
// - substitution added, template left at a literal value → the replace is a
|
|
6
|
+
// no-op, the unit still binds 0.0.0.0, and the Task 2082 security fix ships
|
|
7
|
+
// inert with every pickEdgeBindDecision test passing;
|
|
8
|
+
// - template placeholder added, substitution forgotten → the unit ships a
|
|
9
|
+
// literal `Environment=EDGE_HOSTNAME=__EDGE_HOSTNAME__`, which edge.ts's
|
|
10
|
+
// `process.env.EDGE_HOSTNAME ?? '0.0.0.0'` does NOT catch, because the
|
|
11
|
+
// variable is present, just not an address, so server.listen() gets a
|
|
12
|
+
// non-address and the edge restart-loops.
|
|
13
|
+
//
|
|
14
|
+
// Both files are read from disk at runtime, the same pattern as
|
|
15
|
+
// macos-darwin-branch.test.ts and base-toolchain-deps.test.ts.
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
21
|
+
// dist/__tests__/edge-bind-template-wiring.test.js → ../../src/index.ts
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const INDEX_TS = resolve(here, "../../src/index.ts");
|
|
24
|
+
// dist/__tests__ → up four is the maxy-code root that holds platform/.
|
|
25
|
+
const EDGE_TEMPLATE = resolve(here, "../../../../platform/templates/systemd/edge.service.template");
|
|
26
|
+
const SRC = readFileSync(INDEX_TS, "utf-8");
|
|
27
|
+
const TEMPLATE = readFileSync(EDGE_TEMPLATE, "utf-8");
|
|
28
|
+
test("edge template: EDGE_HOSTNAME is a placeholder, not a hardcoded address", () => {
|
|
29
|
+
assert.match(TEMPLATE, /^Environment=EDGE_HOSTNAME=__EDGE_HOSTNAME__$/m, "edge.service.template must take EDGE_HOSTNAME from the installer's bind decision (Task 2082), not hardcode it");
|
|
30
|
+
});
|
|
31
|
+
test("edge template: every placeholder has a matching substitution in index.ts", () => {
|
|
32
|
+
const placeholders = [...new Set(TEMPLATE.match(/__[A-Z0-9_]+__/g) ?? [])].sort();
|
|
33
|
+
assert.ok(placeholders.includes("__EDGE_HOSTNAME__"), "template must carry __EDGE_HOSTNAME__");
|
|
34
|
+
for (const p of placeholders) {
|
|
35
|
+
assert.ok(SRC.includes(`.replace(/${p}/g,`), `${p} appears in edge.service.template but index.ts has no .replace(/${p}/g, ...) — the rendered unit would ship the literal placeholder`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// style and runs under `node --test dist/__tests__/*.test.js` after build.
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
|
-
import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, isPrivateIPv4, pickBindDecision, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, SAMBA_ENABLE_UNITS, } from "../samba-provision.js";
|
|
9
|
+
import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, isPrivateIPv4, pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, SAMBA_ENABLE_UNITS, } from "../samba-provision.js";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Fixtures
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -49,6 +49,42 @@ const MIXED_PUBLIC_AND_PRIVATE = {
|
|
|
49
49
|
eth0: [{ address: "178.105.251.82", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:02", internal: false, cidr: "178.105.251.82/32" }],
|
|
50
50
|
wg0: [{ address: "10.8.0.3", netmask: "255.255.255.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: false, cidr: "10.8.0.3/24" }],
|
|
51
51
|
};
|
|
52
|
+
// Hetzner Cloud shape: public eth0 plus a provider private network on ens10.
|
|
53
|
+
// pickBindDecision calls this `lan`; the edge must still bind loopback.
|
|
54
|
+
const HETZNER_CLOUD_PUBLIC_PLUS_PRIVNET = {
|
|
55
|
+
lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
|
|
56
|
+
eth0: [{ address: "167.233.21.246", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:03", internal: false, cidr: "167.233.21.246/32" }],
|
|
57
|
+
ens10: [{ address: "10.0.0.2", netmask: "255.255.0.0", family: "IPv4", mac: "96:00:00:00:00:04", internal: false, cidr: "10.0.0.2/16" }],
|
|
58
|
+
};
|
|
59
|
+
// Private wlan0 plus a public address elsewhere. Reaches pickBindDecision's
|
|
60
|
+
// wlan0 early return, which MIXED_PUBLIC_AND_PRIVATE never does because its
|
|
61
|
+
// private address is on wg0.
|
|
62
|
+
const PRIVATE_WLAN0_PLUS_PUBLIC = {
|
|
63
|
+
lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
|
|
64
|
+
wlan0: [{ address: "192.168.1.50", netmask: "255.255.255.0", family: "IPv4", mac: "aa:bb:cc:dd:ee:ff", internal: false, cidr: "192.168.1.50/24" }],
|
|
65
|
+
eth0: [{ address: "203.0.113.7", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:05", internal: false, cidr: "203.0.113.7/32" }],
|
|
66
|
+
};
|
|
67
|
+
// Private eth0 plus a public address elsewhere. Reaches the eth0 early return.
|
|
68
|
+
const PRIVATE_ETH0_PLUS_PUBLIC = {
|
|
69
|
+
lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
|
|
70
|
+
eth0: [{ address: "10.0.0.5", netmask: "255.255.255.0", family: "IPv4", mac: "11:11:11:11:11:11", internal: false, cidr: "10.0.0.5/24" }],
|
|
71
|
+
ens5: [{ address: "198.51.100.9", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:06", internal: false, cidr: "198.51.100.9/32" }],
|
|
72
|
+
};
|
|
73
|
+
// One interface carrying both a public and a private IPv4. No other fixture in
|
|
74
|
+
// this file has a multi-address interface.
|
|
75
|
+
const ONE_IFACE_PUBLIC_AND_PRIVATE = {
|
|
76
|
+
lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
|
|
77
|
+
eth0: [
|
|
78
|
+
{ address: "192.0.2.44", netmask: "255.255.255.255", family: "IPv4", mac: "96:00:00:00:00:07", internal: false, cidr: "192.0.2.44/32" },
|
|
79
|
+
{ address: "172.16.4.9", netmask: "255.255.0.0", family: "IPv4", mac: "96:00:00:00:00:07", internal: false, cidr: "172.16.4.9/16" },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
// CGNAT-only host. isPrivateIPv4 counts 100.64/10 as private, so this binds
|
|
83
|
+
// 0.0.0.0. Pinned so the outcome is declared rather than incidental.
|
|
84
|
+
const CGNAT_ONLY = {
|
|
85
|
+
lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
|
|
86
|
+
eth0: [{ address: "100.100.3.14", netmask: "255.192.0.0", family: "IPv4", mac: "96:00:00:00:00:08", internal: false, cidr: "100.100.3.14/10" }],
|
|
87
|
+
};
|
|
52
88
|
// ---------------------------------------------------------------------------
|
|
53
89
|
// isPrivateIPv4 — RFC1918 / CGNAT / link-local classification
|
|
54
90
|
// ---------------------------------------------------------------------------
|
|
@@ -107,6 +143,72 @@ test("pickBindDecision: none when the only non-loopback iface has no IPv4", () =
|
|
|
107
143
|
assert.deepEqual(pickBindDecision(PI_IPV6_ONLY_LAN), { kind: "none" });
|
|
108
144
|
});
|
|
109
145
|
// ---------------------------------------------------------------------------
|
|
146
|
+
// pickEdgeBindDecision — 0.0.0.0 only when private exists and public does not
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
test("pickEdgeBindDecision: private-only hosts keep the public bind", () => {
|
|
149
|
+
for (const [name, ifaces] of [
|
|
150
|
+
["PI_WLAN0_ONLY", PI_WLAN0_ONLY],
|
|
151
|
+
["PI_BOTH_IFACES", PI_BOTH_IFACES],
|
|
152
|
+
["PI_ETH0_ONLY", PI_ETH0_ONLY],
|
|
153
|
+
["PI_USB0_FALLBACK", PI_USB0_FALLBACK],
|
|
154
|
+
["CGNAT_ONLY", CGNAT_ONLY],
|
|
155
|
+
]) {
|
|
156
|
+
assert.equal(pickEdgeBindDecision(ifaces).hostname, "0.0.0.0", name);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
// The fail-open this task exists to close: pickBindDecision calls every one of
|
|
160
|
+
// these `lan`, so keeping its verdict verbatim would leave the edge public on a
|
|
161
|
+
// cloud box that happens to carry a provider private network.
|
|
162
|
+
test("pickEdgeBindDecision: a public IPv4 forces loopback even when a private one exists", () => {
|
|
163
|
+
for (const [name, ifaces] of [
|
|
164
|
+
["MIXED_PUBLIC_AND_PRIVATE", MIXED_PUBLIC_AND_PRIVATE],
|
|
165
|
+
["HETZNER_CLOUD_PUBLIC_PLUS_PRIVNET", HETZNER_CLOUD_PUBLIC_PLUS_PRIVNET],
|
|
166
|
+
["PRIVATE_WLAN0_PLUS_PUBLIC", PRIVATE_WLAN0_PLUS_PUBLIC],
|
|
167
|
+
["PRIVATE_ETH0_PLUS_PUBLIC", PRIVATE_ETH0_PLUS_PUBLIC],
|
|
168
|
+
["ONE_IFACE_PUBLIC_AND_PRIVATE", ONE_IFACE_PUBLIC_AND_PRIVATE],
|
|
169
|
+
]) {
|
|
170
|
+
const d = pickEdgeBindDecision(ifaces);
|
|
171
|
+
assert.equal(d.hostname, "127.0.0.1", name);
|
|
172
|
+
assert.equal(d.kind, "lan", `${name} still classifies as lan for smbd`);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
test("pickEdgeBindDecision: public-only host binds loopback", () => {
|
|
176
|
+
const d = pickEdgeBindDecision(HETZNER_PUBLIC_ETH0_ONLY);
|
|
177
|
+
assert.equal(d.hostname, "127.0.0.1");
|
|
178
|
+
assert.equal(d.kind, "loopback-only");
|
|
179
|
+
});
|
|
180
|
+
// Unlike the Samba call site, which throws on `none`, the edge binds loopback
|
|
181
|
+
// and lets the install finish: a loopback edge still serves the tunnel.
|
|
182
|
+
test("pickEdgeBindDecision: no non-loopback IPv4 binds loopback rather than throwing", () => {
|
|
183
|
+
for (const ifaces of [PI_LO_ONLY, PI_IPV6_ONLY_LAN]) {
|
|
184
|
+
const d = pickEdgeBindDecision(ifaces);
|
|
185
|
+
assert.equal(d.hostname, "127.0.0.1");
|
|
186
|
+
assert.equal(d.kind, "none");
|
|
187
|
+
assert.deepEqual(d.privateAddresses, []);
|
|
188
|
+
assert.deepEqual(d.publicAddresses, []);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
test("pickEdgeBindDecision: reports the interface and addresses it decided from", () => {
|
|
192
|
+
assert.deepEqual(pickEdgeBindDecision(MIXED_PUBLIC_AND_PRIVATE), {
|
|
193
|
+
hostname: "127.0.0.1",
|
|
194
|
+
kind: "lan",
|
|
195
|
+
iface: "wg0",
|
|
196
|
+
publicAddresses: ["178.105.251.82"],
|
|
197
|
+
privateAddresses: ["10.8.0.3"],
|
|
198
|
+
});
|
|
199
|
+
assert.deepEqual(pickEdgeBindDecision(PI_WLAN0_ONLY), {
|
|
200
|
+
hostname: "0.0.0.0",
|
|
201
|
+
kind: "lan",
|
|
202
|
+
iface: "wlan0",
|
|
203
|
+
publicAddresses: [],
|
|
204
|
+
privateAddresses: ["192.168.1.50"],
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
test("formatEdgeBindDecision: carries both grep tokens and never leaves an empty field", () => {
|
|
208
|
+
assert.equal(formatEdgeBindDecision(pickEdgeBindDecision(PI_WLAN0_ONLY)), "[edge-bind-decision] kind=lan iface=wlan0 private=192.168.1.50 public=none EDGE_HOSTNAME=0.0.0.0");
|
|
209
|
+
assert.equal(formatEdgeBindDecision(pickEdgeBindDecision(HETZNER_PUBLIC_ETH0_ONLY)), "[edge-bind-decision] kind=loopback-only iface=none private=none public=167.233.21.246 EDGE_HOSTNAME=127.0.0.1");
|
|
210
|
+
});
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
110
212
|
// renderBrandStanza — exact spec match
|
|
111
213
|
// ---------------------------------------------------------------------------
|
|
112
214
|
test("renderBrandStanza emits the spec directives in order", () => {
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { classifyPortHolder } from "./preflight-port-classifier.js";
|
|
|
29
29
|
import { memoryControllerMissing, cmdlineAlreadyPatched, patchCmdline } from "./cgroup-memory-controller.js";
|
|
30
30
|
import { parsePluginList, computeInstallActions, parseExternalPlugins, findUnregisteredResyncs, discoverMarketplaceCandidateDirs, } from "./lib/plugin-install.js";
|
|
31
31
|
import { findPremiumMcpDirs } from "./lib/premium-mcp-discover.js";
|
|
32
|
-
import { pickBindDecision, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
|
|
32
|
+
import { pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
|
|
33
33
|
import { networkInterfaces, userInfo, cpus } from "node:os";
|
|
34
34
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
35
35
|
// Brand manifest — read from payload to derive all brand-specific installation values.
|
|
@@ -4493,11 +4493,20 @@ WantedBy=default.target
|
|
|
4493
4493
|
// VNC_DISPLAY (defined above for the main brand unit) is also stamped into
|
|
4494
4494
|
// the edge unit so both services agree on the X display. Per-brand display
|
|
4495
4495
|
// closes the cross-brand cookie leak documented.
|
|
4496
|
+
// Task 2082 — the edge binds 0.0.0.0 only on a host with a private IPv4 and
|
|
4497
|
+
// no public one; anything else binds loopback so a rented server never puts
|
|
4498
|
+
// the admin edge on the internet. Logged with its inputs because a wrong
|
|
4499
|
+
// classification is otherwise invisible until an external probe finds it.
|
|
4500
|
+
// Linux-only by construction: this function returns at the darwin branch
|
|
4501
|
+
// above, and darwin runs no edge process at all.
|
|
4502
|
+
const edgeBind = pickEdgeBindDecision(networkInterfaces());
|
|
4503
|
+
logFile(` ${formatEdgeBindDecision(edgeBind)}`);
|
|
4496
4504
|
const edgeTemplatePath = resolve(INSTALL_DIR, "platform/templates/systemd/edge.service.template");
|
|
4497
4505
|
if (existsSync(edgeTemplatePath)) {
|
|
4498
4506
|
const edgeServiceContent = readFileSync(edgeTemplatePath, "utf-8")
|
|
4499
4507
|
.replace(/__INSTALL_DIR__/g, INSTALL_DIR)
|
|
4500
4508
|
.replace(/__EDGE_PORT__/g, String(PORT))
|
|
4509
|
+
.replace(/__EDGE_HOSTNAME__/g, edgeBind.hostname)
|
|
4501
4510
|
.replace(/__MAXY_UI_PORT__/g, String(MAXY_UI_INTERNAL_PORT))
|
|
4502
4511
|
.replace(/__PERSIST_DIR__/g, persistDir)
|
|
4503
4512
|
.replace(/__VNC_DISPLAY__/g, String(VNC_DISPLAY))
|
|
@@ -4505,7 +4514,7 @@ WantedBy=default.target
|
|
|
4505
4514
|
.replace(/__CDP_PORT__/g, String(CDP_PORT_BRAND))
|
|
4506
4515
|
.replace(/__RFB_PORT__/g, String(RFB_PORT));
|
|
4507
4516
|
writeFileSync(join(serviceDir, edgeUnitName), edgeServiceContent);
|
|
4508
|
-
logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT} VNC_DISPLAY=:${VNC_DISPLAY} RFB_PORT=${RFB_PORT} WEBSOCKIFY_PORT=${WEBSOCKIFY_PORT_BRAND} CDP_PORT=${CDP_PORT_BRAND}`);
|
|
4517
|
+
logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} EDGE_HOSTNAME=${edgeBind.hostname} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT} VNC_DISPLAY=:${VNC_DISPLAY} RFB_PORT=${RFB_PORT} WEBSOCKIFY_PORT=${WEBSOCKIFY_PORT_BRAND} CDP_PORT=${CDP_PORT_BRAND}`);
|
|
4509
4518
|
}
|
|
4510
4519
|
else {
|
|
4511
4520
|
console.error(` WARNING: edge.service.template missing at ${edgeTemplatePath} — VNC transport unavailable`);
|
|
@@ -5389,8 +5398,14 @@ try {
|
|
|
5389
5398
|
console.log("================================================================");
|
|
5390
5399
|
console.log("");
|
|
5391
5400
|
if (isLinux()) {
|
|
5392
|
-
|
|
5393
|
-
|
|
5401
|
+
// Only true when the edge binds 0.0.0.0. On a host classified to loopback
|
|
5402
|
+
// (Task 2082) nothing answers on the LAN address, so printing this line
|
|
5403
|
+
// there would be a false instruction; the SSH-forward recipe below is the
|
|
5404
|
+
// whole story for that host class.
|
|
5405
|
+
if (pickEdgeBindDecision(networkInterfaces()).hostname === "0.0.0.0") {
|
|
5406
|
+
console.log(` Same network (Pi / LAN): http://${DEVICE_HOSTNAME}.local:${PORT}`);
|
|
5407
|
+
console.log("");
|
|
5408
|
+
}
|
|
5394
5409
|
console.log(` Remote access (Hetzner / cloud) — on your local machine run:`);
|
|
5395
5410
|
console.log(` ssh -L ${PORT}:localhost:${PORT} -L ${BRAND.websockifyPort}:localhost:${BRAND.websockifyPort} admin@<server-ipv4>`);
|
|
5396
5411
|
console.log(` Then open:`);
|
package/dist/samba-provision.js
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
// stanza intact (peer-brand isolation is the structural invariant).
|
|
15
15
|
// 5. Report whether any brand stanza remains so the uninstall step can
|
|
16
16
|
// decide whether to apt-purge samba.
|
|
17
|
+
//
|
|
18
|
+
// Despite the file name, the interface classification here serves two
|
|
19
|
+
// consumers, not one: `isPrivateIPv4` and `pickBindDecision` answer where smbd
|
|
20
|
+
// binds, and `pickEdgeBindDecision` (Task 2082) answers where the edge binds.
|
|
21
|
+
// The two deliberately disagree on a dual-homed host; see that function.
|
|
17
22
|
// ---------------------------------------------------------------------------
|
|
18
23
|
// Brand-scoped Samba stanza
|
|
19
24
|
// ---------------------------------------------------------------------------
|
|
@@ -152,6 +157,73 @@ export function pickBindDecision(ifaces) {
|
|
|
152
157
|
const hasNonLoopbackIPv4 = Object.keys(ifaces).some((name) => name !== "lo" && ipv4Addrs(name).length > 0);
|
|
153
158
|
return hasNonLoopbackIPv4 ? { kind: "loopback-only" } : { kind: "none" };
|
|
154
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Decide how the edge should bind, from the same interface table
|
|
162
|
+
* `pickBindDecision` reads. The edge binds `0.0.0.0` only when the host has at
|
|
163
|
+
* least one private IPv4 AND zero public IPv4; every other shape binds
|
|
164
|
+
* `127.0.0.1`. Task 2082.
|
|
165
|
+
*
|
|
166
|
+
* Three properties are deliberate, not oversights:
|
|
167
|
+
*
|
|
168
|
+
* 1. This DISAGREES with `pickBindDecision` on a dual-homed host. For SMB, a
|
|
169
|
+
* private interface alongside a public one keeps the LAN bind (operator
|
|
170
|
+
* confirmation, Task 755 — see the MIXED_PUBLIC_AND_PRIVATE fixture). For
|
|
171
|
+
* the edge the operator decided the opposite: a cloud server carrying a
|
|
172
|
+
* provider private network (Hetzner Cloud's public `eth0` plus private
|
|
173
|
+
* `ens10`) must not silently keep a public bind. Both functions are right
|
|
174
|
+
* for their own consumer; this is not a rename of the other.
|
|
175
|
+
* 2. IPv6 is ignored. `pickBindDecision` already filters to IPv4, and
|
|
176
|
+
* `0.0.0.0` binds IPv4 only, so a public IPv6 address cannot be exposed by
|
|
177
|
+
* this bind under either outcome.
|
|
178
|
+
* 3. A cloud host presenting its public address by 1:1 NAT is NOT covered.
|
|
179
|
+
* Its guest NIC carries only a private address, so this reads a
|
|
180
|
+
* private-only host and binds `0.0.0.0` while the box is publicly
|
|
181
|
+
* reachable. The scoped signal is a public IPv4 alongside the private one;
|
|
182
|
+
* no host in `.docs/install/` behaves that way, and catching it would need
|
|
183
|
+
* an outbound probe rather than the interface table.
|
|
184
|
+
*
|
|
185
|
+
* `kind: "none"` binds `127.0.0.1` rather than throwing, unlike the Samba call
|
|
186
|
+
* site, which hard-fails: a share bound to nothing is useless, whereas a
|
|
187
|
+
* loopback edge still serves the Cloudflare tunnel, which dials
|
|
188
|
+
* `http://localhost:<port>`. Cost: a host that gains an interface later stays
|
|
189
|
+
* loopback-only until the installer is re-run, which the decision line makes
|
|
190
|
+
* visible in the install log.
|
|
191
|
+
*/
|
|
192
|
+
export function pickEdgeBindDecision(ifaces) {
|
|
193
|
+
const bind = pickBindDecision(ifaces);
|
|
194
|
+
const publicAddresses = [];
|
|
195
|
+
const privateAddresses = [];
|
|
196
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
197
|
+
if (name === "lo" || !addrs)
|
|
198
|
+
continue;
|
|
199
|
+
for (const a of addrs) {
|
|
200
|
+
if (a.family !== "IPv4" || a.internal)
|
|
201
|
+
continue;
|
|
202
|
+
if (isPrivateIPv4(a.address))
|
|
203
|
+
privateAddresses.push(a.address);
|
|
204
|
+
else
|
|
205
|
+
publicAddresses.push(a.address);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
hostname: privateAddresses.length > 0 && publicAddresses.length === 0 ? "0.0.0.0" : "127.0.0.1",
|
|
210
|
+
kind: bind.kind,
|
|
211
|
+
iface: bind.kind === "lan" ? bind.lanInterface : null,
|
|
212
|
+
publicAddresses,
|
|
213
|
+
privateAddresses,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The install-log line for an edge bind decision. The shape is load-bearing:
|
|
218
|
+
* the task's diagnostic path greps `bind-decision` and `EDGE_HOSTNAME`, so both
|
|
219
|
+
* tokens must survive any edit. Empty lists render as the literal `none` so a
|
|
220
|
+
* host with no addresses is distinguishable from a truncated line.
|
|
221
|
+
*/
|
|
222
|
+
export function formatEdgeBindDecision(d) {
|
|
223
|
+
const list = (xs) => (xs.length === 0 ? "none" : xs.join(","));
|
|
224
|
+
return `[edge-bind-decision] kind=${d.kind} iface=${d.iface ?? "none"} ` +
|
|
225
|
+
`private=${list(d.privateAddresses)} public=${list(d.publicAddresses)} EDGE_HOSTNAME=${d.hostname}`;
|
|
226
|
+
}
|
|
155
227
|
// ---------------------------------------------------------------------------
|
|
156
228
|
// smb.conf merge / remove
|
|
157
229
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Task 2097 — `email-signature-set` implementation plan
|
|
2
|
+
|
|
3
|
+
Design: [`2026-07-28-task-2097-email-signature-set-design.md`](../specs/2026-07-28-task-2097-email-signature-set-design.md).
|
|
4
|
+
|
|
5
|
+
## Registration surfaces — corrected against the tree
|
|
6
|
+
|
|
7
|
+
The task file names `ADMIN_CORE_TOOLS`. It does not exist: `grep -rn 'ADMIN_CORE_TOOLS' platform/` returns
|
|
8
|
+
nothing outside comments. The live surfaces, each confirmed by reading the file, are four:
|
|
9
|
+
|
|
10
|
+
| # | Surface | Evidence |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| 1 | `plugins/email/mcp/src/index.ts` | `eagerTool(server, "email-setup", …)` at line 124 |
|
|
13
|
+
| 2 | `plugins/email/PLUGIN.md` | frontmatter `description:`, `tools:` list, Capabilities, Skills table |
|
|
14
|
+
| 3 | `platform/templates/specialists/agents/personal-assistant.md` | the only template matching `grep -rl email-setup platform/templates/` |
|
|
15
|
+
| 4 | `services/claude-session-manager/src/canonical-tool-names.generated.ts` | header: generated from `PLUGIN.md` `tools:` blocks, `check-canonical-tool-names.mjs` fails the build on drift |
|
|
16
|
+
|
|
17
|
+
Surface 4 is not in the task file and would have broken the build. It is regenerated, never hand-edited:
|
|
18
|
+
`cd platform && npm run gen:canonical-tools`.
|
|
19
|
+
|
|
20
|
+
Three further files match `email-draft-send` and are deliberately **not** touched:
|
|
21
|
+
`plugins/admin/hooks/preference-consult-gate.sh`, `scripts/lib/provision-account-dir.sh`, and the
|
|
22
|
+
`plugins/admin/PLUGIN.md` prose describing them. All three are the outbound-send preference gate,
|
|
23
|
+
matching sends that carry attachments. `email-signature-set` sends nothing, so it is not a gate member.
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. **`lib/signature.ts`** — add `SIGNATURE_BASENAME`, `signatureDirFor(accountId)`, and
|
|
28
|
+
`saveSignature(accountDir, parts)`. `loadSignature` / `applySignature` unchanged.
|
|
29
|
+
`signatureDirFor` is `accountDirFor(accountId) || process.env.ACCOUNT_DIR || undefined`, which is the
|
|
30
|
+
expression `compose.ts:112` already computes, with an empty-string env normalised to `undefined`
|
|
31
|
+
(both already load as no-signature, so behaviour is unchanged). No import cycle: `attachment-resolve.ts`
|
|
32
|
+
imports only `node:fs` and `node:path`.
|
|
33
|
+
2. **`lib/compose.ts`** — replace the inline `sigDir` expression with `signatureDirFor(accountId)` and the
|
|
34
|
+
literal `"email-signature"` in `assetNote` with `SIGNATURE_BASENAME`. `accountDirFor` stays imported for
|
|
35
|
+
the attachment path at line 75.
|
|
36
|
+
3. **`tools/email-signature-set.ts`** — refuse an all-absent call and an unresolvable directory before any
|
|
37
|
+
write, call `saveSignature`, log, return the resolved paths.
|
|
38
|
+
4. **`index.ts`** — import and `eagerTool` register, placed after the `email-setup` block. Guard with
|
|
39
|
+
`refuseNoAccount`, wrap in try/catch like `email-setup`. The handler's local result variable is named
|
|
40
|
+
`message`, not `text`, so it cannot shadow the `text` parameter.
|
|
41
|
+
5. **`PLUGIN.md`** — `tools:` entry (`riskClass: write_local`, both allowlists false, matching `email-setup`),
|
|
42
|
+
a Config clause in the frontmatter `description:`, a Capabilities bullet, a Skills-table row.
|
|
43
|
+
6. **`personal-assistant.md`** — add `mcp__plugin_email_email__email-signature-set` after `email-setup`.
|
|
44
|
+
7. **Regenerate** `canonical-tool-names.generated.ts`.
|
|
45
|
+
8. **`skills/email-signature/SKILL.md`** — new skill.
|
|
46
|
+
9. **`skills/email-composition/SKILL.md`** — one added rule, see below.
|
|
47
|
+
10. **`__tests__/email-signature-set.test.ts`** — committed, not ephemeral: the plugin already carries 34
|
|
48
|
+
test files and the task file mandates a regression test.
|
|
49
|
+
|
|
50
|
+
## Addition beyond the task file
|
|
51
|
+
|
|
52
|
+
The task specifies that the new `email-signature` skill carries the rule "never hand-build a signature
|
|
53
|
+
inside a draft body". That skill activates when the operator asks to set or change a signature. The
|
|
54
|
+
incident happened while **drafting**, when that skill is not loaded. `grep -in signature` over
|
|
55
|
+
`skills/email-composition/SKILL.md` returns zero hits, so the drafting surface currently says nothing
|
|
56
|
+
about signatures.
|
|
57
|
+
|
|
58
|
+
So the rule is also stated in one sentence in `email-composition`, which is the skill that is loaded while
|
|
59
|
+
drafting. Without it the doctrine lives only in a skill absent at the moment it is needed. Declared in Plan
|
|
60
|
+
Conformity as an addition.
|
|
61
|
+
|
|
62
|
+
## Verification
|
|
63
|
+
|
|
64
|
+
`npm run build` and `npx vitest run` in `plugins/email/mcp`, then `npm run gen:canonical-tools` and the
|
|
65
|
+
`check-canonical-tool-names.mjs` gate in `platform/`. No browser surface, no Pi hook surface.
|
package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Task 2097 — `email-signature-set` design
|
|
2
|
+
|
|
3
|
+
Date 2026-07-28. Lane *Platform / email · signature provisioning*.
|
|
4
|
+
Task file: [`.tasks/pending/2097-email-signature-setter-so-the-deterministic-loader-can-be-populated.md`](../../../../.tasks/pending/2097-email-signature-setter-so-the-deterministic-loader-can-be-populated.md).
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`email-draft` appends an account's signature deterministically, with no model judgement, at
|
|
9
|
+
[`compose.ts:113`](../../../plugins/email/mcp/src/lib/compose.ts). It calls `loadSignature(sigDir)` then
|
|
10
|
+
`applySignature`, where `loadSignature` (Task 1952,
|
|
11
|
+
[`signature.ts:24`](../../../plugins/email/mcp/src/lib/signature.ts)) reads three fixed-convention files at the
|
|
12
|
+
account root: `email-signature.txt`, `email-signature.html`, `email-signature.font`.
|
|
13
|
+
|
|
14
|
+
Nothing in the platform writes those files. Task 1952 shipped the reader with no writer, so the
|
|
15
|
+
path-and-format convention lives only in code and no account can be provisioned. On the G.L. Smith
|
|
16
|
+
account none of the three exist, `applied` was false, no signature was appended, and a drafting
|
|
17
|
+
subagent with no file-read tool reconstructed the HTML from a memory node and pulled a stale logo
|
|
18
|
+
into a client draft.
|
|
19
|
+
|
|
20
|
+
The failure is not a model ignoring a rule. The deterministic path works; it had no files to read.
|
|
21
|
+
|
|
22
|
+
## Outcome
|
|
23
|
+
|
|
24
|
+
An account's signature is installed into the loader's exact path and format through a sanctioned,
|
|
25
|
+
discoverable tool, and every subsequent draft carries it byte-for-byte with no model-authored
|
|
26
|
+
signature markup.
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
`lib/signature.ts` becomes the single owner of the signature convention in both directions. Read and
|
|
31
|
+
write cannot drift because one file defines the base path, the three extensions, and the font
|
|
32
|
+
trimming for both.
|
|
33
|
+
|
|
34
|
+
| Unit | Responsibility | Depends on |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `lib/signature.ts` → `signatureDirFor(accountId)` | Resolve the one directory an account's signature lives in | `attachment-resolve.ts` → `accountDirFor`, `process.env.ACCOUNT_DIR` |
|
|
37
|
+
| `lib/signature.ts` → `saveSignature(accountDir, parts)` | Write present parts, remove absent ones, report both | `node:fs` |
|
|
38
|
+
| `lib/signature.ts` → `loadSignature` / `applySignature` | Unchanged (Task 1952) | — |
|
|
39
|
+
| `tools/email-signature-set.ts` | Param refusals, call `saveSignature`, log, format the response | `lib/signature.ts` |
|
|
40
|
+
| `lib/compose.ts` | Swaps its inline dir expression for `signatureDirFor` | `lib/signature.ts` |
|
|
41
|
+
|
|
42
|
+
### Directory resolution
|
|
43
|
+
|
|
44
|
+
`compose.ts:113` currently resolves the read directory inline as
|
|
45
|
+
`accountDirFor(accountId) || process.env.ACCOUNT_DIR`. The setter must write to the identical
|
|
46
|
+
directory in every environment, so that expression moves into `signatureDirFor(accountId)` and both
|
|
47
|
+
sides call it. Substituting the identical expression leaves `compose.ts` behaviour unchanged; the
|
|
48
|
+
point is that "which directory" now has one definition rather than two copies that can diverge.
|
|
49
|
+
|
|
50
|
+
Writing to `accountDirFor(accountId)` alone was rejected: on a spawn where `PLATFORM_ROOT` is absent
|
|
51
|
+
but `ACCOUNT_DIR` is set, the setter would refuse while drafts would have read the file fine.
|
|
52
|
+
|
|
53
|
+
## Behaviour
|
|
54
|
+
|
|
55
|
+
Parameters, all optional strings: `text`, `html`, `font`. The account is the caller's `accountId`,
|
|
56
|
+
the same authority `email-draft` uses. There is no `mailbox` parameter — a signature is account-scoped,
|
|
57
|
+
because `loadSignature` reads one account root and knows nothing about mailboxes.
|
|
58
|
+
|
|
59
|
+
| Input | Effect on `email-signature.<ext>` |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `text` present | `.txt` written byte-for-byte verbatim, no reflow, no re-encoding |
|
|
62
|
+
| `html` present | `.html` written byte-for-byte verbatim |
|
|
63
|
+
| `font` present | `.font` written trimmed, matching `loadSignature`'s trim-on-read |
|
|
64
|
+
| any one absent | that file removed if it exists; absence of the file is not an error |
|
|
65
|
+
| all three absent | refused, nothing on disk is touched |
|
|
66
|
+
| directory unresolvable | refused, nothing on disk is touched |
|
|
67
|
+
|
|
68
|
+
Absent and empty are different inputs. A parameter is absent only when it is not supplied; a supplied
|
|
69
|
+
empty string is present, so it writes an empty file and counts toward the all-three-absent check. That
|
|
70
|
+
follows from writing verbatim: the tool stores what it was given rather than second-guessing it. The
|
|
71
|
+
consequence is that an empty `.txt` makes the next draft report `applied` true while appending
|
|
72
|
+
nothing visible, which is why an empty string is a deliberate act and omission is the way to remove.
|
|
73
|
+
|
|
74
|
+
The removal rule is what makes a text-only call clear a stale `.html` rather than leave it to keep
|
|
75
|
+
appending. The all-three-absent refusal is what stops a mis-shaped call silently wiping a working
|
|
76
|
+
signature; removal stays reachable, but only as a side effect of setting something.
|
|
77
|
+
|
|
78
|
+
The response names the resolved paths written and the resolved paths removed, so the caller sees the
|
|
79
|
+
account root it landed in rather than trusting it.
|
|
80
|
+
|
|
81
|
+
## Failure handling
|
|
82
|
+
|
|
83
|
+
Files are written in the fixed order `.txt`, `.html`, `.font`. A filesystem error surfaces verbatim
|
|
84
|
+
naming the offending path and the paths already written, so a half-applied state is reported rather
|
|
85
|
+
than hidden. There is no transactional rollback: the task does not ask for one, and reaching the
|
|
86
|
+
partial state requires a filesystem fault mid-call.
|
|
87
|
+
|
|
88
|
+
## Observability
|
|
89
|
+
|
|
90
|
+
`[email-signature-set] op=write accountId=<first-8> files=<written> removed=<removed>` on every
|
|
91
|
+
successful write. The task specifies the line through `files=`; `removed=` is added because the task
|
|
92
|
+
mandates removal as a behaviour and mandates that provisioning be auditable, and an unlogged removal
|
|
93
|
+
is not auditable.
|
|
94
|
+
|
|
95
|
+
The existing `[email-draft] op=signature applied=<bool> asset=<path>` line stays the per-draft
|
|
96
|
+
signal. An account that has run the setter shows `applied=true` on its next draft. That pair is the
|
|
97
|
+
end-to-end evidence: a write line naming the files, then a draft line reporting them applied.
|
|
98
|
+
|
|
99
|
+
## Registration
|
|
100
|
+
|
|
101
|
+
`ADMIN_CORE_TOOLS` is named in the task file but does not exist — `grep -rn 'ADMIN_CORE_TOOLS\s*[:=]'`
|
|
102
|
+
over `platform/` returns nothing, and the name survives only in comments. The live surfaces are:
|
|
103
|
+
|
|
104
|
+
1. `mcp/src/index.ts` — `eagerTool(server, name, description, zodShape, handler)`.
|
|
105
|
+
2. `PLUGIN.md` — a `tools:` entry (`riskClass: write_local`, `publicAllowlist: false`,
|
|
106
|
+
`adminAllowlist: false`, matching `email-setup`), a clause in the frontmatter `description:` prose,
|
|
107
|
+
an operator-docs bullet, and a skills-table row for the new skill.
|
|
108
|
+
3. `platform/templates/specialists/agents/personal-assistant.md` — the `tools:` frontmatter grant, the
|
|
109
|
+
canonical per-tool permission surface since Task 453.
|
|
110
|
+
|
|
111
|
+
`riskClass: write_local` is correct because the tool writes account-local files and sends nothing
|
|
112
|
+
outbound, which is the same class `email-setup` carries.
|
|
113
|
+
|
|
114
|
+
## Skill
|
|
115
|
+
|
|
116
|
+
`platform/plugins/email/skills/email-signature/SKILL.md`, activated when the operator asks to set or
|
|
117
|
+
change an email signature. It states the loader convention, and three standing rules:
|
|
118
|
+
|
|
119
|
+
- A signature is installed only through `email-signature-set`. Hand-writing the three files is not a
|
|
120
|
+
documented path.
|
|
121
|
+
- The agent never hand-builds a signature inside a draft body, because `email-draft` applies the
|
|
122
|
+
stored one automatically and a hand-built one both duplicates it and drifts from the asset.
|
|
123
|
+
- The signature content is read from the operator's own source of truth and passed through verbatim.
|
|
124
|
+
Reconstructing markup from memory or from stripped graph text is what put the stale logo in a
|
|
125
|
+
client draft.
|
|
126
|
+
|
|
127
|
+
Written as outcomes and constraints, not a step recipe, per `maxy-code/CLAUDE.md`.
|
|
128
|
+
|
|
129
|
+
## Testing
|
|
130
|
+
|
|
131
|
+
Added to the plugin's existing vitest surface at `mcp/src/__tests__/`, not substituted for it.
|
|
132
|
+
|
|
133
|
+
- Round trip: set all three, `loadSignature` returns the exact values, `applySignature` reports
|
|
134
|
+
`applied` true.
|
|
135
|
+
- Verbatim: text and html land byte-identical, trailing newlines and all; font lands trimmed.
|
|
136
|
+
- Text-only call writes `.txt` and removes a pre-existing `.html` and `.font`, so a later draft
|
|
137
|
+
carries no stale HTML.
|
|
138
|
+
- All-three-absent call is refused and leaves pre-existing files untouched.
|
|
139
|
+
- Unresolvable directory is refused.
|
|
140
|
+
- A draft composed after a successful set carries body and html exactly equal to `applySignature`
|
|
141
|
+
output, with no model-authored signature text.
|
|
142
|
+
- `signatureDirFor` prefers `accountDirFor` and falls back to `ACCOUNT_DIR`.
|
|
143
|
+
- A supplied empty string writes an empty file and does not count as absent.
|
|
144
|
+
- Existing `signature.test.ts` stays green.
|
|
145
|
+
|
|
146
|
+
## Out of scope
|
|
147
|
+
|
|
148
|
+
- Making `applied=false` a fail-closed refusal on client-facing drafts. Operator scoped this out on
|
|
149
|
+
2026-07-28 as separate hardening; deliberately not filed.
|
|
150
|
+
- The one-off G.L. Smith provisioning and the stale memory-node deletion. That is a runtime action on
|
|
151
|
+
the live install, performed through the tool this task ships.
|
|
152
|
+
- Any change to `loadSignature` / `applySignature` behaviour (Task 1952). `signatureDirFor` moves an
|
|
153
|
+
expression out of `compose.ts`; neither function's behaviour changes.
|
|
154
|
+
- The drafts list/verify capability, which is
|
|
155
|
+
[Task 2098](../../../../.tasks/pending/2098-email-drafts-list-verify-tool-so-draft-presence-is-confirmable.md).
|