@noodleseed/agent-kit 0.20.0 → 0.21.1

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 (31) hide show
  1. package/README.md +2 -2
  2. package/manifest.json +37 -27
  3. package/package.json +2 -2
  4. package/skills/claude-code/SKILL.md +4 -3
  5. package/skills/claude-code/examples/acme-bistro/README.md +1 -1
  6. package/skills/claude-code/examples/acme-tasks/README.md +8 -3
  7. package/skills/claude-code/examples/food-ordering/README.md +8 -0
  8. package/skills/claude-code/examples/food-ordering/src/server.ts +3 -1
  9. package/skills/claude-code/examples/hello/README.md +4 -0
  10. package/skills/claude-code/examples/hello/src/server.ts +5 -2
  11. package/skills/claude-code/examples/hello/test/server.test.ts +11 -0
  12. package/skills/claude-code/references/authoring-workflow.md +1 -1
  13. package/skills/claude-code/references/compile-errors.md +1 -0
  14. package/skills/claude-code/references/deploy-and-ops.md +5 -0
  15. package/skills/claude-code/references/embedded-assistant.md +208 -0
  16. package/skills/claude-code/references/sdk-surface.md +2 -2
  17. package/skills/claude-code/references/widgets-and-apps.md +31 -21
  18. package/skills/codex/SKILL.md +4 -3
  19. package/skills/codex/examples/acme-bistro/README.md +1 -1
  20. package/skills/codex/examples/acme-tasks/README.md +8 -3
  21. package/skills/codex/examples/food-ordering/README.md +8 -0
  22. package/skills/codex/examples/food-ordering/src/server.ts +3 -1
  23. package/skills/codex/examples/hello/README.md +4 -0
  24. package/skills/codex/examples/hello/src/server.ts +5 -2
  25. package/skills/codex/examples/hello/test/server.test.ts +11 -0
  26. package/skills/codex/references/authoring-workflow.md +1 -1
  27. package/skills/codex/references/compile-errors.md +1 -0
  28. package/skills/codex/references/deploy-and-ops.md +5 -0
  29. package/skills/codex/references/embedded-assistant.md +208 -0
  30. package/skills/codex/references/sdk-surface.md +2 -2
  31. package/skills/codex/references/widgets-and-apps.md +31 -21
@@ -38,7 +38,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
38
38
 
39
39
  ### Customer auth
40
40
 
41
- - `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments.
41
+ - `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments. Firebase Web App fields are browser-visible configuration: use `variable(...)`, not `secret(...)`, and restrict the key in Firebase.
42
42
 
43
43
  ### Sessions
44
44
 
@@ -60,7 +60,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
60
60
  - `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
61
61
  - View metadata (`viewTitle`, `viewDescription`, `csp`, `domain`, `permissions`) belongs on the tool that renders it; `asset("./path")` packages local files.
62
62
  - `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
63
- - `state` defines durable widget state handles; `handoff` declares allowed external domains for safe host handoff.
63
+ - `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.
64
64
 
65
65
  ## Recipes
66
66
 
@@ -18,6 +18,12 @@ Use `tool(name, { description, input, output, fulfil, view })` for a model-visib
18
18
 
19
19
  Generated widgets, official examples, and agent-authored MCP Apps must start with `@noodleseed/one/react` primitives and semantic tokens. Custom React/CSS or third-party components remain valid when the kit lacks the required behavior or the developer explicitly requests them.
20
20
 
21
+ `noodle init my-app` defaults to a comprehensive server—tools, resource, prompt, state contract, branding, handoff, widget, and embedded assistant—so begin by adapting that complete surface. Use `--template hello` only when the product explicitly needs the minimal server.
22
+
23
+ The default composition rule is: build the smallest useful conversational surface. Inline has one purpose, one primary action, and at most two visible actions. Use progressive disclosure or a later conversational turn for secondary detail; request fullscreen only when the user asks or the task genuinely needs it. Never use nested scrolling. At 280px and wider, the widget must remain one-column, readable, touch-safe, and free of horizontal overflow. Remove secondary chrome before shrinking essential content.
24
+
25
+ Every production widget handles loading, empty, partial, stale, error, retry, and success states. Prefill fields from known tool results and choose safe, reversible defaults; preserve the user’s work across rerenders, and never preselect a consequential action. Use public React primitives and branding tokens. Never author against `ns-*`, `nsr-*`, or example-local `--nw-*` classes/tokens; those are implementation details, not alternate design systems.
26
+
21
27
  ## React hook surface
22
28
 
23
29
  Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
@@ -47,7 +53,7 @@ Author React. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react
47
53
 
48
54
  ```tsx
49
55
  import type { ServerDefinition } from '@noodleseed/one';
50
- import { generateHelpers } from '@noodleseed/one/react';
56
+ import { Action, ActionBar, AsyncBoundary, Feedback, Field, Flow, Frame, Region, Select, generateHelpers } from '@noodleseed/one/react';
51
57
 
52
58
  // One call wires the typed host bridge; destructure only the hooks this view uses.
53
59
  const { useToolInfo, useCallTool, useViewState, useOpenExternal } =
@@ -71,28 +77,32 @@ export default function OrderStatus() {
71
77
 
72
78
  return (
73
79
  // data-llm mirrors the visible state back to the model as text context.
74
- <main data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
75
- <h1>Pickup order</h1>
76
- <label>
77
- Item
78
- <select value={item} onChange={(event) => setItem(event.currentTarget.value)}>
79
- <option value="falafel_wrap">Falafel Wrap</option>
80
- <option value="lentil_soup">Lentil Soup</option>
81
- <option value="mint_lemonade">Mint Lemonade</option>
82
- </select>
83
- </label>
84
- <button
85
- type="button"
86
- disabled={placeOrder.isPending}
80
+ <Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
81
+ <Flow variant="stack">
82
+ <AsyncBoundary state={placeOrder} loading="Placing order…" error={(error) => error.message}>
83
+ <Region title="Order" description="Choose one item for pickup.">
84
+ <Field label="Item">
85
+ <Select value={item} onChange={(event) => setItem(event.currentTarget.value)} options={[
86
+ { value: 'falafel_wrap', label: 'Falafel Wrap' },
87
+ { value: 'lentil_soup', label: 'Lentil Soup' },
88
+ { value: 'mint_lemonade', label: 'Mint Lemonade' },
89
+ ]} />
90
+ </Field>
91
+ </Region>
92
+ {confirmed?.status ? <Feedback status="success">{confirmed.status}</Feedback> : null}
93
+ <ActionBar>
94
+ <Action variant="primary" pending={placeOrder.isPending} pendingLabel="Placing…"
87
95
  onClick={() => placeOrder.callTool({ customer: shown?.customer ?? 'Guest', item })}
88
96
  >
89
- {placeOrder.isPending ? 'Placing…' : 'Place order'}
90
- </button>
91
- <p>{confirmed?.status ?? `Total: $${total}`}</p>
92
- <button type="button" onClick={() => openExternal(checkoutUrl)}>
97
+ Place order
98
+ </Action>
99
+ <Action onClick={() => openExternal(checkoutUrl)}>
93
100
  Continue checkout
94
- </button>
95
- </main>
101
+ </Action>
102
+ </ActionBar>
103
+ </AsyncBoundary>
104
+ </Flow>
105
+ </Frame>
96
106
  );
97
107
  }
98
108
  ```
@@ -169,7 +179,7 @@ export default server(
169
179
  A "ChatGPT App" is not a separate authoring surface — it is exactly this MCP Apps widget rendered by the ChatGPT host. From the same declaration you author three things:
170
180
 
171
181
  - `domain` on the widget — one https origin per app (required for app-store submission, optional for dev-mode testing).
172
- - `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review).
182
+ - `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review). These three lists are the complete CSP surface — there is no `base-uri` list.
173
183
  - server `handoff.allowedDomains` — the external-link targets `useOpenExternal()` opens.
174
184
 
175
185
  The compiler emits the rest automatically: the `openai/*` metadata (`openai/outputTemplate`, `openai/widgetCSP`, `openai/widgetDescription`) and ChatGPT’s `redirect_domains` (derived from `handoff.allowedDomains`). `window.openai` and Claude’s ext-apps bridge are auto-detected at startup, so the same widget renders in both Claude and ChatGPT with no host-specific code.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: noodle-seed
3
3
  description: Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI.
4
- version: 0.20.0
5
- hash: 39c6a606c728d164
4
+ version: 0.21.1
5
+ hash: cd25535e1d6dbf4f
6
6
  ---
7
7
 
8
8
  # Noodle Seed
@@ -25,7 +25,7 @@ Before authoring, design the experience — the funnel/handoff boundary, tools,
25
25
  4. **Repair** — fix each `error.errors[]` entry at its `path`, then re-run `noodle validate --json`; `noodle validate --fix-prompt` emits ready-to-apply repair prose. Never freeform re-edit (see `references/compile-errors.md`).
26
26
  5. **Smoke** — `noodle test --json`: local compile plus a loopback MCP smoke.
27
27
  6. **Prove real output** — `validate`/`test` prove a connector tool *compiles and registers*, not that its response mapping returns data. Set the secret at the scope your local `noodle dev` resolves (`noodle secrets set <NAME> --runtime local --scope org --org local --from-env <ENV>`; a secret set at the wrong scope leaves the loopback returning `-32600 "not found"` — see `references/connect-an-api.md`), then run a live read — `noodle tools call <read_tool> --args '{...}'` executes the connector against the real API in-process — and confirm the mapped fields are populated, not `undefined`, before trusting it. Only run a live write if it is safe/approved.
28
- 7. **Apps/widgets** — `noodle check --json` (add `--target chatgpt|claude`), then `noodle devtools` (see `references/widgets-and-apps.md`).
28
+ 7. **Apps/widgets/embed** — `noodle check --json` (add `--target chatgpt|claude|embedded-assistant`), then `noodle devtools`; use `references/widgets-and-apps.md` for MCP Apps and `references/embedded-assistant.md` for a SaaS embed.
29
29
  8. **Deploy** — `noodle deploy`; auth fails clean with `error.next` = `noodle login` (see `references/deploy-and-ops.md`).
30
30
  9. **Wire into a host** — `noodle connect <codex|claude-code|chatgpt>` (prove it in a real host per `references/test-in-hosts.md`; debug symptoms with `references/troubleshooting.md`).
31
31
  10. **Health** — `noodle metrics --agent-output`: a health verdict plus the exact next command per attention item.
@@ -39,6 +39,7 @@ Scan all of these during discovery; open in full the ones your task touches:
39
39
  - `references/cli-commands.md` — every `noodle` command, grouped by area.
40
40
  - `references/compile-errors.md` — fix `noodle validate` errors by code.
41
41
  - `references/authoring-workflow.md` — input paths (scrape / OpenAPI import / user interview), the fit check, the validate→test→dev repair loop, connectors, and secrets/variables.
42
+ - `references/embedded-assistant.md` — HTTPS origins, managed model config, deploy-before-client sequencing, backend session exchange, browser mounting, and credential boundaries.
42
43
  - `references/connect-an-api.md` — given an API key: secure it, probe the live API to learn the real shape, model the connector, and prove real output before building.
43
44
  - `references/experience-design.md` — design the app experience before authoring: funnel/handoff boundary, grounding, two-users, display modes, and the wireframe/UX spec.
44
45
  - `references/widgets-and-apps.md` — MCP Apps, React `view` widgets, the widget hook surface, output shaping, and CSP.
@@ -34,7 +34,7 @@ In another terminal:
34
34
  ```sh
35
35
  noodle tools list
36
36
  noodle tools call show_menu --args '{"customer":"Asha"}'
37
- noodle tools call create_checkout --args '{"customer":"Asha","cartToken":"stone_pizzax2-lemon_tartx1","total":36}'
37
+ noodle tools call create_checkout --args '{"customer":"Asha","cartToken":"cart_1","total":36}'
38
38
  noodle check --target chatgpt
39
39
  ```
40
40
 
@@ -97,13 +97,18 @@ assistant: embeddedAssistant({
97
97
  The assistant automatically inherits this server's existing `branding` block, so its name, accent,
98
98
  light/dark surfaces, density, and radius match the `TaskList` widget without a second brand declaration.
99
99
 
100
+ Every assistant origin is exact and HTTPS-only. If the customer web app needs local browser testing, it
101
+ must serve itself over development HTTPS and add its `https://localhost:<port>` origin; `noodle dev` does
102
+ not provide TLS for that separate SaaS app.
103
+
100
104
  The customer backend exchanges its authenticated user through `@noodleseed/assistant/server`; the browser
101
105
  uses the Web Component or React wrapper and never receives the embed client or model secret. Validate with
102
106
  `noodle check --target embedded-assistant`, then create the backend credential with
103
- `noodle assistant clients create` after deployment.
107
+ `noodle assistant clients create` after deployment. Model URL/name/key values stay in Noodle managed config;
108
+ only the Noodle service URL and assistant client ID/secret belong in the authenticated customer backend.
104
109
 
105
- Install the independently versioned embed SDK in the customer web application with
106
- `pnpm add @noodleseed/assistant`.
110
+ Install the independently versioned embed SDK with the customer web application's existing package manager;
111
+ do not introduce a second lockfile.
107
112
 
108
113
  This example has no connector secrets and does not include tokens, caller-key mechanisms, or
109
114
  `.env.noodle` values. All tasks are fictional seed data.
@@ -21,6 +21,14 @@ private customer data.
21
21
  | Handoff | `handoff.allowedDomains` allows only `https://orders.example.com` checkout URLs |
22
22
  | Progressive enhancement | Non-Apps hosts still receive stores, featured items, and a readable fallback summary |
23
23
 
24
+ The example is intentionally richer than the generated starter, but each inline view still follows the
25
+ same default: one immediate purpose, one primary action, at most one subordinate action, and progressive
26
+ disclosure for the rest. Preview it at 280px before adding navigation or local CSS; loading, empty, stale,
27
+ error/retry, and success states must remain readable without nested vertical scrolling.
28
+
29
+ Like the comprehensive default `noodle init my-app` scaffold, this flagship keeps the server feature-rich
30
+ while making each individual widget view focused; server capability breadth and screen density are separate.
31
+
24
32
  ## Local Author Loop
25
33
 
26
34
  ```sh
@@ -151,7 +151,9 @@ const cartStateSchema = z.object({
151
151
  customer: z.string(),
152
152
  notes: z.string().optional(),
153
153
  subtotal: z.number(),
154
- status: z.enum(['draft', 'review', 'handoff']),
154
+ // `.default()`/`.optional()` state fields are optional on write: a cart save that omits
155
+ // `status` still validates, and a fresh cart starts in `draft`.
156
+ status: z.enum(['draft', 'review', 'handoff']).default('draft'),
155
157
  checkoutUrl: z.string().optional(),
156
158
  });
157
159
 
@@ -10,4 +10,8 @@ noodle dev examples/hello/src/server.ts --app hello
10
10
  noodle deploy examples/hello/src/server.ts --org acme --app hello
11
11
  ```
12
12
 
13
+ `noodle export manifest examples/hello/src/server.ts` compiles the same entrypoint locally and prints
14
+ the portable, vendor-neutral manifest JSON — the eject path: your `server.ts` plus this manifest is
15
+ the whole app, yours to read, diff, and keep.
16
+
13
17
  It is also the fixture for `pnpm smoke:dev` and the e2e harness, so keep its tool surface stable.
@@ -1,4 +1,4 @@
1
- import { server, tool, z } from '@noodleseed/one';
1
+ import { annotations, server, tool, z } from '@noodleseed/one';
2
2
 
3
3
  export default server(
4
4
  'hello',
@@ -16,11 +16,14 @@ export default server(
16
16
  tool('greet', {
17
17
  description: 'Greet someone by name.',
18
18
  input: z.object({
19
- name: z.string(),
19
+ // Defaults are advertised to the model and applied at runtime when the argument is omitted.
20
+ name: z.string().default('world'),
20
21
  }),
21
22
  output: z.object({
22
23
  message: z.string(),
23
24
  }),
25
+ // Read-only, closed-world: assistant surfaces run this without a consent prompt.
26
+ annotations: annotations.readOnly(),
24
27
  fulfil: ({ input }) => {
25
28
  return { message: `Hello, ${input.name}!` };
26
29
  },
@@ -5,4 +5,15 @@ describe('hello example', () => {
5
5
  it('exports a Noodle server definition', () => {
6
6
  expect(typeof app.toManifest).toBe('function');
7
7
  });
8
+
9
+ it('advertises the greet default and keeps the argument optional', async () => {
10
+ const manifest = await app.toManifest();
11
+ const greet = manifest.tools?.find((tool) => tool.name === 'greet');
12
+ const schema = greet?.inputSchema as {
13
+ properties?: { name?: { default?: unknown } };
14
+ required?: string[];
15
+ };
16
+ expect(schema.properties?.name?.default).toBe('world');
17
+ expect(schema.required ?? []).not.toContain('name');
18
+ });
8
19
  });
@@ -200,7 +200,7 @@ Author managed config as `secret("NAME")` / `variable("NAME")` and operate it wi
200
200
 
201
201
  ## Embedded assistant
202
202
 
203
- To place the same server tools inside the SaaS web app, declare the shared identity once with `branding: { name: "Acme", accent: "#3157D5", surface: "#FFFFFF", surfaceDark: "#161B26", theme: { dark: { accent: "#9FB4FF" } } }`, then add `assistant: embeddedAssistant({ model: openAICompatible({ baseUrl: variable("ASSISTANT_MODEL_BASE_URL"), model: variable("ASSISTANT_MODEL"), apiKey: secret("ASSISTANT_MODEL_API_KEY") }), allowedOrigins: ["https://app.example.com"], layout: { mode: "floating", position: "bottom-right" } })`. Widgets and the embed inherit the same brand kit. Keep origins exact. Install the independently versioned embed SDK with `pnpm add @noodleseed/assistant`; the customer backend uses its `/server` export to exchange the already-authenticated user for a short-lived browser session, while the browser uses the package root or `/react` export. Client and model secrets never enter the browser. Run `noodle check --target embedded-assistant` before deploy, then manage backend credentials with `noodle assistant clients create|list|rotate|revoke`.
203
+ To place the same server tools inside a SaaS web app, declare `assistant: embeddedAssistant(...)` alongside the one server-level brand kit. Read `embedded-assistant.md` before integrating: it owns the HTTPS-origin rule, managed model configuration, required deploy-before-client sequence, customer-backend exchange, browser mount, and verification checklist.
204
204
 
205
205
  ## Boundaries
206
206
 
@@ -18,6 +18,7 @@ Run `noodle validate` (add `--json` for the machine-readable envelope, `--fix-pr
18
18
  | `invalid_name` | Rename the identifier to match the allowed pattern (lowercase, no spaces/reserved characters) cited at `path`. |
19
19
  | `duplicate_name` | Two tools/components share a name; give each a unique name at the cited `path`. |
20
20
  | `unsupported_manifest_version` | Update the SDK/CLI so the emitted manifest version is supported; do not pin an old manifest shape. |
21
+ | `reserved_for_future_version` | The verb at `path` (e.g. `compute`, `elicit`) is reserved for a future core version; express the step with `use` (a connector operation) or `map` (a pure mapping) instead. |
21
22
  | `invalid_operation_ref` | Fix the connector operation reference to `alias.operation` for an operation that exists on that connector. |
22
23
  | `external_ref` | Remove the external/remote `$ref`; schemas must be self-contained — inline the definition instead of dereferencing a URL. |
23
24
  | `invalid_schema_ref` | Correct the `$use` schema reference syntax at `path`; it does not name a resolvable local schema. |
@@ -5,6 +5,7 @@
5
5
  - Authenticate
6
6
  - Link and target
7
7
  - Deploy and inspect
8
+ - Eject path (portable manifest)
8
9
  - Connect into a host
9
10
  - Access modes
10
11
  - Org and members
@@ -24,6 +25,10 @@
24
25
 
25
26
  `noodle deploy` deploys the server. Then `noodle open` (latest URL), `noodle status`, `noodle inspect` (metadata, no secrets), `noodle smoke` (readiness diagnostics), and `noodle rollback <deploymentId>` to revert.
26
27
 
28
+ ## Eject path (portable manifest)
29
+
30
+ `noodle export manifest [--output <file>]` compiles the entrypoint locally and emits the portable, vendor-neutral manifest JSON — no service, no login, no account. A Noodle app is just `src/server.ts` plus this manifest: the user can read it, diff it, and keep it.
31
+
27
32
  ## Connect into a host
28
33
 
29
34
  Once deployed, register the server as a tool in a host with `noodle connect <host>` (`claude-code`, `codex`, `chatgpt`, `cursor`, `vscode`, `claude`, `inspector`) — it prints the exact config to paste.
@@ -0,0 +1,208 @@
1
+ # Embedded assistant
2
+
3
+ ## Contents
4
+
5
+ - Architecture
6
+ - Author and validate
7
+ - Configure and deploy
8
+ - Access modes and customer auth
9
+ - Create the backend client
10
+ - Integrate the customer backend
11
+ - Verified session context (identity and claims)
12
+ - The session response
13
+ - Mount the browser component
14
+ - Toolchain requirements
15
+ - Verify the boundary
16
+ - Troubleshooting: symptom to diagnosis
17
+
18
+ ## Architecture
19
+
20
+ The browser never receives a model key, assistant client secret, MCP token, or raw application session. The embedding SaaS authenticates its own user, its backend exchanges that verified identity through `@noodleseed/assistant/server`, and the browser receives only a short-lived assistant session.
21
+
22
+ Keep the two configuration sets separate:
23
+
24
+ | Owner | Values | Destination |
25
+ | --- | --- | --- |
26
+ | Noodle deployment | `ASSISTANT_MODEL_BASE_URL`, `ASSISTANT_MODEL`, `ASSISTANT_MODEL_API_KEY` | `noodle variables set` / `noodle secrets set`; never the SaaS environment |
27
+ | SaaS backend | `NOODLE_SERVICE_URL`, `NOODLE_ASSISTANT_CLIENT_ID`, `NOODLE_ASSISTANT_CLIENT_SECRET` | Backend-only environment or secret manager; never browser code or `NEXT_PUBLIC_*`/equivalent variables |
28
+
29
+ ## Author and validate
30
+
31
+ Use the same server tools in the embed; do not create a second tool set. Declare one server-level brand kit and an assistant configuration:
32
+
33
+ ```ts
34
+ branding: { name: "Acme", accent: "#3157D5" },
35
+ assistant: embeddedAssistant({
36
+ model: openAICompatible({
37
+ baseUrl: variable("ASSISTANT_MODEL_BASE_URL"),
38
+ model: variable("ASSISTANT_MODEL"),
39
+ apiKey: secret("ASSISTANT_MODEL_API_KEY"),
40
+ }),
41
+ allowedOrigins: ["http://localhost:3000", "https://app.example.com"],
42
+ layout: { mode: "floating", position: "bottom-right" },
43
+ }),
44
+ ```
45
+
46
+ `allowedOrigins` are exact origins: scheme, host, and optional port, with no path, trailing slash, or wildcard. Production origins must be HTTPS; plain HTTP is accepted only for loopback development origins (`http://localhost:<port>`, `http://127.0.0.1:<port>`). `noodle dev` serves the MCP project, not the embedding SaaS.
47
+
48
+ Run:
49
+
50
+ ```sh
51
+ noodle validate --json
52
+ noodle check --target embedded-assistant --json
53
+ ```
54
+
55
+ Use `noodle commands --json` before proposing command flags; do not invent flags from memory.
56
+
57
+ ## Configure and deploy
58
+
59
+ Local MCP authoring and tests need no account, but an external browser embed needs an active assistant-enabled deployment before a backend client can be created. Set the model values on the target org/app/env, then deploy:
60
+
61
+ ```sh
62
+ noodle variables set ASSISTANT_MODEL_BASE_URL --scope env --org <org> --app <app> --env <env> --value <https-model-base-url>
63
+ noodle variables set ASSISTANT_MODEL --scope env --org <org> --app <app> --env <env> --value <model>
64
+ noodle secrets set ASSISTANT_MODEL_API_KEY --scope env --org <org> --app <app> --env <env> --from-env ASSISTANT_MODEL_API_KEY
65
+ noodle deploy --org <org> --app <app> --env <env>
66
+ ```
67
+
68
+ Do not put these model values in the embedding SaaS environment. A production deployment may omit a local origin; include a loopback origin only when local browser integration is required.
69
+
70
+ ## Access modes and customer auth
71
+
72
+ Session exchange authenticates with the backend client credentials, so the embed works under any `--access` mode. Add `--access customers` only when verified end customers should also call the MCP endpoint directly. That mode requires `server.auth`; `noodle deploy` preflights the rule locally and fails with `server_auth_required` before contacting the service. Fix by adding auth to server options:
73
+
74
+ ```ts
75
+ auth: customerAuth.bridge({ verifyUrl: "https://app.example.com/api/auth/verify" }),
76
+ // or a concrete adapter: customerAuth.firebase({ projectId, apiKey })
77
+ ```
78
+
79
+ ## Create the backend client
80
+
81
+ After the deployment is active:
82
+
83
+ ```sh
84
+ noodle assistant clients create --name web --org <org> --app <app> --env <env>
85
+ ```
86
+
87
+ The CLI writes `{ clientId, clientSecret }` to a mode-`0600` file and prints only its path. Move the values into the SaaS backend secret manager without printing or committing them. Rotation invalidates the previous secret.
88
+
89
+ ## Integrate the customer backend
90
+
91
+ Read the customer repository lockfile or `packageManager` field and install `@noodleseed/assistant` with that existing package manager; never introduce a second lockfile.
92
+
93
+ Create an authenticated same-origin backend route:
94
+
95
+ ```ts
96
+ import { createAssistantSession } from "@noodleseed/assistant/server";
97
+
98
+ export async function POST(request: Request) {
99
+ const user = await requireCurrentUser(request);
100
+ const { context } = await request.json();
101
+ const session = await createAssistantSession({
102
+ serviceUrl: process.env.NOODLE_SERVICE_URL!,
103
+ clientId: process.env.NOODLE_ASSISTANT_CLIENT_ID!,
104
+ clientSecret: process.env.NOODLE_ASSISTANT_CLIENT_SECRET!,
105
+ origin: process.env.PUBLIC_APP_ORIGIN!,
106
+ user: { id: user.id, email: user.email, roles: user.roles },
107
+ context,
108
+ });
109
+ return Response.json(session);
110
+ }
111
+ ```
112
+
113
+ Authenticate before exchange. Source `origin` from trusted server configuration or strictly match the request origin against the same exact allowlist; never accept an arbitrary request header. Treat page context as untrusted model context, never authorization. Forward the helper response unchanged.
114
+
115
+ `serviceUrl` is the Noodle Seed control-plane base URL: the value `noodle assistant clients create` prints, also stored as `serviceUrl` in `deployment.json`. It is NOT the deployment MCP endpoint (`url`, which ends in `/v1/mcp` and rejects session exchange). Never probe or guess endpoints with real credentials.
116
+
117
+ ## Verified session context (identity and claims)
118
+
119
+ The embedding developer defines what authenticated session context the assistant receives. One mechanism, three hops:
120
+
121
+ 1. The authenticated backend passes standard identity and any verified claims at session exchange (flat scalars only):
122
+
123
+ ```ts
124
+ const session = await createAssistantSession({
125
+ serviceUrl, clientId, clientSecret, origin,
126
+ user: { id: user.id, email: user.email, name: user.name },
127
+ claims: { displayName: user.name, accountTier: account.tier, region: account.region },
128
+ });
129
+ ```
130
+
131
+ 2. The server author declares the allowlist in `server.ts` — undeclared claims are dropped at session exchange (never rejected, so backend and server deploys may skew safely):
132
+
133
+ ```ts
134
+ assistant: embeddedAssistant({
135
+ model, allowedOrigins,
136
+ sessionClaims: {
137
+ displayName: { exposeToModel: true },
138
+ accountTier: { exposeToModel: true },
139
+ region: {}, // tools only, never in the prompt
140
+ },
141
+ }),
142
+ ```
143
+
144
+ 3. Consumption. Tools read the verified identity and declared claims through the `user` scope:
145
+
146
+ ```ts
147
+ tool("greet", {
148
+ description: "Greet the signed-in user.",
149
+ input: z.object({}),
150
+ annotations: annotations.readOnly(),
151
+ fulfil: ({ user }) => ({ message: `Hello, ${user.name}!`, tier: user.claims.accountTier }),
152
+ });
153
+ ```
154
+
155
+ Manifest expressions use `${user.name}`, `${user.email}`, `${user.subject}`, `${user.claims.<key>}`. The model receives one platform identity line automatically: standard identity (name/email) whenever present, plus only the claims marked `exposeToModel: true` — so the assistant greets the actual user and can pass identity into tool arguments. `noodle check --target embedded-assistant` lists the declared claim contract.
156
+
157
+ Page `context` from the widget remains untrusted hint data; verified facts belong in `claims`, never in `context`.
158
+
159
+ ## The session response
160
+
161
+ The exchange returns the versioned Embedded Assistant v1 contract. `token`, `expiresAt`, and `endpoints.turns` / `endpoints.toolConfirmations` (absolute URLs) are always present; `configuration` is optional theming data. Forward the body unchanged; the widget posts turns to `endpoints.turns` itself. Do not rebuild, filter, or rewrite the response.
162
+
163
+ ## Mount the browser component
164
+
165
+ Use the React wrapper in React applications:
166
+
167
+ ```tsx
168
+ import { NoodleAssistant } from "@noodleseed/assistant/react";
169
+
170
+ <NoodleAssistant sessionEndpoint="/api/assistant/session" theme="auto" />;
171
+ ```
172
+
173
+ Or import the package root once and mount `<noodle-assistant session-endpoint="/api/assistant/session" theme="auto"></noodle-assistant>`. Mount only inside the authenticated application surface.
174
+
175
+ 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`.
176
+
177
+ ## Toolchain requirements
178
+
179
+ - Node.js 20+ for `@noodleseed/assistant/server`.
180
+ - The package ships ESM and CommonJS with full export conditions; no bundler aliases, `transpilePackages`, or ambient type shims are needed. If resolution fails, the installed package version is outdated: update `@noodleseed/assistant` instead of adding workarounds.
181
+ - TypeScript `moduleResolution` `bundler` or `node16` recommended; classic `node` also resolves the `/react` and `/server` subpaths.
182
+
183
+ ## Verify the boundary
184
+
185
+ - Signed-out session exchange returns `401`.
186
+ - The browser network/DOM/storage contains no client secret or model key.
187
+ - The local and production origins match `allowedOrigins` character-for-character.
188
+ - Auto-run requires the full safe-read annotation (`annotations.readOnly()`: read-only, non-destructive, closed-world); unannotated or partially annotated tools always confirm. Writes require confirmation by design.
189
+ - An expired turn re-exchanges once; confirmations never replay.
190
+ - Wrong-origin and malformed-origin requests fail closed.
191
+
192
+ ## Troubleshooting: symptom to diagnosis
193
+
194
+ | Symptom | Diagnosis | Fix |
195
+ | --- | --- | --- |
196
+ | Widget renders but no reply arrives and model usage stays zero | Turns are not reaching the service: outdated `@noodleseed/assistant` package, or the session response was rebuilt/filtered by the backend route | Update the package to the latest version; forward the session response unchanged |
197
+ | `assistant-error` with code `invalid_response` | The turn endpoint returned HTML or non-SSE content (auth redirect, proxy page) | Check the backend session route path and any middleware/rewrites on the embedding app |
198
+ | Build error `Package path ./react is not exported` | Outdated package version with import-only export conditions | Update `@noodleseed/assistant`; do not add webpack aliases or type shims |
199
+ | Deploy fails with `server_auth_required` | `--access customers` without `server.auth` | Add `customerAuth.bridge(...)` or an adapter to server options |
200
+ | Validate rejects an origin | Non-loopback HTTP origin in `allowedOrigins` | Use the exact HTTPS production origin; HTTP is only for `localhost`/`127.0.0.1` |
201
+ | Session exchange returns 404 | `serviceUrl` points at the deployment MCP endpoint | Use the control-plane service URL printed by `noodle assistant clients create` |
202
+ | Session exchange returns 403 `origin is not allowed` | Request origin differs from `allowedOrigins` character-for-character | Align the exact scheme/host/port on both sides and redeploy |
203
+ | Hydration or `HTMLElement is not defined` errors | The component mounted during server rendering | Mount client-only (`"use client"` or `next/dynamic` with `ssr: false`) |
204
+ | A read-only tool still asks for confirmation | Its annotations fail the safe-read rule: auto-run requires `readOnlyHint: true`, `destructiveHint: false`, AND `openWorldHint: false` (use `annotations.readOnly()`; `readOnly({ openWorld: true })` confirm-gates) | Fix the annotations; `noodle check --target embedded-assistant` lists every confirm-gated tool |
205
+ | `${user.claims.<key>}` is empty | Claim not declared in `sessionClaims` (or key typo) — undeclared claims are dropped at exchange | Declare the key in `embeddedAssistant({ sessionClaims })` and redeploy |
206
+ | `${user.name}` is empty | Backend did not pass `user.name` to `createAssistantSession` | Pass the verified name from the authenticated backend session |
207
+ | The model does not know a claim you passed | Claim is tools-only | Mark it `exposeToModel: true` in `sessionClaims` |
208
+ | Behavior does not change after `noodle deploy` | Outdated platform: before the 2026-07 fix, clients were pinned to their creation-time deployment | Update the platform; sessions now follow the tenant's active deployment |
@@ -38,7 +38,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
38
38
 
39
39
  ### Customer auth
40
40
 
41
- - `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments.
41
+ - `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments. Firebase Web App fields are browser-visible configuration: use `variable(...)`, not `secret(...)`, and restrict the key in Firebase.
42
42
 
43
43
  ### Sessions
44
44
 
@@ -60,7 +60,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
60
60
  - `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
61
61
  - View metadata (`viewTitle`, `viewDescription`, `csp`, `domain`, `permissions`) belongs on the tool that renders it; `asset("./path")` packages local files.
62
62
  - `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
63
- - `state` defines durable widget state handles; `handoff` declares allowed external domains for safe host handoff.
63
+ - `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.
64
64
 
65
65
  ## Recipes
66
66
 
@@ -18,6 +18,12 @@ Use `tool(name, { description, input, output, fulfil, view })` for a model-visib
18
18
 
19
19
  Generated widgets, official examples, and agent-authored MCP Apps must start with `@noodleseed/one/react` primitives and semantic tokens. Custom React/CSS or third-party components remain valid when the kit lacks the required behavior or the developer explicitly requests them.
20
20
 
21
+ `noodle init my-app` defaults to a comprehensive server—tools, resource, prompt, state contract, branding, handoff, widget, and embedded assistant—so begin by adapting that complete surface. Use `--template hello` only when the product explicitly needs the minimal server.
22
+
23
+ The default composition rule is: build the smallest useful conversational surface. Inline has one purpose, one primary action, and at most two visible actions. Use progressive disclosure or a later conversational turn for secondary detail; request fullscreen only when the user asks or the task genuinely needs it. Never use nested scrolling. At 280px and wider, the widget must remain one-column, readable, touch-safe, and free of horizontal overflow. Remove secondary chrome before shrinking essential content.
24
+
25
+ Every production widget handles loading, empty, partial, stale, error, retry, and success states. Prefill fields from known tool results and choose safe, reversible defaults; preserve the user’s work across rerenders, and never preselect a consequential action. Use public React primitives and branding tokens. Never author against `ns-*`, `nsr-*`, or example-local `--nw-*` classes/tokens; those are implementation details, not alternate design systems.
26
+
21
27
  ## React hook surface
22
28
 
23
29
  Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
@@ -47,7 +53,7 @@ Author React. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react
47
53
 
48
54
  ```tsx
49
55
  import type { ServerDefinition } from '@noodleseed/one';
50
- import { generateHelpers } from '@noodleseed/one/react';
56
+ import { Action, ActionBar, AsyncBoundary, Feedback, Field, Flow, Frame, Region, Select, generateHelpers } from '@noodleseed/one/react';
51
57
 
52
58
  // One call wires the typed host bridge; destructure only the hooks this view uses.
53
59
  const { useToolInfo, useCallTool, useViewState, useOpenExternal } =
@@ -71,28 +77,32 @@ export default function OrderStatus() {
71
77
 
72
78
  return (
73
79
  // data-llm mirrors the visible state back to the model as text context.
74
- <main data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
75
- <h1>Pickup order</h1>
76
- <label>
77
- Item
78
- <select value={item} onChange={(event) => setItem(event.currentTarget.value)}>
79
- <option value="falafel_wrap">Falafel Wrap</option>
80
- <option value="lentil_soup">Lentil Soup</option>
81
- <option value="mint_lemonade">Mint Lemonade</option>
82
- </select>
83
- </label>
84
- <button
85
- type="button"
86
- disabled={placeOrder.isPending}
80
+ <Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
81
+ <Flow variant="stack">
82
+ <AsyncBoundary state={placeOrder} loading="Placing order…" error={(error) => error.message}>
83
+ <Region title="Order" description="Choose one item for pickup.">
84
+ <Field label="Item">
85
+ <Select value={item} onChange={(event) => setItem(event.currentTarget.value)} options={[
86
+ { value: 'falafel_wrap', label: 'Falafel Wrap' },
87
+ { value: 'lentil_soup', label: 'Lentil Soup' },
88
+ { value: 'mint_lemonade', label: 'Mint Lemonade' },
89
+ ]} />
90
+ </Field>
91
+ </Region>
92
+ {confirmed?.status ? <Feedback status="success">{confirmed.status}</Feedback> : null}
93
+ <ActionBar>
94
+ <Action variant="primary" pending={placeOrder.isPending} pendingLabel="Placing…"
87
95
  onClick={() => placeOrder.callTool({ customer: shown?.customer ?? 'Guest', item })}
88
96
  >
89
- {placeOrder.isPending ? 'Placing…' : 'Place order'}
90
- </button>
91
- <p>{confirmed?.status ?? `Total: $${total}`}</p>
92
- <button type="button" onClick={() => openExternal(checkoutUrl)}>
97
+ Place order
98
+ </Action>
99
+ <Action onClick={() => openExternal(checkoutUrl)}>
93
100
  Continue checkout
94
- </button>
95
- </main>
101
+ </Action>
102
+ </ActionBar>
103
+ </AsyncBoundary>
104
+ </Flow>
105
+ </Frame>
96
106
  );
97
107
  }
98
108
  ```
@@ -169,7 +179,7 @@ export default server(
169
179
  A "ChatGPT App" is not a separate authoring surface — it is exactly this MCP Apps widget rendered by the ChatGPT host. From the same declaration you author three things:
170
180
 
171
181
  - `domain` on the widget — one https origin per app (required for app-store submission, optional for dev-mode testing).
172
- - `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review).
182
+ - `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review). These three lists are the complete CSP surface — there is no `base-uri` list.
173
183
  - server `handoff.allowedDomains` — the external-link targets `useOpenExternal()` opens.
174
184
 
175
185
  The compiler emits the rest automatically: the `openai/*` metadata (`openai/outputTemplate`, `openai/widgetCSP`, `openai/widgetDescription`) and ChatGPT’s `redirect_domains` (derived from `handoff.allowedDomains`). `window.openai` and Claude’s ext-apps bridge are auto-detected at startup, so the same widget renders in both Claude and ChatGPT with no host-specific code.