stitchkit 0.121.0 → 0.122.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/agent-runtime/browser-control-contract.d.ts +12 -0
  3. package/dist/agent-runtime/browser-control-contract.d.ts.map +1 -1
  4. package/dist/agent-runtime/control-schema.d.ts +27 -4
  5. package/dist/agent-runtime/control-schema.d.ts.map +1 -1
  6. package/dist/agent-runtime/event-schema.d.ts +33 -0
  7. package/dist/agent-runtime/event-schema.d.ts.map +1 -1
  8. package/dist/agent-runtime/run-acquisition.d.ts.map +1 -1
  9. package/dist/agent-runtime/run-execution-state.d.ts +6 -0
  10. package/dist/agent-runtime/run-execution-state.d.ts.map +1 -1
  11. package/dist/agent-runtime/run-model-attempts.d.ts.map +1 -1
  12. package/dist/agent-runtime/run-stream-parts.d.ts +11 -0
  13. package/dist/agent-runtime/run-stream-parts.d.ts.map +1 -1
  14. package/dist/agent-runtime/tool-progress.d.ts +21 -0
  15. package/dist/agent-runtime/tool-progress.d.ts.map +1 -0
  16. package/dist/entrypoints/agent-runtime/browser.js +7 -3
  17. package/dist/entrypoints/agent-runtime/harness.js +5 -5
  18. package/dist/entrypoints/agent-runtime/realtime.js +5 -5
  19. package/dist/entrypoints/agent-runtime.d.ts +1 -1
  20. package/dist/entrypoints/agent-runtime.d.ts.map +1 -1
  21. package/dist/entrypoints/agent-runtime.js +6 -4
  22. package/dist/entrypoints/cli.js +2 -1
  23. package/dist/entrypoints/server.d.ts +1 -0
  24. package/dist/entrypoints/server.d.ts.map +1 -1
  25. package/dist/entrypoints/server.js +107 -42
  26. package/dist/entrypoints/tools/agent.js +1 -1
  27. package/dist/entrypoints/tools/connections.js +7 -1
  28. package/dist/entrypoints/tools/mcp.js +2 -1
  29. package/dist/entrypoints/tools/toolkit.js +4 -3
  30. package/dist/{index-7zwtbak8.js → index-1fyea34p.js} +9 -4
  31. package/dist/index-332d0m9g.js +37 -0
  32. package/dist/{index-4qnzda76.js → index-7qyv50r4.js} +1 -1
  33. package/dist/{index-qctbzmg9.js → index-am1h8c9v.js} +69 -27
  34. package/dist/{index-0fra8xvy.js → index-aswegq1r.js} +18 -6
  35. package/dist/{index-yt7gyg5x.js → index-b51tzcgh.js} +1 -1
  36. package/dist/{index-aw6zahv1.js → index-dqpesdck.js} +2 -2
  37. package/dist/{index-sb9dr3tq.js → index-eh71zr32.js} +4 -4
  38. package/dist/{index-mnkk1ffz.js → index-hs76yzt7.js} +13 -1
  39. package/dist/{index-b0w7rbpt.js → index-t05a2ry4.js} +39 -3
  40. package/dist/{index-knmnj11j.js → index-y6ye582c.js} +7 -5
  41. package/dist/server/openapi-security.d.ts +73 -0
  42. package/dist/server/openapi-security.d.ts.map +1 -0
  43. package/dist/server/openapi.d.ts +12 -0
  44. package/dist/server/openapi.d.ts.map +1 -1
  45. package/dist/tools/agent.d.ts.map +1 -1
  46. package/dist/tools/cli/format.d.ts +11 -2
  47. package/dist/tools/cli/format.d.ts.map +1 -1
  48. package/dist/tools/cli/run-managed.d.ts.map +1 -1
  49. package/dist/tools/cli/run-native.d.ts.map +1 -1
  50. package/dist/tools/connections/mcp.d.ts.map +1 -1
  51. package/dist/tools/mcp/catalog.d.ts.map +1 -1
  52. package/dist/tools/mcp/mount.d.ts.map +1 -1
  53. package/dist/tools/mcp/register.d.ts +1 -2
  54. package/dist/tools/mcp/register.d.ts.map +1 -1
  55. package/dist/tools/mcp/runtime-tools.d.ts.map +1 -1
  56. package/dist/tools/read-operation.d.ts +21 -0
  57. package/dist/tools/read-operation.d.ts.map +1 -0
  58. package/dist/tools/runtime-call-context.d.ts +15 -0
  59. package/dist/tools/runtime-call-context.d.ts.map +1 -0
  60. package/llms/agent-runtime-browser.txt +4 -1
  61. package/llms/agent-runtime.2.txt +13 -108
  62. package/llms/agent-runtime.3.txt +111 -1
  63. package/llms/cli.txt +30 -9
  64. package/llms/server.2.txt +48 -0
  65. package/llms/server.5.txt +2 -0
  66. package/llms/stitchkit.txt +3 -3
  67. package/llms/tools.2.txt +109 -0
  68. package/llms/tools.txt +34 -110
  69. package/llms/{upgrading-0.106.0-0.121.0.txt → upgrading-0.106.0-0.122.0.txt} +31 -3
  70. package/llms/upgrading.txt +1 -1
  71. package/llms-full.txt +165 -15
  72. package/llms.txt +6 -6
  73. package/package.json +2 -1
  74. package/dist/tools/durability-context.d.ts +0 -7
  75. package/dist/tools/durability-context.d.ts.map +0 -1
@@ -14,6 +14,114 @@ Parts of this slice:
14
14
  # Guide: Agent application runtime (docs/guide/agent-runtime.md) (continued)
15
15
  ==============================================================================
16
16
 
17
+ ## Observability
18
+
19
+ `createAgentObservability` emits a separate operator-only `AgentRunEvent`. It
20
+ reuses the same bounded sink lifecycle as request/tool observability without
21
+ sending new event kinds to existing request sinks. Product events omit provider
22
+ causes. Operator `internalCause` is also redacted by default; an operator-only sink must explicitly
23
+ set `includeInternalCause` and own its retention policy.
24
+
25
+ ### What a run says it spent
26
+
27
+ Usage values carry `provider-reported`, `computed`, `estimated` or `unavailable`
28
+ provenance, **per field**. Cost additionally carries an ISO currency code;
29
+ OpenRouter-reported cost is normalized as USD.
30
+
31
+ Read the provenance before the number (→ ADR 0109):
32
+
33
+ - **`provider-reported`** — the provider handed us exactly this. On
34
+ `step-finished`, that is what a step's figures are.
35
+ - **`computed`** — a total, added up over steps. **Every figure on a terminal
36
+ event is this**, tokens included: the AI SDK's `totalUsage` is a sum it
37
+ performed, not a number a provider reported for the run. It is not a figure to
38
+ bill against unchanged.
39
+ - **`unavailable`** — nobody reported it. Not zero.
40
+
41
+ Two rules follow from that last one, and they differ by field on purpose:
42
+
43
+ - **A token total with an unreported step is a floor**, labelled `computed`. A
44
+ token count is a diagnostic, and a floor is a useful one.
45
+ - **A cost with an unreported step is `unavailable`, not a floor.** Money is what
46
+ people bill against, and "at least $1.00" reported as `$1.00` is the same class
47
+ of lie this whole section exists to remove. One step that did not report its
48
+ cost makes the run's cost unknown — not smaller. It also stays unknown: later
49
+ steps reporting normally cannot revive it.
50
+
51
+ **A terminal event always carries `usage`.** A run that ended before the provider
52
+ reported anything — superseded, interrupted, timed out, shut down, failed —
53
+ carries every field `unavailable`. That is deliberately different from a run that
54
+ spent nothing, and an omitted object could not tell you which one you had.
55
+
56
+ Two costs in different currencies do not add: the sum reports `unavailable`
57
+ rather than picking a label. The core records a currency and never converts one.
58
+
59
+ `step-finished.response` identifies the provider response for that one step. The terminal event
60
+ does not copy the last response: response identity remains per-step, while every terminal usage
61
+ figure remains a `computed` aggregate.
62
+
63
+ **A run's figure is durable, and that is where to read it when a channel loses
64
+ it.** `AgentRun.usage` is written at every checkpoint and again with the terminal
65
+ record, so a crashed process leaves behind what it had already spent and a
66
+ dropped event is not a lost number:
67
+
68
+ ```ts
69
+ const snapshot = await store.loadSnapshot(conversationId)
70
+ const spent = snapshot.runs.find((run) => run.id === runId)?.usage
71
+ ```
72
+
73
+ Two gaps are open and are stated rather than left to be discovered:
74
+
75
+ - **Both event sinks are bounded and drop under load**, by arrival order — the
76
+ event carrying the money is exactly as droppable as the one carrying nothing.
77
+ The run record is the recovery, and this paragraph is the only place that says
78
+ so.
79
+ - **`AgentRuntimeResult.metrics` is `undefined` when this executor did not win
80
+ the terminal race.** Read `result.run.usage` — the durable figure — rather than
81
+ `result.metrics`, which is the channel that can go missing.
82
+ - **Compaction spend is invisible unless you report it.** `config.history.compact`
83
+ calls a model inside the turn and produces no step and no event of its own.
84
+ Return `usage` from `AgentCompactionResult` and it joins the run's figure;
85
+ omit it and the run under-reports by whatever summarising cost.
86
+
87
+ What stitchkit does **not** keep is a ledger: one figure, on the run that
88
+ produced it, never aggregated and never reconciled against a provider invoice
89
+ (→ ADR 0110).
90
+
91
+ ### Reconciling with the provider's own accounting
92
+
93
+ Whether a provider bills for a call that was aborted mid-flight cannot be known
94
+ inside the process: the authoritative number arrives later, from the provider's
95
+ accounting. **stitchkit does not accept it back** (→ ADR 0110) — a terminal run
96
+ is an absorbing state, and a write that reached it through the conversation
97
+ aggregate could conflict a concurrent compaction into discarding a summary it had
98
+ just paid a model to produce.
99
+
100
+ The join is yours, and `runId` is the key:
101
+
102
+ ```ts
103
+ // when the run terminates — write what the runtime observed
104
+ await ledger.record({
105
+ runId: terminal.run.id,
106
+ conversationId: terminal.run.conversationId,
107
+ // The durable figure, not `terminal.metrics` — that one is absent whenever
108
+ // this executor did not win the terminal race.
109
+ costUsd: terminal.run.usage?.cost?.value ?? null, // null when `unavailable`
110
+ provenance: terminal.run.usage?.cost?.provenance ?? 'unavailable',
111
+ })
112
+
113
+ // later — the provider's accounting names the same generation
114
+ await ledger.reconcile({ runId, costUsd: billed, provenance: 'provider-reported' })
115
+ ```
116
+
117
+ Record the row even when the figure is `unavailable`: that row is the evidence
118
+ that a run happened and cost something nobody has counted yet, and it is what the
119
+ provider's later figure attaches to. A run with no row is a run you cannot
120
+ reconcile.
121
+
122
+ The sink deduplicates stable event IDs by default. Cross-crash exactly-once still requires a durable
123
+ outbox.
124
+
17
125
  ## Deterministic race and adapter proof
18
126
 
19
127
  `stitchkit/agent-runtime/testing` exports `createAgentRaceBarrier`, `createAgentRaceDriver`,
@@ -391,7 +499,9 @@ Delivery exports are `AgentAdmissionEventSchema`, `AgentAttemptResetEventSchema`
391
499
  `AgentRunStateEventSchema`, `AgentRunOperationEventSchema`, `AgentTerminalEventSchema`,
392
500
  `AgentTransientDeltaEventSchema`,
393
501
  `AgentReasoningStartEventSchema`, `AgentReasoningDeltaEventSchema`,
394
- `AgentReasoningEndEventSchema`, `AgentToolStatusEventSchema`, `AgentRuntimeEvent`,
502
+ `AgentReasoningEndEventSchema`, `AgentToolStatusEventSchema`, `AgentToolProgressEventSchema` (what a
503
+ running tool reported through `ctx.reportProgress`, transient and never part of the model's input),
504
+ `AgentRuntimeEvent`,
395
505
  `AgentRuntimeEventCursor`, `AgentRuntimeEventCursorSchema`, `AgentRuntimeCursorAdvance`,
396
506
  `advanceAgentRuntimeEventCursor`, `agentDurableEventId`, `AgentRuntimeEventSink`,
397
507
  `AgentRuntimeEventSinkConfig` and `createAgentRuntimeEventSink`. Cursor gaps require a canonical
package/llms/cli.txt CHANGED
@@ -702,15 +702,36 @@ Rules worth knowing before you rely on them:
702
702
  A failed call still reports its own error and exit code. An aggregate over an
703
703
  error is not an answer to the question that was asked.
704
704
 
705
- If the command succeeds but its requested view cannot be built, the CLI writes
706
- the complete original result to stdout, using the normal JSON format (`--json`
707
- still selects compact output). It writes a separate structured
708
- `CLI_PRESENTATION_FAILED` diagnostic to stderr and exits with `8`, configurable
709
- through `exitCodes`. Its `details.operationSucceeded: true` and `retryable: false`
710
- distinguish a presentation refusal from a failed operation; `details.cause`
711
- preserves the safe reason. Recover the answer from stdout instead of repeating
712
- the mutation. Domain retry hints are not appended to this diagnostic. CLI
713
- argument errors detected before execution still exit with `2`
705
+ If the command succeeds but its requested view cannot be built, the CLI writes a
706
+ structured `CLI_PRESENTATION_FAILED` diagnostic to stderr and exits with `8`,
707
+ configurable through `exitCodes`. Its `details.operationSucceeded: true` and
708
+ `retryable: false` distinguish a presentation refusal from a failed operation;
709
+ `details.cause` preserves the safe reason, such as the misspelled field and the
710
+ fields that are there. Domain retry hints are not appended to this diagnostic.
711
+
712
+ What reaches stdout depends on whether the command can be called again for its answer,
713
+ and `details.resultOnStdout` states which case occurred:
714
+
715
+ | Command | stdout | Recover the answer by |
716
+ |---|---|---|
717
+ | has an effect | the complete original result in the normal JSON format (`--json` still selects compact output) | reading stdout — never by repeating the mutation |
718
+ | only reads | nothing | repeating the command with a corrected view |
719
+
720
+ A managed command only reads when its tool declares `annotations.readOnlyHint: true`,
721
+ or declares no `readOnlyHint` and its method is `GET`. `readOnlyHint: false`, or
722
+ `destructiveHint: true` without a `readOnlyHint`, on a `GET` endpoint keeps the result on stdout. A native
723
+ `defineCliCommand` declares neither fact, so a failed `present` always keeps the validated
724
+ result on stdout.
725
+
726
+ A CLI that mounts a server's tools through `defineMcpClientConnection` never sees the
727
+ method — every MCP call is a `POST` — so it reads the `readOnlyHint` the server
728
+ publishes. A Stitchkit server publishes it for every `GET` by the rule above
729
+ ([MCP → `readOnlyHint`](./mcp-and-agents.md#saying-which-tools-only-read--readonlyhint)),
730
+ so the same command behaves the same over the contract and over MCP. The hint is the
731
+ server's word: a server that labels a write as read-only makes the CLI withhold that
732
+ write's result on a refused view, and repeating the command repeats the effect.
733
+
734
+ CLI argument errors detected before execution still exit with `2`
714
735
  and have no successful result to preserve.
715
736
 
716
737
  ## Named profiles
package/llms/server.2.txt CHANGED
@@ -507,3 +507,51 @@ is simply never emitted, so nothing about a hidden endpoint leaks.
507
507
  const internal = openApiRoute('/internal/openapi.json', fullDoc) // behind auth
508
508
  const publicSpec = openApiRoute('/openapi.json', publicDoc) // curated
509
509
  ```
510
+
511
+ ### Security schemes — `security`
512
+
513
+ A generic client reads the spec to learn how to authenticate, and a scope name
514
+ alone does not tell it which header to send. Pass `security` to describe that in
515
+ standard OpenAPI terms: named schemes become `components.securitySchemes`, and
516
+ every protected operation gets a `security` requirement. It is keyed by the
517
+ scopes your contracts already declare, so there is no second list of which
518
+ endpoint is protected:
519
+
520
+ ```ts
521
+ const publicDoc = generateOpenApiDocument({
522
+ info: { title: 'Public API', version: '1.0.0' },
523
+ services: [account],
524
+ includeMethod: (m) => m.meta?.public === true,
525
+ security: {
526
+ schemes: {
527
+ apiKey: { type: 'http', scheme: 'bearer', description: 'An account API key' },
528
+ },
529
+ // Each protected scope → the schemes that satisfy it, any one of them.
530
+ scopes: { account: ['apiKey'] },
531
+ },
532
+ })
533
+ ```
534
+
535
+ A scheme is `{ type: 'http', scheme: 'bearer' | 'basic' }` or
536
+ `{ type: 'apiKey', in: 'header' | 'query' | 'cookie', name }` (a session cookie
537
+ is the `cookie` form). An operation is described by the scope the auth hook
538
+ checks it under:
539
+
540
+ | The operation's scope | In the document |
541
+ |---|---|
542
+ | `'public'` | no `security`, no 401/403 |
543
+ | listed in `scopes` with schemes | `security: [{ apiKey: [] }, …]` and 401/403 |
544
+ | listed with an empty list — a custom scope whose rule is `'public'` | no `security`, no 401/403 |
545
+ | not listed | generation throws, naming the operation |
546
+ | none of its own | read as `defaultScope` — pass the same value as `createAuthHook({ defaultScope })` |
547
+
548
+ Generation refuses a protected scope it cannot describe instead of emitting the
549
+ operation as open — a client would otherwise call it without a credential. Only
550
+ the operations the document advertises need a mapping: a method `includeMethod`
551
+ filters out is never checked. A scheme `scopes` names but `schemes` does not
552
+ declare, and an entry for `'public'`, are refused the same way.
553
+
554
+ The metadata describes your policy; it does not enforce it. The auth hook is
555
+ still the only gate, and removing `security` changes the document, never a
556
+ 401 or 403. Credential values never belong here: describe where the key goes,
557
+ not what it is.
package/llms/server.5.txt CHANGED
@@ -236,6 +236,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
236
236
  | `OpenApiConfig` | _type_ | config for `generateOpenApiDocument` (incl. `includeMethod` — curate a public subset) — [guide](../guide/server.md#curating-the-spec--includemethod) |
237
237
  | `OpenApiDocument` | _type_ | the generated document |
238
238
  | `OpenApiInfo` | _type_ | the spec `info` block |
239
+ | `OpenApiSecurity` | _type_ | `OpenApiConfig.security` — named schemes and the schemes each protected scope accepts — [guide](../guide/server.md#security-schemes--security) |
240
+ | `OpenApiSecurityScheme` | _type_ | one way a caller presents its credential: HTTP bearer or basic, or an API key in a header, query or cookie |
239
241
  | `OpenApiServer` | _type_ | a spec `servers` entry |
240
242
 
241
243
  ---
@@ -92,13 +92,13 @@ own, recorded as an ADR.
92
92
  | `stitchkit/xlsx` | browser **and** server | evolving | a workbook from rows and typed columns, with no dependency; text never runs as a formula |
93
93
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
94
94
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
95
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 66 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
95
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 67 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
96
96
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, the managed-resource conformance kit and manual time for schedules and retries |
97
97
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
98
98
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
99
99
  | `stitchkit/react/keyboard` | browser | evolving | which part of a screen gets a key — ordered layers, one Escape per level, list zones with real focus; see [keyboard guide](keyboard.md) |
100
100
  | `stitchkit/react/keyboard/testing` | server (Bun or Node) | evolving | the keyboard layers and zones without a DOM — fakes for the window, elements and key events beside the two engines, to test the order of layers, `scope`/`inert` and the edges of a zone in milliseconds |
101
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 26 of the 66 minors since 0.56.2, most recently 0.121.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
101
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 26 of the 67 minors since 0.56.2, most recently 0.121.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
102
102
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
103
103
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults, deterministic race controls and the agent-store conformance kit |
104
104
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -110,7 +110,7 @@ own, recorded as an ADR.
110
110
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
111
111
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
112
112
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
113
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 14 of the 66 minors since 0.56.2, most recently 0.110.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
113
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 14 of the 67 minors since 0.56.2, most recently 0.110.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
114
114
  | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
115
115
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
116
116
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
package/llms/tools.2.txt CHANGED
@@ -13,6 +13,115 @@ Parts of this slice:
13
13
  # Guide: MCP & agents (docs/guide/mcp-and-agents.md) (continued)
14
14
  ==============================================================================
15
15
 
16
+ ## OAuth 2.1 — a native remote connector
17
+
18
+ A remote MCP server is connectable from Claude (Desktop / web "custom
19
+ connector") only through the MCP authorization spec: OAuth 2.1 with PKCE, plus
20
+ the discovery documents (RFC 9728 / 8414), Client ID Metadata Documents (CIMD)
21
+ and resource indicators (RFC 8707). A Bearer-only server returns a
22
+ bare `401` and the connector never establishes.
23
+
24
+ stitchkit ships the OAuth **protocol mechanics**; the app supplies only
25
+ **identity and storage**. Three pieces wire it together:
26
+
27
+ ```ts
28
+ import { mountOAuthProvider, oauthProtectedResourceRoute } from 'stitchkit/tools'
29
+ import { createMcpHandler, createMcpHttpRoute } from 'stitchkit/tools/mcp'
30
+ import { createServer } from 'stitchkit/server'
31
+
32
+ const resource = 'https://api.example.com/mcp'
33
+ const issuer = 'https://api.example.com'
34
+
35
+ // 1. Resource server — the 401 now points at the metadata.
36
+ const mcp = createMcpHandler({
37
+ serverInfo: { name: 'my-app', version: '1.0.0' },
38
+ auth: resolveOAuthToken, // validate the Bearer JWT (verifyJwt + audience)
39
+ services,
40
+ protectedResource: { resource, authorizationServers: [issuer] },
41
+ })
42
+
43
+ // 2. Authorization server — CIMD, /authorize (PKCE), /token.
44
+ const oauthRoutes = mountOAuthProvider({
45
+ issuer,
46
+ resource,
47
+ signingSecret: env.OAUTH_SECRET,
48
+ clientRegistration: {
49
+ preRegistered: { get: getFirstPartyClient },
50
+ // CIMD is enabled with secure defaults; DCR remains disabled.
51
+ },
52
+ codes, refreshTokens, // your stores (DB or in-memory)
53
+ authorizeUser, // → { userId, approvedScopes } | Response
54
+ })
55
+
56
+ createServer({
57
+ services,
58
+ rawRoutes: [
59
+ createMcpHttpRoute({ path: '/mcp', handler: mcp }),
60
+ oauthProtectedResourceRoute({ resource, authorizationServers: [issuer] }),
61
+ ...oauthRoutes,
62
+ ],
63
+ })
64
+ ```
65
+
66
+ Access tokens are signed HS256 JWTs (`signJwt`) whose `aud` is the resource and
67
+ whose `iss` is the issuer — validate both in `auth` with
68
+ `verifyJwt(token, secret, { audience: resource, issuer })`. `authorizeUser` is
69
+ where the app authenticates the user (reuse an existing session) and records
70
+ consent; return `{ userId, approvedScopes }`, where approved scopes are a subset
71
+ of the requested/supported values, or a `Response` to redirect the browser to a
72
+ login page first. The AS and resource server can co-locate or live
73
+ on separate origins. See
74
+ [ADR 0015](../decisions/0015-oauth-resource-server.md).
75
+
76
+ Client resolution is deterministic: exact pre-registered client, then an HTTPS
77
+ URL client id resolved as CIMD, then explicitly enabled DCR. CIMD is on by
78
+ default; DCR is off unless `clientRegistration.dcr` supplies both `register` and
79
+ `get`. A CIMD-only deployment therefore exposes neither `/register` nor a
80
+ `registration_endpoint`. The metadata document must declare a `client_id` that
81
+ exactly equals its URL, explicit redirect URIs and
82
+ `token_endpoint_auth_method: 'none'`.
83
+
84
+ The token endpoint and `/register` read at most 64 KiB of a request body, or
85
+ `maxRequestBodyBytes` when you set it; a larger one is answered `413` with
86
+ `invalid_request` and the rest is not read.
87
+
88
+ The production fetcher rejects credentials, fragments, non-HTTPS URLs and
89
+ private/reserved targets. Every redirect is DNS-resolved and IP-pinned again;
90
+ timeouts, redirect count and body size are bounded. Its bounded cache respects
91
+ `Cache-Control` (including `no-store`/`no-cache`), `Age`, `Expires`, `ETag` and
92
+ `Last-Modified`, coalesces concurrent misses and reports sanitized cache events;
93
+ invalid or unavailable identity receives only a short fail-closed cache entry.
94
+
95
+ Resolution itself is rate-limited on two levels (`CimdCachePolicy`):
96
+ `maxResolutionsPerClient` (default 10) bounds ONE `client_id` per
97
+ `resolutionWindowMs` window (default 60 s) — a client whose document disables
98
+ caching burns its own budget, not the server's — while `maxResolutions`
99
+ (default 120) caps the server-wide outbound rate as the flood backstop.
100
+ Positive and negative cache entries keep separate pools of `maxEntries` each,
101
+ so a flood of unresolvable ids cannot evict warmed clients and a burst of
102
+ successes cannot reset a failing client's backoff.
103
+
104
+ ### Authorization hardening (MCP 2026-07-28)
105
+
106
+ - **`iss` on every authorization response (RFC 9207, SEP-2468).** Success *and*
107
+ error redirects carry `iss`, and the AS metadata advertises
108
+ `authorization_response_iss_parameter_supported: true`. A client that talks to
109
+ several authorization servers validates `iss` before redeeming the code, which
110
+ closes the **mix-up attack** — an attacker's server cannot pass its response
111
+ off as this issuer's. Additive on the wire: a client that ignores `iss` is
112
+ unaffected.
113
+ - **`application_type` on registration (SEP-837).** A client may declare
114
+ `"native"` (desktop / CLI) or `"web"` in its DCR body. A **native** client may
115
+ register an `http` loopback redirect (`http://127.0.0.1:…`, RFC 8252 §7.3); a
116
+ **web** client is held to `https` only — that mismatch is the usual cause of
117
+ the `redirect_uri` rejection CLI clients hit. Omit the field and registration
118
+ behaves exactly as before (loopback allowed); an unknown value is rejected
119
+ rather than silently defaulted.
120
+
121
+ > Dynamic Client Registration is an explicit interoperability mode, not the
122
+ > default. Enabling it changes public discovery and adds `/register`; keep it off
123
+ > unless a client that cannot publish CIMD requires it.
124
+
16
125
  ## Proxying a remote API — `implementRemote`
17
126
 
18
127
  `implement` binds a contract to local handlers. `implementRemote` binds it to a
package/llms/tools.txt CHANGED
@@ -317,6 +317,36 @@ schema and annotations, and nothing else — it moves when the contract moves an
317
317
  not when a handler does. Raw SDK registrations stay consumer-owned and are
318
318
  outside it.
319
319
 
320
+ ### Saying which tools only read — `readOnlyHint`
321
+
322
+ A host reads a tool's `annotations` to group tools and to choose permission
323
+ defaults, and a CLI mounted over this surface reads them to decide what a
324
+ refused view may withhold ([CLI → views](./cli.md#aggregate-views)). The server publishes the
325
+ annotations the tool declared, and states `readOnlyHint: true` for a read that
326
+ declared none:
327
+
328
+ | The tool | Published `readOnlyHint` |
329
+ |---|---|
330
+ | declares `readOnlyHint`, either way | as declared |
331
+ | `GET` with no `readOnlyHint` | `true` |
332
+ | `GET` declaring `destructiveHint: true` and no `readOnlyHint` | none |
333
+ | any other method with no `readOnlyHint` | none |
334
+
335
+ A runtime tool follows the same rule by its `identity.method`. A raw SDK
336
+ registration (`mountWait`, `mountViewFile` and the like) publishes only what it
337
+ passes. The catalog digest hashes the published annotations, so it moves once
338
+ when a server starts publishing the derived hint.
339
+
340
+ > **A `GET` with an effect must say so.** Hosts may run a tool that publishes
341
+ > `readOnlyHint: true` without asking. An endpoint that changes something on
342
+ > `GET` — marks as seen, rotates a token — declares
343
+ > `tool: { annotations: { readOnlyHint: false } }`.
344
+
345
+ A client connection (`defineMcpClientConnection`) carries the hints a server
346
+ published onto the tools it mounts, field by field: a hint of the wrong type is
347
+ dropped and the tool stays. The mounted tool is as read-only as its server says
348
+ it is, and a surface that re-publishes mounted tools passes those hints on.
349
+
320
350
  ### Shaping arguments before validation — `beforeToolCall`
321
351
 
322
352
  `beforeToolCall` sees the arguments as they arrived and may **replace** them:
@@ -498,7 +528,10 @@ render: async (ctx) => {
498
528
 
499
529
  `reportProgress` is present on every **tool** call — MCP, agent and CLI — so a
500
530
  handler never branches on transport, and it does nothing wherever nobody is
501
- listening. It is absent on the HTTP transport, which has no channel for it, so a
531
+ listening. Under the agent runtime somebody is: each report becomes a transient
532
+ `tool-progress` event of the run, so the person watching the agent sees the
533
+ stage while the model still reads only the tool's result (see
534
+ [Agent runtime → events](./agent-runtime.md)). It is absent on the HTTP transport, which has no channel for it, so a
502
535
  handler that also serves HTTP writes `ctx.reportProgress?.(…)`. It never throws and never
503
536
  rejects: a message about work must not be able to kill the work it describes, so
504
537
  a refused notification leaves the host exactly where a host that never asked for
@@ -862,112 +895,3 @@ OS default disposition instead of reporting a fictional forced result.
862
895
  Both transports build the server through the shared `buildMcpServer` — same
863
896
  contract/runtime pipeline, same surface selection, context, hooks, raw escape
864
897
  hatch and instructions.
865
-
866
- ## OAuth 2.1 — a native remote connector
867
-
868
- A remote MCP server is connectable from Claude (Desktop / web "custom
869
- connector") only through the MCP authorization spec: OAuth 2.1 with PKCE, plus
870
- the discovery documents (RFC 9728 / 8414), Client ID Metadata Documents (CIMD)
871
- and resource indicators (RFC 8707). A Bearer-only server returns a
872
- bare `401` and the connector never establishes.
873
-
874
- stitchkit ships the OAuth **protocol mechanics**; the app supplies only
875
- **identity and storage**. Three pieces wire it together:
876
-
877
- ```ts
878
- import { mountOAuthProvider, oauthProtectedResourceRoute } from 'stitchkit/tools'
879
- import { createMcpHandler, createMcpHttpRoute } from 'stitchkit/tools/mcp'
880
- import { createServer } from 'stitchkit/server'
881
-
882
- const resource = 'https://api.example.com/mcp'
883
- const issuer = 'https://api.example.com'
884
-
885
- // 1. Resource server — the 401 now points at the metadata.
886
- const mcp = createMcpHandler({
887
- serverInfo: { name: 'my-app', version: '1.0.0' },
888
- auth: resolveOAuthToken, // validate the Bearer JWT (verifyJwt + audience)
889
- services,
890
- protectedResource: { resource, authorizationServers: [issuer] },
891
- })
892
-
893
- // 2. Authorization server — CIMD, /authorize (PKCE), /token.
894
- const oauthRoutes = mountOAuthProvider({
895
- issuer,
896
- resource,
897
- signingSecret: env.OAUTH_SECRET,
898
- clientRegistration: {
899
- preRegistered: { get: getFirstPartyClient },
900
- // CIMD is enabled with secure defaults; DCR remains disabled.
901
- },
902
- codes, refreshTokens, // your stores (DB or in-memory)
903
- authorizeUser, // → { userId, approvedScopes } | Response
904
- })
905
-
906
- createServer({
907
- services,
908
- rawRoutes: [
909
- createMcpHttpRoute({ path: '/mcp', handler: mcp }),
910
- oauthProtectedResourceRoute({ resource, authorizationServers: [issuer] }),
911
- ...oauthRoutes,
912
- ],
913
- })
914
- ```
915
-
916
- Access tokens are signed HS256 JWTs (`signJwt`) whose `aud` is the resource and
917
- whose `iss` is the issuer — validate both in `auth` with
918
- `verifyJwt(token, secret, { audience: resource, issuer })`. `authorizeUser` is
919
- where the app authenticates the user (reuse an existing session) and records
920
- consent; return `{ userId, approvedScopes }`, where approved scopes are a subset
921
- of the requested/supported values, or a `Response` to redirect the browser to a
922
- login page first. The AS and resource server can co-locate or live
923
- on separate origins. See
924
- [ADR 0015](../decisions/0015-oauth-resource-server.md).
925
-
926
- Client resolution is deterministic: exact pre-registered client, then an HTTPS
927
- URL client id resolved as CIMD, then explicitly enabled DCR. CIMD is on by
928
- default; DCR is off unless `clientRegistration.dcr` supplies both `register` and
929
- `get`. A CIMD-only deployment therefore exposes neither `/register` nor a
930
- `registration_endpoint`. The metadata document must declare a `client_id` that
931
- exactly equals its URL, explicit redirect URIs and
932
- `token_endpoint_auth_method: 'none'`.
933
-
934
- The token endpoint and `/register` read at most 64 KiB of a request body, or
935
- `maxRequestBodyBytes` when you set it; a larger one is answered `413` with
936
- `invalid_request` and the rest is not read.
937
-
938
- The production fetcher rejects credentials, fragments, non-HTTPS URLs and
939
- private/reserved targets. Every redirect is DNS-resolved and IP-pinned again;
940
- timeouts, redirect count and body size are bounded. Its bounded cache respects
941
- `Cache-Control` (including `no-store`/`no-cache`), `Age`, `Expires`, `ETag` and
942
- `Last-Modified`, coalesces concurrent misses and reports sanitized cache events;
943
- invalid or unavailable identity receives only a short fail-closed cache entry.
944
-
945
- Resolution itself is rate-limited on two levels (`CimdCachePolicy`):
946
- `maxResolutionsPerClient` (default 10) bounds ONE `client_id` per
947
- `resolutionWindowMs` window (default 60 s) — a client whose document disables
948
- caching burns its own budget, not the server's — while `maxResolutions`
949
- (default 120) caps the server-wide outbound rate as the flood backstop.
950
- Positive and negative cache entries keep separate pools of `maxEntries` each,
951
- so a flood of unresolvable ids cannot evict warmed clients and a burst of
952
- successes cannot reset a failing client's backoff.
953
-
954
- ### Authorization hardening (MCP 2026-07-28)
955
-
956
- - **`iss` on every authorization response (RFC 9207, SEP-2468).** Success *and*
957
- error redirects carry `iss`, and the AS metadata advertises
958
- `authorization_response_iss_parameter_supported: true`. A client that talks to
959
- several authorization servers validates `iss` before redeeming the code, which
960
- closes the **mix-up attack** — an attacker's server cannot pass its response
961
- off as this issuer's. Additive on the wire: a client that ignores `iss` is
962
- unaffected.
963
- - **`application_type` on registration (SEP-837).** A client may declare
964
- `"native"` (desktop / CLI) or `"web"` in its DCR body. A **native** client may
965
- register an `http` loopback redirect (`http://127.0.0.1:…`, RFC 8252 §7.3); a
966
- **web** client is held to `https` only — that mismatch is the usual cause of
967
- the `redirect_uri` rejection CLI clients hit. Omit the field and registration
968
- behaves exactly as before (loopback allowed); an unknown value is rejected
969
- rather than silently defaulted.
970
-
971
- > Dynamic Client Registration is an explicit interoperability mode, not the
972
- > default. Enabling it changes public discovery and adds `/register`; keep it off
973
- > unless a client that cannot publish CIMD requires it.
@@ -1,14 +1,42 @@
1
- # Upgrading 0.106.0 → 0.121.0 — stitchkit docs slice
1
+ # Upgrading 0.106.0 → 0.122.0 — stitchkit docs slice
2
2
 
3
3
  > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
4
  > `llms.txt` in this package.
5
5
 
6
- Contains: Guide: Upgrading, 0.106.0 → 0.121.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.106.0 → 0.122.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.106.0 → 0.121.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.106.0 → 0.122.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.122.0
13
+
14
+ ### read-view-refusal
15
+
16
+ **Who must act:** scripts and agents that read stdout after a managed CLI command exits with
17
+ `CLI_PRESENTATION_FAILED` (`8` by default) when that command is a `GET` endpoint or a tool
18
+ declaring `annotations.readOnlyHint: true`. Commands with an effect and native
19
+ `defineCliCommand` presenters change nothing.
20
+
21
+ A read no longer prints its complete result when the requested view is refused; stdout is
22
+ empty and the diagnostic carries `details.resultOnStdout: false`.
23
+
24
+ ```sh
25
+ # before: the full list arrived on stdout beside the diagnostic
26
+ app user_list --table id,nosuchcolumn > users.json # exit 8, users.json holds every record
27
+
28
+ # after: repeat the read with a field the diagnostic names
29
+ app user_list --table id,nosuchcolumn # exit 8, stdout empty
30
+ app user_list --table id,name
31
+ ```
32
+
33
+ A `GET` endpoint whose call has an effect that must not be repeated declares it, and keeps
34
+ its result on stdout:
35
+
36
+ ```ts
37
+ tool: { name: 'token_rotate', annotations: { readOnlyHint: false } }
38
+ ```
39
+
12
40
  ## Released migration: 0.121.0
13
41
 
14
42
  ### bounded-coding-search
@@ -11,7 +11,7 @@ Contains: Guide: Upgrading, general (docs/guide/upgrading.md)
11
11
 
12
12
  Load this slice, then every range slice between your installed version and the target:
13
13
 
14
- - 0.106.0 → 0.121.0: `llms/upgrading-0.106.0-0.121.0.txt`
14
+ - 0.106.0 → 0.122.0: `llms/upgrading-0.106.0-0.122.0.txt`
15
15
  - 0.96.0 → 0.105.0: `llms/upgrading-0.96.0-0.105.0.txt`
16
16
  - 0.69.0 → 0.95.0: `llms/upgrading-0.69.0-0.95.0.txt`
17
17
  - 0.55.0 → 0.68.0: `llms/upgrading-0.55.0-0.68.0.txt`