@noodleseed/agent-kit 0.90.0 → 0.92.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 (102) hide show
  1. package/manifest.json +707 -275
  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 +7 -5
  31. package/skills/claude-code/examples/customer-auth/README.md +200 -52
  32. package/skills/claude-code/examples/customer-auth/src/server.ts +3 -0
  33. package/skills/claude-code/examples/customer-auth/test/server.test.ts +6 -0
  34. package/skills/claude-code/examples/customer-auth/vitest.config.ts +6 -0
  35. package/skills/claude-code/examples/food-ordering/README.md +1 -1
  36. package/skills/claude-code/examples/food-ordering/src/server.ts +1 -0
  37. package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
  38. package/skills/claude-code/publishing-mcp-integrations/SKILL.md +3 -3
  39. package/skills/claude-code/publishing-mcp-integrations/references/app-directory-compliance.md +59 -0
  40. package/skills/claude-code/publishing-mcp-integrations/references/publishing.md +63 -0
  41. package/skills/claude-code/references/authoring-workflow.md +8 -0
  42. package/skills/claude-code/references/embedded-assistant.md +259 -67
  43. package/skills/claude-code/references/sdk-surface.md +1 -1
  44. package/skills/claude-code/reporting-noodle-feedback/SKILL.md +2 -2
  45. package/skills/claude-code/reporting-noodle-feedback/references/feedback.md +80 -0
  46. package/skills/claude-code/verifying-mcp-delivery/SKILL.md +3 -3
  47. package/skills/claude-code/verifying-mcp-delivery/references/test-in-hosts.md +39 -0
  48. package/skills/claude-code/verifying-mcp-delivery/references/verify-and-recover.md +67 -0
  49. package/skills/claude-code/wrapping-existing-applications/SKILL.md +4 -4
  50. package/skills/claude-code/wrapping-existing-applications/references/authoring-workflow.md +451 -0
  51. package/skills/claude-code/wrapping-existing-applications/references/tool-design.md +107 -0
  52. package/skills/claude-code/wrapping-existing-applications/references/wrap-existing-app.md +65 -0
  53. package/skills/codex/SKILL.md +1 -1
  54. package/skills/codex/authoring-mcp-servers/SKILL.md +4 -4
  55. package/skills/codex/authoring-mcp-servers/references/authoring-workflow.md +451 -0
  56. package/skills/codex/authoring-mcp-servers/references/build-an-mcp-server.md +56 -0
  57. package/skills/codex/authoring-mcp-servers/references/sdk-surface.md +258 -0
  58. package/skills/codex/building-mcp-apps/SKILL.md +4 -4
  59. package/skills/codex/building-mcp-apps/references/build-an-mcp-app.md +53 -0
  60. package/skills/codex/building-mcp-apps/references/experience-design.md +155 -0
  61. package/skills/codex/building-mcp-apps/references/widgets-and-apps.md +277 -0
  62. package/skills/codex/connecting-apis-to-mcp/SKILL.md +3 -3
  63. package/skills/codex/connecting-apis-to-mcp/references/authoring-workflow.md +451 -0
  64. package/skills/codex/connecting-apis-to-mcp/references/connect-an-api.md +245 -0
  65. package/skills/codex/creating-product-agent-guides/SKILL.md +2 -2
  66. package/skills/codex/creating-product-agent-guides/references/product-agent-guides.md +55 -0
  67. package/skills/codex/debugging-mcp-delivery/SKILL.md +4 -4
  68. package/skills/codex/debugging-mcp-delivery/references/inspect-hosted.md +26 -0
  69. package/skills/codex/debugging-mcp-delivery/references/troubleshooting.md +39 -0
  70. package/skills/codex/debugging-mcp-delivery/references/verify-and-recover.md +67 -0
  71. package/skills/codex/deploying-mcp-services/SKILL.md +3 -3
  72. package/skills/codex/deploying-mcp-services/references/cli-commands.md +106 -0
  73. package/skills/codex/deploying-mcp-services/references/deploy-and-ops.md +25 -0
  74. package/skills/codex/designing-mcp-products/SKILL.md +3 -3
  75. package/skills/codex/designing-mcp-products/references/authoring-workflow.md +451 -0
  76. package/skills/codex/designing-mcp-products/references/experience-design.md +155 -0
  77. package/skills/codex/embedding-mcp-assistants/SKILL.md +11 -10
  78. package/skills/codex/embedding-mcp-assistants/references/authoring-workflow.md +451 -0
  79. package/skills/codex/embedding-mcp-assistants/references/embedded-assistant.md +876 -0
  80. package/skills/codex/examples/acme-discovery/README.md +7 -5
  81. package/skills/codex/examples/customer-auth/README.md +200 -52
  82. package/skills/codex/examples/customer-auth/src/server.ts +3 -0
  83. package/skills/codex/examples/customer-auth/test/server.test.ts +6 -0
  84. package/skills/codex/examples/customer-auth/vitest.config.ts +6 -0
  85. package/skills/codex/examples/food-ordering/README.md +1 -1
  86. package/skills/codex/examples/food-ordering/src/server.ts +1 -0
  87. package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
  88. package/skills/codex/publishing-mcp-integrations/SKILL.md +3 -3
  89. package/skills/codex/publishing-mcp-integrations/references/app-directory-compliance.md +59 -0
  90. package/skills/codex/publishing-mcp-integrations/references/publishing.md +63 -0
  91. package/skills/codex/references/authoring-workflow.md +8 -0
  92. package/skills/codex/references/embedded-assistant.md +259 -67
  93. package/skills/codex/references/sdk-surface.md +1 -1
  94. package/skills/codex/reporting-noodle-feedback/SKILL.md +2 -2
  95. package/skills/codex/reporting-noodle-feedback/references/feedback.md +80 -0
  96. package/skills/codex/verifying-mcp-delivery/SKILL.md +3 -3
  97. package/skills/codex/verifying-mcp-delivery/references/test-in-hosts.md +39 -0
  98. package/skills/codex/verifying-mcp-delivery/references/verify-and-recover.md +67 -0
  99. package/skills/codex/wrapping-existing-applications/SKILL.md +4 -4
  100. package/skills/codex/wrapping-existing-applications/references/authoring-workflow.md +451 -0
  101. package/skills/codex/wrapping-existing-applications/references/tool-design.md +107 -0
  102. package/skills/codex/wrapping-existing-applications/references/wrap-existing-app.md +65 -0
@@ -0,0 +1,451 @@
1
+ # Authoring workflow
2
+
3
+ ## Contents
4
+
5
+ - Input paths
6
+ - Fit check
7
+ - Product-guide decision
8
+ - Repair loop
9
+ - Connectors
10
+ - HTTP connector example (full server)
11
+ - Customer OAuth for remote MCP clients
12
+ - Auth-derived customer API endpoints
13
+ - Per-tool authorization
14
+ - Delegated downstream auth (call your API as the signed-in user)
15
+ - Invocation context
16
+ - Compute connector example
17
+ - Tests
18
+ - Secrets and variables
19
+ - Embedded assistant
20
+ - Boundaries
21
+
22
+ ## Input paths
23
+
24
+ 1. **Website scrape** — if the user gives a URL, scrape it for surface hints (products, services, hours, contact, pricing). Stop there: the URL does not reveal CRM, booking systems, custom APIs, auth model, eligibility rules, quoting logic, or approval flows. Those live in the business systems and the owner’s head — ask.
25
+ 2. **OpenAPI import** — `noodle import openapi <file>` emits a starter `server.ts` from a spec. Use it when the user provides an OpenAPI document.
26
+ 3. **Upstream MCP import** — `noodle import mcp <url> --name <slug> --output <dir>` discovers `tools/list` once, validates and freezes tool schemas into TypeScript, and writes a secret-free drift snapshot. Upstream annotations are untrusted, so generated tools remain destructive confirmed actions until an author verifies and narrows them. Use `--header-env <header>=<ENV_NAME>` for import-only auth and `--check` for classified, non-mutating drift detection. Runtime never performs discovery.
27
+ 4. **User interview** — Noodle does not interview; you do. Cover custom APIs/integrations, eligibility rules, quoting/approval logic, and private schemas (SQL DDL or JSON samples for custom `connector` declarations). Ask for concrete examples and sample payloads; do not guess a schema from a URL or invent endpoints.
28
+
29
+ ## Fit check
30
+
31
+ Before building, confirm the idea fits a conversational surface: 1–3 focused actions where saying it beats clicking, plus data or actions the model lacks on its own. Poor fits — long-form or static content, dashboards, deep multi-step navigation, or a full app port. When an idea does not fit, narrow the scope to the actions that do.
32
+
33
+ ## Product-guide decision
34
+
35
+ Before authoring, record whether the product is guided or unguided and why. Do not wait for the user to name `agentGuide`; load `references/product-agent-guides.md` for the canonical decision criteria and TypeScript shape whenever product-level workflow guidance may add value.
36
+
37
+ ## Repair loop
38
+
39
+ Author in `server.ts`, then `noodle validate` → fix cited errors (see `compile-errors.md`) → re-validate → `noodle test` → `noodle dev`. Keep the loop tight and error-driven.
40
+
41
+ ## Connectors
42
+
43
+ Declare connectors as data, not imperative code:
44
+
45
+ - **HTTP**: `connector("id").version("1.0.0").http({ baseUrl, allowedOrigins, auth, operations })` with per-operation `request`/`response` mapping using `${args...}` / `${response...}` expressions.
46
+ - **MCP**: `connector("id").version("1.0.0").mcp({ endpoint, allowedOrigins, auth?, operations })` where each operation freezes the separate upstream `tool` wire name plus input/output schema. Import with `noodle import mcp`; do not hand-copy a live surface or call `tools/list` at runtime.
47
+ - **Compute**: `connector("id").version("1.0.0").compute(name, { input, output, calls?, run })` — a self-contained, sandboxed function (no imports/closure capture) that may call allowlisted operations via `callOperation`.
48
+
49
+ Tools record connector calls into a flow; recording is not execution. Do not branch on runtime outputs with native `if` — use declarative `when(...)` conditions.
50
+
51
+ MCP connectors are curated backing operations, never transparent proxies. Publish ordinary Noodle tools
52
+ with stable intent-shaped names, descriptions, annotations, authored schemas, confirmation, visibility,
53
+ and optional React `view`. Normalize a text-only upstream result through compute before rendering when the
54
+ view needs a richer stable output. Upstream resources, prompts, `_meta`, annotations, widgets, and CSP are
55
+ not imported or forwarded. The runtime opens one guarded session for one operation, uses only a broker-minted
56
+ credential, and closes it; it does not act as an agent for upstream sampling, roots, or elicitation.
57
+
58
+ 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.
59
+
60
+ When one connector needs independently selectable accounts, declare catalog `credentialProfiles` plus each operation’s accepted `credentials.profiles`, then bind each `server.use` alias with `bind(connector, { profile, connection: connection("logical_id", managedSecret(secret("NAME"), { scopes, audience })) })`. The alias is the stable account boundary; never put provider account ids, labels, or credential values in it. `gmailConnector()` is the curated Gmail catalog helper; reuse it under independent aliases and accept canonical `accounts` arrays in tools (one account for writes, or an explicitly ordered supported combination for reads). See the bundled `gmail-multi-account` flagship. Bound managed secrets are supported by hosted execution. For deployed-server access to Google APIs, use `googleWorkloadIdentity({ provider: variable("GOOGLE_WIF_PROVIDER"), access: { kind: "direct" } })`, or add `serviceAccountImpersonation` with a managed service-account email. This is keyless Google Workload Identity Federation: exact Google scopes/audience come from the catalog operation, while `noodle auth google prepare|status|doctor|revoke` owns operator lifecycle. See the bundled `google-bigquery` flagship. `externalExchange()` is runnable only when the deployment operator injects an exact HTTPS provider endpoint/origin/audience and durable shared subject-pin store through service ports; Noodle sends a short-lived platform-signed deployment workload assertion and accepts only a bounded bearer response. Provider implementations must consume assertion replay ids through durable shared atomic storage across instances and restarts. There is intentionally no hosted enrollment or provider CRUD surface yet. The provider wire contract is public, but its conformance kit is workspace/source-only and is not an installable npm package. Bound `clientCredentials(...)` remains fail-closed until its provider slice lands.
61
+
62
+ ## HTTP connector example (full server)
63
+
64
+ Declare the API as data, bind it with `use`, then record calls in tools. The operation mapping in detail: `request` builds the JSON request body, `query: [...]` names the input args sent as URL query parameters, and `response` maps the parsed HTTP body (bound to `${response}`) into your typed `output`. `auth` reads a managed `secret(...)` — never inline a key. This whole example is compile-verified on every `pnpm test`.
65
+
66
+ ```ts
67
+ import { connector, secret, variable, server, tool, z } from '@noodleseed/one';
68
+
69
+ const crm = connector('crm').version('1.0.0').http({
70
+ baseUrl: variable('CRM_BASE_URL'),
71
+ allowedOrigins: ['https://api.crm.example'],
72
+ auth: { kind: 'bearer', secret: secret('CRM_TOKEN') },
73
+ operations: {
74
+ find_customer: {
75
+ type: 'read',
76
+ method: 'GET',
77
+ path: '/customers',
78
+ query: ['email'],
79
+ input: z.object({ email: z.string() }),
80
+ output: z.object({ id: z.string(), name: z.string().optional() }),
81
+ response: { id: '${response.data[0].id}', name: '${response.data[0].name}' },
82
+ },
83
+ create_ticket: {
84
+ type: 'action',
85
+ method: 'POST',
86
+ path: '/tickets',
87
+ input: z.object({ customer_id: z.string(), body: z.string() }),
88
+ output: z.object({ ticket_id: z.string() }),
89
+ request: { customer_id: '${args.customer_id}', body: '${args.body}' },
90
+ response: { ticket_id: '${response.id}' },
91
+ },
92
+ },
93
+ });
94
+
95
+ export default server('support', { title: 'Support', version: '1.0.0', use: { crm } }, [
96
+ tool('find_customer', {
97
+ description: 'Find a customer by email address.',
98
+ input: z.object({ email: z.string() }),
99
+ output: z.object({ id: z.string(), name: z.string().optional() }),
100
+ fulfil: ({ input, connectors }) => {
101
+ const customer = connectors.crm.find_customer({ email: input.email });
102
+ return { id: customer.id, name: customer.name };
103
+ },
104
+ }),
105
+ tool('open_ticket', {
106
+ description: 'Open a support ticket for a customer.',
107
+ input: z.object({ customer_id: z.string(), body: z.string() }),
108
+ output: z.object({ ticket_id: z.string() }),
109
+ fulfil: ({ input, connectors }) => {
110
+ const ticket = connectors.crm.create_ticket({ customer_id: input.customer_id, body: input.body });
111
+ return { ticket_id: ticket.ticket_id };
112
+ },
113
+ }),
114
+ ]);
115
+ ```
116
+
117
+ 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` is the SSRF allowlist: use literal exact origins, or the same `variable("STORE_ORIGIN")` as `baseUrl` when one reusable single-origin app is bound per business. A managed origin must resolve to one canonical bare HTTPS origin (exact loopback HTTP is the development exception); never use a wildcard, path, or tenant source edit.
118
+
119
+ 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.
120
+
121
+ ## Customer OAuth for remote MCP clients
122
+
123
+ For `customerAuth.oidc(...)` and `.federatedOidc(...)`, the application developer owns the standards-compliant authorization server. Noodle verifies its access tokens; it does not proxy discovery, create OAuth clients, or repair the upstream server. For issuer `https://id.example.com/oauth`, publish the path-inserted RFC 8414 document at `https://id.example.com/.well-known/oauth-authorization-server/oauth` as direct unauthenticated HTTP 200 JSON — never a login redirect.
124
+
125
+ That metadata must expose HTTPS `authorization_endpoint`, `token_endpoint`, `jwks_uri`, and RFC 7591 `registration_endpoint`; advertise authorization-code and refresh-token grants, Dynamic Client Registration, PKCE with `code_challenge_methods_supported: ["S256"]`, and public clients with `token_endpoint_auth_methods_supported: ["none"]`. Validate the exact RFC 8707 `resource` on authorize, code exchange, and refresh, then map approved versions of one app/environment to the stable audience configured in `customerAuth`; use distinct audiences across apps and environments. Publish only public signing keys in JWKS. Run `noodle auth doctor src/server.ts`; its issuer-readiness probes perform bounded read-only GET checks and never register a client. A successful `noodle deploy --access customers` reports the same readiness without turning a diagnostic failure into a failed deployment.
126
+
127
+ The MCP client, or Devtools during local testing, opens the configured `authorization_endpoint`; Noodle does not submit the login form or manage the authorization server's session or CSRF cookies. Keep the login and consent UI plus credential POST on the same origin as the authorization endpoint. If a separate-site UI is unavoidable, the authorization server must explicitly trust the UI origin, use exact credentialed CORS where browser JavaScript calls it, issue the session and CSRF cookies needed by cross-site POSTs as `SameSite=None; Secure`, and return a CSRF token that the UI submits with credentials. Cookies cannot be shared across unrelated registrable domains. Browser privacy controls may still block this cross-site flow, so a same-origin route or reverse proxy remains the reliable design.
128
+
129
+ Verify customer OAuth in three explicit layers. First run `noodle auth doctor src/server.ts --json`; a pass proves metadata and JWKS readiness, but it does not prove that registration or token issuance succeeds. Next run `noodle test src/server.ts --json`; for a protected app it proves that anonymous MCP access fails closed with exact protected-resource metadata and returns `interactiveRequired: true`. Finally run `noodle devtools src/server.ts`, complete sign-in, and make one authenticated `tools/list` request or one representative safe read. Only that final layer proves issuer, signature, stable audience, and exact-resource binding together. Never report a passing doctor or anonymous boundary smoke as working end-to-end authentication.
130
+
131
+ ## Auth-derived customer API endpoints
132
+
133
+ Use a customer endpoint when the verified IdP selects a different API base URL for each SaaS customer. The claim contains the complete base URL; tool input and `${user}` do not select it. Declare one named policy, use that reference as the connector `baseUrl`, and map its claim path in direct OIDC or on every federated issuer:
134
+
135
+ ```ts
136
+ import { annotations, connector, customerAuth, customerEndpoint, secret, server, tool, variable, z } from '@noodleseed/one';
137
+
138
+ const customerApi = customerEndpoint('customer_api', {
139
+ allowedHttpsHostSuffixes: ['api.noodleseed.dev'],
140
+ });
141
+
142
+ const api = connector('customer_api_connector').version('1.0.0').http({
143
+ baseUrl: customerApi,
144
+ auth: {
145
+ kind: 'delegatedTokenExchange',
146
+ tokenUrl: 'https://id.noodleseed.dev/oauth/token',
147
+ clientId: variable('CUSTOMER_API_CLIENT_ID'),
148
+ clientSecret: secret('CUSTOMER_API_CLIENT_SECRET'),
149
+ },
150
+ operations: {
151
+ list_records: {
152
+ type: 'read',
153
+ method: 'GET',
154
+ path: '/records',
155
+ input: z.object({}),
156
+ output: z.object({ records: z.array(z.unknown()).max(100) }),
157
+ },
158
+ archive_record: {
159
+ type: 'action',
160
+ method: 'POST',
161
+ path: '/records/${args.record_id}/archive',
162
+ input: z.object({ record_id: z.string() }),
163
+ output: z.object({ archived: z.boolean() }),
164
+ },
165
+ },
166
+ });
167
+
168
+ export default server(
169
+ 'customer_records',
170
+ {
171
+ title: 'Customer records',
172
+ version: '1.0.0',
173
+ use: { api },
174
+ interactions: { confirmationFallback: 'host' },
175
+ auth: customerAuth.oidc({
176
+ issuer: 'https://id.noodleseed.dev',
177
+ audience: 'noodleseed-customer-records-prod',
178
+ routing: {
179
+ endpoints: {
180
+ customer_api: { claim: 'tenant.api_base_url' },
181
+ },
182
+ },
183
+ }),
184
+ },
185
+ [
186
+ tool('list_records', {
187
+ description: 'List records for the signed-in customer.',
188
+ input: z.object({}),
189
+ output: z.object({ records: z.array(z.unknown()).max(100) }),
190
+ fulfil: ({ connectors }) => {
191
+ const result = connectors.api.listRecords();
192
+ return { records: result.records };
193
+ },
194
+ }),
195
+ tool('archive_record', {
196
+ description: 'Archive one record for the signed-in customer.',
197
+ input: z.object({ record_id: z.string() }),
198
+ output: z.object({ archived: z.boolean() }),
199
+ annotations: annotations.openAction({ destructive: false, confirm: true }),
200
+ fulfil({ input, connectors }) {
201
+ const result = connectors.api.archiveRecord({ record_id: input.record_id });
202
+ return { archived: result.archived };
203
+ },
204
+ }),
205
+ ],
206
+ );
207
+ ```
208
+
209
+ `customerEndpoint` accepts exactly one non-empty policy arm: exact HTTPS origins, or HTTPS hostname suffixes. Exact policies may authorize an explicit non-default port; suffix policies allow port 443 and match only the exact host or dot-boundary subdomains. Do not add connector `allowedOrigins` to a customer-routed connector. Its fixed credential/token endpoints are validated independently and cannot come from caller claims.
210
+
211
+ Customer-routed connector auth must be omitted or use `delegatedTokenExchange` at both connector and operation level. The compiler validates the concrete connector definition emitted from TypeScript, including connector defaults and operation overrides. Do not emit bearer, API-key, client-credentials, or managed-provider fallbacks for local mode; use operation fakes while leaving auth declarative. `customer_endpoint_unsupported_auth` reports the exact failing path and auth kind.
212
+
213
+ On a bidirectional MCP transport whose client negotiated form elicitation, Noodle sends the standard confirmation form. The current stateless hosted MCP transport cannot initiate that exchange, so the example explicitly declares `server.interactions.confirmationFallback: 'host'`. That fallback trusts the MCP host to have collected native write approval before the tool call reaches Noodle; it is never inferred from client identity and does not replace authentication, authorization, policy, or accurate action/destructive annotations. Omit it when connected hosts are not trusted to provide that approval; confirmation then fails closed with `interaction_unavailable` when the standard exchange is unavailable.
214
+
215
+ Every federated issuer must repeat every endpoint key used by the app, although each issuer may choose a different claim path. Endpoint names use lowercase letters, numbers, and underscores. Resolved claims must be exact absolute HTTPS URLs of at most 2,048 UTF-8 bytes with no userinfo, query, fragment, IP literal, special-use host, or unsafe whitespace/control characters. The runtime preserves a canonical optional base path.
216
+
217
+ Resolved customer URLs are private routing authority: they never enter the manifest/artifact, `${user}`, logs, model output, widgets, public confirmation review, cache keys, or delegated-token-exchange assertions. Routed reads may be used by tools, including declared nested calls. A routed action—including one reached through a connector wrapper—requires exact `annotations.confirm: true`; omitted or `false` fails with `customer_endpoint_action_unsupported`. Routed resources, prompts, and ambient context fail with `customer_endpoint_surface_unsupported`.
218
+
219
+ At runtime, an initially missing, malformed, or disallowed claim returns the same safe `connector_route_unavailable` tool error before credential lookup or connector egress. Preparation stores only sorted route `{ key, fingerprint }` bindings in the private server-held continuation. Acceptance re-resolves the current request routes; a missing or changed binding returns `invalid_continuation` before policy, credentials, or egress, then the matching frozen snapshot is reused for the action and nested/later calls. `tools/list` remains based only on roles and scopes, so route availability neither reveals tenant topology nor changes the existing authorization filter.
220
+
221
+ ## Per-tool authorization
222
+
223
+ Keep endpoint authentication in `customerAuth.*(...)`, then narrow individual tools with the optional typed `authorization` rule. Every `requiredScopes` value is required; any one `allowedRoles` value is sufficient; when both lists are present, both conditions apply. Omit `authorization` for an unrestricted tool. Do not invent a policy expression language or infer authorization from tool arguments, page context, connector output, email domains, or other unverified data.
224
+
225
+ ```ts
226
+ auth: customerAuth.oidc({
227
+ issuer: 'https://id.example.com',
228
+ audience: 'https://api.example.com/mcp',
229
+ claims: { roles: 'permissions.roles', scopes: 'permissions.scopes' },
230
+ }),
231
+
232
+ tool('list_org_apps', {
233
+ authorization: {
234
+ requiredScopes: ['org_apps:read'],
235
+ allowedRoles: ['org_admin', 'org_member'],
236
+ },
237
+ // input, output, and fulfilment...
238
+ })
239
+ ```
240
+
241
+ Role values are trusted only from the explicitly configured claim path (or the platform-private bridge role claim). Direct OIDC scopes default to standard `scope`, `scp`, or `scopes` claims unless `claims.scopes` is configured. Embedded-assistant backends pass verified `user.roles` and `user.scopes` separately during `createAssistantSession(...)`; page context never grants either. Claim values must be a string or string array; malformed or oversized values fail closed. Eligible tools remain in authored order in `tools/list`; ineligible tools are omitted and a guessed direct call is still denied before argument validation or connector execution. Scope denials use MCP OAuth step-up metadata without disclosing role names.
242
+
243
+ ## Delegated downstream auth (call your API as the signed-in user)
244
+
245
+ 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:
246
+
247
+ `delegatedTokenExchange` consumes a verified customer caller; an MCP access mode does not create one. The server must declare `customerAuth.*(...)` or `embeddedAssistant(...)` so Noodle can establish the caller subject, issuer, and audience. Otherwise `noodle validate`, `noodle auth doctor`, and deploy fail early with `delegated_token_exchange_identity_required`, before secrets are resolved or any connector egress. A successful local Devtools exchange is not evidence that the hosted server has an identity source. Devtools supplies a separate, loopback-only local identity context that is never accepted by hosted deployment.
248
+
249
+ - **`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). It works with verified customer OIDC identities and the built-in Firebase/Microsoft adapters; no per-user OAuth enrollment. Embedded-assistant sessions can bind customer-routed connectors when the authenticated embedding backend resolves each route from server-owned tenancy data and passes it during session exchange. Browser input, page context, session claims, and tool arguments cannot supply or override that private route authority.
250
+ - **`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`.
251
+ - **`delegatedSessionCookie`** — Firebase-managed session-cookie apps only; not a generic mechanism.
252
+
253
+ ### The connector (your `server.ts`)
254
+
255
+ ```ts
256
+ auth: {
257
+ kind: 'delegatedTokenExchange',
258
+ tokenUrl: 'https://app.example.com/api/assistant/oauth/token', // fixed HTTPS; static connectors also allowlist its origin
259
+ clientId: variable('EXAMPLE_DELEG_CLIENT_ID'),
260
+ clientSecret: secret('EXAMPLE_DELEG_CLIENT_SECRET'),
261
+ scopes: ['time_off'], // optional
262
+ audience: 'example-api', // optional; assertion + request audience, defaults to tokenUrl
263
+ authMethod: 'client_secret_basic', // default; client_secret_post supported
264
+ }
265
+ ```
266
+
267
+ Inside tools, `${user.subject}` / `${user.email}` / `${user.name}` / `${user.locale}` / `${user.timeZone}` / `${user.claims.*}` stay available as verified context; the delegated credential is what makes the *downstream call itself* run as that user.
268
+
269
+ ### The exchange request your endpoint receives
270
+
271
+ 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`):
272
+
273
+ ```
274
+ grant_type=urn:ietf:params:oauth:grant-type:token-exchange
275
+ subject_token=<RS256 JWT signed by the platform>
276
+ subject_token_type=urn:ietf:params:oauth:token-type:jwt
277
+ scope=time_off (space-joined, when configured)
278
+ audience=example-api (when configured)
279
+ ```
280
+
281
+ 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`, `customer_identity: { version: 1, issuer }`, `iat`, `exp` (about 120 s), `jti`. Key downstream users by `(customer_identity.issuer, sub)`, never by `sub` alone. Firebase uses `https://securetoken.google.com/<project-id>`; Microsoft uses `https://login.microsoftonline.com/<tenant-id>/v2.0`. A routed exchange adds `route: { key, fingerprint }`, never the customer URL; credential cache and single-flight keys include that route binding. Respond with `{ "access_token": "...", "token_type": "Bearer", "expires_in": 900 }`; the broker caches per user + connector + scopes + route until `expires_in` minus 300 s and presents the token downstream as `Authorization: Bearer`.
282
+
283
+ ### The downstream token endpoint (your backend)
284
+
285
+ ```ts
286
+ // POST /api/assistant/oauth/token — Node example with jose.
287
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
288
+
289
+ const PLATFORM_ISSUER = process.env.NOODLE_PLATFORM_ISSUER!; // e.g. https://cloud.noodleseed.dev
290
+ const jwks = createRemoteJWKSet(new URL(`${PLATFORM_ISSUER}/.well-known/jwks.json`));
291
+
292
+ export async function tokenEndpoint(req: Request): Promise<Response> {
293
+ // 1. Authenticate the broker client credential (client_secret_basic).
294
+ const basic = req.headers.get('authorization') ?? '';
295
+ const [clientId, clientSecret] = atob(basic.replace(/^Basic /, '')).split(':');
296
+ if (!isValidClient(clientId, clientSecret)) return new Response(null, { status: 401 });
297
+ // 2. Verify the platform-signed user assertion (never trust a plaintext user id).
298
+ const form = new URLSearchParams(await req.text());
299
+ const { payload } = await jwtVerify(form.get('subject_token') ?? '', jwks, {
300
+ issuer: PLATFORM_ISSUER,
301
+ audience: 'https://app.example.com/api/assistant/oauth/token', // your tokenUrl or configured audience
302
+ });
303
+ if (payload.deployment !== undefined && payload.tenant !== 'your-org/your-app/prod') {
304
+ return new Response(null, { status: 403 }); // optionally pin the calling deployment
305
+ }
306
+ // 3. Mint your own short-lived user-scoped token; your API enforces per-user rules from it.
307
+ const accessToken = await issueAccessToken(String(payload.sub), clientId, form.get('scope') ?? '');
308
+ return Response.json({ access_token: accessToken, token_type: "Bearer", expires_in: 900 });
309
+ }
310
+ ```
311
+
312
+ ### Test delegated exchange locally
313
+
314
+ Local customer OIDC sign-in and delegated-exchange assertion trust are two distinct boundaries. OIDC proves the caller to the local MCP server; Devtools uses a separate local issuer only for the RFC 8693 assertion sent to your downstream token endpoint. This requires no `server.ts` change or additional flag, environment variable, or config surface.
315
+
316
+ 1. Configure the OIDC authorization server for the exact loopback callback and RFC 8707 resource. Do not add the Devtools assertion key to OIDC issuer metadata or change its signing keys.
317
+ 2. Start Devtools, complete customer sign-in, and copy the displayed `{ issuer, jwks }` from **Local delegated exchange**.
318
+ 3. Pin both values only in the customer-owned development RFC 8693 token endpoint.
319
+ 4. Restrict that trust to development client credentials, audience, API, and data.
320
+ 5. Run the delegated tool until its binding reads **Exchange verified**.
321
+ 6. Use hosted preview or `noodle auth doctor --live` to prove the production platform issuer.
322
+
323
+ **Never trust the Devtools issuer in production: anyone holding the local private key could impersonate a customer.**
324
+
325
+ Diagnose statically with `noodle auth doctor`; set a short-lived real customer token only in `NOODLE_CUSTOMER_TOKEN` and add `--live --org <org> --app <app> --env <env>` to perform one exchange per delegated binding without invoking a business tool. Add `--version <version>` to test that exact pinned MCP resource. Common failures include structured `credential_unavailable` reasons such as `caller_identity_not_customer`. For a Firebase/Microsoft bridge, `caller_issuer_missing` means the session predates issuer binding: reconnect once through customer authentication, then retry. No IdP custom claim or `server.ts` change is required. Direct/federated OIDC verification assigns the customer identity at the trusted verifier boundary; never ask an IdP to mint a Noodle-specific classification claim.
326
+
327
+ ## Design tools for the model
328
+
329
+ Shape tools around what a user says, not 1:1 around API endpoints. `references/tool-design.md` owns the doctrine: intent-shaped tools, titles and annotations, bounded outputs, a small tool surface, and deliberate context.
330
+
331
+ ## Invocation context
332
+
333
+ Every executable invocation receives one immutable server-authoritative temporal snapshot. TypeScript authoring does not create a hidden context tool. Use `server(..., { context })` for locale/time-zone defaults and trusted ambient facts, and designate one normal zero-input tool with `contextProvider: true` when the model needs portable application context. The embedded host preloads it per turn; Claude, ChatGPT, and other MCP hosts call it normally.
334
+
335
+ ```ts
336
+ context: {
337
+ defaults: { locale: 'en-GB', timeZone: 'Europe/London' },
338
+ ambient: {
339
+ output: z.object({ defaultTeamId: z.string(), holidays: z.array(z.string()) }),
340
+ fulfil: ({ user, context, connectors }) => {
341
+ const calendar = connectors.people.getCalendar({
342
+ subject: user.subject,
343
+ asOf: context.temporal.instant,
344
+ });
345
+ return { defaultTeamId: calendar.default_team_id, holidays: calendar.holidays };
346
+ },
347
+ },
348
+ },
349
+ ```
350
+
351
+ Ambient providers are recorded as fulfilment data at author time, may call read-only connector operations only, and have a declared output schema. Later fulfilments read `${context.temporal.localDate}`, `${context.temporal.timeZone}`, `${context.ambient.defaultTeamId}`, and `${context.ambientStatus}`. If ambient resolution fails, the status is `unavailable`; never invent the missing business facts. Ambient/model-visible context is capped at 16 KiB serialized JSON, depth 8, and 128 entries per container; credential-shaped keys are rejected.
352
+
353
+ Direct MCP tool calls may also expose an optional, untrusted client hint as `${context.location.latitude}` and `${context.location.longitude}`, with optional `${context.location.city}`, `${context.location.region}`, `${context.location.country}`, and `${context.location.timeZone}`. Noodle exposes location only when the host supplies one complete, finite, in-range coordinate pair. Treat it as a proximity convenience only: never use it for authentication, authorization, policy, compliance, or delivery-address proof. An explicit tool input must override the hint. When location is absent and the operation needs it, ask the user or return a structured location-required result; never substitute a fixed default location.
354
+
355
+ ## Ask for structured missing input
356
+
357
+ Use `ctx.elicit` inside a tool fulfilment when execution needs one bounded value from the user. The call records an `elicit` flow step and returns its symbolic scope; it does not prompt at author time:
358
+
359
+ ```ts
360
+ tool('prepare_time_off', {
361
+ description: 'Resolve a time-off request before proposing the write.',
362
+ input: z.object({ start: z.string(), end: z.string() }),
363
+ output: z.object({ start: z.string(), end: z.string(), teamId: z.string() }),
364
+ fulfil: ({ input, elicit }) => {
365
+ const answer = elicit({
366
+ id: 'choose_team',
367
+ message: 'Which team should receive this request?',
368
+ input: z.object({ teamId: z.string().describe('Team') }),
369
+ });
370
+ return { start: input.start, end: input.end, teamId: answer.teamId };
371
+ },
372
+ });
373
+ ```
374
+
375
+ Use a stable lowercase/number/underscore id and a flat form of string/number/integer/boolean, string choices or multi-select, with optional `email`, `uri`, `date`, or `date-time` formats. Nested objects and credential-shaped fields fail with `invalid_elicitation_schema`. Every interactive flow must place all `ctx.elicit` calls before its first connector operation or compilation fails with `invalid_elicitation_flow`. Embedded/headless clients receive `input_requested`; bidirectional MCP transports map the primitive to standard form `elicitation/create`. On stateless hosts, the adapter returns a structured non-executing `interaction_unavailable` result; linked Apps render its business-user form and retry in request `_meta`, while models can use the advertised reserved retry field. Accept validates and replays only the operation-free input prefix; invalid content returns `arg_invalid`, and decline/cancel stop. Elicitation gathers missing input and does not replace confirmation. In a flow marked `confirm: true`, every eligible `input_requested` precedes `tool_proposed`; the final proposal reviews the original input, elicited values, and sole exact eligible connector action. Conditional branches may declare multiple candidate actions only when preparation resolves exactly one eligible action from input/context or completed pure steps; zero or multiple actions fail with `invalid_confirmation_flow` before I/O. Acceptance is bound to that exact action. Later read-only operations and pure compute may assemble output, but a second eligible action fails closed. Use `ref.at(index)` for array access in recorded expressions. MCP uses final standard form confirmation on capable bidirectional transports and fails closed otherwise. Setting `interactions: { confirmationFallback: "host" }` in the server options explicitly trusts native host approval only when confirmation transport is unavailable and after every elicited field is collected; it still uses preparation and prepared execution, is never inferred from client name, and does not replace authorization. Omitted or `false` annotations execute directly; hints alone never gate. `annotations.action({ confirm: true })` explicitly enables confirmation; `annotations.action({ confirm: false })` explicitly preserves direct execution.
376
+
377
+ ## Compute connector example
378
+
379
+ ```ts
380
+ const scoring = connector('scoring').version('1.0.0').compute('normalize', {
381
+ input: z.object({ email: z.string(), priority: z.string().optional() }),
382
+ output: z.object({ score: z.number() }),
383
+ calls: { find_customer: 'crm.find_customer' },
384
+ limits: { timeoutMs: 1000, maxHostCalls: 2 },
385
+ run(input, { callOperation }) {
386
+ const customer = callOperation("find_customer", { email: input.email }) as { id?: string };
387
+ return { score: customer.id && input.priority === "high" ? 100 : 50 };
388
+ },
389
+ });
390
+ ```
391
+
392
+ Compute `run` functions are serialized and sandboxed: no imports, no closure capture, no `fetch`, no `process`. Any backing-system call must be declared in acyclic `calls` and invoked through `callOperation`. For conditional flow edges, use `when(...)` in recorded fulfilment instead of native branching on connector outputs.
393
+
394
+ ## Tests
395
+
396
+ Use Vitest for app-local tests. The generated `npm test` command scans only the project-owned `test/` directory; skill-local example tests are reference material, not part of the app suite. Keep fixtures project-local; do not import from `examples/`. A minimum test suite imports the default server, checks the intended definitions compile, then lets `noodle test --json` perform the loopback MCP smoke.
397
+
398
+ ```ts
399
+ import { describe, expect, it } from 'vitest';
400
+ import app from '../src/server.js';
401
+
402
+ describe('server', () => {
403
+ it('declares the expected tool surface', () => {
404
+ expect(app.name).toBe('support_assistant');
405
+ });
406
+ });
407
+ ```
408
+
409
+ After focused tests pass, run `noodle validate --json`, `noodle test --json`, and then `noodle dev` for interactive local verification.
410
+
411
+ ## Secrets and variables
412
+
413
+ Author managed config as `secret("NAME")` / `variable("NAME")` and operate it with `noodle secrets set` / `noodle variables set` (scoped org/app/env). Never inline secret values in `server.ts`, tests, or generated files.
414
+
415
+ ## Embedded assistant
416
+
417
+ 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.
418
+
419
+ ## Managed capability reuse
420
+
421
+ Before authoring an HTTP connector for public-web search, page extraction, places/local search, routing, weather, translation, or media discovery, inspect the generated SDK surface reference. If the matching Noodle capability is actually exported, use its one documented declaration and generated tools directly—never wrap it, rename it, or build an assistant-only provider call. That single declaration is available to both the embedded assistant and external MCP agents under the same authorization and policy.
422
+
423
+ Use `provider: noodleManaged()` in reusable capability intent and let the app/environment operator bind a supported BYO provider through managed configuration. Keep provider names, endpoints, and credentials out of `server.ts`. Live fares, inventory, bookings, pricing, orders, accounts, and payments remain connectors to authoritative systems. Source order is not truth: preserve citations, freshness, and rights evidence returned by the capability.
424
+
425
+ A roadmap is not an SDK. If the capability is absent from the generated export list, do not invent an import or generic platform helper. With explicit developer agreement, a bounded application connector may prove the immediate use case; keep it application-specific and do not claim it as the canonical Noodle primitive.
426
+
427
+ ## Knowledge components
428
+
429
+ Ground an assistant in controlled documents and the customer's live public site with one declaration — never a handwritten `search`/`fetch` tool pair, a provider name, a sync job, or an index manifest. Declare `knowledge(...)` with `file(...)` documents (UTF-8 `.md`/`.txt`, project-root relative, ≤100 files, ≤1 MiB each, ≤25 MiB per component) and `site(...)` live scopes (exact HTTPS origin plus positive path globs), pass the declaration in the server's `knowledge` array, and include it in a public website surface's `capabilities` to project the generated `search_<name>` capability:
430
+
431
+ ```ts
432
+ const product = knowledge('product', {
433
+ title: 'Product knowledge',
434
+ description: 'Public product, pricing, and support information.',
435
+ documents: [
436
+ file('./knowledge/product.md', { title: 'Product guide' }),
437
+ file('./knowledge/faq.txt', { title: 'FAQ' }),
438
+ ],
439
+ sites: [
440
+ site({ origin: 'https://www.acme.example', include: ['/docs/**', '/pricing'], refresh: '6h' }),
441
+ ],
442
+ });
443
+ ```
444
+
445
+ The compiler validates and hashes every document at build time (bad extensions, root escapes, symlinks, oversize, and non-UTF-8 fail `noodle validate` with the exact path); deployment publishes versioned files transactionally with the app, crawls declared sites, and re-crawls them on the `refresh` cadence (`15m`–`7d`, default daily; `noodle knowledge refresh <name>` crawls on demand). Component names are lowercase snake-case; each component implies exactly one generated bounded search capability with cited results.
446
+
447
+ The managed crawler and managed index are the defaults and need no configuration. A component may instead bring its own crawler (`crawler: firecrawl({ apiKey: secret('FIRECRAWL_API_KEY') })` or `tavily(...)`) and/or its own index (`index: algolia({ appId: variable('ALGOLIA_APP_ID'), apiKey: secret('ALGOLIA_API_KEY') })` or `meilisearch({ host: variable(...), apiKey: secret(...) })`). The code declares only config names; operators supply values with `noodle secrets set` / `noodle variables set`, and deploy preflight fails closed naming any unset reference.
448
+
449
+ ## Boundaries
450
+
451
+ Import public app authoring only from `@noodleseed/one` (and its documented `/react` or `/platform` subpaths). Packages and subpaths under `@noodle-borg/*` are runtime implementation details, including `/portable` boundaries, and must never appear in customer `server.ts` code. Do not hand-author manifest JSON/YAML, runtime artifacts, connector IR, or hosted asset metadata. Do not read or copy secrets, bearer tokens, refresh tokens, static access keys, `.env`, `.env.noodle`, or `~/.noodle/config.json`. Hosted access is identity-based — do not add static data-plane credential paths.