@noodleseed/agent-kit 0.91.0 → 0.93.0

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 (110) hide show
  1. package/manifest.json +743 -279
  2. package/package.json +1 -1
  3. package/skills/claude-code/SKILL.md +1 -1
  4. package/skills/claude-code/authoring-mcp-servers/SKILL.md +4 -4
  5. package/skills/claude-code/authoring-mcp-servers/references/authoring-workflow.md +451 -0
  6. package/skills/claude-code/authoring-mcp-servers/references/build-an-mcp-server.md +56 -0
  7. package/skills/claude-code/authoring-mcp-servers/references/sdk-surface.md +258 -0
  8. package/skills/claude-code/building-mcp-apps/SKILL.md +4 -4
  9. package/skills/claude-code/building-mcp-apps/references/build-an-mcp-app.md +53 -0
  10. package/skills/claude-code/building-mcp-apps/references/experience-design.md +155 -0
  11. package/skills/claude-code/building-mcp-apps/references/widgets-and-apps.md +277 -0
  12. package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +3 -3
  13. package/skills/claude-code/connecting-apis-to-mcp/references/authoring-workflow.md +451 -0
  14. package/skills/claude-code/connecting-apis-to-mcp/references/connect-an-api.md +245 -0
  15. package/skills/claude-code/creating-product-agent-guides/SKILL.md +2 -2
  16. package/skills/claude-code/creating-product-agent-guides/references/product-agent-guides.md +55 -0
  17. package/skills/claude-code/debugging-mcp-delivery/SKILL.md +4 -4
  18. package/skills/claude-code/debugging-mcp-delivery/references/inspect-hosted.md +26 -0
  19. package/skills/claude-code/debugging-mcp-delivery/references/troubleshooting.md +39 -0
  20. package/skills/claude-code/debugging-mcp-delivery/references/verify-and-recover.md +67 -0
  21. package/skills/claude-code/deploying-mcp-services/SKILL.md +3 -3
  22. package/skills/claude-code/deploying-mcp-services/references/cli-commands.md +106 -0
  23. package/skills/claude-code/deploying-mcp-services/references/deploy-and-ops.md +25 -0
  24. package/skills/claude-code/designing-mcp-products/SKILL.md +3 -3
  25. package/skills/claude-code/designing-mcp-products/references/authoring-workflow.md +451 -0
  26. package/skills/claude-code/designing-mcp-products/references/experience-design.md +155 -0
  27. package/skills/claude-code/embedding-mcp-assistants/SKILL.md +11 -10
  28. package/skills/claude-code/embedding-mcp-assistants/references/authoring-workflow.md +451 -0
  29. package/skills/claude-code/embedding-mcp-assistants/references/embedded-assistant.md +876 -0
  30. package/skills/claude-code/examples/acme-discovery/README.md +39 -1
  31. package/skills/claude-code/examples/acme-discovery/site/index.html +189 -0
  32. package/skills/claude-code/examples/acme-discovery/src/server.ts +5 -0
  33. package/skills/claude-code/examples/acme-discovery/test/server.test.ts +18 -0
  34. package/skills/claude-code/examples/acme-discovery/test/site-page.test.ts +50 -0
  35. package/skills/claude-code/examples/customer-auth/README.md +200 -52
  36. package/skills/claude-code/examples/customer-auth/src/server.ts +3 -0
  37. package/skills/claude-code/examples/customer-auth/test/server.test.ts +6 -0
  38. package/skills/claude-code/examples/customer-auth/vitest.config.ts +6 -0
  39. package/skills/claude-code/examples/food-ordering/README.md +1 -1
  40. package/skills/claude-code/examples/food-ordering/src/server.ts +1 -0
  41. package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
  42. package/skills/claude-code/publishing-mcp-integrations/SKILL.md +3 -3
  43. package/skills/claude-code/publishing-mcp-integrations/references/app-directory-compliance.md +59 -0
  44. package/skills/claude-code/publishing-mcp-integrations/references/publishing.md +63 -0
  45. package/skills/claude-code/references/authoring-workflow.md +8 -0
  46. package/skills/claude-code/references/embedded-assistant.md +258 -66
  47. package/skills/claude-code/references/sdk-surface.md +1 -1
  48. package/skills/claude-code/reporting-noodle-feedback/SKILL.md +2 -2
  49. package/skills/claude-code/reporting-noodle-feedback/references/feedback.md +80 -0
  50. package/skills/claude-code/verifying-mcp-delivery/SKILL.md +3 -3
  51. package/skills/claude-code/verifying-mcp-delivery/references/test-in-hosts.md +39 -0
  52. package/skills/claude-code/verifying-mcp-delivery/references/verify-and-recover.md +67 -0
  53. package/skills/claude-code/wrapping-existing-applications/SKILL.md +4 -4
  54. package/skills/claude-code/wrapping-existing-applications/references/authoring-workflow.md +451 -0
  55. package/skills/claude-code/wrapping-existing-applications/references/tool-design.md +107 -0
  56. package/skills/claude-code/wrapping-existing-applications/references/wrap-existing-app.md +65 -0
  57. package/skills/codex/SKILL.md +1 -1
  58. package/skills/codex/authoring-mcp-servers/SKILL.md +4 -4
  59. package/skills/codex/authoring-mcp-servers/references/authoring-workflow.md +451 -0
  60. package/skills/codex/authoring-mcp-servers/references/build-an-mcp-server.md +56 -0
  61. package/skills/codex/authoring-mcp-servers/references/sdk-surface.md +258 -0
  62. package/skills/codex/building-mcp-apps/SKILL.md +4 -4
  63. package/skills/codex/building-mcp-apps/references/build-an-mcp-app.md +53 -0
  64. package/skills/codex/building-mcp-apps/references/experience-design.md +155 -0
  65. package/skills/codex/building-mcp-apps/references/widgets-and-apps.md +277 -0
  66. package/skills/codex/connecting-apis-to-mcp/SKILL.md +3 -3
  67. package/skills/codex/connecting-apis-to-mcp/references/authoring-workflow.md +451 -0
  68. package/skills/codex/connecting-apis-to-mcp/references/connect-an-api.md +245 -0
  69. package/skills/codex/creating-product-agent-guides/SKILL.md +2 -2
  70. package/skills/codex/creating-product-agent-guides/references/product-agent-guides.md +55 -0
  71. package/skills/codex/debugging-mcp-delivery/SKILL.md +4 -4
  72. package/skills/codex/debugging-mcp-delivery/references/inspect-hosted.md +26 -0
  73. package/skills/codex/debugging-mcp-delivery/references/troubleshooting.md +39 -0
  74. package/skills/codex/debugging-mcp-delivery/references/verify-and-recover.md +67 -0
  75. package/skills/codex/deploying-mcp-services/SKILL.md +3 -3
  76. package/skills/codex/deploying-mcp-services/references/cli-commands.md +106 -0
  77. package/skills/codex/deploying-mcp-services/references/deploy-and-ops.md +25 -0
  78. package/skills/codex/designing-mcp-products/SKILL.md +3 -3
  79. package/skills/codex/designing-mcp-products/references/authoring-workflow.md +451 -0
  80. package/skills/codex/designing-mcp-products/references/experience-design.md +155 -0
  81. package/skills/codex/embedding-mcp-assistants/SKILL.md +11 -10
  82. package/skills/codex/embedding-mcp-assistants/references/authoring-workflow.md +451 -0
  83. package/skills/codex/embedding-mcp-assistants/references/embedded-assistant.md +876 -0
  84. package/skills/codex/examples/acme-discovery/README.md +39 -1
  85. package/skills/codex/examples/acme-discovery/site/index.html +189 -0
  86. package/skills/codex/examples/acme-discovery/src/server.ts +5 -0
  87. package/skills/codex/examples/acme-discovery/test/server.test.ts +18 -0
  88. package/skills/codex/examples/acme-discovery/test/site-page.test.ts +50 -0
  89. package/skills/codex/examples/customer-auth/README.md +200 -52
  90. package/skills/codex/examples/customer-auth/src/server.ts +3 -0
  91. package/skills/codex/examples/customer-auth/test/server.test.ts +6 -0
  92. package/skills/codex/examples/customer-auth/vitest.config.ts +6 -0
  93. package/skills/codex/examples/food-ordering/README.md +1 -1
  94. package/skills/codex/examples/food-ordering/src/server.ts +1 -0
  95. package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
  96. package/skills/codex/publishing-mcp-integrations/SKILL.md +3 -3
  97. package/skills/codex/publishing-mcp-integrations/references/app-directory-compliance.md +59 -0
  98. package/skills/codex/publishing-mcp-integrations/references/publishing.md +63 -0
  99. package/skills/codex/references/authoring-workflow.md +8 -0
  100. package/skills/codex/references/embedded-assistant.md +258 -66
  101. package/skills/codex/references/sdk-surface.md +1 -1
  102. package/skills/codex/reporting-noodle-feedback/SKILL.md +2 -2
  103. package/skills/codex/reporting-noodle-feedback/references/feedback.md +80 -0
  104. package/skills/codex/verifying-mcp-delivery/SKILL.md +3 -3
  105. package/skills/codex/verifying-mcp-delivery/references/test-in-hosts.md +39 -0
  106. package/skills/codex/verifying-mcp-delivery/references/verify-and-recover.md +67 -0
  107. package/skills/codex/wrapping-existing-applications/SKILL.md +4 -4
  108. package/skills/codex/wrapping-existing-applications/references/authoring-workflow.md +451 -0
  109. package/skills/codex/wrapping-existing-applications/references/tool-design.md +107 -0
  110. package/skills/codex/wrapping-existing-applications/references/wrap-existing-app.md +65 -0
@@ -3,7 +3,7 @@
3
3
  ## Contents
4
4
 
5
5
  - Architecture
6
- - Choose the host experience
6
+ - Select the architecture before code
7
7
  - Author and validate
8
8
  - Product workflow guidance
9
9
  - Customize the presentation
@@ -16,6 +16,7 @@
16
16
  - The session response
17
17
  - Choose a browser renderer
18
18
  - Host readiness and promotion
19
+ - Launch and qualified-usage proof
19
20
  - Toolchain requirements
20
21
  - Verify the boundary
21
22
  - Troubleshooting: symptom to diagnosis
@@ -35,9 +36,32 @@ Keep every credential and identity layer separate:
35
36
  | SaaS backend | `NOODLE_SERVICE_URL`, `NOODLE_ASSISTANT_CLIENT_ID`, `NOODLE_ASSISTANT_CLIENT_SECRET`, `PUBLIC_APP_ORIGIN` | Backend-only environment or secret manager; never browser code or public-prefixed variables |
36
37
  | Browser | Short-lived assistant session only | In memory; never a client secret, model key, connector credential, or raw application session |
37
38
 
38
- ## Choose the host experience
39
+ ## Select the architecture before code
39
40
 
40
- Before choosing code, ask the user which experience belongs in the existing product: the built-in floating, inline, or drawer assistant; a custom chat-first renderer; or a headless client feeding application-owned UI. Default to the built-in floating assistant only when the user has no preference. Preserve the host application until the user opens or submits into the assistant; do not copy one flagship layout into every product.
41
+ Do not edit either codebase until one architecture brief is decision-complete. Name the external end user, the conversational job, one to three workflows, the exact access mode, renderer, existing host framework, identity/session boundary, server-owned routing authority, model owner, exact origin and mount point, and requested evidence level.
42
+
43
+ If the external user, job, or workflow set is still vague, hand off to `designing-mcp-products` before code. Preserve its bounded product outcome and resume this workflow only when the value path and one representative success path are explicit.
44
+
45
+ Treat the MCP application and embedding application as two code ownership roots, usually two repositories and sometimes two explicitly separate directories in one repository:
46
+
47
+ | Code owner | Owns | Must not own |
48
+ | --- | --- | --- |
49
+ | MCP application | `server.ts`, tools, App views, access surfaces, portable branding defaults, model declaration, validation, and deployment | Host cookies, login UI, application middleware, or browser mounting |
50
+ | Embedding application | Existing framework and package manager, user authentication, same-origin session route when required, browser mount, CSP, cookies/CSRF, and production build | MCP tool semantics, model keys, assistant client secrets in browser code, or caller-selected tenant routing |
51
+
52
+ Develop locally as two local processes: `noodle dev` serves the MCP application, while the embedding application runs its existing development command at the exact authored loopback origin. Do not replace either process with a second scaffold, package manager, chat transport, or copied flagship shell.
53
+
54
+ Select exactly one recommended topology; do not return an unranked menu:
55
+
56
+ | User boundary | Recommended topology | Identity and routing consequence |
57
+ | --- | --- | --- |
58
+ | Anonymous public visitor | `publicWebsite(...)` plus the hosted embed ID and managed renderer | No backend session route, `${user}`, roles/scopes, delegated credentials, or customer routing. Require the explicit capability allowlist and confirmation for side effects. |
59
+ | Signed-in application user | `authenticatedWebsite(...)` plus a same-origin backend session route and the managed renderer | The host backend authenticates the user, calls `createAssistantSession`, and supplies only verified identity, claims, preferences, and server-owned routing. |
60
+ | Visitor who may sign in mid-conversation | `publicWebsite({ signIn: true })` plus the hosted embed ID, host application login, and ticket elevation | Anonymous capabilities remain bounded; the host backend spends the ticket after login and continues through a same-origin session endpoint on an allowed origin. |
61
+
62
+ Use the existing host framework and its package manager. Recommend the managed renderer unless the product names a concrete UI or integration benefit that requires the headless client; a custom renderer accepts the complete transcript-part obligation described below. Recommend `noodleManaged()` when Noodle owns the managed-cloud model for the exact eligible target; recommend `openAICompatible(...)` only when the operator or self-hosted deployment owns that model endpoint and credentials. Never present both as unfinished choices.
63
+
64
+ The architecture brief must state the selected topology, why it fits the named user and job, the two code owners and two local processes, credential and identity flow, routing authority, model owner, files expected to change, and the first requested evidence boundary. If identity/session ownership, exact origin, tenant-routing authority, model ownership, or evidence target is unavailable, return a blocked architecture brief naming the missing owner or decision and do not edit either codebase.
41
65
 
42
66
  ## Author and validate
43
67
 
@@ -117,11 +141,17 @@ labels: {
117
141
 
118
142
  The sign-in card renders on the same themed chrome as every proposal card and follows the server `branding`. Authoring `signUpAction` adds a second button; leaving it out renders none. Both buttons raise the same `assistant-sign-in-requested` event with the same single-use ticket — the detail adds `intent: "sign-in" | "sign-up"` so the page routes `sign-up` to its registration page instead of its login. The ticket spend after account creation is identical to the one after sign-in; the service does not care which path produced the session. Headless renderers receive the same moment as a `data-sign-in` transcript part from `subscribeChat` (it has no status and is not respondable through `client.respond` — resolution is the elevated session).
119
143
 
120
- Elevation runs through the **host application own login**, never a Noodle-operated one. The widget raises `assistant-sign-in-requested` with a single-use `signInTicket` in its detail; the page signs the visitor in as it already does, then its backend spends the ticket with `createAssistantSession({ ..., signInTicket })` from `@noodleseed/assistant/server` — the same session exchange, its own client credentials. A refused spend throws a typed `AssistantSessionExchangeError`: branch on `elevationRefusal` (`elevation_ticket_expired` re-prompt; `elevation_tenant_mismatch` alert, never retry). Possession of the ticket alone elevates nothing, and the service checks the client tenant owns that conversation. The ticket is not the server-held interaction continuation — that value never reaches browser code; this one exists to travel through the page.
144
+ #### Complete mixed-mode handoff
145
+
146
+ 1. **Capture the ticket.** The widget raises `assistant-sign-in-requested` with a single-use `signInTicket`. POST it to the host application same-origin backend and bind it to the short-lived login transaction; keep it out of query strings, logs, analytics, and durable browser storage.
147
+ 2. **Use the existing login.** Send the visitor through the host application full-page redirect for sign-in or sign-up. Noodle never becomes the customer identity provider.
148
+ 3. **Elevate on the backend.** After authentication, recover the bound ticket and call `createAssistantSession({ ..., signInTicket })` with the verified user, server-owned routing, backend client credentials, and the exact **origin the conversation will continue on**. The allowed destination may differ from the anonymous marketing origin; successful elevation rebinds the session to it.
149
+ 4. **Reattach on the destination origin.** Forward the helper response unchanged through the customer-owned same-origin session endpoint. The widget uses `credentials: "same-origin"`; pointing it at a cross-origin endpoint guarantees a cookie-less `401`. The elevated token replaces the anonymous token, which is dead.
150
+ 5. **Resume and replay.** The widget repaints the bounded visible transcript from `endpoints.transcript`, emits one secure-continuation status, re-resolves the latest linked App view against the current surface, restores at most one live pending confirmation/input request, and then shows follow-up suggestions. Restoring an interaction suppresses auto-resume. Replay includes only rows the panel showed, never tool internals or a spent ticket, and never App HTML, a DOM snapshot, or exact scroll/focus. On an older service without that endpoint the panel starts visually fresh while the model still remembers, so promise only that "the assistant remembers". Pass `restoreConversation: false` when a privacy-sensitive application must suppress browser replay while retaining bounded server/model continuity.
121
151
 
122
- The conversation is kept server-side: same session, new token, the anonymous one dead. By default the pending request also completes itself: the service re-attempts the intercepted tool under the new principal and streams it as the elevated session first turn (one-shot; mooted if the visitor types first; confirm-gated tools stop at their confirmation card; pass `resume: false` beside the ticket to disable). On a backend-exchanged reattach the widget also repaints the bounded visible transcript (`endpoints.transcript`) before the resume runs only rows the panel actually showed replay, never tool internals or a spent ticket. On an older service without the endpoint the panel starts visually fresh while the model still remembers, so keep copy honest either way: "the assistant remembers". Do not build a second identity provider or client-side resume scaffolding for this.
152
+ Possession of the ticket alone elevates nothing, and the service checks that the backend client tenant owns the conversation. A refused spend throws a typed `AssistantSessionExchangeError`: branch on `elevationRefusal` (`elevation_ticket_expired` re-prompt; `elevation_tenant_mismatch` alert and never retry; `elevation_state_conflict` means the ticket is spent, so open or restart the signed-in flow and never merge generic state). A refused origin does not burn the ticket. The ticket is not the server-held interaction continuation that value never reaches browser code.
123
153
 
124
- When the login lives on a different origin (marketing site + app), the flow is: the visitor signs in via full-page redirect as the site already does; the backend spends the ticket presenting the **origin the conversation will continue on** (any origin in the deployment allowlist elevation re-pins the session there, and CORS follows); the token reaches the widget through the customer own **same-origin** session endpoint on that origin. The redirect handoff is mandatory, not stylistic: the widget calls the session endpoint with `credentials: "same-origin"`, so pointing a marketing page at a cross-origin endpoint is a guaranteed cookie-less 401. Persist the ticket across the login redirect (single-use, expires in minutes); a refused origin does not burn it.
154
+ By default the pending request completes itself: the service re-attempts the intercepted tool under the new principal and streams it as the elevated session first turn. This is one-shot, is mooted if the visitor types first, and stops at the confirmation card for a confirm-gated tool. Pass `resume: false` beside the ticket only when the application deliberately owns that affordance. Do not build client-side resume scaffolding or a second identity provider.
125
155
 
126
156
  A connector-backed side effect needs **two** independent declarations to be reachable from a public or mixed surface: inclusion in `capabilities` **and** `{ confirm: true }` on the operation. Signing in proves who the visitor is; it does not pre-authorize an effect, so confirmation still applies on a mixed surface. Confirmation is never authentication or business authorization — the customer backend still owns payload validation, abuse controls, and idempotency. Local or session-only widget state needs no confirmation.
127
157
 
@@ -216,6 +246,10 @@ Read the customer repository lockfile or `packageManager` field and install `@no
216
246
 
217
247
  Create an authenticated same-origin backend route:
218
248
 
249
+ The route must return a JSON `401` for a signed-out caller rather than an HTML login redirect, require JSON, and compare the request against the exact configured Origin before exchange. The managed renderer sends same-origin cookies but does not invent an application CSRF token. Treat JSON plus exact Origin as the minimum cookie/CSRF boundary; when the host keeps token-based CSRF middleware, use the DOM-free client with an injected `fetch` that supplies the token instead of exempting the route. Resolve claims and connector routes from the authenticated user and server-owned membership only.
250
+
251
+ For the complete framework-neutral server-to-server HTTP exchange in a non-Node host, link to the [Django and Vue guide](https://docs.noodleseed.dev/docs/guides/embedded-assistant-django-vue); keep its framework-specific middleware and mounting mechanics there rather than copying them into agent instructions.
252
+
219
253
  ```ts
220
254
  import { createAssistantSession } from "@noodleseed/assistant/server";
221
255
 
@@ -393,7 +427,9 @@ That custom element is the complete managed assistant in Vue, Angular, or plain
393
427
 
394
428
  The component renders a custom element and must mount client-side. In a Next.js App Router tree, put the mount in a `"use client"` component; from a server component or the Pages Router, load it with `next/dynamic` and `ssr: false`.
395
429
 
396
- For a customer-owned React renderer, use the renderer-free hook. It owns client lifetime and React subscription while `client` remains the one command surface:
430
+ ### Minimal fail-closed custom renderer skeleton
431
+
432
+ Use the renderer-free hook only when the product has a concrete reason to own the conversation UI. This is a minimal fail-closed skeleton, not a production-complete renderer: it keeps one client and App host, renders safe transcript content, and refuses interactions whose complete business presentation the host has not implemented. Start from the managed renderer unless the application accepts the full obligation checklist below.
397
433
 
398
434
  ```tsx
399
435
  "use client";
@@ -402,9 +438,22 @@ import { useEffect, useState } from "react";
402
438
  import { NoodleAppView } from "@noodleseed/assistant/react";
403
439
  import { useNoodleAssistant } from "@noodleseed/assistant/react/client";
404
440
 
405
- export function CustomAssistant({ principalKey, resolvedTheme }: { principalKey: string; resolvedTheme: "light" | "dark" }) {
441
+ type SignInRequest = { readonly signInTicket: string; readonly expiresAt: string };
442
+
443
+ export function CustomAssistant({
444
+ principalKey,
445
+ resolvedTheme,
446
+ onSignInRequested,
447
+ }: {
448
+ principalKey: string;
449
+ resolvedTheme: "light" | "dark";
450
+ onSignInRequested: (request: SignInRequest) => Promise<"started" | "cancelled">;
451
+ }) {
406
452
  const [draft, setDraft] = useState("");
407
- const { client, messages, status, error } = useNoodleAssistant({
453
+ const [sessionNotice, setSessionNotice] = useState("");
454
+ const [turnNotice, setTurnNotice] = useState("");
455
+ const [pendingSignInTicket, setPendingSignInTicket] = useState<string>();
456
+ const { client, messages, suggestions, status, error } = useNoodleAssistant({
408
457
  sessionEndpoint: "/api/assistant/session",
409
458
  principalKey,
410
459
  clientContext: () => ({
@@ -418,6 +467,16 @@ export function CustomAssistant({ principalKey, resolvedTheme }: { principalKey:
418
467
  // The hook exposes this same structured failure through `error`.
419
468
  });
420
469
  };
470
+ useEffect(
471
+ () =>
472
+ client.subscribe((event) => {
473
+ if (event.event === "session_expired") setSessionNotice("Session expired.");
474
+ if (event.event === "session_started" || event.event === "session_reset") {
475
+ setSessionNotice("");
476
+ }
477
+ }),
478
+ [client],
479
+ );
421
480
 
422
481
  return (
423
482
  <section aria-label="Assistant" aria-busy={busy}>
@@ -431,20 +490,18 @@ export function CustomAssistant({ principalKey, resolvedTheme }: { principalKey:
431
490
  <section key={review.id} aria-label="Review proposed action">
432
491
  <h3>{review.title ?? "Review proposed action"}</h3>
433
492
  {review.description ? <p>{review.description}</p> : null}
434
- <pre aria-label="Proposed action arguments">
435
- {JSON.stringify(review.arguments ?? {}, null, 2)}
436
- </pre>
493
+ <p>This custom renderer has not implemented a complete schema-aware review.</p>
437
494
  <button
438
495
  disabled={busy || review.status !== "pending"}
439
- onClick={() => settle(client.respond(review.id, { action: "accept" }))}
496
+ onClick={() => settle(client.respond(review.id, { action: "decline" }))}
440
497
  >
441
- Confirm
498
+ Don't proceed
442
499
  </button>
443
500
  <button
444
501
  disabled={busy || review.status !== "pending"}
445
- onClick={() => settle(client.respond(review.id, { action: "decline" }))}
502
+ onClick={() => settle(client.respond(review.id, { action: "cancel" }))}
446
503
  >
447
- Don't proceed
504
+ Cancel
448
505
  </button>
449
506
  </section>
450
507
  );
@@ -454,21 +511,28 @@ export function CustomAssistant({ principalKey, resolvedTheme }: { principalKey:
454
511
  return (
455
512
  <section key={request.id} aria-label="Assistant needs input">
456
513
  <p>{request.message}</p>
457
- <p>This renderer has not implemented the requested form.</p>
514
+ {/* request.requestedSchema is the sole input-form contract. */}
515
+ <p>This custom renderer has not implemented the requested schema form.</p>
458
516
  <button
459
517
  disabled={busy || request.status !== "pending"}
460
518
  onClick={() => settle(client.respond(request.id, { action: "decline" }))}
461
519
  >
462
- Cancel request
520
+ Don't proceed
521
+ </button>
522
+ <button
523
+ disabled={busy || request.status !== "pending"}
524
+ onClick={() => settle(client.respond(request.id, { action: "cancel" }))}
525
+ >
526
+ Cancel
463
527
  </button>
464
528
  </section>
465
529
  );
466
530
  }
467
531
  if (part.type === "data-tool-result") {
468
532
  return (
469
- <pre key={part.data.id} aria-label={`${part.data.tool} result`}>
470
- {JSON.stringify(part.data.result, null, 2)}
471
- </pre>
533
+ <p key={part.data.id} role="status">
534
+ A result is available, but this renderer has no trusted presentation for it.
535
+ </p>
472
536
  );
473
537
  }
474
538
  if (part.type === "data-view") {
@@ -481,17 +545,67 @@ export function CustomAssistant({ principalKey, resolvedTheme }: { principalKey:
481
545
  />
482
546
  );
483
547
  }
548
+ if (part.type === "data-sign-in") {
549
+ const request = part.data;
550
+ return (
551
+ <section key={request.id} aria-label="Sign in required">
552
+ <p>Continue with your account to use this capability.</p>
553
+ <button
554
+ disabled={busy || pendingSignInTicket !== undefined}
555
+ onClick={() => {
556
+ setPendingSignInTicket(request.signInTicket);
557
+ void Promise.resolve()
558
+ .then(() =>
559
+ onSignInRequested({
560
+ signInTicket: request.signInTicket,
561
+ expiresAt: request.expiresAt,
562
+ }),
563
+ )
564
+ .then(
565
+ (result) => {
566
+ if (result === "cancelled") setPendingSignInTicket(undefined);
567
+ },
568
+ () => setPendingSignInTicket(undefined),
569
+ );
570
+ }}
571
+ >
572
+ Sign in
573
+ </button>
574
+ </section>
575
+ );
576
+ }
484
577
  return <p key={index}>Unsupported assistant content.</p>;
485
578
  })}
486
579
  </article>
487
580
  ))}
488
- {error ? <p role="alert">{error.message}</p> : null}
581
+ <p role="status" aria-live="polite">
582
+ {sessionNotice || turnNotice || (busy ? "Assistant is working" : "")}
583
+ </p>
584
+ {suggestions?.prompts.length ? (
585
+ <nav aria-label="Suggested messages">
586
+ {suggestions.prompts.map((prompt) => (
587
+ <button
588
+ key={prompt}
589
+ type="button"
590
+ disabled={busy}
591
+ onClick={() => {
592
+ setTurnNotice("");
593
+ settle(client.sendMessage(prompt));
594
+ }}
595
+ >
596
+ {prompt}
597
+ </button>
598
+ ))}
599
+ </nav>
600
+ ) : null}
601
+ {error ? <p role="alert">The assistant could not complete that request.</p> : null}
489
602
  <form
490
603
  onSubmit={(event) => {
491
604
  event.preventDefault();
492
605
  const message = draft.trim();
493
606
  if (!message) return;
494
607
  setDraft("");
608
+ setTurnNotice("");
495
609
  settle(client.sendMessage(message));
496
610
  }}
497
611
  >
@@ -501,7 +615,15 @@ export function CustomAssistant({ principalKey, resolvedTheme }: { principalKey:
501
615
  onChange={(event) => setDraft(event.currentTarget.value)}
502
616
  />
503
617
  {busy ? (
504
- <button type="button" onClick={() => client.abort()}>Stop</button>
618
+ <button
619
+ type="button"
620
+ onClick={() => {
621
+ client.abort();
622
+ setTurnNotice("Response stopped. This does not undo a started action.");
623
+ }}
624
+ >
625
+ Stop
626
+ </button>
505
627
  ) : (
506
628
  <button type="submit">Send</button>
507
629
  )}
@@ -511,37 +633,37 @@ export function CustomAssistant({ principalKey, resolvedTheme }: { principalKey:
511
633
  }
512
634
  ```
513
635
 
514
- `principalKey` is a browser-local identity for the authenticated user/tenant and is never sent to Noodle. Change it whenever that principal changes; the hook then aborts and clears the previous session and transcript. The hook does not register `<noodle-assistant>` or render Noodle markup.
636
+ `principalKey` is a browser-local identity for the authenticated user/tenant and is never sent to Noodle. Change it whenever that principal changes; the hook then aborts and clears the previous session and transcript. The hook does not register `<noodle-assistant>` or render Noodle markup. `suggestions` is hook-owned state: each prompt starts an ordinary message through the same guarded `client.sendMessage` path and disappears when the client clears it.
515
637
 
516
- If the product deliberately sends a first turn on mount, make the effect cleanup-aware. React Strict Mode discards the provisional effect, so a persistent "already sent" ref can suppress the stable remount:
517
-
518
- ```tsx
519
- useEffect(() => {
520
- let active = true;
521
- queueMicrotask(() => {
522
- if (active) settle(client.sendMessage(initialMessage));
523
- });
524
- return () => {
525
- active = false;
526
- };
527
- }, [client, initialMessage]);
528
- ```
638
+ Do not send a first turn on mount by default. React effect cleanup can suppress one provisional Strict Mode effect, but it cannot make a remount, dependency change, or client replacement idempotent. Require an explicit user action unless the host owns a durable one-shot state and an application idempotency key that makes repeated sends safe.
529
639
 
530
640
  `settle` must await or catch the command promise; the same structured failure also appears in the hook `error` state.
531
641
 
532
- The sample fails closed on input requests until you replace that branch with a form generated from `requestedSchema`. A custom renderer must show the complete confirmation review and both decisions, handle every part it supports, and surface an explicit unsupported state for the rest. For `data-view`, use the canonical `<noodle-app-view>` host (or its React `NoodleAppView` adapter) to render the linked App, or deliberately map `resourceUri`/tool plus the bounded redacted `result` to an application-trusted native component. JSON result data is not the linked App UI. Never inject `part.data.html`, assign it to `srcdoc`, fetch a `ui://` URI, or reproduce the bridge with a direct Ext Apps dependency. Do not wrap this client in another chat transport or invent user messages for interaction continuations.
642
+ This sample deliberately does not render Confirm until the application supplies a complete schema-aware review of every non-sensitive business field. It also refuses accepted elicitation until a form covers the compiler-approved portable `requestedSchema` subset; decline and cancel remain available and distinct. Use the managed renderer instead of shipping either unsupported branch. A production custom renderer must handle text, suggestions, sign-in, confirmation, elicitation, tool results, linked views, streaming, cancellation, terminal interaction states, and unknown future parts without silently dropping content.
643
+
644
+ `data-sign-in` is not an interaction and has no status: never pass its id to `client.respond`. Give `signInTicket` to an application callback that binds it to the host's existing short-lived login transaction; the authenticated backend spends it after login. Resolve the callback as `started` only after the host owns one active login transaction. Resolve it as `cancelled`, or reject it, when no transaction was started so the renderer can restore the sign-in affordance. A mixed custom renderer uses separate public and authenticated client mounts: the public shell starts with `embedId`, `serviceUrl`, and its visitor `principalKey`; after login, the authenticated destination mounts a new client against the same-origin `sessionEndpoint` with the user/tenant `principalKey`. Never pass both source options or mutate the public client source in place. The elevated session response owns bounded transcript repaint and optional resume. Never put the ticket in a query string, log, analytics event, or durable browser storage.
645
+
646
+ The `session_expired` notice is observational. The client owns its one pre-execution `401` re-exchange and message retry; never rebuild tokens or replay an interaction in renderer code. A retryable transport detail is diagnostic, not proof that resending an ordinary message or business action is safe, so the skeleton never offers a generic Retry button. Reconcile a lost interaction response only by repeating the same id and the same decision through an explicit application recovery affordance. Budget exhaustion and tenant-mismatch elevation never retry.
647
+
648
+ For `data-tool-result`, do not expose technical tool names or raw JSON as customer UI. Prefer the linked `data-view`; otherwise map a known bounded result to an application-trusted native component or keep the explicit unsupported-result state. For `data-view`, use the canonical `<noodle-app-view>` host (or its React `NoodleAppView` adapter). Never inject `part.data.html`, assign it to `srcdoc`, fetch a `ui://` URI, reproduce the bridge, wrap the client in another chat transport, or invent user messages for continuations.
533
649
 
534
650
  `<noodle-app-view>` owns one bridge for the semantic view identity: client + `view.id` + `view.resourceUri`; `NoodleAppView` delegates to it. The host retains the iframe across fresh payload/callback/theme rerenders, publishes later resolved-theme changes through MCP Apps host context, and sends standard App teardown when that semantic identity changes, the element disconnects, or the App requests teardown. App views remain inline by default; the host advertises only inline presentation and rejects widget fullscreen requests. Opt in with `allowFullscreen` on `NoodleAppView` or `allow-fullscreen` on `<noodle-app-view>` only when fullscreen is an intentional part of the customer-owned experience. When fullscreen is accepted, the shared host adds an accessible top-right exit control that returns the same mounted App to inline mode without losing its state. Pass the same resolved application theme used by the conversation shell. Do not key an ancestor by a view object or callback. If the embedding page sets Content-Security-Policy, include the Noodle service origin in both `connect-src` and `frame-src`.
535
651
 
536
- Outside React, use the same DOM-free client directly and import the isolated App-view entry only when rendering linked Apps. The client keeps the session token in memory, the transcript stays React-free, and the element owns only App presentation:
652
+ ### Framework-neutral DOM client
653
+
654
+ Use the same DOM-free client directly without a component wrapper, and import the isolated App-view entry only when rendering linked Apps. The client keeps the session token in memory, and the element owns only App presentation:
537
655
 
538
656
  ```html
539
- <noodle-app-view id="assistant-app-view"></noodle-app-view>
657
+ <div id="assistant-app-views"></div>
540
658
  ```
541
659
 
542
660
  ```ts
543
661
  import { createAssistantClient } from "@noodleseed/assistant/client";
544
662
  import "@noodleseed/assistant/app-view";
663
+ import {
664
+ type AssistantViewAvailableDetail,
665
+ type NoodleAppViewElement,
666
+ } from "@noodleseed/assistant/app-view";
545
667
 
546
668
  const assistant = createAssistantClient({
547
669
  sessionEndpoint: "/api/assistant/session",
@@ -551,47 +673,62 @@ const assistant = createAssistantClient({
551
673
  }),
552
674
  });
553
675
 
554
- const appView = document.querySelector("#assistant-app-view");
555
- if (!appView) throw new Error("Missing App view host");
556
- appView.client = assistant;
557
- appView.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
676
+ const appViews = document.querySelector("#assistant-app-views");
677
+ if (!appViews) throw new Error("Missing App views host");
678
+ const mountedViews = new Map<string, NoodleAppViewElement>();
679
+ const readResolvedTheme = (): "light" | "dark" =>
680
+ document.documentElement.classList.contains("dark") ? "dark" : "light";
681
+ let resolvedTheme: "light" | "dark" = readResolvedTheme();
682
+ const syncResolvedTheme = () => {
683
+ resolvedTheme = readResolvedTheme();
684
+ for (const mountedView of mountedViews.values()) mountedView.theme = resolvedTheme;
685
+ };
686
+ new MutationObserver(syncResolvedTheme).observe(document.documentElement, {
687
+ attributes: true,
688
+ attributeFilter: ["class"],
689
+ });
690
+ const appViewFor = (view: AssistantViewAvailableDetail) => {
691
+ const key = `${view.id}:${view.resourceUri}`;
692
+ let appView = mountedViews.get(key);
693
+ if (!appView) {
694
+ appView = document.createElement("noodle-app-view") as NoodleAppViewElement;
695
+ appView.client = assistant;
696
+ mountedViews.set(key, appView);
697
+ appViews.append(appView);
698
+ }
699
+ appView.theme = resolvedTheme;
700
+ return appView;
701
+ };
558
702
 
559
703
  assistant.updateModelContext({
560
704
  content: [{ type: 'text', text: 'The time-off form is mounted.' }],
561
705
  structuredContent: { widget: { name: 'time-off', lifecycle: 'mounted' } },
562
706
  });
563
707
 
564
- let pending: { id: string; requestedSchema?: Readonly<Record<string, unknown>> } | undefined;
565
708
  assistant.subscribeChat((state) => {
566
- renderUIMessageState(state);
567
- pending = undefined;
709
+ renderUIMessageState(state, {
710
+ respond: (id, response) => assistant.respond(id, response),
711
+ });
712
+ const activeViewKeys = new Set<string>();
568
713
  for (const message of state.messages) {
569
714
  for (const part of message.parts) {
570
- if (part.type === 'data-confirmation' && part.data.status === 'pending') {
571
- pending = { id: part.data.id };
572
- }
573
- if (part.type === 'data-input-request' && part.data.status === 'pending') {
574
- pending = { id: part.data.id, requestedSchema: part.data.requestedSchema };
575
- }
576
715
  if (part.type === 'data-view') {
577
- appView.view = part.data;
716
+ const key = `${part.data.id}:${part.data.resourceUri}`;
717
+ activeViewKeys.add(key);
718
+ appViewFor(part.data).view = part.data;
578
719
  }
579
720
  }
580
721
  }
722
+ for (const [key, mountedView] of mountedViews) {
723
+ if (!activeViewKeys.has(key)) {
724
+ mountedView.remove();
725
+ mountedViews.delete(key);
726
+ }
727
+ }
581
728
  });
582
-
583
- await assistant.sendMessage("Book next Thursday and Friday off");
584
- if (pending) {
585
- const requestedSchema = pending.requestedSchema;
586
- const resolution = requestedSchema
587
- ? { action: 'accept' as const, content: await renderPortableForm(requestedSchema) }
588
- : { action: 'accept' as const };
589
- await assistant.respond(pending.id, resolution);
590
- }
591
- // The same pending id also accepts { action: 'decline' } or { action: 'cancel' }.
592
729
  ```
593
730
 
594
- `subscribeChat` immediately emits a detached `{ messages, status, error? }` snapshot and then emits as `UIMessage.parts` change. Text uses `text`; Noodle confirmations, input requests, tool results, and linked views use `data-confirmation`, `data-input-request`, `data-tool-result`, and `data-view`. Interaction data moves through pending/submitting/accepted/declined/cancelled. Use raw `subscribe(...)` only for transport/session lifecycle events that are not transcript content.
731
+ `subscribeChat` immediately emits a detached `{ messages, status, error? }` snapshot and then emits as `UIMessage.parts` change. Text uses `text`; Noodle confirmations, input requests, sign-in requests, tool results, and linked views use `data-confirmation`, `data-input-request`, `data-sign-in`, `data-tool-result`, and `data-view`. `renderUIMessageState` is application code: it must show a complete schema-aware review or requested form and require an explicit user gesture before calling the supplied `respond`. Never call `respond` while scanning a transcript snapshot. Interaction data moves through pending/submitting/accepted/declined/cancelled. Use raw `subscribe(...)` only for transport/session lifecycle events that are not transcript content.
595
732
 
596
733
  For a chat-first custom host, raw `tool_started` carries the direct invocation call `id` and technical `tool` name before execution. Map known tools through a finite application-owned label table and use a neutral fallback such as "Working"; never mechanically expose an internal identifier as customer copy. Reserve one stable region with `role="status"` and `aria-live="polite"` for thinking, the mapped activity label, and the linked-view skeleton. `view_available` makes the view ready; raw `error` or chat `error` replaces it with `role="alert"`. Mark decorative skeleton shapes `aria-hidden="true"` and disable shimmer/transitions under `@media (prefers-reduced-motion: reduce)`.
597
734
 
@@ -613,7 +750,7 @@ assistant.subscribe((event) => {
613
750
  });
614
751
  ```
615
752
 
616
- `data-view` means a completed tool has a linked MCP App view. Pass that typed part and the existing client to `<noodle-app-view>` in Vue, Angular, or plain DOM, or to its `NoodleAppView` React adapter. It retains one bridge for client + `view.id` + `view.resourceUri` and requests standard App teardown on semantic replacement, disconnect, or App request. That pair is transport identity: different call ids are distinct invocations and must not be deduplicated generically. If the product intentionally owns one current panel for a known resource, declare an application-owned slot map and replace only that slot. Deliberately map the bounded result to an application-trusted native component only when replacing the linked App UI.
753
+ `data-view` means a completed tool has a linked MCP App view. Pass that typed part and the existing client to `<noodle-app-view>` in another component framework or plain DOM, or to its `NoodleAppView` React adapter. It retains one bridge for client + `view.id` + `view.resourceUri` and requests standard App teardown on semantic replacement, disconnect, or App request. That pair is transport identity: different call ids are distinct invocations and must not be deduplicated generically. If the product intentionally owns one current panel for a known resource, declare an application-owned slot map and replace only that slot. Deliberately map the bounded result to an application-trusted native component only when replacing the linked App UI.
617
754
 
618
755
  `clientContext` and typed `pageContext` are recomputed for each turn. `updateContext(...)` remains the legacy session-exchange context; `updatePageContext(...)` replaces the fresh per-turn application hint. `updateModelContext({ content, structuredContent })` publishes one cohesive renderer snapshot for later message turns without starting a turn; every call replaces the prior snapshot rather than merging fields. These are untrusted data, not conversation history or authorization input, and the boundaries reject credential-shaped or unbounded updates. A message may re-exchange once after a pre-execution `401`; the client never auto-retries interaction decisions. `tool_proposed.arguments` is a complete schema-aware review projection and, for connector-backed tools, names the sole exact connector version/operation/resolved arguments. Sensitive/write-only fields are redacted; truncating or omitting any non-sensitive action field fails closed. Accept is bound to the server-held action and claims at most one execution attempt—clients cannot replace it. Normal terminal outcomes scrub private arguments and continuations immediately; only an accepted action still executing retains them for the one-hour unknown-outcome recovery window, after which it records `interaction_outcome_unknown` and scrubs. Without downstream idempotency this is not an exactly-once business-effect guarantee. To reconcile a lost response, explicitly repeat the same id and decision: the service returns its durable stored outcome without re-execution.
619
756
 
@@ -628,12 +765,67 @@ noodle assistant embed --check --json --require-env EXAMPLE_DELEG_CLIENT_SECRET
628
765
 
629
766
  The check reports only required and missing environment names, never their values. Pass `--surface public|mixed|authenticated` to match the deployment: `public` drops the backend client id/secret requirement (a public embed has neither), and `public`/`mixed` additionally require `script-src` — the one directive whose failure runs no widget code at all, so nothing can report it from inside the page. CSP directives verify against the service origin exactly, via the env placeholder, or through a covering wildcard (`https://*.example.com`); a dynamic expression is marked unverified instead of guessed. Additional `--require-env` names are application-owned; `--env-alias NAME=HOST_NAME` follows a host repo that names an env var differently.
630
767
 
768
+ Read `evidence.levels` in order and stop at `evidence.firstUnproven`:
769
+
770
+ | Level | What can prove it |
771
+ | --- | --- |
772
+ | Static host | `assistant embed --check`: environment-name presence, control-plane URL shape, canonical session-route/mount risks, same-origin endpoint shape, and statically visible CSP |
773
+ | Local contract | The host's existing local test/build: JSON signed-out `401`, signed-in session exchange, unchanged response, client-only mount, and cookie/CSRF behavior |
774
+ | Hosted session | `assistant doctor` plus one representative routed safe read against the exact deployment |
775
+ | Production browser | A real deployed browser completing keyboard submission, session exchange, one streamed turn, one interaction, and one linked App with console/network inspection |
776
+ | Operations | The launch, rollback, rotation, budget, usage, and qualified-outcome checks below |
777
+
778
+ `static-host` is `passed` only when every canonical static diagnostic is ready or not applicable. It is `partial` when a custom or missing source location is not detected, and `failed` for a known blocker. A top-level `ready: true` means only that the check found no static blocker; static success is never local-contract, hosted-session, or production-browser proof.
779
+
780
+ The value-free diagnostics distinguish `mcp-endpoint` (a deployment MCP URL was supplied where the control-plane service URL belongs), `html-redirect-risk` (the canonical API route appears to redirect instead of returning JSON), `ssr-risk` (the canonical mount is not provably client-only), and `cross-origin-risk` (the browser session endpoint would omit same-origin cookies). `not-detected` and `unverified` are evidence gaps, not passes. The listed `postDeployProbes` are next actions; this command does not execute them.
781
+
782
+ There is no live or browser flag. Do not add one without human approval of its network, identity, data, timeout, and redaction contract; use the host's own test and browser tooling for later evidence levels.
783
+
631
784
  Inspect the host repository for generated environment bindings after adding names. Run its existing generator, review the diff, commit generated types only when that repository requires them, then run the production-equivalent host build. Do not invent a framework command or add a second generator.
632
785
 
633
786
  Promotion checklist: provision each environment in the backend secret manager; map names through the CI environment and any secret allowlist or secrets file; run the presence-only host preflight; run the canonical deploy so its configuration preflight completes before asset upload; run the post-deploy probes from the JSON contract; rotate the assistant client and delegated credential independently, then rerun the same checks.
634
787
 
635
788
  Devtools privacy gate: default model and connector exercises to synthetic or mock data. Before Devtools Chat sends real connector data to an external model, disclose the data flow and obtain the user's approval. A local validation pass is not that approval.
636
789
 
790
+ ## Launch and qualified-usage proof
791
+
792
+ Treat deployed capability, production-browser proof, actual qualified usage, and measured customer outcome as four separate claims. Passing one never proves the next.
793
+
794
+ ### Pre-launch checklist
795
+
796
+ 1. Run `noodle assistant embed --check --json` in the host repository and stop at `evidence.firstUnproven`; complete the host-owned local contract and production-browser checks described above.
797
+ 2. Run `noodle assistant doctor --user-id <real-test-user> --origin <exact-origin> --org <org> --app <app> --env <env> --json`, then exercise one representative routed safe read. Use an approved synthetic account unless real customer data use was explicitly approved.
798
+ 3. Run `noodle assistant appearance show --org <org> --app <app> --env <env> --json` and have the customer owner verify the rendered brand, keyboard path, reduced motion, narrow viewport, confirmation, input, and linked App view in the deployed browser.
799
+ 4. Run `noodle assistant clients list --org <org> --app <app> --env <env> --json`; name the active credential owner and rehearse the secret-manager update and verification sequence for `noodle assistant clients rotate <client-id> --org <org> --app <app> --env <env> --json`. Rotation returns a new secret once; never put it in the PR, shell history, logs, or analytics.
800
+ 5. For a public or mixed surface, inspect its live caps with `noodle assistant embeds list --org <org> --app <app> --env <env> --json`. Record the authorized kill switch, `noodle assistant budget set --surface <embed-id> --turns-per-day 0 --org <org> --app <app> --env <env> --json`, but do not execute it during a rehearsal.
801
+ 6. Record the current and previous deployment identifiers and the named rollback owner. The recovery command is `noodle rollback <previous-deployment-id> --org <org> --app <app> --env <env> --json`; do not run it merely to prove syntax.
802
+ 7. Have the customer workflow owner execute and sign off the behavioral acceptance fixture below against the production-equivalent environment.
803
+
804
+ ### Customer-owned behavioral acceptance fixture
805
+
806
+ Use customer-recognizable inputs and expected business outcomes. Record only the minimum identifiers needed to reproduce a failure; do not paste raw conversations, secrets, or customer records into issue trackers.
807
+
808
+ | Case | Customer-owned setup | Expected observable outcome |
809
+ | --- | --- | --- |
810
+ | normal | One common, authorized job with known data | Completes the named task and presents the business result or linked App clearly |
811
+ | ambiguous | A request with two materially different interpretations | Asks the smallest useful clarifying question before action |
812
+ | missing-data | Omit one required business field or make the read return no record | Requests only the missing input or states that no data exists; never invents it |
813
+ | unauthorized | Use a real test role lacking the required scope | Refuses or withholds the capability without leaking its result, arguments, or internal tool name |
814
+ | tenant-switch | Change the signed-in organization while the panel is open | Aborts and clears the prior principal session; no prior-tenant transcript, route, or result survives |
815
+ | confirmation | Propose one consequential operation, then test decline, cancel, and accept separately | Shows the complete non-sensitive action review; only explicit accept executes, at most once |
816
+ | recovery | Expire a session before execution and lose one interaction response after submission | Safe pre-execution re-exchange succeeds once; same-id/same-decision reconciliation returns the stored outcome without re-execution |
817
+
818
+ ### Post-launch checklist
819
+
820
+ 1. Run `noodle assistant usage --window 7d --org <org> --app <app> --env <env> --json` on an agreed cadence. It reports traffic, depth, engagement, latency, and tokens; it does not establish customer success.
821
+ 2. Re-run the customer-owned fixture with named external users after any tool, permission, model, identity, renderer, or routing change. Record results by case and deployment, not as a single green launch claim.
822
+ 3. Report task completion, refusal correctness, repeat usage, safety, latency, and abandonment separately: completed qualified attempts / qualified attempts; correct refusals / refusal cases; distinct returning qualified users; confirmed cross-tenant, duplicate-effect, or secret-exposure incidents; p50/p95 end-to-end time for the named job; and started qualified attempts with no completion or explicit refusal.
823
+ 4. Treat raw turn volume as utilization evidence, not an outcome. A user can generate many turns because the assistant is useful, confused, slow, or unsafe.
824
+ 5. Recheck caps and credential age. Run the rehearsed `noodle assistant clients rotate <client-id>` sequence only under the named owner, update the backend secret manager, rerun doctor and one browser turn, then revoke the superseded credential after the overlap window.
825
+ 6. When an agreed safety, cost, or correctness threshold is crossed, use the recorded public-surface budget kill switch or the recorded deployment rollback. Verify the restored state with doctor, the browser path, and the affected acceptance cases.
826
+
827
+ Do not add a platform-owned completion event or infer completion from a final assistant message. Any such schema requires a separate human-approved analytics and customer-data decision covering event semantics, customer consent, retention, access, deletion, tenant isolation, and whether the customer application or Noodle is authoritative.
828
+
637
829
  ## Toolchain requirements
638
830
 
639
831
  - Node.js 20+ for `@noodleseed/assistant/server`.
@@ -79,7 +79,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
79
79
  - `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
80
80
  - View metadata (`viewTitle`, `viewDescription`, `csp`, `domain`, `permissions`) belongs on the tool that renders it; `asset("./path")` packages local files.
81
81
  - `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
82
- - `state` defines durable widget state handles; handle schemas may use `.optional()`/`.default()` — defaulted fields are optional on write, so a save that omits them still validates. `handoff` declares allowed external domains for safe host handoff.
82
+ - `state` defines durable widget state handles; handle schemas may use `.optional()`/`.default()` — defaulted fields are optional on write, so a save that omits them still validates. Add `claimOnAuthentication: true` only to an explicitly caller-scoped handle with a finite TTL when a mixed public assistant should atomically adopt that expiring draft on sign-in-ticket spend. `handoff` declares allowed external domains for safe host handoff.
83
83
 
84
84
  ## Recipes
85
85
 
@@ -3,7 +3,7 @@ name: reporting-noodle-feedback
3
3
  description: "Use when a Noodle Seed bug, misleading instruction, missing capability, or concrete product improvement should be proposed to the user."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.91.0 hash:0f404109f4845683 -->
6
+ <!-- noodle-skill version:0.93.0 hash:660cc6ad9469d90f -->
7
7
 
8
8
  # reporting-noodle-feedback
9
9
 
@@ -27,7 +27,7 @@ Preview one sanitized feedback proposal and submit it once only after informed e
27
27
 
28
28
  ## Workflow
29
29
 
30
- Read and follow the canonical playbook `references/feedback.md` at `../noodle-seed/references/feedback.md`. It owns the workflow; do not recreate it here or load the command catalog speculatively.
30
+ Read and follow the canonical playbook at `references/feedback.md`. It owns the workflow; do not recreate it here or load the command catalog speculatively.
31
31
 
32
32
  ## Verification evidence
33
33