@noodleseed/agent-kit 0.21.0 → 0.22.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.
package/manifest.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "packageVersion": "0.21.0",
2
+ "packageVersion": "0.22.0",
3
3
  "files": [
4
4
  {
5
5
  "path": "skills/codex/SKILL.md",
6
- "sha256": "09469929851e57fae24fc411a173d0e356aabacb3743beed81232d03e2cc44dd",
6
+ "sha256": "854f9f274724ac7eeb5bdeb933f83b36cbcbeb3643e0de806ecc33fce89da832",
7
7
  "agentTarget": "codex"
8
8
  },
9
9
  {
@@ -28,12 +28,12 @@
28
28
  },
29
29
  {
30
30
  "path": "skills/codex/references/authoring-workflow.md",
31
- "sha256": "b4ca23d7545c8e96dbfee6a4cf2235d4eadfb27302e453fe631031041a17a8c6",
31
+ "sha256": "a2389c7cdbb28a817b8e5b616b625c02fed25fc017845349892c6e1b546a71c0",
32
32
  "agentTarget": "codex"
33
33
  },
34
34
  {
35
35
  "path": "skills/codex/references/embedded-assistant.md",
36
- "sha256": "db1eec6f6eed10b6525efc721b22756ff769b0234358821c15deb45cd61b39fb",
36
+ "sha256": "e72fbea15298b980ccd876ced45f9e0b17b472859e1f5731283d7677ed9b0f13",
37
37
  "agentTarget": "codex"
38
38
  },
39
39
  {
@@ -78,7 +78,7 @@
78
78
  },
79
79
  {
80
80
  "path": "skills/codex/references/examples.md",
81
- "sha256": "bd19dead1f7b1b6610a53694a06569d3c71d149bd6c72c4a1c94c83ded13a34f",
81
+ "sha256": "69ab3b0e85454f3d5df5f270b760216de7b1a69803a2f081447bc27bb634c524",
82
82
  "agentTarget": "codex"
83
83
  },
84
84
  {
@@ -251,6 +251,31 @@
251
251
  "sha256": "c486293ce4a0d38450f61533da17e419ca9fa490ef9577e8404f4fd700797747",
252
252
  "agentTarget": "codex"
253
253
  },
254
+ {
255
+ "path": "skills/codex/examples/customer-auth/README.md",
256
+ "sha256": "ae55e8c5e88216a3a3b1f57084f16e7af0edbf1e5b35ade7be5d5394887347ae",
257
+ "agentTarget": "codex"
258
+ },
259
+ {
260
+ "path": "skills/codex/examples/customer-auth/noodle.json",
261
+ "sha256": "15824af8846e8da4f9d14c28216ff95076dfc9f9964fcf127f441d44517d0eea",
262
+ "agentTarget": "codex"
263
+ },
264
+ {
265
+ "path": "skills/codex/examples/customer-auth/package.json",
266
+ "sha256": "bfbc1e9aa5f3c7f5449a6ade8ff78876f46ce77b7b867f26bdb556edee3171db",
267
+ "agentTarget": "codex"
268
+ },
269
+ {
270
+ "path": "skills/codex/examples/customer-auth/src/server.ts",
271
+ "sha256": "549dd7d0ccfb2fff7b0f3ce23194145e7364f5fc337a27e2b6d50b3dd7fb264d",
272
+ "agentTarget": "codex"
273
+ },
274
+ {
275
+ "path": "skills/codex/examples/customer-auth/test/server.test.ts",
276
+ "sha256": "97e9f00f102ea65f74dd9bca93090eb961af431a8c1285c80028dfae191476d9",
277
+ "agentTarget": "codex"
278
+ },
254
279
  {
255
280
  "path": "skills/codex/examples/food-ordering/README.md",
256
281
  "sha256": "4d42dfbaf46ee8003ae84b3ef79c9d2a8b716943b85e2e9329fa875007019524",
@@ -318,7 +343,7 @@
318
343
  },
319
344
  {
320
345
  "path": "skills/codex/examples/hello/src/server.ts",
321
- "sha256": "f5ac538d951adc4f18b2c14205f4f710e7920da2ea911444d52adeab8f801521",
346
+ "sha256": "5b7dd35428128cfe0f506054e80b060d8a87ef157b03d2ce94d304b14df6e521",
322
347
  "agentTarget": "codex"
323
348
  },
324
349
  {
@@ -353,7 +378,7 @@
353
378
  },
354
379
  {
355
380
  "path": "skills/claude-code/SKILL.md",
356
- "sha256": "e31b696b0bcf72de525e0c28ceba8c4d44bd2ce5c6522476a0c33ffc8cb6d677",
381
+ "sha256": "6b0569348845fc943f50fe9a4ba408034316c9f8bf4867734b41bfba494f8310",
357
382
  "agentTarget": "claude-code"
358
383
  },
359
384
  {
@@ -378,12 +403,12 @@
378
403
  },
379
404
  {
380
405
  "path": "skills/claude-code/references/authoring-workflow.md",
381
- "sha256": "b4ca23d7545c8e96dbfee6a4cf2235d4eadfb27302e453fe631031041a17a8c6",
406
+ "sha256": "a2389c7cdbb28a817b8e5b616b625c02fed25fc017845349892c6e1b546a71c0",
382
407
  "agentTarget": "claude-code"
383
408
  },
384
409
  {
385
410
  "path": "skills/claude-code/references/embedded-assistant.md",
386
- "sha256": "db1eec6f6eed10b6525efc721b22756ff769b0234358821c15deb45cd61b39fb",
411
+ "sha256": "e72fbea15298b980ccd876ced45f9e0b17b472859e1f5731283d7677ed9b0f13",
387
412
  "agentTarget": "claude-code"
388
413
  },
389
414
  {
@@ -428,7 +453,7 @@
428
453
  },
429
454
  {
430
455
  "path": "skills/claude-code/references/examples.md",
431
- "sha256": "bd19dead1f7b1b6610a53694a06569d3c71d149bd6c72c4a1c94c83ded13a34f",
456
+ "sha256": "69ab3b0e85454f3d5df5f270b760216de7b1a69803a2f081447bc27bb634c524",
432
457
  "agentTarget": "claude-code"
433
458
  },
434
459
  {
@@ -601,6 +626,31 @@
601
626
  "sha256": "c486293ce4a0d38450f61533da17e419ca9fa490ef9577e8404f4fd700797747",
602
627
  "agentTarget": "claude-code"
603
628
  },
629
+ {
630
+ "path": "skills/claude-code/examples/customer-auth/README.md",
631
+ "sha256": "ae55e8c5e88216a3a3b1f57084f16e7af0edbf1e5b35ade7be5d5394887347ae",
632
+ "agentTarget": "claude-code"
633
+ },
634
+ {
635
+ "path": "skills/claude-code/examples/customer-auth/noodle.json",
636
+ "sha256": "15824af8846e8da4f9d14c28216ff95076dfc9f9964fcf127f441d44517d0eea",
637
+ "agentTarget": "claude-code"
638
+ },
639
+ {
640
+ "path": "skills/claude-code/examples/customer-auth/package.json",
641
+ "sha256": "bfbc1e9aa5f3c7f5449a6ade8ff78876f46ce77b7b867f26bdb556edee3171db",
642
+ "agentTarget": "claude-code"
643
+ },
644
+ {
645
+ "path": "skills/claude-code/examples/customer-auth/src/server.ts",
646
+ "sha256": "549dd7d0ccfb2fff7b0f3ce23194145e7364f5fc337a27e2b6d50b3dd7fb264d",
647
+ "agentTarget": "claude-code"
648
+ },
649
+ {
650
+ "path": "skills/claude-code/examples/customer-auth/test/server.test.ts",
651
+ "sha256": "97e9f00f102ea65f74dd9bca93090eb961af431a8c1285c80028dfae191476d9",
652
+ "agentTarget": "claude-code"
653
+ },
604
654
  {
605
655
  "path": "skills/claude-code/examples/food-ordering/README.md",
606
656
  "sha256": "4d42dfbaf46ee8003ae84b3ef79c9d2a8b716943b85e2e9329fa875007019524",
@@ -668,7 +718,7 @@
668
718
  },
669
719
  {
670
720
  "path": "skills/claude-code/examples/hello/src/server.ts",
671
- "sha256": "f5ac538d951adc4f18b2c14205f4f710e7920da2ea911444d52adeab8f801521",
721
+ "sha256": "5b7dd35428128cfe0f506054e80b060d8a87ef157b03d2ce94d304b14df6e521",
672
722
  "agentTarget": "claude-code"
673
723
  },
674
724
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/agent-kit",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "private": false,
5
5
  "description": "Self-checking, self-updating agent skills for the Noodle Seed CLI. Authored in this repo by @noodle-borg/agent-kit; this is the published, independently-versioned canonical skills artifact the CLI fetches and verifies.",
6
6
  "license": "Apache-2.0",
@@ -1,7 +1,7 @@
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.21.0
4
+ version: 0.22.0
5
5
  hash: b4fc528d406e2149
6
6
  ---
7
7
 
@@ -0,0 +1,237 @@
1
+ # Customer Auth - NoodleSeed.com Firebase customer identity
2
+
3
+ This curated example owns the customer/end-user authentication with Firebase bridge capability slot. It is
4
+ the NoodleSeed.com dogfood app for proving that a SaaS app can protect an MCP endpoint with its own customer
5
+ identity provider while still using the generic Noodle Seed authoring API.
6
+
7
+ It also owns the embedded-assistant showcase: the same authenticated MCP surface can be dropped into the
8
+ SaaS web application as a fully customer-branded assistant with independent light and dark themes. The
9
+ assistant loads the active deployment's instructions and model-visible tools rather than installing a stale
10
+ second skill bundle. Interactive MCP Apps widgets continue to render in supported external hosts; the
11
+ initial embedded surface renders text and native tool confirmations.
12
+
13
+ The public developer entrypoint is [`src/server.ts`](src/server.ts). It declares `customerAuth.firebase(...)` with
14
+ the NoodleSeed.com Firebase project and Firebase Web App public config. It exposes a deliberately small MCP
15
+ surface for org discovery:
16
+
17
+ - `list_my_organizations` lists the NoodleSeed.com organizations the signed-in customer belongs to (no
18
+ arguments — the org set comes from the verified customer session).
19
+ - `list_org_apps` lists apps for one of those organizations through the dev app API.
20
+
21
+ The two tools chain: `list_my_organizations` surfaces the `org_id`s the customer can act on, and
22
+ `list_org_apps` takes one of those `org_id`s. There is no NoodleSeed-specific SDK helper. The downstream API
23
+ is an ordinary authored HTTP connector.
24
+
25
+ During MCP OAuth login, Noodle Cloud hosts the Firebase bridge page at
26
+ `https://cloud.noodleseed.dev/oauth/customer/firebase/authorize`. The customer app does not add an
27
+ authorization route. The SaaS operator only configures Firebase Auth to allow the Noodle Cloud origin, and
28
+ Noodle Cloud signs the customer in with Firebase before posting the Firebase ID token back to its own bridge
29
+ callback.
30
+
31
+ ## How delegated customer credentials are used
32
+
33
+ The example has two declarations that work together:
34
+
35
+ ```ts
36
+ auth: customerAuth.firebase({
37
+ projectId: variable('FIREBASE_PROJECT_ID'),
38
+ apiKey: variable('FIREBASE_WEB_API_KEY'),
39
+ authDomain: variable('FIREBASE_AUTH_DOMAIN'),
40
+ user: {
41
+ id: 'sub',
42
+ email: 'email',
43
+ name: 'name',
44
+ tenant: 'firebase.tenant',
45
+ orgs: 'claims.orgs',
46
+ roles: 'claims.roles',
47
+ },
48
+ }),
49
+ ```
50
+
51
+ That protects the MCP endpoint with the SaaS customer's Firebase identity. The connector then opts into
52
+ delegated customer credentials:
53
+
54
+ ```ts
55
+ auth: {
56
+ kind: 'delegatedSessionCookie',
57
+ provider: 'firebase',
58
+ sessionUrl: `${noodleseedApiOrigin}/api/auth/session`,
59
+ tokenField: 'idToken',
60
+ },
61
+ ```
62
+
63
+ Tool code calls the connector normally:
64
+
65
+ ```ts
66
+ fulfil({ input, connectors }) {
67
+ const apps = connectors.app_api.listOrgApps({
68
+ org_id: input.org_id,
69
+ skip: input.skip,
70
+ limit: input.limit,
71
+ });
72
+
73
+ return { result: apps.result };
74
+ }
75
+ ```
76
+
77
+ At runtime, Noodle Seed verifies the Firebase customer during MCP OAuth, stores that customer's delegated
78
+ Firebase refresh token in the credential broker, and refreshes a short-lived Firebase ID token only when a
79
+ connector-backed tool calls the NoodleSeed.com API. For this app API, the broker exchanges that ID token at
80
+ the existing Next.js `/api/auth/session` route and sends the resulting session cookie to the API. The MCP
81
+ access token remains a Noodle-issued resource-bound token and is never sent to the downstream API.
82
+
83
+ ## Delegated downstream auth for your own API (token exchange)
84
+
85
+ The Firebase path above only works for Firebase-session downstreams. When the downstream is **your own
86
+ API** with its own token issuance, use `delegatedTokenExchange` instead
87
+ ([ADR 0152](../../docs/decisions/0152-delegated-token-exchange-connector-auth.md)): the platform signs a
88
+ short-lived, JWKS-verifiable assertion of the signed-in user and exchanges it (RFC 8693) at a token
89
+ endpoint you implement, which mints your own user-scoped token — so your API enforces its own per-user
90
+ authorization on every call. It works for identities from `customerAuth.bridge(...)` and from embedded
91
+ assistant sessions, with no per-user OAuth enrollment.
92
+
93
+ ```ts
94
+ auth: {
95
+ kind: 'delegatedTokenExchange',
96
+ tokenUrl: 'https://app.example.com/api/assistant/oauth/token', // origin must be in allowedOrigins
97
+ clientId: variable('EXAMPLE_DELEG_CLIENT_ID'),
98
+ clientSecret: secret('EXAMPLE_DELEG_CLIENT_SECRET'),
99
+ scopes: ['time_off'],
100
+ },
101
+ ```
102
+
103
+ Your endpoint authenticates the broker's client credential, verifies the `subject_token` JWT against the
104
+ platform issuer JWKS (claims include the verified `sub`, `email`, `name`, declared session `claims`,
105
+ `tenant`, and `deployment`), mints a short-lived user-scoped token, and returns the standard
106
+ `{ access_token, token_type, expires_in }` response. The exact wire contract and a copyable endpoint
107
+ implementation live in [docs/spec/connectors.md](../../docs/spec/connectors.md) and the Agent Kit
108
+ authoring-workflow reference ("Delegated downstream auth"). `noodle auth doctor` reports each declared
109
+ exchange endpoint.
110
+
111
+ ## Validate
112
+
113
+ ```bash
114
+ noodle auth doctor examples/customer-auth/src/server.ts
115
+ noodle validate examples/customer-auth/src/server.ts
116
+ ```
117
+
118
+ ## Run locally
119
+
120
+ ```bash
121
+ noodle dev examples/customer-auth/src/server.ts --app noodleseed-customer-auth
122
+ ```
123
+
124
+ ## Configuration
125
+
126
+ The embedded assistant uses a customer-supplied OpenAI Chat Completions-compatible endpoint. Configure its
127
+ managed values at the Noodle deployment environment; none of these values belongs in the customer web
128
+ application environment, and the API key never reaches the browser:
129
+
130
+ ```bash
131
+ noodle variables set ASSISTANT_MODEL_BASE_URL https://model.example.com/v1 --scope env
132
+ noodle variables set ASSISTANT_MODEL your-model --scope env
133
+ noodle secrets set ASSISTANT_MODEL_API_KEY --scope env
134
+ noodle variables set FIREBASE_PROJECT_ID your-firebase-project --scope env
135
+ noodle variables set FIREBASE_WEB_API_KEY your-firebase-web-api-key --scope env
136
+ noodle variables set FIREBASE_AUTH_DOMAIN your-firebase-project.firebaseapp.com --scope env
137
+ noodle check --target embedded-assistant src/server.ts
138
+ ```
139
+
140
+ Assistant origins are exact and HTTPS-only. A local embedding application must serve itself over HTTPS and
141
+ declare an origin such as `https://localhost:3000`; `noodle dev` does not provide TLS for that separate web
142
+ application.
143
+
144
+ Create the backend credential after deployment. The CLI writes it to a mode-0600 file and never prints the
145
+ secret:
146
+
147
+ ```bash
148
+ noodle assistant clients create --name web --org noodleseed --app customer-auth --env prod
149
+ ```
150
+
151
+ Only the Noodle service URL, assistant client ID, and assistant client secret belong in the authenticated
152
+ customer backend. The model URL, model name, and model API key remain managed by the Noodle deployment.
153
+
154
+ The customer's authenticated backend calls `createAssistantSession(...)` from
155
+ `@noodleseed/assistant/server`, passing the already-verified user and browser origin. The browser then uses
156
+ the returned short-lived session through the Web Component or React wrapper:
157
+
158
+ ```bash
159
+ pnpm add @noodleseed/assistant
160
+ ```
161
+
162
+ ```tsx
163
+ import { NoodleAssistant } from '@noodleseed/assistant/react';
164
+
165
+ <NoodleAssistant
166
+ sessionEndpoint="/api/noodle-assistant/session"
167
+ theme="auto"
168
+ onSessionExpired={() => console.info('Assistant session renewed')}
169
+ />;
170
+ ```
171
+
172
+ `theme="auto"` follows the SaaS application. The server-level `branding` block is inherited by both MCP App
173
+ widgets and the assistant; documented `--ns-assistant-*` semantic CSS variables remain the final integration
174
+ escape hatch. There is no second assistant branding declaration.
175
+ The end-user UI contains only customer branding.
176
+ Text streams progressively. Expired turns re-exchange through the authenticated backend and retry once;
177
+ consent-bound tool confirmations never replay automatically.
178
+
179
+ The Firebase project ID is required because Firebase ID tokens use the project ID as the token audience and
180
+ issuer suffix. The runtime verifies `aud` against the project ID and `iss` against
181
+ `https://securetoken.google.com/<projectId>`.
182
+
183
+ The Firebase Web API key and auth domain are public Firebase browser configuration. They let the Noodle
184
+ Cloud-hosted bridge initialize Firebase Auth for this customer project; they are not server secrets. Keep
185
+ them out of source with `variable(...)`, restrict the Firebase key to the expected browser origins and APIs,
186
+ and use `secret(...)` only for credentials that must never reach a browser.
187
+
188
+ The `noodleseed_app_api` connector currently points at the NoodleSeed.com dev app surface:
189
+
190
+ ```text
191
+ https://dev.noodleseed.com
192
+ ```
193
+
194
+ When the customer app moves from `dev.noodleseed.com` to `app.noodleseed.com`, update the connector's
195
+ `noodleseedApiOrigin` constant to the production API origin that serves the same paths.
196
+
197
+ The connector uses delegated Firebase customer credentials. During the customer OAuth bridge, Noodle Seed
198
+ verifies the Firebase ID token, stores the Firebase refresh token through the credential broker, and refreshes
199
+ a Firebase ID token when the connector calls the NoodleSeed.com app API. The broker then exchanges that ID
200
+ token for the app's existing Next.js session cookie. There is no shared `NOODLESEED_APP_API_TOKEN` for this
201
+ example.
202
+
203
+ Firebase Auth must list `cloud.noodleseed.dev` as an authorized domain before browser sign-in works in
204
+ production.
205
+
206
+ ## Deploy customer-protected to Noodle Seed Cloud
207
+
208
+ ```bash
209
+ noodle deploy examples/customer-auth/src/server.ts \
210
+ --org noodleseed \
211
+ --app customer-auth \
212
+ --env prod \
213
+ --access customers
214
+ ```
215
+
216
+ Endpoint:
217
+
218
+ ```text
219
+ https://cloud.noodleseed.dev/o/noodleseed/customer-auth/mcp
220
+ ```
221
+
222
+ ## MCP Primitives
223
+
224
+ - Tool `list_my_organizations`: calls `GET /api/organizations` and returns the organizations the signed-in
225
+ customer is a member of. Takes no arguments; the org set is scoped by the verified customer session.
226
+ - Tool `list_org_apps`: calls `GET /api/organizations/{org_id}/apps` for one organization `org_id`.
227
+
228
+ ## Auth boundary
229
+
230
+ Firebase ID-token verification is handled by Noodle Seed's generic Firebase bridge adapter during OAuth
231
+ issuance. The MCP client receives a Noodle-issued, resource-bound access token marked as a Firebase customer
232
+ identity; raw Firebase tokens and inbound MCP bearer tokens are never forwarded to tools, connectors,
233
+ widgets, or downstream systems.
234
+
235
+ The connector-backed tools use the credential broker to turn the signed-in Firebase customer session into the
236
+ same session-cookie credential that the existing NoodleSeed.com Next.js API already expects. The inbound MCP
237
+ bearer token is never used as an app API credential.
@@ -0,0 +1,4 @@
1
+ {
2
+ "entrypoint": "src/server.ts",
3
+ "name": "customer-auth"
4
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "customer-auth",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "vitest run",
8
+ "validate": "noodle validate",
9
+ "dev": "noodle dev",
10
+ "deploy": "noodle deploy"
11
+ },
12
+ "devDependencies": {
13
+ "@noodleseed/one": "latest",
14
+ "vitest": "latest"
15
+ }
16
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ annotations,
3
+ connector,
4
+ customerAuth,
5
+ embeddedAssistant,
6
+ openAICompatible,
7
+ secret,
8
+ server,
9
+ tool,
10
+ variable,
11
+ z,
12
+ } from '@noodleseed/one';
13
+
14
+ const noodleseedApiOrigin = 'https://dev.noodleseed.com';
15
+
16
+ const noodleseedApi = connector('noodleseed_app_api')
17
+ .version('1.0.0')
18
+ .http({
19
+ baseUrl: noodleseedApiOrigin,
20
+ allowedOrigins: [noodleseedApiOrigin],
21
+ auth: {
22
+ kind: 'delegatedSessionCookie',
23
+ provider: 'firebase',
24
+ sessionUrl: `${noodleseedApiOrigin}/api/auth/session`,
25
+ tokenField: 'idToken',
26
+ },
27
+ operations: {
28
+ list_org_apps: {
29
+ type: 'read',
30
+ method: 'GET',
31
+ path: '/api/organizations/${args.org_id}/apps',
32
+ query: ['skip', 'limit'],
33
+ input: z.object({
34
+ org_id: z.string(),
35
+ skip: z.number().optional(),
36
+ limit: z.number().optional(),
37
+ }),
38
+ output: z.object({ result: z.unknown().optional() }),
39
+ response: {
40
+ result: '${response}',
41
+ },
42
+ },
43
+ list_organizations: {
44
+ type: 'read',
45
+ method: 'GET',
46
+ path: '/api/organizations',
47
+ output: z.object({ organizations: z.array(z.unknown()).optional() }),
48
+ response: {
49
+ organizations: '${response.organizations}',
50
+ },
51
+ },
52
+ },
53
+ });
54
+
55
+ export default server(
56
+ 'noodleseed_customer_auth',
57
+ {
58
+ title: 'NoodleSeed.com Customer Auth',
59
+ version: '1.0.0',
60
+ branding: {
61
+ name: 'Noodle Seed Assistant',
62
+ accent: '#E85D24',
63
+ surface: '#FFFFFF',
64
+ surfaceDark: '#171310',
65
+ colorScheme: 'auto',
66
+ theme: {
67
+ light: { accentText: '#FFFFFF', text: '#1C1714' },
68
+ dark: { accent: '#FF8A4C', accentText: '#1C100A', text: '#FFF8F2' },
69
+ },
70
+ },
71
+ use: { app_api: noodleseedApi },
72
+ auth: customerAuth.firebase({
73
+ projectId: variable('FIREBASE_PROJECT_ID'),
74
+ apiKey: variable('FIREBASE_WEB_API_KEY'),
75
+ authDomain: variable('FIREBASE_AUTH_DOMAIN'),
76
+ user: {
77
+ id: 'sub',
78
+ email: 'email',
79
+ name: 'name',
80
+ tenant: 'firebase.tenant',
81
+ orgs: 'claims.orgs',
82
+ roles: 'claims.roles',
83
+ },
84
+ }),
85
+ instructions:
86
+ 'Customer-authenticated demo. Firebase proves the customer identity, while read-only NoodleSeed.com API calls use broker-managed delegated Firebase customer credentials.',
87
+ assistant: embeddedAssistant({
88
+ model: openAICompatible({
89
+ baseUrl: variable('ASSISTANT_MODEL_BASE_URL'),
90
+ model: variable('ASSISTANT_MODEL'),
91
+ apiKey: secret('ASSISTANT_MODEL_API_KEY'),
92
+ }),
93
+ // Production origins are exact HTTPS; http://localhost:<port> is allowed for local development.
94
+ allowedOrigins: [
95
+ 'https://app.noodleseed.com',
96
+ 'https://dev.noodleseed.com',
97
+ 'http://localhost:3000',
98
+ ],
99
+ layout: { mode: 'floating', position: 'bottom-right', panelWidth: 420 },
100
+ labels: {
101
+ welcomeHeading: 'How can I help with Noodle Seed?',
102
+ composerPlaceholder: 'Ask about your apps…',
103
+ },
104
+ suggestedPrompts: ['Show my organizations', 'List the apps in my organization'],
105
+ }),
106
+ },
107
+ [
108
+ tool('list_org_apps', {
109
+ description: 'List NoodleSeed.com apps for an organization from the dev app API.',
110
+ input: z.object({
111
+ org_id: z.string(),
112
+ skip: z.number().int().min(0).optional(),
113
+ limit: z.number().int().min(1).max(100).optional(),
114
+ }),
115
+ output: z.object({
116
+ result: z.unknown(),
117
+ }),
118
+ annotations: annotations.readOnly(),
119
+ fulfil({ input, connectors }) {
120
+ const apps = connectors.app_api.listOrgApps({
121
+ org_id: input.org_id,
122
+ skip: input.skip,
123
+ limit: input.limit,
124
+ });
125
+ return {
126
+ result: apps.result,
127
+ };
128
+ },
129
+ }),
130
+ tool('list_my_organizations', {
131
+ description: 'List the NoodleSeed.com organizations the signed-in customer belongs to.',
132
+ input: z.object({}),
133
+ output: z.object({
134
+ organizations: z.array(z.unknown()),
135
+ }),
136
+ annotations: annotations.readOnly(),
137
+ fulfil({ connectors }) {
138
+ const organizations = connectors.app_api.listOrganizations();
139
+ return {
140
+ organizations: organizations.organizations,
141
+ };
142
+ },
143
+ }),
144
+ ],
145
+ );
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import app from '../src/server.js';
3
+
4
+ describe('customer-auth example', () => {
5
+ it('exports a customer-authenticated, customer-branded embedded assistant', async () => {
6
+ expect(typeof app.toManifest).toBe('function');
7
+ const manifest = await app.toManifest();
8
+ expect(manifest.server.assistant).toMatchObject({
9
+ model: { kind: 'openai-compatible', apiKey: 'ASSISTANT_MODEL_API_KEY' },
10
+ layout: { mode: 'floating' },
11
+ });
12
+ expect(
13
+ manifest.server.assistant?.allowedOrigins.every((origin) => origin.startsWith('https://')),
14
+ ).toBe(true);
15
+ expect(manifest.server.branding).toMatchObject({
16
+ name: 'Noodle Seed Assistant',
17
+ colorScheme: 'auto',
18
+ });
19
+ expect(manifest.server.auth).toMatchObject({
20
+ projectId: '${env.FIREBASE_PROJECT_ID}',
21
+ apiKey: '${env.FIREBASE_WEB_API_KEY}',
22
+ authDomain: '${env.FIREBASE_AUTH_DOMAIN}',
23
+ });
24
+ });
25
+ });
@@ -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',
@@ -22,6 +22,8 @@ export default server(
22
22
  output: z.object({
23
23
  message: z.string(),
24
24
  }),
25
+ // Read-only, closed-world: assistant surfaces run this without a consent prompt.
26
+ annotations: annotations.readOnly(),
25
27
  fulfil: ({ input }) => {
26
28
  return { message: `Hello, ${input.name}!` };
27
29
  },