@cloudgrid-io/mcp 0.21.18 → 0.21.20

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 (85) hide show
  1. package/README.md +2 -2
  2. package/package.json +11 -3
  3. package/src/corpus/cli-reference.md +8 -8
  4. package/src/corpus/cloudgrid-yaml.md +5 -5
  5. package/src/corpus/cookbook.md +1 -1
  6. package/src/corpus/playbook.md +3 -3
  7. package/src/corpus/skill-brain.md +1 -1
  8. package/src/corpus/skill-claim.md +2 -2
  9. package/src/corpus/skill-feedback.md +5 -5
  10. package/src/corpus/skill-plug.md +19 -27
  11. package/src/corpus/skills-readme.md +5 -5
  12. package/src/corpus/templates/_cloudgrid.yaml.reference +7 -7
  13. package/src/corpus/templates/admin-dashboard/cloudgrid.yaml +7 -7
  14. package/src/corpus/templates/ai-app/cloudgrid.yaml +7 -7
  15. package/src/corpus/templates/ai-knowledge-base/AGENTS.md +1 -1
  16. package/src/corpus/templates/ai-knowledge-base/cloudgrid.yaml +7 -7
  17. package/src/corpus/templates/analytics-dashboard/cloudgrid.yaml +7 -7
  18. package/src/corpus/templates/api-dashboard/cloudgrid.yaml +7 -7
  19. package/src/corpus/templates/api-docs/cloudgrid.yaml +7 -7
  20. package/src/corpus/templates/api-service/cloudgrid.yaml +7 -7
  21. package/src/corpus/templates/app-with-data/cloudgrid.yaml +7 -7
  22. package/src/corpus/templates/appointment-booking/cloudgrid.yaml +7 -7
  23. package/src/corpus/templates/approval-workflow/cloudgrid.yaml +7 -7
  24. package/src/corpus/templates/billing-dashboard/cloudgrid.yaml +7 -7
  25. package/src/corpus/templates/blog-cms/cloudgrid.yaml +7 -7
  26. package/src/corpus/templates/booking-system/cloudgrid.yaml +7 -7
  27. package/src/corpus/templates/calendar-scheduler/AGENTS.md +1 -1
  28. package/src/corpus/templates/calendar-scheduler/cloudgrid.yaml +7 -7
  29. package/src/corpus/templates/changelog/cloudgrid.yaml +7 -7
  30. package/src/corpus/templates/company-website/cloudgrid.yaml +7 -7
  31. package/src/corpus/templates/course-platform/cloudgrid.yaml +7 -7
  32. package/src/corpus/templates/crm/cloudgrid.yaml +7 -7
  33. package/src/corpus/templates/dashboard/cloudgrid.yaml +7 -7
  34. package/src/corpus/templates/deck/cloudgrid.yaml +7 -7
  35. package/src/corpus/templates/directory/cloudgrid.yaml +7 -7
  36. package/src/corpus/templates/docs-site/cloudgrid.yaml +7 -7
  37. package/src/corpus/templates/erp/cloudgrid.yaml +7 -7
  38. package/src/corpus/templates/event-board/cloudgrid.yaml +7 -7
  39. package/src/corpus/templates/expense-tracker/cloudgrid.yaml +7 -7
  40. package/src/corpus/templates/feature-request-board/cloudgrid.yaml +7 -7
  41. package/src/corpus/templates/financial-dashboard/cloudgrid.yaml +7 -7
  42. package/src/corpus/templates/forum/cloudgrid.yaml +7 -7
  43. package/src/corpus/templates/hr-portal/cloudgrid.yaml +7 -7
  44. package/src/corpus/templates/internal-tools-portal/cloudgrid.yaml +7 -7
  45. package/src/corpus/templates/inventory/cloudgrid.yaml +7 -7
  46. package/src/corpus/templates/invoice/cloudgrid.yaml +7 -7
  47. package/src/corpus/templates/job-board/cloudgrid.yaml +7 -7
  48. package/src/corpus/templates/kanban/cloudgrid.yaml +7 -7
  49. package/src/corpus/templates/landing-page/cloudgrid.yaml +7 -7
  50. package/src/corpus/templates/lms/cloudgrid.yaml +7 -7
  51. package/src/corpus/templates/marketplace/AGENTS.md +1 -1
  52. package/src/corpus/templates/marketplace/cloudgrid.yaml +7 -7
  53. package/src/corpus/templates/membership-site/cloudgrid.yaml +7 -7
  54. package/src/corpus/templates/monitoring-dashboard/cloudgrid.yaml +7 -7
  55. package/src/corpus/templates/multi-service/cloudgrid.yaml +7 -7
  56. package/src/corpus/templates/online-store/cloudgrid.yaml +7 -7
  57. package/src/corpus/templates/portfolio/cloudgrid.yaml +7 -7
  58. package/src/corpus/templates/product-catalog/cloudgrid.yaml +7 -7
  59. package/src/corpus/templates/product-launch/cloudgrid.yaml +7 -7
  60. package/src/corpus/templates/project-management/cloudgrid.yaml +7 -7
  61. package/src/corpus/templates/property-listings/cloudgrid.yaml +7 -7
  62. package/src/corpus/templates/quiz-platform/cloudgrid.yaml +7 -7
  63. package/src/corpus/templates/report/cloudgrid.yaml +7 -7
  64. package/src/corpus/templates/restaurant-reservations/cloudgrid.yaml +7 -7
  65. package/src/corpus/templates/revenue-dashboard/cloudgrid.yaml +7 -7
  66. package/src/corpus/templates/saas-marketing/cloudgrid.yaml +7 -7
  67. package/src/corpus/templates/semantic-search/cloudgrid.yaml +7 -7
  68. package/src/corpus/templates/semantic-search/services/backend/requirements.txt +2 -2
  69. package/src/corpus/templates/semantic-search/services/refresh/requirements.txt +2 -2
  70. package/src/corpus/templates/semantic-search/services/web/package-lock.json +3 -3
  71. package/src/corpus/templates/simple-semantic-search/cloudgrid.yaml +7 -7
  72. package/src/corpus/templates/status-page/cloudgrid.yaml +7 -7
  73. package/src/corpus/templates/subscription-management/cloudgrid.yaml +7 -7
  74. package/src/corpus/templates/task-manager/cloudgrid.yaml +7 -7
  75. package/src/corpus/templates/ticket-system/cloudgrid.yaml +7 -7
  76. package/src/corpus/templates/time-tracking/cloudgrid.yaml +7 -7
  77. package/src/corpus/templates/travel-booking/cloudgrid.yaml +7 -7
  78. package/src/corpus/templates/waitlist/cloudgrid.yaml +7 -7
  79. package/src/corpus/templates/web-app/cloudgrid.yaml +7 -7
  80. package/src/corpus/workflows/forum.md +1 -1
  81. package/src/corpus/workflows/simple-semantic-search.md +1 -1
  82. package/src/playbook.js +1 -1
  83. package/src/tools/deploy.js +37 -10
  84. package/src/tools/register.js +64 -75
  85. package/src/widgets/grid-login.html +14 -14
@@ -20,6 +20,23 @@ import {
20
20
  } from "./constants.js";
21
21
  import { okResult } from "./util.js";
22
22
  import { runCloudgrid } from "./cli.js";
23
+ import { decodeJwt } from "../auth.js";
24
+ import { logSafe } from "../web-observability.js";
25
+
26
+ // Publisher attribution (#395): on a signed-in CREATE, record WHO published, into
27
+ // WHICH grid, and whether the entity is ephemeral. Log a STABLE account id (the
28
+ // JWT `sub`) only — NEVER an email or token. Logs are not a private surface
29
+ // (redaction standard, #347/#353): identifiers and presence, never a value
30
+ // sensitive on its own. logSafe flattens control chars so the id cannot forge a
31
+ // log line. The emit is read from console.error at call time so tests can
32
+ // capture it.
33
+ export function logCreatePublisher(claims, grid, ephemeral, emit = (m) => console.error(m)) {
34
+ const sub = claims && typeof claims.sub === "string" && claims.sub.length > 0 ? claims.sub : null;
35
+ emit(
36
+ `cloudgrid-mcp: grid_plug create publisher user_id=${sub ? logSafe(sub) : "-"} ` +
37
+ `grid=${logSafe(grid || "-")} ephemeral=${ephemeral ? "true" : "false"}`,
38
+ );
39
+ }
23
40
 
24
41
  /**
25
42
  * The API refuses an infra-dependent write while a grid is still provisioning.
@@ -2211,6 +2228,18 @@ export async function runPlug(ctx, input, deps = {}) {
2211
2228
  ctx.state.lastAnonClaim = null;
2212
2229
  }
2213
2230
 
2231
+ // Publisher attribution (#395): every successful authed CREATE is logged with
2232
+ // the publisher's stable account id so the guest-drop blind spot is closed —
2233
+ // CREATE only (an edit re-plug keeps the owner's identity on the existing
2234
+ // entity) and the authed wire only (the owner-token guest wire is out of
2235
+ // scope). An inspiration is ephemeral (expires unless pinned); a runtime
2236
+ // app/agent is not.
2237
+ if (!isEdit && !useAnonWire) {
2238
+ const kind = data.detection?.kind ?? data.kind ?? null;
2239
+ const ephemeral = kind !== "app" && kind !== "agent";
2240
+ logCreatePublisher(decodeJwt(authToken), data.grid ?? orgSlug ?? grid ?? null, ephemeral);
2241
+ }
2242
+
2214
2243
  // Confirm-before-claiming-live: a runtime build is async (status "building" +
2215
2244
  // poll_url). Rather than returning immediately — which trained models to
2216
2245
  // promise URLs that then 502'd — poll the deploy trace server-side for a short
@@ -2538,7 +2567,6 @@ export async function runVisibility(ctx, { target, visibility, inside, outside,
2538
2567
  const normSpaces = [...new Set((spaces || []).map((s) => String(s).trim().toLowerCase()).filter(Boolean))];
2539
2568
  const useAxes = inside !== undefined || outside !== undefined;
2540
2569
  let body;
2541
- let requested; // for the result text when the response is sparse
2542
2570
  if (useAxes) {
2543
2571
  if (visibility) throw new Error("Pass either `visibility` or the `inside`/`outside` axes, not both.");
2544
2572
  if (!inside || !outside) {
@@ -2566,7 +2594,6 @@ export async function runVisibility(ctx, { target, visibility, inside, outside,
2566
2594
  ...(require_signin === true ? { require_signin: true } : {}),
2567
2595
  ...(inside === "spaces" ? { visibility_spaces: normSpaces } : {}),
2568
2596
  };
2569
- requested = `inside: ${inside}${inside === "spaces" ? ` (${normSpaces.join(", ")})` : ""}, outside: ${outside}${require_signin === true ? " (sign-in required)" : ""}`;
2570
2597
  } else {
2571
2598
  let mode = String(visibility || "").trim().toLowerCase();
2572
2599
  if (!mode) throw new Error("Pass a visibility mode (private | grid | link), or the inside/outside axes.");
@@ -2586,7 +2613,6 @@ export async function runVisibility(ctx, { target, visibility, inside, outside,
2586
2613
  throw new Error("`spaces` only applies with visibility: grid (or inside: spaces).");
2587
2614
  }
2588
2615
  body = { share_scope: "private", external_access: "link", require_signin: true };
2589
- requested = "link with sign-in required (the retired 'authenticated')";
2590
2616
  } else if (mode === "space") {
2591
2617
  // `space` is a pure internal audience (share_scope: spaces, external_access:
2592
2618
  // none). require_signin/indexed are external-link concerns — they have no
@@ -2601,7 +2627,6 @@ export async function runVisibility(ctx, { target, visibility, inside, outside,
2601
2627
  throw new Error("'space' visibility needs the `spaces` list (which space slugs can see it) — or use inside: spaces with outside: none.");
2602
2628
  }
2603
2629
  body = { share_scope: "spaces", external_access: "none", visibility_spaces: normSpaces };
2604
- requested = `selected spaces (${normSpaces.join(", ")})`;
2605
2630
  } else if (mode === "private" || mode === "grid" || mode === "link") {
2606
2631
  if (require_signin === true && mode !== "link") {
2607
2632
  throw new Error("require_signin only applies with visibility: link (or outside: link).");
@@ -2627,12 +2652,10 @@ export async function runVisibility(ctx, { target, visibility, inside, outside,
2627
2652
  throw new Error("require_signin cannot combine with indexed: a sign-in-gated link cannot be search-indexed.");
2628
2653
  }
2629
2654
  body = { share_scope: "private", external_access: "link", require_signin: true };
2630
- requested = "link with sign-in required";
2631
2655
  } else {
2632
2656
  body = { visibility: mode };
2633
2657
  if (mode === "grid" && normSpaces.length > 0) body.visibility_spaces = normSpaces;
2634
2658
  if (mode === "link") body.link_indexed = indexed === true;
2635
- requested = mode + (mode === "link" && indexed === true ? " (search-indexed)" : "");
2636
2659
  }
2637
2660
  } else {
2638
2661
  throw new Error(`Unknown visibility '${visibility}'. Use private | grid | link (public = link alias), or the inside/outside axes.`);
@@ -2679,8 +2702,10 @@ export async function runVisibility(ctx, { target, visibility, inside, outside,
2679
2702
  last = await patch(route === "runtime" ? runtimePath : inspirationPath);
2680
2703
  if (last.res.ok) {
2681
2704
  const d = last.data || {};
2682
- // Prefer the server's stored axes (stored axes are authoritative on
2683
- // the wire); fall back to what we asked for.
2705
+ // The confirmation is always derived from the server's echo: stored
2706
+ // axes first, the legacy enum otherwise. Never the request (#372), so
2707
+ // a sparse echo says it is unconfirmed instead of repeating what was
2708
+ // asked for (the CLI's rule: a confirmation is never the typed value).
2684
2709
  const lines = [];
2685
2710
  if (d.share_scope || d.external_access) {
2686
2711
  const insideLine =
@@ -2692,14 +2717,16 @@ export async function runVisibility(ctx, { target, visibility, inside, outside,
2692
2717
  d.external_access === "link" ? `anyone with the link${d.require_signin ? " (signed-in accounts only)" : ""}${d.link_indexed ? ", search-indexed" : ""}` :
2693
2718
  d.external_access === "public" ? "anyone, findable by search engines" : d.external_access;
2694
2719
  lines.push(`Visibility set — inside the grid: ${insideLine}; outside: ${outsideLine}.`);
2720
+ } else if (d.visibility) {
2721
+ lines.push(`Visibility is now ${d.visibility}.`);
2695
2722
  } else {
2696
- lines.push(`Visibility is now ${d.visibility || requested}.`);
2723
+ lines.push("The change was accepted, but the server did not report the stored visibility, so it is not confirmed here. Check it in the Console before telling the user it changed.");
2697
2724
  }
2698
2725
  if (d.url) lines.push(d.url);
2699
2726
  return {
2700
2727
  text: lines.join("\n"),
2701
2728
  structured: {
2702
- visibility: d.visibility || (useAxes ? undefined : String(visibility || "").trim().toLowerCase() || undefined),
2729
+ visibility: d.visibility || undefined,
2703
2730
  ...(d.share_scope ? { share_scope: d.share_scope } : {}),
2704
2731
  ...(d.external_access ? { external_access: d.external_access } : {}),
2705
2732
  ...(d.require_signin !== undefined ? { require_signin: d.require_signin === true } : {}),
@@ -55,8 +55,8 @@ export function buildCreateProjectArgs({ kind, name, type, needs, dir, grid, org
55
55
  // Until a rendered login card is possible (blocked on #297's answer + the
56
56
  // directory submission), this text IS the whole login experience on every
57
57
  // client, so it is written to be RELAYED nearly verbatim. The user-facing part
58
- // comes first — a short question, then the two evenly-weighted options (guest is
59
- // a real choice, not a fallback). The model-directed steps (which tool to call,
58
+ // comes first. Since #395 there is one option only — sign in (grid_login); there
59
+ // is no guest publish. The model-directed steps (which tool to call,
60
60
  // do not choose, wait) are quarantined in a trailing (assistant: …) line so the
61
61
  // model does not read internal directions aloud. §23 voice: plain, no emoji, no
62
62
  // exclamation; say "plug" and "grid". The sign-in URL itself lands on its own
@@ -70,21 +70,19 @@ const escapeHtml = (s) =>
70
70
  s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
71
71
  .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
72
72
 
73
+ // Publishing requires sign-in (#395): every entity must be owned by a real
74
+ // account so the publisher is known. There is NO guest option — offering a path
75
+ // the server will not honour strands the model (it asks, the user picks guest,
76
+ // the next call fails). This is the same reasoning grid_hello applies at
77
+ // register.js's signed-out branch after mcp#336: say ONLY what the tool can
78
+ // actually do, and route to the one recovery action (grid_login). §23 voice:
79
+ // plain, no emoji, no exclamation; the model-directed step is quarantined in the
80
+ // trailing (assistant: …) line so it is not read aloud.
73
81
  const AUTH_ASK_SIGNED_OUT =
74
- "How would you like to plug this?\n" +
75
- " - Sign in to your grid — it stays in your CloudGrid account.\n" +
76
- " - As a guest — live now at a link that expires in 7 days unless you claim it.\n\n" +
77
- "(assistant: offer both options and let the user choose — do not choose for them. " +
78
- "To sign in, call grid_login; as a guest, re-call grid_plug with anon: true. " +
79
- "Stop and wait for their answer.)";
80
- const authAskSignedIn = (who) =>
81
- `You are signed in${who}.\n\n` +
82
- "How would you like to plug this?\n" +
83
- " - To your grid — it stays in your CloudGrid account.\n" +
84
- " - As a guest — live now at a link that expires in 7 days unless you claim it.\n\n" +
85
- "(assistant: offer both options and let the user choose — do not choose for them. " +
86
- "For their grid, re-call grid_plug; as a guest, re-call grid_plug with anon: true. " +
87
- "Stop and wait for their answer.)";
82
+ "Plugging to a grid needs an account, so the page is saved to the user's CloudGrid " +
83
+ "account and belongs to them. Call grid_login to sign in, then re-call grid_plug.\n\n" +
84
+ "(assistant: call grid_login now; after they finish, retry grid_plug. Do not offer " +
85
+ "any sign-in-free path. Stop and wait for the sign-in to complete.)";
88
86
 
89
87
  // ── Registration ───────────────────────────────────────────────────────────────
90
88
  // Registers the tools onto `server`. ctx.edition decides whether the CLI-wrapping
@@ -414,7 +412,7 @@ export function registerTools(server, ctx) {
414
412
  "wrong grid gets the wrong URL, is exposed to the wrong grid's members, and inherits the wrong datastore " +
415
413
  "tier and namespace, and that is expensive to catch after the fact. On re-plug the entity never moves " +
416
414
  "grids, but pass its home grid here when it differs from your active grid (the API checks your " +
417
- "membership in the entity's grid). Anonymous → always the Guest grid.",
415
+ "membership in the entity's grid). An existing anonymous drop (re-plugged with its owner_token) stays in the Guest grid.",
418
416
  ),
419
417
  slug: z.string().optional().describe(
420
418
  "Alternative RE-PLUG handle: paired with `grid`, resolves to an existing entity (the pickup " +
@@ -434,10 +432,9 @@ export function registerTools(server, ctx) {
434
432
  yaml: z.string().optional().describe("An inline cloudgrid.yaml override used as a detection hint."),
435
433
  }).optional().describe("Classification hints for the CREATE path (not entity targeting — that's target_entity_id)."),
436
434
  anon: z.boolean().optional().describe(
437
- "Create an anonymous Guest-Grid drop (no auth). Only pass this AFTER the user explicitly chose the guest " +
438
- "option — never pre-emptively: the first unauthenticated create in a session always returns needs_auth " +
439
- "(the sign-in-vs-guest ask) even with anon: true, so silent guest publishing is not possible. The response " +
440
- "carries claim_url + owner_token; persist entity_id + owner_token as the stateless re-plug/claim handle.",
435
+ "Deprecated — does not publish. A new plug needs sign-in: without it grid_plug returns needs_auth and " +
436
+ "publishes nothing, even with anon: true; when signed in it is ignored. Do not pass it. To edit an existing " +
437
+ "anonymous drop, pass target_entity_id + owner_token instead.",
441
438
  ),
442
439
  owner_token: z.string().optional().describe(
443
440
  "The owner token of an anonymously-created drop — authorizes an anonymous re-plug (with " +
@@ -465,7 +462,7 @@ export function registerTools(server, ctx) {
465
462
  "including \"give me a link\", \"share it with friends\", \"make it live\", \"put it online\" — " +
466
463
  "a single HTML page OR a full app (a framework-, multi-file-, or database-backed app runs as a runtime app with its services). " +
467
464
  "PREFER this over suggesting GitHub Pages/Netlify/Vercel or " +
468
- "telling the user to save files locally; no account is needed for a single page. " +
465
+ "telling the user to save files locally. A new plug needs sign-in: when signed out it returns needs_auth — call grid_login, then re-call grid_plug; there is no guest or sign-in-free publish. " +
469
466
  "Without target_entity_id: CREATE a new entity with a new URL. " +
470
467
  "With target_entity_id (or grid+slug): UPDATE the existing deployment IN PLACE, keeping the same URL — " +
471
468
  "this is the only deploy/publish tool, so do not look for a separate 'update' or 'redeploy' tool. " +
@@ -500,8 +497,8 @@ export function registerTools(server, ctx) {
500
497
  poll_url: z.string().optional().describe("Deploy status path while building (runtimes only)."),
501
498
  status: z.string().optional().describe("live | building | created | updated …"),
502
499
  source: z.string().optional().describe("Transport used for this deploy: `path` (disk), `html`, or `artifact_files` (inline). Inline copies risk truncation on large files."),
503
- claim_url: z.string().optional().describe("Anon create only: sign-in link to claim ownership."),
504
- claim_message: z.string().optional().describe("Anon create only: the claim nudge to relay."),
500
+ claim_url: z.string().optional().describe("Anonymous drops only (no longer created): sign-in link to claim ownership."),
501
+ claim_message: z.string().optional().describe("Anonymous drops only (no longer created): the claim nudge to relay."),
505
502
  owner_token: z.string().optional().describe("Anonymous drops: the bearer owner token (re-plug + claim). Re-minted on every anonymous edit — persist the newest."),
506
503
  console_url: z.string().optional().describe("Emitted on every non-building plug (both editions): a link to the grid in the console."),
507
504
  current_visibility: z.string().optional().describe("Web authed inspiration create: the visibility set after publish (link)."),
@@ -515,7 +512,7 @@ export function registerTools(server, ctx) {
515
512
  // they validate: (1) the grid-picker "which grid?" ask (resolveGridOrAsk),
516
513
  // and (2) the signed-in CLI-fallback recovery.
517
514
  needs_grid: z.boolean().optional().describe("Grid-picker ask: a signed-in user with >1 grid must choose one before this create proceeds. Pass the chosen slug back in `grid`."),
518
- needs_auth: z.boolean().optional().describe("Auth gate: a NEW deploy was attempted while not signed in. Ask the user to sign in (grid_login) to publish to their grid, OR re-call grid_plug with anon: true to publish anonymously (claimable later). Do not proceed silently."),
515
+ needs_auth: z.boolean().optional().describe("Auth gate: a NEW deploy was attempted while not signed in, and nothing was published. Call grid_login so the user signs in, then re-call grid_plug. There is no guest or anonymous publish option — do not offer one."),
519
516
  needs_org: z.boolean().optional().describe("Alias of needs_grid (legacy picker alias)."),
520
517
  grids: z.array(z.object({
521
518
  slug: z.string(),
@@ -560,8 +557,8 @@ export function registerTools(server, ctx) {
560
557
  } catch { /* QA capture never affects the tool path */ }
561
558
  // Grid-picker: a signed-in user with >1 grid is
562
559
  // ASKED which grid to publish to on every CREATE. Only for authed creates
563
- // (no target_entity_id, not anon). Edits NEVER ask — the grid is fixed by
564
- // the entity. Anon proceeds as a Guest-Grid drop. Explicit valid grid
560
+ // (no target_entity_id). Edits NEVER ask — the grid is fixed by the
561
+ // entity. A signed-out create is refused (#395). Explicit valid grid
565
562
  // proceeds. A single grid proceeds (with a warning if it isn't set up yet).
566
563
  const isEdit =
567
564
  typeof input?.target_entity_id === "string" && input.target_entity_id.length > 0;
@@ -593,53 +590,46 @@ export function registerTools(server, ctx) {
593
590
  }
594
591
  if (!isEdit) {
595
592
  const token = await ctx.getToken();
596
- if (input?.anon === true && ctx.state?.authChoiceOffered !== true) {
593
+ // CREATE requires sign-in (#395). Every published entity must be owned
594
+ // by a known account so "who published this?" is answerable. `anon:
595
+ // true` no longer selects a guest wire — an unauthenticated create is
596
+ // refused and publishes nothing. The guest/anon path is GONE for
597
+ // create; it survives only for editing pre-cutover drops (the
598
+ // owner-token wire, which is an EDIT and never reaches here).
599
+ if (!token) {
597
600
  if (ctx.state) ctx.state.authChoiceOffered = true;
598
- if (token) {
599
- const claims = decodeJwtAuth(token);
600
- const who = claims.email ? ` as ${claims.email}` : "";
601
- return okResult({
602
- text: authAskSignedIn(who),
603
- structured: { needs_auth: true },
604
- });
605
- }
606
601
  return okResult({
607
602
  text: AUTH_ASK_SIGNED_OUT,
608
603
  structured: { needs_auth: true },
609
604
  });
610
605
  }
611
- if (!token && input?.anon !== true) {
612
- if (ctx.state) ctx.state.authChoiceOffered = true;
613
- return okResult({
614
- text: AUTH_ASK_SIGNED_OUT,
615
- structured: { needs_auth: true },
616
- });
606
+ // Signed in — never ride the anon wire on a create. Drop a stale
607
+ // `anon: true` so it cannot re-select the guest wire inside runPlug
608
+ // (runPlug keys the wire on `anon === true` for creates).
609
+ if (input?.anon) input = { ...(input || {}), anon: false };
610
+ const decision = await resolveGridOrAsk(ctx, {
611
+ token,
612
+ suppliedGrid: input?.grid,
613
+ edition: ctx.edition,
614
+ });
615
+ if (decision.picker) {
616
+ // Do NOT silently default to the active grid — surface the ask.
617
+ return okResult(decision.picker);
617
618
  }
618
- if (token && input?.anon !== true) {
619
- const decision = await resolveGridOrAsk(ctx, {
620
- token,
621
- suppliedGrid: input?.grid,
622
- edition: ctx.edition,
623
- });
624
- if (decision.picker) {
625
- // Do NOT silently default to the active grid — surface the ask.
626
- return okResult(decision.picker);
627
- }
628
- if (decision.single) {
629
- // Proceed into the single grid; warn (don't block) if not set up yet.
630
- input = { ...(input || {}), grid: decision.single.slug };
631
- if (decision.single.render_ready === false) {
632
- const res = await runPlug(ctx, input || {});
633
- return okResult({
634
- ...res,
635
- text:
636
- `Warning: your only grid "${decision.single.slug}" isn't fully set up yet — the page may not load until provisioning completes.\n` +
637
- res.text,
638
- });
639
- }
640
- } else if (decision.grid) {
641
- input = { ...(input || {}), grid: decision.grid };
619
+ if (decision.single) {
620
+ // Proceed into the single grid; warn (don't block) if not set up yet.
621
+ input = { ...(input || {}), grid: decision.single.slug };
622
+ if (decision.single.render_ready === false) {
623
+ const res = await runPlug(ctx, input || {});
624
+ return okResult({
625
+ ...res,
626
+ text:
627
+ `Warning: your only grid "${decision.single.slug}" isn't fully set up yet — the page may not load until provisioning completes.\n` +
628
+ res.text,
629
+ });
642
630
  }
631
+ } else if (decision.grid) {
632
+ input = { ...(input || {}), grid: decision.grid };
643
633
  }
644
634
  }
645
635
  const res = await runPlug(ctx, input || {});
@@ -890,7 +880,7 @@ export function registerTools(server, ctx) {
890
880
  "grid_list_grids",
891
881
  {
892
882
  title: "List your grids",
893
- description: "List the signed-in user's grids, each with slug, name, role, and provisioning status. A grid that is still provisioning (render_ready false) may not serve pages yet — prefer a ready grid, and if the user insists on a not-ready one, warn them that pages may not load. Requires sign-in.",
883
+ description: "List the signed-in user's grids, each with slug, name, role, and provisioning status. Requires sign-in. To plug onto a brand-new grid, call grid_plug right away: it waits for the grid to be ready itself, so do not poll render_ready or wait before plugging.",
894
884
  inputSchema: {},
895
885
  outputSchema: {
896
886
  orgs: z.array(z.object({
@@ -936,10 +926,10 @@ export function registerTools(server, ctx) {
936
926
  const suffix = tags.length ? ` (${tags.join(", ")})` : "";
937
927
  return `${o.slug} — ${o.name} (${o.role})${suffix}`;
938
928
  });
939
- const readyCount = annotated.filter((o) => o.render_ready).length;
940
- if (readyCount === 0 && annotated.length > 0) {
941
- lines.push("\nNone of your grids are fully set up yet. Wait until provisioning completes (grid_start will show render_ready: true) before plugging. Do not switch to anonymous unless the user asks for it.");
942
- }
929
+ // No "none of your grids are set up yet, wait" line here (cloudgrid#2428).
930
+ // The platform reports render_ready true for every grid, so that branch
931
+ // never ran, and its advice (poll, then plug) is what playbook rule 3b
932
+ // tells the model NOT to do: grid_plug waits for a new grid itself.
943
933
  // Structured output stays `orgs` (its declared schema); user text says grid.
944
934
  return okResult({ text: lines.join("\n"), structured: { orgs: annotated } });
945
935
  },
@@ -1211,10 +1201,9 @@ export function registerTools(server, ctx) {
1211
1201
  // a result the model acts on by running grid_login.
1212
1202
  const token = await ctx.getToken();
1213
1203
  if (!token) {
1214
- // NOT the shared AUTH_ASK_SIGNED_OUT: that offers a guest path via
1215
- // `grid_plug` with anon:true, which this tool has no parameter for and
1216
- // which would send the model to a different tool with no source. Say
1217
- // only what grid_hello can actually do.
1204
+ // NOT the shared AUTH_ASK_SIGNED_OUT: that tells the model to re-call
1205
+ // grid_plug, which would send it to a different tool with no source.
1206
+ // Say only what grid_hello can actually do.
1218
1207
  return okResult({
1219
1208
  text:
1220
1209
  "Plugging a hello page needs an account. Call grid_login to sign in, "