@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 +61 -11
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +1 -1
- package/skills/claude-code/examples/customer-auth/README.md +237 -0
- package/skills/claude-code/examples/customer-auth/noodle.json +4 -0
- package/skills/claude-code/examples/customer-auth/package.json +16 -0
- package/skills/claude-code/examples/customer-auth/src/server.ts +145 -0
- package/skills/claude-code/examples/customer-auth/test/server.test.ts +25 -0
- package/skills/claude-code/examples/hello/src/server.ts +3 -1
- package/skills/claude-code/references/authoring-workflow.md +72 -2
- package/skills/claude-code/references/embedded-assistant.md +54 -2
- package/skills/claude-code/references/examples.md +1 -1
- package/skills/codex/SKILL.md +1 -1
- package/skills/codex/examples/customer-auth/README.md +237 -0
- package/skills/codex/examples/customer-auth/noodle.json +4 -0
- package/skills/codex/examples/customer-auth/package.json +16 -0
- package/skills/codex/examples/customer-auth/src/server.ts +145 -0
- package/skills/codex/examples/customer-auth/test/server.test.ts +25 -0
- package/skills/codex/examples/hello/src/server.ts +3 -1
- package/skills/codex/references/authoring-workflow.md +72 -2
- package/skills/codex/references/embedded-assistant.md +54 -2
- package/skills/codex/references/examples.md +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- Repair loop
|
|
8
8
|
- Connectors
|
|
9
9
|
- HTTP connector example (full server)
|
|
10
|
+
- Delegated downstream auth (call your API as the signed-in user)
|
|
10
11
|
- Design tools for the model
|
|
11
12
|
- Compute connector example
|
|
12
13
|
- Tests
|
|
@@ -37,7 +38,7 @@ Declare connectors as data, not imperative code:
|
|
|
37
38
|
|
|
38
39
|
Tools record connector calls into a flow; recording is not execution. Do not branch on runtime outputs with native `if` — use declarative `when(...)` conditions.
|
|
39
40
|
|
|
40
|
-
HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_TOKEN") }`), `apiKey` (`{ kind: "apiKey", header: "X-API-Key", secret: secret("API_KEY") }`), `clientCredentials`, `delegatedOAuth`, and `
|
|
41
|
+
HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_TOKEN") }`), `apiKey` (`{ kind: "apiKey", header: "X-API-Key", secret: secret("API_KEY") }`), `clientCredentials`, `delegatedOAuth`, `delegatedSessionCookie`, and `delegatedTokenExchange` (per-user calls to your own API — see "Delegated downstream auth" below). Use managed `secret(...)` / `variable(...)` refs for all values that differ by org/app/env.
|
|
41
42
|
|
|
42
43
|
## HTTP connector example (full server)
|
|
43
44
|
|
|
@@ -96,7 +97,76 @@ export default server('support', { title: 'Support', version: '1.0.0', use: { cr
|
|
|
96
97
|
|
|
97
98
|
Naming: connector operation names and tool names are lowercase-with-underscores. Map with `${args.field}` for tool/operation inputs and `${response.path}` for the response — the parsed JSON body is bound directly to `${response}`, so there is **no `.body` envelope**; use bracket syntax for array indices (`${response.data[0].id}`) — a dotted numeric index like `.0.` is invalid. Declare URL query parameters with the operation-level `query: ["arg"]` array, **not** inside `request` (which builds only the JSON body). `allowedOrigins` must be literal origin URLs (the SSRF allowlist); `baseUrl` may be a `variable(...)` that differs by env.
|
|
98
99
|
|
|
99
|
-
More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials` | `delegatedOAuth` | `delegatedSessionCookie`. For client credentials use `{ kind: "clientCredentials", tokenUrl, clientId, clientSecret, scopes? }` (RFC-6749 grant); for a non-standard partner token endpoint add `profile: "custom"` with a `custom: { requestFormat, clientIdField, clientSecretField, tokenResponsePath, expirySource }` descriptor. Do not put credential headers in operation `headers`; use connector `auth`.
|
|
100
|
+
More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials` | `delegatedOAuth` | `delegatedSessionCookie` | `delegatedTokenExchange`. For client credentials use `{ kind: "clientCredentials", tokenUrl, clientId, clientSecret, scopes? }` (RFC-6749 grant); for a non-standard partner token endpoint add `profile: "custom"` with a `custom: { requestFormat, clientIdField, clientSecretField, tokenResponsePath, expirySource }` descriptor. Do not put credential headers in operation `headers`; use connector `auth`. Use `.compute(name, { input, output, run })` for a sandboxed transform; `provides:` (instead of `use:`) exposes a connector only to compute `callOperation`; and `noodle import openapi <file>` generates a connector from an OpenAPI spec.
|
|
101
|
+
|
|
102
|
+
## Delegated downstream auth (call your API as the signed-in user)
|
|
103
|
+
|
|
104
|
+
Use delegated connector auth when the downstream API must enforce its own per-user authorization — a shared service credential plus a forwarded user id would bypass it. Three shapes exist; pick by who owns the downstream:
|
|
105
|
+
|
|
106
|
+
- **`delegatedTokenExchange`** — your own API. The platform signs a short-lived, verifiable assertion of the signed-in user and exchanges it at a token endpoint you implement (RFC 8693). Works with `customerAuth.bridge(...)` identities and embedded-assistant sessions; no per-user OAuth enrollment.
|
|
107
|
+
- **`delegatedOAuth` with `provider: "firebase" | "microsoft"`** — Noodle-managed bridge providers using stored per-user refresh tokens. Requires the matching `customerAuth` bridge; any other provider string is the compile error `unsupported_delegated_provider`.
|
|
108
|
+
- **`delegatedSessionCookie`** — Firebase-managed session-cookie apps only; not a generic mechanism.
|
|
109
|
+
|
|
110
|
+
### The connector (your `server.ts`)
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
auth: {
|
|
114
|
+
kind: 'delegatedTokenExchange',
|
|
115
|
+
tokenUrl: 'https://app.example.com/api/assistant/oauth/token', // origin must be in allowedOrigins
|
|
116
|
+
clientId: variable('EXAMPLE_DELEG_CLIENT_ID'),
|
|
117
|
+
clientSecret: secret('EXAMPLE_DELEG_CLIENT_SECRET'),
|
|
118
|
+
scopes: ['time_off'], // optional
|
|
119
|
+
audience: 'example-api', // optional; assertion + request audience, defaults to tokenUrl
|
|
120
|
+
authMethod: 'client_secret_basic', // default; client_secret_post supported
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Inside tools, `${user.id}` / `${user.email}` / `${user.name}` / `${user.claims.*}` stay available as verified context; the delegated credential is what makes the *downstream call itself* run as that user.
|
|
125
|
+
|
|
126
|
+
### The exchange request your endpoint receives
|
|
127
|
+
|
|
128
|
+
The broker POSTs `application/x-www-form-urlencoded` to `tokenUrl` with `Authorization: Basic base64(clientId:clientSecret)` (or `client_id`/`client_secret` form fields for `client_secret_post`):
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
|
|
132
|
+
subject_token=<RS256 JWT signed by the platform>
|
|
133
|
+
subject_token_type=urn:ietf:params:oauth:token-type:jwt
|
|
134
|
+
scope=time_off (space-joined, when configured)
|
|
135
|
+
audience=example-api (when configured)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The `subject_token` claims: `iss` (platform issuer; JWKS at `{iss}/.well-known/jwks.json`), `sub` (verified user id), `aud` (your configured audience or the tokenUrl), `email`, `name`, `claims` (declared session claims), `tenant` (`org/app/env`), `deployment`, `iat`, `exp` (about 120 s), `jti`. Respond with `{ "access_token": "...", "token_type": "Bearer", "expires_in": 900 }`; the broker caches per user + connector + scopes until `expires_in` minus 300 s and presents the token downstream as `Authorization: Bearer`.
|
|
139
|
+
|
|
140
|
+
### The downstream token endpoint (your backend)
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// POST /api/assistant/oauth/token — Node example with jose.
|
|
144
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
145
|
+
|
|
146
|
+
const PLATFORM_ISSUER = process.env.NOODLE_PLATFORM_ISSUER!; // e.g. https://cloud.noodleseed.dev
|
|
147
|
+
const jwks = createRemoteJWKSet(new URL(`${PLATFORM_ISSUER}/.well-known/jwks.json`));
|
|
148
|
+
|
|
149
|
+
export async function tokenEndpoint(req: Request): Promise<Response> {
|
|
150
|
+
// 1. Authenticate the broker client credential (client_secret_basic).
|
|
151
|
+
const basic = req.headers.get('authorization') ?? '';
|
|
152
|
+
const [clientId, clientSecret] = atob(basic.replace(/^Basic /, '')).split(':');
|
|
153
|
+
if (!isValidClient(clientId, clientSecret)) return new Response(null, { status: 401 });
|
|
154
|
+
// 2. Verify the platform-signed user assertion (never trust a plaintext user id).
|
|
155
|
+
const form = new URLSearchParams(await req.text());
|
|
156
|
+
const { payload } = await jwtVerify(form.get('subject_token') ?? '', jwks, {
|
|
157
|
+
issuer: PLATFORM_ISSUER,
|
|
158
|
+
audience: 'https://app.example.com/api/assistant/oauth/token', // your tokenUrl or configured audience
|
|
159
|
+
});
|
|
160
|
+
if (payload.deployment !== undefined && payload.tenant !== 'your-org/your-app/prod') {
|
|
161
|
+
return new Response(null, { status: 403 }); // optionally pin the calling deployment
|
|
162
|
+
}
|
|
163
|
+
// 3. Mint your own short-lived user-scoped token; your API enforces per-user rules from it.
|
|
164
|
+
const accessToken = await issueAccessToken(String(payload.sub), clientId, form.get('scope') ?? '');
|
|
165
|
+
return Response.json({ access_token: accessToken, token_type: "Bearer", expires_in: 900 });
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Diagnose with `noodle auth doctor` (reports each delegated token exchange endpoint and whether managed delegated providers pair with the declared `customerAuth`). Common failures: `disallowed_token_origin` (add the tokenUrl origin to `allowedOrigins`), `unsupported_delegated_provider` (custom provider strings never reach a broker; use `delegatedTokenExchange`), and `delegated token exchange requires a verified customer caller` at runtime (the surface calling the tool has no verified customer identity — check `customerAuth` and, for embeds, `createAssistantSession({ user })`).
|
|
100
170
|
|
|
101
171
|
## Design tools for the model
|
|
102
172
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- Access modes and customer auth
|
|
9
9
|
- Create the backend client
|
|
10
10
|
- Integrate the customer backend
|
|
11
|
+
- Verified session context (identity and claims)
|
|
11
12
|
- The session response
|
|
12
13
|
- Mount the browser component
|
|
13
14
|
- Toolchain requirements
|
|
@@ -113,6 +114,50 @@ Authenticate before exchange. Source `origin` from trusted server configuration
|
|
|
113
114
|
|
|
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.
|
|
115
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
|
+
To make the *downstream API call itself* run as the signed-in user (your API enforces its own per-user authorization instead of trusting a forwarded id), give the connector `auth.kind: "delegatedTokenExchange"` — the platform signs a verifiable assertion of this session identity and exchanges it at a token endpoint you implement. Assistant sessions carry the identity this needs; the full contract and a copyable endpoint implementation are in `references/authoring-workflow.md` ("Delegated downstream auth").
|
|
160
|
+
|
|
116
161
|
## The session response
|
|
117
162
|
|
|
118
163
|
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.
|
|
@@ -142,7 +187,7 @@ The component renders a custom element and must mount client-side. In a Next.js
|
|
|
142
187
|
- Signed-out session exchange returns `401`.
|
|
143
188
|
- The browser network/DOM/storage contains no client secret or model key.
|
|
144
189
|
- The local and production origins match `allowedOrigins` character-for-character.
|
|
145
|
-
-
|
|
190
|
+
- 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.
|
|
146
191
|
- An expired turn re-exchanges once; confirmations never replay.
|
|
147
192
|
- Wrong-origin and malformed-origin requests fail closed.
|
|
148
193
|
|
|
@@ -157,4 +202,11 @@ The component renders a custom element and must mount client-side. In a Next.js
|
|
|
157
202
|
| 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` |
|
|
158
203
|
| Session exchange returns 404 | `serviceUrl` points at the deployment MCP endpoint | Use the control-plane service URL printed by `noodle assistant clients create` |
|
|
159
204
|
| 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 |
|
|
160
|
-
| Hydration or `HTMLElement is not defined` errors | The component mounted during server rendering | Mount client-only (`"use client"` or `next/dynamic` with `ssr: false`) |
|
|
205
|
+
| Hydration or `HTMLElement is not defined` errors | The component mounted during server rendering | Mount client-only (`"use client"` or `next/dynamic` with `ssr: false`) |
|
|
206
|
+
| 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 |
|
|
207
|
+
| `${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 |
|
|
208
|
+
| `${user.name}` is empty | Backend did not pass `user.name` to `createAssistantSession` | Pass the verified name from the authenticated backend session |
|
|
209
|
+
| The model does not know a claim you passed | Claim is tools-only | Mark it `exposeToModel: true` in `sessionClaims` |
|
|
210
|
+
| 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 |
|
|
211
|
+
| A delegated connector tool fails with `delegated token exchange requires a verified customer caller` | The calling surface has no verified customer identity (or an old session minted before the platform carried the resource audience) | Verify `customerAuth` is configured and the backend passes the verified `user` to `createAssistantSession`; re-mint the session |
|
|
212
|
+
| Deploy fails with `unsupported_delegated_provider` | `delegatedOAuth.provider` only supports the managed `firebase`/`microsoft` bridges | Use `auth.kind: "delegatedTokenExchange"` for your own token endpoint (see authoring-workflow.md) |
|
|
@@ -14,12 +14,12 @@ Paths are relative to this skill directory. Assets (images/fonts) are omitted fr
|
|
|
14
14
|
| `acme-discovery` | Top-of-funnel discovery→handoff: a discovery carousel, a `create_handoff` deep link, and a design-first UX spec + wireframe. | `examples/acme-discovery/src/server.ts` + `design/` |
|
|
15
15
|
| `acme-tasks` | A two-way productivity app designed around its top-3 prioritized flows (capture/prioritize/complete), with a design-first flow spec + wireframe. | `examples/acme-tasks/src/server.ts` + `design/` |
|
|
16
16
|
| `acme-bistro` | End-to-end ordering with a payment-only handoff; ships a gold-standard `design/` set (UX doc, wireframe with compliance audit, API contract). | `examples/acme-bistro/src/server.ts` + `design/` |
|
|
17
|
+
| `customer-auth` | End-user (customer) auth via OIDC/Firebase bridge with delegated credentials. | `examples/customer-auth/src/server.ts` |
|
|
17
18
|
|
|
18
19
|
## In the repository only — `examples/<name>/` on GitHub
|
|
19
20
|
|
|
20
21
|
| Example | Use when |
|
|
21
22
|
| :-- | :-- |
|
|
22
|
-
| `customer-auth` | End-user (customer) auth via OIDC/Firebase bridge with delegated credentials. |
|
|
23
23
|
| `stateful-draft` | Durable, caller-scoped widget state handles with optimistic revisions. |
|
|
24
24
|
| `perplexity` | A real SaaS API with bearer auth and a managed `secret`. |
|
|
25
25
|
| `bitcoin` | API-key HTTP connector, custom auth header, and compute normalization. |
|