@rubytech/create-maxy-code 0.1.189 → 0.1.190

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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/schema.cypher +16 -1
  3. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  4. package/payload/platform/plugins/admin/PLUGIN.md +3 -4
  5. package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-authoring-skill-gate.test.d.ts +2 -0
  6. package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-authoring-skill-gate.test.d.ts.map +1 -0
  7. package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-authoring-skill-gate.test.js +79 -0
  8. package/payload/platform/plugins/admin/mcp/dist/__tests__/admin-authoring-skill-gate.test.js.map +1 -0
  9. package/payload/platform/plugins/admin/mcp/dist/index.js +21 -95
  10. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  11. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts +9 -0
  12. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts.map +1 -1
  13. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js +47 -0
  14. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js.map +1 -1
  15. package/payload/platform/plugins/admin/skills/file-presentation/SKILL.md +6 -12
  16. package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +1 -1
  17. package/payload/platform/plugins/docs/references/admin-ui.md +1 -2
  18. package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
  19. package/payload/platform/plugins/scheduling/mcp/dist/index.js +1 -1
  20. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  21. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -1
  23. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  24. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  25. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +7 -1
  26. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  27. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +1 -1
  28. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  29. package/payload/platform/templates/specialists/agents/content-producer.md +2 -2
  30. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  31. package/payload/server/public/assets/{admin-Cst6NkUE.js → admin-Cv2A5vnQ.js} +53 -53
  32. package/payload/server/public/assets/{data-COwlCdrl.js → data-DIa-MKNj.js} +1 -1
  33. package/payload/server/public/assets/{graph-CkUZEqdW.js → graph-Y5y3kw6J.js} +1 -1
  34. package/payload/server/public/assets/graph-labels-BIRCh328.js +1 -0
  35. package/payload/server/public/assets/page-BCCqlOmj.js +1 -0
  36. package/payload/server/public/assets/{page-DC2hsMPj.js → page-DwNvJOvr.js} +3 -3
  37. package/payload/server/public/data.html +3 -3
  38. package/payload/server/public/graph.html +3 -3
  39. package/payload/server/public/index.html +4 -4
  40. package/payload/server/server.js +586 -292
  41. package/payload/server/public/assets/graph-labels-Dt1bmq6W.js +0 -1
  42. package/payload/server/public/assets/page-CGXxYYRR.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.189",
3
+ "version": "0.1.190",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -459,7 +459,8 @@ FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organi
459
459
  |EmailAccount
460
460
  |Position|Credential|Agent|Idea|Concept
461
461
  |VoiceProfile|VoiceEdit|SocialPost|AEOAudit|Page
462
- |TimelineEvent|CompiledTruthRevision|Report|OrphanCandidate|CitationProposal)
462
+ |TimelineEvent|CompiledTruthRevision|Report|OrphanCandidate|CitationProposal
463
+ |FileArtifact)
463
464
  ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
464
465
  n.title, n.currentTitle, n.summary, n.body, n.content, n.text, n.description, n.headline, n.abstract,
465
466
  n.email, n.note, n.label, n.value, n.message, n.preview, n.tagline,
@@ -1724,6 +1725,20 @@ OPTIONS {
1724
1725
  }
1725
1726
  };
1726
1727
 
1728
+ // Task 514: physical files on disk indexed as :FileArtifact so the /data
1729
+ // search box runs the same hybrid (vector + BM25) retrieval over files that
1730
+ // the agent runs over the graph. discoverIndexes() enrols this at boot; the
1731
+ // file-index reconciler populates f.embedding (768-d nomic-embed-text over
1732
+ // name + extracted content).
1733
+ CREATE VECTOR INDEX file_artifact_embedding IF NOT EXISTS
1734
+ FOR (f:FileArtifact) ON (f.embedding)
1735
+ OPTIONS {
1736
+ indexConfig: {
1737
+ `vector.dimensions`: 768,
1738
+ `vector.similarity_function`: 'cosine'
1739
+ }
1740
+ };
1741
+
1727
1742
  CREATE CONSTRAINT orphan_candidate_id_unique IF NOT EXISTS
1728
1743
  FOR (o:OrphanCandidate) REQUIRE o.orphanCandidateId IS UNIQUE;
1729
1744
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "admin",
3
- "description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, file-attach, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform.",
3
+ "description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: admin
3
3
  surface: platform
4
- description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, file-attach, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform."
4
+ description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform."
5
5
  tools:
6
6
  - name: system-status
7
7
  publicAllowlist: false
@@ -57,9 +57,6 @@ tools:
57
57
  - name: session-resume
58
58
  publicAllowlist: false
59
59
  adminAllowlist: true
60
- - name: file-attach
61
- publicAllowlist: false
62
- adminAllowlist: false
63
60
  - name: action-pending
64
61
  publicAllowlist: false
65
62
  adminAllowlist: true
@@ -104,6 +101,8 @@ Platform management tools for both the admin and public agents. The `plugin-read
104
101
 
105
102
  Tools are available via the `admin` MCP server.
106
103
 
104
+ **Admin-seat authoring-skill gate (Task 516).** `skill-load` and `plugin-read` refuse to return the body of a content-producer-owned authoring skill — `professional-document`, `a4-print-documents`, `publish-site` — when the caller is the admin seat (`MAXY_SESSION_ROLE=admin` with no `MAXY_SPECIALIST`). The refusal relays one line — "This deliverable is owned by content-producer. Dispatch it with the Agent tool and subagent_type content-producer." — so the admin's next action is the dispatch, not inline authoring. `plugin-read` extracts the skill slug from a `skills/<slug>/…` file path (or a slug buried in `pluginName`), so the raw-SKILL.md read path can't bypass the gate; `PLUGIN.md` and plain `references/*` reads are untouched. content-producer's own spawns carry `MAXY_SPECIALIST=content-producer` and resolve these skills normally (it reaches them via the native `Skill` tool regardless), and the public seat is out of scope. The gate is a pure name+env check before any filesystem read and fails open for any non-admin or unidentified seat — it never bricks a legitimate load. Each block emits `[admin-skill-gate] role=admin skill=<name> decision=block reason=content-producer-owned`. This is the enforcement layer; the IDENTITY.md delegation paragraph documents the intent but is not the gate ([[feedback_deterministic_means_remove_llm]], [[feedback_doctrine_paragraph_is_not_a_gate]]). Skills the admin invokes directly are deliberately excluded: `unzip-attachment` (its SKILL.md says "Invoked by the admin agent directly — admin owns all unzipping", then routes the extracted tree to a specialist) and `deck-pages` (admin-owned, a content-producer refusal target). The set is each skill's own SKILL.md "Invoked by" declaration, not the admin plugin's skill catalogue (which lists these admin-hosted skills for discovery — the catalogue is the inline path this gate closes).
105
+
107
106
  **Three-store admin auth invariant.** `admin-add` writes to all three identity stores (`users.json` PIN auth at the persistent `~/{configDir}/users.json` location , `account.json` `admins[]` role, Neo4j `:AdminUser`/`:Person` graph identity) with per-leg `[admin-auth-store]` log lines plus the `[admins-write]` chokepoint line, and returns `is_error: true` on any leg failure naming what's already written. `admin-update-pin` writes `users.json` only and emits the same `[admin-auth-store]` line. **Single chokepoint:** every `users.json` + `account.json admins[]` mutation routes through `platform/lib/admins-write` (`writeAdminEntry` for admin-add/set-pin, `removeAdminFromAccount` for admin-remove); the static check `grep -rnE 'admins\.push\|config\.admins\s*=' platform/ \| grep -v lib/admins-write` returns 0. Direct `Edit`/`Write` on `account.json` is blocked at the `pre-tool-use` hook — mutations go through `account-update`, `plugin-toggle-enabled`, or the `admin-*` tools. **Install + boot invariants:** the installer and admin-server boot walk every account.json admins[] vs users.json; `[install-invariant]` / `[admin-invariant] direction=… userId=<id8> source=<file>` fires per divergence with a `check complete divergences=<n>` summary. Log-only — does not block. See `.docs/agents.md` § "Three-store admin auth invariant" for the full contract.
108
107
 
109
108
  **AdminUser graph stamp + boot self-heal.** The seed cypher in `platform/scripts/seed-neo4j.sh` stamps `:AdminUser.accountId` + `:AdminUser.role='owner'` ON CREATE so the graph-write gate's account-scoped MATCH succeeds on the first user-domain write. Admin-server boot additionally runs `platform/ui/app/lib/adminuser-self-heal.ts` to repair legacy nodes whose `accountId` or `role` is null — emits `[adminuser-self-heal] healed=<N> userId=<8> accountId=<8>` once per boot, idempotent. The `graph-write-gate` reject log gains a `subReason=` field (`no-admin-user-node` / `admin-user-no-accountid` / `no-business-or-personal-profile`) for operator diagnosis. See `.docs/agents.md` § "AdminUser graph stamp contract" for the full contract.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=admin-authoring-skill-gate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-authoring-skill-gate.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/admin-authoring-skill-gate.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,79 @@
1
+ // Admin-seat authoring-skill gate (Task 516).
2
+ //
3
+ // Locks the pure decision helpers that make content-producer-owned
4
+ // authoring skills unreachable from the admin seat via `skill-load` and
5
+ // `plugin-read`. The MCP handlers in index.ts are thin glue over these —
6
+ // they emit the dispatch-hint text and the `[admin-skill-gate]` line — so
7
+ // the decision logic is tested here in isolation, mirroring the
8
+ // loadSkill / findSkillOwners test convention (no stdio transport).
9
+ import { describe, it, expect } from "vitest";
10
+ import { CONTENT_PRODUCER_AUTHORING_SKILLS, isAdminSeat, skillSlugFromReadArgs, adminAuthoringSkillBlock, } from "../skill-resolution.js";
11
+ const ADMIN = { MAXY_SESSION_ROLE: "admin", MAXY_SPECIALIST: "" };
12
+ const CONTENT_PRODUCER = { MAXY_SESSION_ROLE: "admin", MAXY_SPECIALIST: "content-producer" };
13
+ const PUBLIC = { MAXY_SESSION_ROLE: "public", MAXY_SPECIALIST: "" };
14
+ describe("isAdminSeat", () => {
15
+ it("true only when role is admin and no specialist is set", () => {
16
+ expect(isAdminSeat(ADMIN)).toBe(true);
17
+ expect(isAdminSeat({ MAXY_SESSION_ROLE: "admin" })).toBe(true); // specialist undefined
18
+ });
19
+ it("false for a specialist seat even when role is admin", () => {
20
+ expect(isAdminSeat(CONTENT_PRODUCER)).toBe(false);
21
+ });
22
+ it("false for the public seat", () => {
23
+ expect(isAdminSeat(PUBLIC)).toBe(false);
24
+ });
25
+ it("false when role is unknown (fail-open — never block an unidentified seat)", () => {
26
+ expect(isAdminSeat({})).toBe(false);
27
+ });
28
+ });
29
+ describe("CONTENT_PRODUCER_AUTHORING_SKILLS", () => {
30
+ it("is exactly the three content-producer-invoked authoring/render/publish skills", () => {
31
+ expect([...CONTENT_PRODUCER_AUTHORING_SKILLS].sort()).toEqual(["a4-print-documents", "professional-document", "publish-site"].sort());
32
+ });
33
+ it("excludes admin-invoked skills (unzip-attachment, deck-pages)", () => {
34
+ // unzip-attachment SKILL.md: "Invoked by the admin agent directly".
35
+ // deck-pages is admin-owned (a content-producer refusal target).
36
+ expect(CONTENT_PRODUCER_AUTHORING_SKILLS.has("unzip-attachment")).toBe(false);
37
+ expect(CONTENT_PRODUCER_AUTHORING_SKILLS.has("deck-pages")).toBe(false);
38
+ });
39
+ });
40
+ describe("skillSlugFromReadArgs", () => {
41
+ it("extracts the slug from a skills/<slug>/SKILL.md file path", () => {
42
+ expect(skillSlugFromReadArgs("admin", "skills/professional-document/SKILL.md")).toBe("professional-document");
43
+ });
44
+ it("extracts the slug from a reference under the skill dir", () => {
45
+ expect(skillSlugFromReadArgs("admin", "skills/professional-document/references/x.md")).toBe("professional-document");
46
+ });
47
+ it("extracts the slug when buried in pluginName", () => {
48
+ expect(skillSlugFromReadArgs("admin/skills/publish-site", "PLUGIN.md")).toBe("publish-site");
49
+ });
50
+ it("returns undefined for PLUGIN.md and plain reference reads", () => {
51
+ expect(skillSlugFromReadArgs("admin", "PLUGIN.md")).toBeUndefined();
52
+ expect(skillSlugFromReadArgs("sales", "references/pricing.md")).toBeUndefined();
53
+ });
54
+ });
55
+ describe("adminAuthoringSkillBlock", () => {
56
+ it("blocks every content-producer-owned authoring skill from the admin seat", () => {
57
+ for (const slug of CONTENT_PRODUCER_AUTHORING_SKILLS) {
58
+ expect(adminAuthoringSkillBlock(slug, ADMIN)).toBe(true);
59
+ }
60
+ });
61
+ it("does not block the admin's own / admin-invoked skills", () => {
62
+ // unzip-attachment is admin-invoked (admin unzips then routes); deck-pages
63
+ // is admin-owned. plainly/voice-mirror/datetime are admin operational skills.
64
+ for (const slug of ["plainly", "voice-mirror", "datetime", "deck-pages", "unzip-attachment"]) {
65
+ expect(adminAuthoringSkillBlock(slug, ADMIN)).toBe(false);
66
+ }
67
+ });
68
+ it("never blocks the content-producer seat (it owns these skills)", () => {
69
+ expect(adminAuthoringSkillBlock("professional-document", CONTENT_PRODUCER)).toBe(false);
70
+ });
71
+ it("never blocks the public seat (out of scope)", () => {
72
+ expect(adminAuthoringSkillBlock("professional-document", PUBLIC)).toBe(false);
73
+ });
74
+ it("fail-open on an undefined slug or unidentified seat", () => {
75
+ expect(adminAuthoringSkillBlock(undefined, ADMIN)).toBe(false);
76
+ expect(adminAuthoringSkillBlock("professional-document", {})).toBe(false);
77
+ });
78
+ });
79
+ //# sourceMappingURL=admin-authoring-skill-gate.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-authoring-skill-gate.test.js","sourceRoot":"","sources":["../../src/__tests__/admin-authoring-skill-gate.test.ts"],"names":[],"mappings":"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"}
@@ -18,7 +18,7 @@ import { createConnection } from "node:net";
18
18
  import { homedir, hostname as osHostname } from "node:os";
19
19
  import QRCode from "qrcode";
20
20
  import { getSession, closeDriver } from "./lib/neo4j.js";
21
- import { findSkillOwners, computePluginReadHint, loadSkill, parseRequiredInputs } from "./skill-resolution.js";
21
+ import { findSkillOwners, computePluginReadHint, loadSkill, parseRequiredInputs, adminAuthoringSkillBlock, skillSlugFromReadArgs, AUTHORING_DISPATCH_HINT } from "./skill-resolution.js";
22
22
  import { resolvePublicHostname } from "./lib/public-hostname.js";
23
23
  import { publishSite } from "./tools/publish-site.js";
24
24
  function refusalGuidance(kind) {
@@ -1668,6 +1668,16 @@ eagerTool(server, "plugin-read", "Read a plugin definition (PLUGIN.md) or one of
1668
1668
  return refuseNoAccount("plugin-read");
1669
1669
  try {
1670
1670
  const resolvedFile = file ?? "PLUGIN.md";
1671
+ // Task 516 — plugin-read is the raw-SKILL.md read path that would
1672
+ // otherwise bypass the skill-load authoring gate. Block the admin seat
1673
+ // from reading any content-producer-owned authoring skill's files
1674
+ // (SKILL.md or its references); the slug is extracted from the file
1675
+ // path or pluginName. Same fail-open contract as skill-load.
1676
+ const authoringSlug = skillSlugFromReadArgs(pluginName, resolvedFile);
1677
+ if (adminAuthoringSkillBlock(authoringSlug, process.env)) {
1678
+ console.log(`[admin-skill-gate] role=admin skill=${authoringSlug} decision=block reason=content-producer-owned`);
1679
+ return { content: [{ type: "text", text: AUTHORING_DISPATCH_HINT }], isError: true };
1680
+ }
1671
1681
  const pluginPath = resolve(PLATFORM_ROOT, "plugins", pluginName, resolvedFile);
1672
1682
  if (!existsSync(pluginPath)) {
1673
1683
  // when the agent's pluginName/file combination misses
@@ -1796,6 +1806,15 @@ eagerTool(server, "skill-load", "Load a plugin skill's SKILL.md body by skill na
1796
1806
  }, async ({ skillName }) => {
1797
1807
  if (!ACCOUNT_ID)
1798
1808
  return refuseNoAccount("skill-load");
1809
+ // Task 516 — the admin seat may not load content-producer-owned
1810
+ // authoring skills inline; it must dispatch content-producer instead.
1811
+ // The gate is a pure name+env check before any filesystem read, and
1812
+ // fails open for non-admin / unidentified seats (content-producer's own
1813
+ // spawns carry MAXY_SPECIALIST and resolve normally).
1814
+ if (adminAuthoringSkillBlock(skillName, process.env)) {
1815
+ console.log(`[admin-skill-gate] role=admin skill=${skillName} decision=block reason=content-producer-owned`);
1816
+ return { content: [{ type: "text", text: AUTHORING_DISPATCH_HINT }], isError: true };
1817
+ }
1799
1818
  const start = Date.now();
1800
1819
  try {
1801
1820
  const outcome = await loadSkill(PLATFORM_ROOT, skillName);
@@ -1855,7 +1874,7 @@ eagerTool(server, "skill-load", "Load a plugin skill's SKILL.md body by skill na
1855
1874
  // sentinel that read like a successful render to the agent while the
1856
1875
  // chat surface saw nothing. Admin flows now use plain markdown for
1857
1876
  // reviewable artefacts and the `file-presentation` skill for downloadable
1858
- // artefacts (the skill picks `SendUserFile` or `file-attach` by inventory).
1877
+ // artefacts (delivered via `SendUserFile`, the native Claude Code surface).
1859
1878
  // The :Component / :KnowledgeDocument
1860
1879
  // persistence pipeline is preserved for historical conversations that
1861
1880
  // already carry the rows; no new conversations write to it.
@@ -2551,99 +2570,6 @@ server.tool("premium-deliver", "Deliver a purchased premium plugin. Copies plugi
2551
2570
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
2552
2571
  };
2553
2572
  });
2554
- // ---------------------------------------------------------------------------
2555
- // file-attach — copy a generated file into the attachment store for download
2556
- // ---------------------------------------------------------------------------
2557
- eagerTool(server, "file-attach", "Attach a file from the account directory for delivery to the user on non-PTY chat " +
2558
- "surfaces (web chat, WhatsApp) where the platform UI mediates attachment rendering. " +
2559
- "On the native Claude Code PTY surface, `SendUserFile` is the delivery — `file-attach` " +
2560
- "returns an `attachmentId` envelope only and renders no chip there. Tool selection is " +
2561
- "by inventory: see the `file-presentation` skill. Copies the file into the attachment " +
2562
- "store and returns metadata the chat surface uses to present the download. The file " +
2563
- "must be within the account directory tree. The returned metadata is the operator-visible " +
2564
- "record of the attachment; quote it back to the user in plain text so they have the " +
2565
- "filename and size on screen.", {
2566
- filePath: z.string().describe("Absolute path to the file to attach"),
2567
- }, async ({ filePath }) => {
2568
- if (!ACCOUNT_ID)
2569
- return refuseNoAccount("file-attach");
2570
- const TAG = "[file-attach]";
2571
- if (!filePath || !filePath.trim()) {
2572
- return {
2573
- content: [{ type: "text", text: `${TAG} filePath is required` }],
2574
- isError: true,
2575
- };
2576
- }
2577
- const accountDir = getAccountDir();
2578
- // --- Path validation: must be within the account directory ---
2579
- let resolvedPath;
2580
- try {
2581
- const { realpathSync: rp } = await import("node:fs");
2582
- resolvedPath = rp(filePath);
2583
- const accountResolved = rp(accountDir);
2584
- if (!resolvedPath.startsWith(accountResolved + "/")) {
2585
- const sanitised = filePath.replace(accountDir, "<account>/");
2586
- console.error(`${TAG} REJECTED path=${sanitised} reason=outside_account_directory`);
2587
- return {
2588
- content: [{ type: "text", text: `${TAG} Access denied: file is outside the account directory.` }],
2589
- isError: true,
2590
- };
2591
- }
2592
- }
2593
- catch (err) {
2594
- const code = err.code;
2595
- if (code === "ENOENT") {
2596
- console.error(`${TAG} ENOENT path=${filePath}`);
2597
- return {
2598
- content: [{ type: "text", text: `${TAG} File not found: ${filePath}` }],
2599
- isError: true,
2600
- };
2601
- }
2602
- console.error(`${TAG} path validation error: ${err instanceof Error ? err.message : String(err)}`);
2603
- return {
2604
- content: [{ type: "text", text: `${TAG} Failed to validate file path: ${err instanceof Error ? err.message : String(err)}` }],
2605
- isError: true,
2606
- };
2607
- }
2608
- // --- Store in attachment store ---
2609
- const targetUrl = `http://127.0.0.1:${PLATFORM_PORT}/api/admin/file-attach`;
2610
- try {
2611
- const res = await fetch(targetUrl, {
2612
- method: "POST",
2613
- headers: { "Content-Type": "application/json" },
2614
- body: JSON.stringify({ filePath: resolvedPath, accountId: ACCOUNT_ID }),
2615
- });
2616
- const result = await res.json();
2617
- if (!res.ok || result.error) {
2618
- const msg = result.error || `HTTP ${res.status}`;
2619
- console.error(`${TAG} store failed url=${targetUrl} status=${res.status} error=${msg}`);
2620
- return {
2621
- content: [{ type: "text", text: `${TAG} Failed to attach file: ${msg} (url=${targetUrl})` }],
2622
- isError: true,
2623
- };
2624
- }
2625
- console.error(`${TAG} attached path=${resolvedPath} size=${result.sizeBytes} mime=${result.mimeType} id=${result.attachmentId}`);
2626
- return {
2627
- content: [{
2628
- type: "text",
2629
- text: JSON.stringify({
2630
- attachmentId: result.attachmentId,
2631
- filename: result.filename,
2632
- sizeBytes: result.sizeBytes,
2633
- mimeType: result.mimeType,
2634
- }),
2635
- }],
2636
- };
2637
- }
2638
- catch (err) {
2639
- const errMsg = err instanceof Error ? err.message : String(err);
2640
- console.error(`${TAG} fetch failed url=${targetUrl} error=${errMsg}`);
2641
- return {
2642
- content: [{ type: "text", text: `${TAG} Failed to attach file: could not reach platform server at ${targetUrl} (${errMsg})` }],
2643
- isError: true,
2644
- };
2645
- }
2646
- });
2647
2573
  // ===================================================================
2648
2574
  // Action approval tools (EU AI Act Article 14 — human oversight)
2649
2575
  //