stitchkit 0.55.0 → 0.56.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 (78) hide show
  1. package/dist/browser/socket-io.d.ts +14 -6
  2. package/dist/browser/socket-io.d.ts.map +1 -1
  3. package/dist/cli.js +4 -4
  4. package/dist/contract/errors.d.ts +7 -0
  5. package/dist/contract/errors.d.ts.map +1 -1
  6. package/dist/contract/index.js +1 -1
  7. package/dist/files/boundary.d.ts +8 -5
  8. package/dist/files/boundary.d.ts.map +1 -1
  9. package/dist/files.js +1 -1
  10. package/dist/{index-rgd18hx3.js → index-28cqssm3.js} +7 -0
  11. package/dist/index-557by2db.js +34 -0
  12. package/dist/index-5r13htq1.js +283 -0
  13. package/dist/index-6y759j86.js +1031 -0
  14. package/dist/{index-npye5kt7.js → index-75njxz7p.js} +69 -7
  15. package/dist/{index-1k16zv57.js → index-bfcpjw20.js} +125 -31
  16. package/dist/{index-0gv7k0ra.js → index-escqg10p.js} +73 -666
  17. package/dist/{index-v4bbq3p4.js → index-j3dem06f.js} +1 -1
  18. package/dist/{index-qx84bqzk.js → index-jcc611vh.js} +1 -1
  19. package/dist/{index-pzc32v75.js → index-sm2tjx06.js} +41 -4
  20. package/dist/{index-js7yexng.js → index-xy8fmh6w.js} +83 -4
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +27 -6
  24. package/dist/node.js +5 -4
  25. package/dist/observability/audit.d.ts +4 -0
  26. package/dist/observability/audit.d.ts.map +1 -1
  27. package/dist/observability/event.d.ts +5 -0
  28. package/dist/observability/event.d.ts.map +1 -1
  29. package/dist/observability/index.js +17 -11
  30. package/dist/remote.js +1 -1
  31. package/dist/server/context-contribution.d.ts +8 -0
  32. package/dist/server/context-contribution.d.ts.map +1 -1
  33. package/dist/server/create.d.ts.map +1 -1
  34. package/dist/server/error-hook.d.ts +1 -1
  35. package/dist/server/error-hook.d.ts.map +1 -1
  36. package/dist/server/index.d.ts +1 -1
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +10 -6
  39. package/dist/server/logger.d.ts.map +1 -1
  40. package/dist/server/middleware/auth.d.ts +29 -2
  41. package/dist/server/middleware/auth.d.ts.map +1 -1
  42. package/dist/server/request-body.d.ts.map +1 -1
  43. package/dist/testing/surface-conformance.d.ts +143 -10
  44. package/dist/testing/surface-conformance.d.ts.map +1 -1
  45. package/dist/testing/surface-manifest.d.ts +132 -33
  46. package/dist/testing/surface-manifest.d.ts.map +1 -1
  47. package/dist/testing.d.ts +2 -2
  48. package/dist/testing.d.ts.map +1 -1
  49. package/dist/testing.js +470 -122
  50. package/dist/tools/async-operation.d.ts +210 -18
  51. package/dist/tools/async-operation.d.ts.map +1 -1
  52. package/dist/tools/define-download-tool.d.ts.map +1 -1
  53. package/dist/tools/define-upload-tool.d.ts.map +1 -1
  54. package/dist/tools/internal/surface-projector.d.ts +101 -0
  55. package/dist/tools/internal/surface-projector.d.ts.map +1 -0
  56. package/dist/tools/managed-file-error.d.ts +6 -0
  57. package/dist/tools/managed-file-error.d.ts.map +1 -0
  58. package/dist/tools/mcp-prepare.d.ts +3 -21
  59. package/dist/tools/mcp-prepare.d.ts.map +1 -1
  60. package/dist/tools/mcp-round-policy.d.ts +9 -0
  61. package/dist/tools/mcp-round-policy.d.ts.map +1 -0
  62. package/dist/tools/mcp-round.d.ts +0 -4
  63. package/dist/tools/mcp-round.d.ts.map +1 -1
  64. package/dist/tools/mount-download.d.ts.map +1 -1
  65. package/dist/tools/mount-upload.d.ts.map +1 -1
  66. package/dist/tools/mount.d.ts +5 -9
  67. package/dist/tools/mount.d.ts.map +1 -1
  68. package/dist/tools/runtime-tool.d.ts.map +1 -1
  69. package/dist/tools/surface.d.ts +7 -0
  70. package/dist/tools/surface.d.ts.map +1 -1
  71. package/dist/tools/view-file.d.ts.map +1 -1
  72. package/dist/tools.d.ts +1 -1
  73. package/dist/tools.d.ts.map +1 -1
  74. package/dist/tools.js +348 -383
  75. package/llms-full.txt +425 -66
  76. package/package.json +1 -1
  77. package/dist/index-h60df7rj.js +0 -110
  78. package/dist/index-k4dftwf7.js +0 -66
package/llms-full.txt CHANGED
@@ -989,6 +989,18 @@ createServer({
989
989
  | `'pretty'` | two coloured lines per request — `→` on arrival, `←` on completion | no — a line sized for a terminal is not a record |
990
990
  | `'json'` | one structured line per completed request | yes |
991
991
 
992
+ Status `499` has one framework-wide meaning: the client closed the request. It
993
+ is logged at `info`, not under the ordinary `4xx → warn` rule. A confirmed
994
+ disconnect is not sent through project `onError`, `normalizeError` or the
995
+ request-error recorder; an `AbortError` while the request signal is still active
996
+ remains an internal failure. A runtime abort reason may be preserved by identity
997
+ through at most eight cycle-safe standard `cause` links; error messages and codes
998
+ are never classifiers. The same rule applies when the disconnect happens while
999
+ Stitchkit is reading a JSON upload body: bounded reads race every pending stream
1000
+ read against the request signal and never parse a cancelled partial body. `499`
1001
+ is transport telemetry, not a response declared in the contract or generated
1002
+ OpenAPI document.
1003
+
992
1004
  Unset, `format` follows `NODE_ENV`: `json` under `production`, `pretty`
993
1005
  otherwise. That default is read **per request** — not at import, not when this
994
1006
  package was built — so it reflects the environment your app actually runs in.
@@ -1159,7 +1171,9 @@ createServer({
1159
1171
  - **`afterHandle`** — receives the handler result; return a replacement to
1160
1172
  transform it.
1161
1173
  - **`onError`** — receives any thrown error; return a `Response` to customise
1162
- the error body. Without it, errors render through the standard envelope.
1174
+ the error body. Without it, errors render through the standard envelope. A
1175
+ confirmed client disconnect is a transport cancellation rather than an
1176
+ application error and deliberately bypasses this hook.
1163
1177
 
1164
1178
  Hooks see `RuntimeContext` (loose types); handlers see `HandlerContext` (typed).
1165
1179
  That split is deliberate — see [ADR 0003](../decisions/0003-two-context-types.md).
@@ -3024,7 +3038,13 @@ import {
3024
3038
  import { createManagedFileBoundary } from 'stitchkit/files'
3025
3039
  import { z } from 'zod'
3026
3040
 
3027
- const files = await createManagedFileBoundary({ root: '/srv/job-media' })
3041
+ const files = await createManagedFileBoundary({
3042
+ root: '/srv/job-media',
3043
+ createRoot: true, // creates only this final directory; /srv must already be trusted
3044
+ inspectionTimeoutMs: 15_000,
3045
+ inspect: ({ prefix, name, declaredMediaType, signal }) =>
3046
+ inspectFile(prefix, { name, declaredMediaType, signal }),
3047
+ })
3028
3048
 
3029
3049
  const waitForJob = defineWaitTool({
3030
3050
  name: 'wait_for_job',
@@ -3131,16 +3151,14 @@ const runtimeTools = exportOperation.runtimeTools
3131
3151
  ```
3132
3152
 
3133
3153
  Every follow-up repeats `authorize`; an opaque id is never authority. Aborting
3134
- `wait` only stops waiting and never calls optional domain `cancel`. A
3135
- contract-backed operation uses `bindContractAsyncOperation` with literal methods
3136
- from a dedicated existing contract, so it creates no second HTTP router or
3137
- duplicate schemas. Capability keys are narrowed by schema-compatible TypeScript
3138
- input/output types. At runtime, the binder additionally requires the exact same
3139
- id and snapshot Zod instances; declare each once and reuse it:
3154
+ `wait` only stops waiting and never calls optional domain `cancel`.
3155
+
3156
+ For a new HTTP surface, derive the canonical capability contract once. The
3157
+ factory defines schemas and routes only; the application still supplies normal
3158
+ handlers to `implement` and owns the job:
3140
3159
 
3141
3160
  ```ts
3142
- import { defineContract } from 'stitchkit/contract'
3143
- import { bindContractAsyncOperation } from 'stitchkit/tools'
3161
+ import { defineAsyncOperationContract } from 'stitchkit/tools'
3144
3162
  import { z } from 'zod'
3145
3163
 
3146
3164
  const operationId = z.object({ id: z.string() })
@@ -3148,38 +3166,82 @@ const operationSnapshot = z.object({
3148
3166
  phase: z.enum(['pending', 'running', 'succeeded', 'failed', 'cancelled']),
3149
3167
  })
3150
3168
 
3151
- const operations = defineContract(
3152
- { prefix: 'exports' },
3153
- {
3154
- start: {
3155
- method: 'POST', path: '/', desc: 'Start export',
3156
- input: z.object({ projectId: z.string() }), output: operationId,
3157
- },
3158
- status: {
3159
- method: 'POST', path: '/status', desc: 'Read export status',
3160
- input: operationId, output: operationSnapshot,
3161
- },
3162
- wait: {
3163
- method: 'POST', path: '/wait', desc: 'Wait for export',
3164
- input: operationId, output: operationSnapshot,
3165
- },
3169
+ const operations = defineAsyncOperationContract({
3170
+ prefix: 'exports',
3171
+ scope: 'user',
3172
+ description: 'Export project data',
3173
+ startInput: z.object({ projectId: z.string() }),
3174
+ id: operationId,
3175
+ snapshot: operationSnapshot,
3176
+ cancel: true,
3177
+ result: z.object({ downloadUrl: z.url() }),
3178
+ })
3179
+
3180
+ // Pass operations.contract and ordinary handlers to implement(...).
3181
+ // operations.capabilities/schemas/adapters are the canonical protocol metadata.
3182
+ ```
3183
+
3184
+ If an existing dedicated contract has a different wire shape — for example
3185
+ `start` returns a snapshot while follow-ups accept separate envelopes — bind it
3186
+ without casts or duplicate endpoints. Inline callbacks are contextually typed;
3187
+ every returned id/input is parsed by the destination schema:
3188
+
3189
+ ```ts
3190
+ import { defineContract } from 'stitchkit/contract'
3191
+ import { bindContractAsyncOperation } from 'stitchkit/tools'
3192
+
3193
+ const startOutput = z.object({ operation: operationId, acceptedAt: z.string() })
3194
+ const statusInput = z.object({ operationId: z.string() })
3195
+ const waitInput = z.object({ lookup: operationId })
3196
+
3197
+ const existing = defineContract({ prefix: 'exports' }, {
3198
+ start: {
3199
+ method: 'POST', path: '/', desc: 'Start export',
3200
+ input: z.object({ projectId: z.string() }), output: startOutput,
3166
3201
  },
3167
- )
3202
+ status: {
3203
+ method: 'POST', path: '/status', desc: 'Read export status',
3204
+ input: statusInput, output: operationSnapshot,
3205
+ },
3206
+ wait: {
3207
+ method: 'POST', path: '/wait', desc: 'Wait for export',
3208
+ input: waitInput, output: operationSnapshot,
3209
+ },
3210
+ })
3168
3211
 
3169
3212
  const operationHandlers = {
3170
- start: () => ({ id: 'example' }),
3213
+ start: () => ({ operation: { id: 'example' }, acceptedAt: new Date().toISOString() }),
3171
3214
  status: (): z.output<typeof operationSnapshot> => ({ phase: 'pending' }),
3172
3215
  wait: (): z.output<typeof operationSnapshot> => ({ phase: 'succeeded' }),
3173
3216
  }
3174
3217
 
3175
3218
  const bound = bindContractAsyncOperation({
3176
3219
  mode: 'contract-backed',
3177
- contract: operations,
3220
+ binding: 'adapted',
3221
+ contract: existing,
3222
+ id: operationId,
3178
3223
  capabilities: { start: 'start', status: 'status', wait: 'wait' },
3224
+ adapters: {
3225
+ idFromStart: (output) => output.operation,
3226
+ inputFor: {
3227
+ status: (id) => ({ operationId: id.id }),
3228
+ wait: (id) => ({ lookup: id }),
3229
+ },
3230
+ },
3179
3231
  handlers: operationHandlers,
3180
3232
  })
3181
3233
  ```
3182
3234
 
3235
+ Direct binding remains the short form when start already returns the id and
3236
+ every follow-up reuses that exact schema instance. Its ID schema must be
3237
+ wire-stable: input and output types match and parsing has no transform,
3238
+ coercion, default or overwrite. This also excludes same-type transforms because
3239
+ the direct adapters would parse an already parsed ID again. Use adapted binding
3240
+ and explicitly project the parsed ID back into each follow-up input. In both
3241
+ modes the binder creates no router;
3242
+ it returns the existing contract, handlers and protocol metadata for
3243
+ application composition.
3244
+
3183
3245
  ### Explicit raw SDK registration
3184
3246
 
3185
3247
  `rawTools` is deliberately named as an escape hatch. A tool registered there
@@ -3758,6 +3820,24 @@ socket.on('note:created', (note) => { /* typed note */ })
3758
3820
  socket.emit('room:join', 'general', ({ joined }) => { /* typed + validated */ })
3759
3821
  ```
3760
3822
 
3823
+ When an application already owns the low-level Stitchkit transport, bind the
3824
+ contract without opening a second connection:
3825
+
3826
+ ```ts
3827
+ import { bindRealtimeClient, createSocketIOClient } from 'stitchkit'
3828
+
3829
+ const transport = createSocketIOClient({ url: 'https://api.example.com' })
3830
+ const events = bindRealtimeClient(realtimeContract, transport, { onRejected })
3831
+
3832
+ transport.connect() // lifecycle stays with the transport owner
3833
+ events.on('note:created', handleNote)
3834
+ await events.request('room:join', 'general', { timeoutMs: 5_000 })
3835
+ ```
3836
+
3837
+ The bound handle intentionally has no `connect()` or `disconnect()`. Its
3838
+ `on`/`emit`/`request`, rejection and timeout semantics are exactly the path used
3839
+ by `createRealtimeClient`; only transport construction/lifecycle differs.
3840
+
3761
3841
  ### Request-response over realtime
3762
3842
 
3763
3843
  For an event with an `ack` schema, `request()` is the Promise form of the same
@@ -3792,6 +3872,7 @@ transport.
3792
3872
  `createSocketIOClient` remains the low-level Socket.IO transport wrapper for
3793
3873
  schema-agnostic infrastructure. Application wire events should use
3794
3874
  `createRealtimeClient`; it adds the shared contract without replacing Socket.IO.
3875
+ Use `bindRealtimeClient` when that low-level transport already exists.
3795
3876
 
3796
3877
  ### Durable subscriptions
3797
3878
 
@@ -4344,10 +4425,10 @@ fields; `true | object` correctly makes them optional.
4344
4425
  Annotate `rules` with `satisfies Record<MyScope, AuthRule<User>>` so the compiler
4345
4426
  catches a scope you forgot to cover. With the scoped rule objects below, widen
4346
4427
  the annotation to
4347
- `satisfies Record<MyScope, AuthRule<User> | ScopedAuthRule<User, object>>` — it
4348
- keeps the coverage check and does not disturb the derivation. (When every scope
4349
- comes from the derived map, the check is already implicit: a scope missing from
4350
- `rules` is no key of the map, and a contract using it fails to compile.)
4428
+ `satisfies Record<MyScope, AuthRule<User> | ScopedAuthRule<User, AuthRuleContribution>>`
4429
+ — it keeps the coverage check and does not disturb the derivation. (When every
4430
+ scope comes from the derived map, the check is already implicit: a scope missing
4431
+ from `rules` is no key of the map, and a contract using it fails to compile.)
4351
4432
 
4352
4433
  ### Scoped rules — the map `createScopedImplement` consumes
4353
4434
 
@@ -4393,11 +4474,58 @@ widens its inject and degrades that scope's fields to `object`.
4393
4474
 
4394
4475
  Derivation needs the identity generic to be **inferred** (write no explicit
4395
4476
  `createAuthHook<User>` — TypeScript has no partial inference); with an explicit
4396
- generic, or fields injected outside the hook, keep the hand-written map. Two
4397
- hooks guarding different scopes compose by intersection:
4398
- `createScopedImplement<AuthScopes<typeof userHook> & AuthScopes<typeof botHook>>()`.
4477
+ generic, or fields injected outside the hook, keep the hand-written map.
4399
4478
  → ADR 0078
4400
4479
 
4480
+ ### Multiple auth domains — `composeAuthHooks`
4481
+
4482
+ Independent identity domains should keep separate resolvers and rules. Compose
4483
+ their canonical hooks and derive one handler context without a manual dispatcher
4484
+ or scope-map intersection:
4485
+
4486
+ ```ts
4487
+ const userAuth = createAuthHook({
4488
+ resolve: resolveUser,
4489
+ resolveFromContext: resolveToolUser,
4490
+ rules: {
4491
+ user: (user) => ({ userId: user.id }),
4492
+ workspace: (user) => ({ userId: user.id }),
4493
+ },
4494
+ })
4495
+
4496
+ const workspaceAuth = createAuthHook({
4497
+ resolve: resolveMembership,
4498
+ resolveFromContext: resolveToolMembership,
4499
+ rules: {
4500
+ workspace: (membership) => ({ workspaceId: membership.workspaceId }),
4501
+ },
4502
+ })
4503
+
4504
+ export const auth = composeAuthHooks({
4505
+ hooks: [userAuth, workspaceAuth],
4506
+ defaultScope: 'user',
4507
+ })
4508
+ export const implementFor = createScopedImplement<AuthScopes<typeof auth>>()
4509
+ ```
4510
+
4511
+ Only hooks that declare the selected scope run. An unknown scope fails closed;
4512
+ when several hooks own one scope, every owner must pass in declaration order.
4513
+ Each owner evaluates on an isolated shadow context. Stitchkit validates all
4514
+ changed fields, rejects reserved/unsafe fields and cross-owner key collisions,
4515
+ then commits the combined contribution once. A rejected or cancelled composite
4516
+ therefore exposes no partial fields to the handler.
4517
+
4518
+ Isolation is per-key, not deep: a contribution is the set of context keys whose
4519
+ descriptor changed. Mutating an existing value **in place**
4520
+ (`ctx.user.role = 'admin'`) edits the object every owner already shares, so it
4521
+ is neither reported as a contribution nor checked for collisions — contribute
4522
+ new fields instead of editing another owner's object. External side effects
4523
+ likewise remain consumer-owned and are not rolled back.
4524
+
4525
+ The composite's `defaultScope` is the only implicit scope. A child hook's own
4526
+ default must be absent or equal to it, so reordering hooks cannot change which
4527
+ scope protects an endpoint.
4528
+
4401
4529
  ### Auth on the tool surface — `resolveFromContext`
4402
4530
 
4403
4531
  The same hook guards every transport, but the lifecycle slot and identity
@@ -4764,9 +4892,11 @@ a request context, and event projections — and you usually touch only the last
4764
4892
  ### createObservability
4765
4893
 
4766
4894
  `createObservability` configures request and tool projections independently.
4767
- Every completed call is normalised into one `RequestEvent`; HTTP completion is
4768
- owned directly by `createHandler`, while MCP/Agent completion uses the canonical
4769
- `ToolCallHooks` runner. There is no nested HTTP audit wrapper.
4895
+ Every completed application call is normalised into one `RequestEvent`; HTTP
4896
+ completion is owned directly by `createHandler`, while MCP/Agent completion uses
4897
+ the canonical `ToolCallHooks` runner. Confirmed HTTP client cancellation stays
4898
+ in the access log by default and becomes a structured event only when the
4899
+ request sink opts in. There is no nested HTTP audit wrapper.
4770
4900
 
4771
4901
  ```ts
4772
4902
  import { createObservability } from 'stitchkit/observability'
@@ -4787,6 +4917,7 @@ export const observability = createObservability({
4787
4917
  request: {
4788
4918
  write,
4789
4919
  includePayload: false, // default: no Request.clone(), payload is null
4920
+ includeCancelled: false, // default: keep client closes in access logs only
4790
4921
  filter: (event) => event.method !== 'GET',
4791
4922
  },
4792
4923
  tools: {
@@ -4809,6 +4940,40 @@ Each sink runs fire-and-forget and fails independently: a slow or broken request
4809
4940
  sink cannot block the response, suppress operational logging or break the tool
4810
4941
  sink.
4811
4942
 
4943
+ #### Client cancellation
4944
+
4945
+ A physical client close is classified only when the request's own signal is
4946
+ aborted and the thrown value is either an `AbortError` or contains the exact
4947
+ `request.signal.reason` by identity at the top level or within at most eight
4948
+ standard `cause` links. Cause traversal is cycle-safe. No message or error-code
4949
+ matching is used; active requests and unrelated/deeper causes remain application
4950
+ failures. The access completion is always `499/info`, without application error
4951
+ fields or project `onError`.
4952
+
4953
+ Structured request sinks are default-preserving: existing sinks receive no row
4954
+ for this outcome. Opt in when cancellation frequency belongs in the durable
4955
+ stream:
4956
+
4957
+ ```ts
4958
+ const observability = createObservability({
4959
+ request: {
4960
+ includeCancelled: true,
4961
+ write: (event) => {
4962
+ if (event.outcome === 'cancelled') return recordClientClose(event)
4963
+ return recordApplicationRequest(event)
4964
+ },
4965
+ },
4966
+ })
4967
+ ```
4968
+
4969
+ An opted-in row has `outcome: 'cancelled'`, `ok: false`, `statusCode: 499`, the
4970
+ ordinary identity/trace/timing fields and no `errorCode`, `errorMessage` or
4971
+ `errorDetail`. `ok` remains the legacy success bit: branch on `outcome` first.
4972
+ Cancellation rows use the same filter, capacity, diagnostics, ordering,
4973
+ `flush()` and `close()` machinery as every request event. MCP protocol
4974
+ cancellation remains represented by `event.mcp.outcome`; Agent and CLI have no
4975
+ generic client-disconnect signal and are not inferred from error text.
4976
+
4812
4977
  The fire-and-forget work has an explicit bounded lifecycle:
4813
4978
 
4814
4979
  ```ts
@@ -4889,7 +5054,8 @@ queryable across all three:
4889
5054
  | `httpMethod` | the contract verb on **tool** events (their `method` is `TOOL`) — filter reads vs writes across both surfaces with `(event.httpMethod ?? event.method) !== 'GET'` |
4890
5055
  | `dimensions` | app-defined domain dimensions (tenant / project / entity id) — see [request context](#request-context) |
4891
5056
  | `traceId` / `spanId` / `parentSpanId` | [W3C trace context](#trace-context) |
4892
- | `ok` / `statusCode` | outcome real HTTP status, or `200`/`400` for a tool |
5057
+ | `outcome` | optional `'cancelled'` on explicitly enabled HTTP client-close rows; ordinary rows omit it |
5058
+ | `ok` / `statusCode` | legacy success bit plus real HTTP status, or `200`/`400` for a tool; an opted-in cancellation is `false` / `499` |
4893
5059
  | `durationMs` / `startedAt` | timing |
4894
5060
  | `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
4895
5061
  | `payload` | sanitised tool arguments; HTTP is `null` unless request `includePayload` is enabled |
@@ -5139,6 +5305,10 @@ success and error alike — carrying the tool name, the arguments, the result, t
5139
5305
  duration, the call context, the endpoint identity, and (only when the call failed
5140
5306
  by throwing) the raw thrown value.
5141
5307
 
5308
+ A confirmed HTTP client disconnect is the exception to the raw-hook table: it
5309
+ is transport cancellation, so neither `afterHandle` nor project `onError` runs.
5310
+ Use the `499/info` access completion or opt-in request cancellation event above.
5311
+
5142
5312
  ```ts
5143
5313
  createMcpHandler({
5144
5314
  serverInfo, auth, services,
@@ -5332,35 +5502,98 @@ exercise the same framework pipeline.
5332
5502
 
5333
5503
  ### Transport conformance
5334
5504
 
5335
- `buildSurfaceManifest` snapshots actual HTTP topology, MCP/Agent/CLI names,
5336
- CLI-only commands, extensions and canonical schema digests. Compare real runner
5337
- discovery with `assertSurfaceDiscovery`, then use `runSurfaceProbes` only for
5338
- the transports you explicitly provide:
5505
+ `buildSurfaceManifest` v2 snapshots canonical operations separately from actual
5506
+ HTTP topology and mounted MCP/Agent/CLI projections. Named MCP surfaces keep
5507
+ their own advertised input digests (including `extend`, flattening and schema
5508
+ policy), and named realtime contracts keep directional argument/ack input and
5509
+ output digests. Compare only discovery you actually observed, then run explicit
5510
+ drivers for the transports you provide:
5339
5511
 
5340
5512
  ```ts
5513
+ import { bindRealtimeClient } from 'stitchkit'
5341
5514
  import {
5342
5515
  assertSurfaceDiscovery,
5343
5516
  buildSurfaceManifest,
5517
+ createRealtimeProbeDriver,
5518
+ defineRealtimeProbe,
5344
5519
  runSurfaceProbes,
5345
5520
  } from 'stitchkit/testing'
5346
5521
 
5347
- const manifest = buildSurfaceManifest({ services, runtimeTools, cliCommands })
5522
+ const manifest = buildSurfaceManifest({
5523
+ groups: [{ pathPrefix: '/api', services: httpServices }],
5524
+ mcpPreparation: { extend, schemaValidation, multiRound },
5525
+ mcpSurfaces: {
5526
+ member: { services: memberServices, runtimeTools },
5527
+ admin: { services: adminServices, runtimeTools },
5528
+ },
5529
+ toolSurfaces: {
5530
+ AGENT: { services: agentServices, runtimeTools },
5531
+ CLI: { services: cliServices, runtimeTools: cliRuntimeTools },
5532
+ },
5533
+ realtime: { primary: realtimeContract },
5534
+ cliCommands,
5535
+ })
5348
5536
  assertSurfaceDiscovery(manifest, {
5349
5537
  openApi,
5350
- MCP: (await mcpClient.listTools()).tools.map((tool) => tool.name),
5538
+ toolSurfaces: [{
5539
+ transport: 'MCP',
5540
+ surface: 'member',
5541
+ names: (await mcpClient.listTools()).tools.map((tool) => tool.name),
5542
+ }],
5351
5543
  AGENT: Object.keys(agentTools),
5352
5544
  CLI: cliHelpNames,
5545
+ realtime: {
5546
+ primary: { serverToClient: observedServerEvents, clientToServer: observedClientEvents },
5547
+ },
5548
+ })
5549
+
5550
+ const invalidInbound = defineRealtimeProbe({
5551
+ name: 'invalid inbound payload',
5552
+ scenario: 'invalid_arguments',
5553
+ fixture: invalidPayloadFixture,
5554
+ expected: {
5555
+ outcome: 'realtime_rejected',
5556
+ code: 'REALTIME_CONTRACT_VIOLATION',
5557
+ rejection: {
5558
+ direction: 'server-inbound', phase: 'arguments',
5559
+ reason: 'invalid-arguments', fault: 'peer',
5560
+ },
5561
+ handlerCalls: 0,
5562
+ },
5563
+ })
5564
+
5565
+ const realtimeDriver = createRealtimeProbeDriver({
5566
+ bind: (onRejected, fixture) => {
5567
+ const client = bindRealtimeClient(realtimeContract, existingTransport, { onRejected })
5568
+ const scenario = bindApplicationRealtimeScenario(client, fixture)
5569
+ return {
5570
+ connected: () => client.connected,
5571
+ invoke: scenario.invoke,
5572
+ dispose: scenario.dispose, // subscriptions only; never disconnect the transport
5573
+ }
5574
+ },
5575
+ handlerCalls: () => applicationRealtimeHandlerCalls,
5353
5576
  })
5354
5577
 
5355
5578
  await runSurfaceProbes({
5356
- probes,
5357
- drivers: { HTTP: httpDriver, MCP: mcpDriver, AGENT: agentDriver, CLI: cliDriver },
5579
+ probes: [invalidInbound],
5580
+ drivers: { REALTIME: realtimeDriver },
5358
5581
  })
5359
5582
  ```
5360
5583
 
5361
- The kit never starts a server or invents credentials. Each probe declares its
5362
- expected normalized outcome and has bounded timeout, setup/teardown and an
5363
- AbortSignal. A missing driver is unsupported, not silently marked conformant.
5584
+ MCP preparation is global because the real MCP mount has one preparation
5585
+ policy; named surfaces select tools, while `extend.filter` decides which
5586
+ selected operations receive extra fields. CLI selection is deliberately plain,
5587
+ and Agent owns its own reachable presentation shaping.
5588
+
5589
+ The kit never starts a server, discovers Socket.IO topology remotely, invents
5590
+ credentials or synthesises invalid Zod values. Fixtures and observations come
5591
+ from the application. The realtime driver creates a rejection channel per
5592
+ scenario, observes connection state before invocation, and disposes only
5593
+ probe-owned subscriptions. Each scenario has one absolute deadline shared by
5594
+ signalled setup, invocation and teardown. An outer timeout stops waiting; it
5595
+ does not disconnect a foreign transport or retract an already emitted packet.
5596
+ A missing driver is unsupported, not silently marked conformant.
5364
5597
 
5365
5598
  ### Test handlers with raw Requests
5366
5599
 
@@ -5921,7 +6154,95 @@ current one *up to* your target, and apply each snippet.
5921
6154
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
5922
6155
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
5923
6156
 
5924
- ## Unreleased migration: peer-free `implementRemote`
6157
+ ## Released migration: 0.56.0
6158
+
6159
+ ### Surface manifests are version 2
6160
+
6161
+ `operation.tools` could not describe a role-selected MCP surface, a different
6162
+ Agent or CLI selection, or an advertised `extend` schema, so projections moved
6163
+ out of the canonical operation:
6164
+
6165
+ ```ts
6166
+ // before
6167
+ manifest.operations[0].tools.MCP
6168
+ // after
6169
+ manifest.toolSurfaces.find((s) => s.transport === 'MCP' && s.surface === null)?.tools
6170
+
6171
+ // before
6172
+ buildSurfaceManifest({ mcpSurfaces: { admin: { services, extend } } })
6173
+ // after
6174
+ buildSurfaceManifest({ mcpSurfaces: { admin: { services } }, mcpPreparation: { extend } })
6175
+ ```
6176
+
6177
+ A committed snapshot is regenerated once, deliberately: `manifestVersion` is
6178
+ `2`, and `ConformanceTransport` gained `REALTIME`, so any exhaustive
6179
+ `Record<ConformanceTransport, …>` must handle it.
6180
+
6181
+ ### `FILE_*` codes joined the error registry
6182
+
6183
+ `StitchErrorCode` gained `FILE_INVALID_PATH`, `FILE_OUTSIDE_ROOT`,
6184
+ `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`, `FILE_INSPECTION_REJECTED`,
6185
+ `FILE_TOO_LARGE` and `FILE_EXISTS`. Only exhaustive maps break:
6186
+
6187
+ ```ts
6188
+ // before — compiled while the registry had no file codes
6189
+ const copy: Record<StitchErrorCode, string> = { …, RATE_LIMITED: '…' }
6190
+ // after — add the seven managed-file codes (or use Partial<Record<…>>)
6191
+ ```
6192
+
6193
+ Unexpected IO stays scrubbed as `INTERNAL_SERVER_ERROR`: the new codes are the
6194
+ caller-safe ones only.
6195
+
6196
+ ### `ScopedAuthHook` is nominal
6197
+
6198
+ A hand-written function shaped like an auth hook is no longer assignable —
6199
+ identity now comes from the factory, so scope ownership and the inferred
6200
+ context cannot drift apart:
6201
+
6202
+ ```ts
6203
+ // before — a structural stand-in
6204
+ const auth: ScopedAuthHook<Scopes> = async (ctx, endpoint) => { … }
6205
+ // after — create it, then compose domains
6206
+ const auth = createAuthHook({ resolve, rules })
6207
+ const composed = composeAuthHooks({ hooks: [auth, billingAuth], defaultScope: 'public' })
6208
+ ```
6209
+
6210
+ ### Managed-file inspectors also run on reads
6211
+
6212
+ An inspector is no longer write-only, and it has a finite default deadline
6213
+ (15 s). Make it read-aware and idempotent — a read carries no
6214
+ `declaredMediaType`:
6215
+
6216
+ ```ts
6217
+ // before
6218
+ inspect: ({ declaredMediaType }) => inspectDeclaredType(declaredMediaType!)
6219
+ // after
6220
+ inspect: ({ prefix, declaredMediaType, signal }) =>
6221
+ inspectBytes(prefix, { declaredMediaType, signal })
6222
+ ```
6223
+
6224
+ Set `inspectionTimeoutMs` explicitly when 15 seconds is the wrong budget.
6225
+
6226
+ ### Direct async-operation binding needs a wire-stable ID
6227
+
6228
+ A direct binding reuses the start output as the follow-up wire input, so the ID
6229
+ schema must parse to itself (`z.input` equals `z.output`, no transform,
6230
+ coercion, default or overwrite). Anything else is parsed twice:
6231
+
6232
+ ```ts
6233
+ // before — a transform silently ran on start and again on every follow-up
6234
+ defineAsyncOperationContract({ binding: 'direct', id: z.string().transform(Number) })
6235
+ // after — keep the wire shape, adapt explicitly
6236
+ defineAsyncOperationContract({
6237
+ binding: 'adapted',
6238
+ id,
6239
+ adapters: { idFromStart, inputFor },
6240
+ })
6241
+ ```
6242
+
6243
+ ## Released migration: 0.55.0
6244
+
6245
+ ### Peer-free `implementRemote`
5925
6246
 
5926
6247
  `implementRemote` now has one canonical, optional-peer-free owner. This keeps
5927
6248
  MCP SDK and AI SDK modules out of CLI bundles that only proxy HTTP calls:
@@ -5933,7 +6254,7 @@ import { implementRemote } from 'stitchkit/tools'
5933
6254
  import { implementRemote } from 'stitchkit/remote'
5934
6255
  ```
5935
6256
 
5936
- ## Unreleased migration: managed file boundary and strict auth returns
6257
+ ### Managed file boundary and strict auth returns
5937
6258
 
5938
6259
  Create one boundary during application bootstrap and pass the capability, never
5939
6260
  a per-call directory or host path:
@@ -6927,6 +7248,7 @@ The browser-and-server entrypoint. Re-exports everything from
6927
7248
  | `createSocketIOClient` | function | low-level typed Socket.IO transport primitive — [guide](../guide/realtime.md#low-level-transport) |
6928
7249
  | `defineRealtimeContract` | function | Zod-first shared Socket.IO event contract — [guide](../guide/realtime.md#zod-first-event-contract) |
6929
7250
  | `createRealtimeClient` | function | inferred, runtime-validated Socket.IO client — [guide](../guide/realtime.md#client--createrealtimeclient) |
7251
+ | `bindRealtimeClient` | function | bind contract validation and typed acknowledgements to an existing Stitchkit client transport without owning its lifecycle |
6930
7252
  | `createRetainedTopics` | function | retained last-value store for sticky events — [guide](../guide/realtime.md#sticky-events) |
6931
7253
  | `parseSSE` | function | parse an SSE `Response` into an async generator — [guide](../guide/client.md#sse) |
6932
7254
  | `SocketIOClient` | _type_ | low-level client handle; `emit` reports disconnected drops and `emitWithAck` exposes the native Promise primitive used by validated `request()` |
@@ -6934,6 +7256,9 @@ The browser-and-server entrypoint. Re-exports everything from
6934
7256
  | `SocketEventMap` | _type_ | the shape of an event map |
6935
7257
  | `RealtimeClient` | _type_ | validated client inferred from a realtime contract |
6936
7258
  | `RealtimeClientOptions` | _type_ | transport options and the rejected-event hook for `createRealtimeClient` |
7259
+ | `BoundRealtimeClient` | _type_ | validated non-owning `on`/`emit`/`request` client with no `connect`/`disconnect` |
7260
+ | `RealtimeClientTransport` | _type_ | minimal existing transport capability accepted by `bindRealtimeClient` |
7261
+ | `BindRealtimeClientOptions` | _type_ | rejection/logger options for a bound existing transport |
6937
7262
  | `RealtimeAcknowledgedEvent` | _type_ | event-name union restricted to definitions with an `ack` schema |
6938
7263
  | `RealtimeAcknowledgement` | _type_ | validated acknowledgement output inferred from an event definition |
6939
7264
  | `RealtimeRequestArguments` | _type_ | request arguments inferred from an acknowledged event tuple |
@@ -7140,6 +7465,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
7140
7465
  | Export | Kind | Summary |
7141
7466
  |--------|------|---------|
7142
7467
  | `createAuthHook` | function | one scope gate for HTTP `authorize` and tool `beforeHandle` — [guide](../guide/auth-and-errors.md#createauthhook) |
7468
+ | `composeAuthHooks` | function | route multiple canonical auth domains by owned scope and atomically commit their typed contributions |
7143
7469
  | `createErrorHook` | function | an async-capable, endpoint-aware `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
7144
7470
  | `ErrorHookConfig` | _type_ | async observer/renderer config for `createErrorHook` |
7145
7471
  | `ResolvedError` | _type_ | the normalised error handed to `createErrorHook`'s `render` |
@@ -7159,6 +7485,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
7159
7485
  | `RuleScopes` | _type_ | scope→context map derived from a `rules` object; `'public'` fields become optional |
7160
7486
  | `ScopedAuthHook` | _type_ | an auth hook carrying its derived scope map at the type level |
7161
7487
  | `AuthScopes` | _type_ | recover the derived map — `createScopedImplement<AuthScopes<typeof hook>>()` |
7488
+ | `ComposeAuthHooksConfig` | _type_ | ordered canonical hooks plus the optional explicit composite default scope |
7489
+ | `ComposedAuthScopes` | _type_ | scope→context map derived from every owner in a composed auth hook |
7162
7490
  | `BearerResolverConfig` | _type_ | config for `createBearerResolver` |
7163
7491
  | `JwtPayload` | _type_ | a decoded JWT payload |
7164
7492
  | `SignJwtOptions` | _type_ | options for `signJwt` (expiry, claims) |
@@ -7265,7 +7593,7 @@ audit event. See the [Observability guide](../guide/observability.md).
7265
7593
  | Export | Kind | Summary |
7266
7594
  |--------|------|---------|
7267
7595
  | `createObservability` | function | configure framework-owned request completion and canonical tool event sinks — [guide](../guide/observability.md#createobservability) |
7268
- | `RequestEvent` | _type_ | the normalised audit event handed to the sink |
7596
+ | `RequestEvent` | _type_ | the normalised audit event handed to the sink; opt-in HTTP cancellation rows carry `outcome: 'cancelled'` |
7269
7597
  | `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
7270
7598
  | `Observability` | _type_ | `{ request?, toolCall, getStatus(), flush(), close() }` with bounded sink lifecycle |
7271
7599
  | `ObservabilitySinkStatus` | _type_ | immutable counters for one bounded request/tool sink |
@@ -7273,11 +7601,11 @@ audit event. See the [Observability guide](../guide/observability.md).
7273
7601
  | `ObservabilityDrainReport` | _type_ | final closed/drained snapshot plus duration |
7274
7602
  | `ObservabilitySinkStatusSchema` / `ObservabilityStatusSchema` / `ObservabilityDrainReportSchema` | schema | runtime schemas for status/report integration boundaries |
7275
7603
  | `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
7276
- | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture |
7604
+ | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture and default-off `includeCancelled` rows |
7277
7605
  | `SinkDropReason` | _type_ | `'capacity' \| 'closed'` |
7278
7606
  | `SinkError` | _type_ | isolated sink/projection failure and optional event |
7279
7607
  | `SinkDrop` | _type_ | rejected event, reason and current pending count |
7280
- | `HttpRequestCompletion` | _type_ | the single framework-owned HTTP outcome projected to logging and request events |
7608
+ | `HttpRequestCompletion` | _type_ | the single framework-owned HTTP outcome, including optional neutral cancellation, projected to logging and request events |
7281
7609
  | `HttpRequestObserver` | _type_ | server-facing projection consumed by `HandlerConfig.observability` |
7282
7610
 
7283
7611
  ### Request context
@@ -7477,6 +7805,7 @@ runtime-tool runner, plus deliberate raw MCP adapters over the same mechanics.
7477
7805
  | `ManagedWaitRender` | _type_ | optional managed wait terminal text and failure classification |
7478
7806
  | `UploadToolInputSchema` | constant | fixed `{ path: string }` input schema for `defineUploadTool` |
7479
7807
  | `defineAsyncOperation` | function | runtime-only start/status/wait plus configured cancel/result/artifacts definitions |
7808
+ | `defineAsyncOperationContract` | function | define one canonical Zod-first HTTP contract for start/status/wait plus optional capabilities |
7480
7809
  | `bindContractAsyncOperation` | function | bind literal methods from an existing contract without creating another HTTP surface |
7481
7810
  | `createAsyncOperationSnapshotSchema` | function | canonical pending/running/succeeded/failed/cancelled Zod snapshot |
7482
7811
  | `AsyncOperationCancelResultSchema` | constant | validated accepted/already_terminal/rejected cancellation result |
@@ -7488,9 +7817,18 @@ runtime-tool runner, plus deliberate raw MCP adapters over the same mechanics.
7488
7817
  | `AsyncOperationOutputCapability` | _type_ | optional result/artifact schema plus handler |
7489
7818
  | `RuntimeAsyncOperationConfig` | _type_ | runtime-only descriptor configuration |
7490
7819
  | `RuntimeAsyncOperation` | _type_ | inferred generated definitions and schemas |
7820
+ | `AsyncOperationContractConfig` | _type_ | canonical contract config where start returns the operation id |
7821
+ | `AsyncOperationContractWithStartOutputConfig` | _type_ | canonical contract config with an application start envelope and typed id extractor |
7822
+ | `DefinedAsyncOperationContract` | _type_ | generated contract, capability keys, schemas and parsed adapters |
7491
7823
  | `ContractAsyncOperationConfig` | _type_ | literal contract method binding and handlers |
7824
+ | `AdaptedContractAsyncOperationConfig` | _type_ | existing-contract binding with explicit id and per-capability input adapters |
7825
+ | `BoundAdaptedContractAsyncOperation` | _type_ | inferred adapted binding with parsed id/input projectors |
7826
+ | `AdaptedContractAsyncOperationStartKey` | _type_ | existing-contract keys with a declared start output |
7827
+ | `AdaptedContractAsyncOperationFollowKey` | _type_ | existing-contract keys with both input and output schemas |
7828
+ | `AdaptedContractAsyncOperationWaitKey` | _type_ | adapted follow-up keys whose output matches the selected status snapshot |
7829
+ | `ContractAsyncOperationInputAdapters` | _type_ | guaranteed direct-binding status/wait input adapters plus configured optional capabilities |
7492
7830
  | `ContractAsyncOperationKeys` | _type_ | literal method-key union of a bound contract |
7493
- | `ContractAsyncOperationStartKey` | _type_ | contract keys with a declared output schema, valid for `start` |
7831
+ | `ContractAsyncOperationStartKey` | _type_ | direct-binding start keys whose declared ID schema has stable input/output |
7494
7832
  | `ContractAsyncOperationFollowKey` | _type_ | contract keys whose input schema type matches the selected start output |
7495
7833
  | `ContractAsyncOperationWaitKey` | _type_ | follow-up keys whose output schema type also matches the selected status output |
7496
7834
  | `composeToolLifecycle` | function | ordered composition of tool before/after phases |
@@ -7573,21 +7911,42 @@ handler pipeline without opening a TCP port.
7573
7911
  | `HandlerTestClientConfig` | _type_ | handler, contract, path prefix, scoped config and client request defaults |
7574
7912
  | `HandlerTestClientsConfig` | _type_ | batch helper configuration |
7575
7913
  | `HandlerTestTransportConfig` | _type_ | shared in-process handler, origin, prefix, client defaults and optional server handle |
7576
- | `buildSurfaceManifest` | function | deterministic actual HTTP/tool/CLI/extension topology with versioned schema digests |
7914
+ | `buildSurfaceManifest` | function | deterministic manifest v2 with HTTP topology, transport-specific tool projections, named MCP surfaces and realtime schema digests |
7577
7915
  | `assertSurfaceManifestSnapshot` | function | bounded deterministic manifest drift assertion |
7578
- | `assertSurfaceDiscovery` | function | compare real OpenAPI/MCP/Agent/CLI discovery to a manifest |
7579
- | `runSurfaceProbes` | function | execute explicit transport drivers with per-probe setup, teardown, timeout and cancellation |
7580
- | `TransportObservationSchema` | constant | normalized success/validation/domain-error/aborted probe outcome |
7916
+ | `assertSurfaceDiscovery` | function | compare real OpenAPI/tool/realtime caller-observed discovery to the exact manifest projection |
7917
+ | `runSurfaceProbes` | function | execute explicit transport drivers under one per-scenario setup/invoke/teardown deadline |
7918
+ | `defineRealtimeProbe` | function | declare one canonical realtime scenario for a caller-supplied real transport driver |
7919
+ | `createRealtimeProbeDriver` | function | bind each scenario to a caller-owned transport and normalize actual event/ack/rejection/disconnect outcomes without lifecycle ownership |
7920
+ | `TransportObservationSchema` | constant | normalized HTTP/tool/realtime outcome including rejection fields and handler-call evidence |
7921
+ | `RealtimeRejectionObservationSchema` | constant | direction/phase/reason/fault observation for a rejected realtime event |
7922
+ | `RealtimeDisconnectObservationSchema` | constant | observed before-invoke versus in-flight disconnect phase |
7581
7923
  | `ConformanceTransport` | _type_ | explicit probe transport union |
7582
7924
  | `RunSurfaceProbesConfig` | _type_ | probes, drivers and diagnostic byte cap |
7583
7925
  | `SurfaceDiscoveryObservation` | _type_ | real OpenAPI/tool/CLI/extension discovery values |
7926
+ | `SurfaceToolDiscoveryObservation` | _type_ | transport, optional named surface and caller-observed tool names |
7927
+ | `SurfaceRealtimeDiscoveryObservation` | _type_ | caller-observed server→client and client→server event names |
7584
7928
  | `SurfaceProbe` / `SurfaceProbeDriver` | _type_ | one bounded scenario and its consumer-supplied runner |
7929
+ | `DefineRealtimeProbeConfig` | _type_ | name, canonical scenario, explicit fixture and expected realtime outcome |
7930
+ | `CreateRealtimeProbeDriverConfig` | _type_ | per-scenario foreign-transport binder and optional handler-call counter |
7931
+ | `RealtimeProbeAdapter` | _type_ | connected-state observation, scenario invocation and subscription-only cleanup |
7932
+ | `RealtimeProbeFixture` / `RealtimeProbeScenario` | _type_ | driver input and supported event/ack/invalid/disconnect/timeout scenario vocabulary |
7933
+ | `RealtimeRejectionObservation` | _type_ | parsed structured realtime rejection observation |
7934
+ | `RealtimeDisconnectObservation` | _type_ | normalized physical timing of a realtime disconnect |
7585
7935
  | `TransportObservation` | _type_ | validated normalized driver result |
7586
7936
  | `SurfaceManifest` / `SurfaceManifestConfig` | _type_ | deterministic surface snapshot and its inputs |
7587
7937
  | `SurfaceManifestOperation` / `SurfaceManifestOperationSchema` | _type_ / schema | one contract or runtime operation row |
7938
+ | `SurfaceManifestTool` / `SurfaceManifestToolSchema` | _type_ / schema | one mounted tool row with advertised input digest |
7939
+ | `SurfaceManifestToolSurface` / `SurfaceManifestToolSurfaceSchema` | _type_ / schema | one static transport projection, optionally keyed for a finite MCP surface |
7940
+ | `SurfaceManifestRealtimeEvent` / `SurfaceManifestRealtimeEventSchema` | _type_ / schema | one named directional realtime event with input/output args and ack digests |
7588
7941
  | `SurfaceManifestExtension` / `SurfaceManifestExtensionSchema` | _type_ / schema | declared transport extension row |
7589
7942
  | `SurfaceManifestSchema` | schema | complete deterministic manifest schema |
7590
- | `SurfaceRuntimeToolDefinition` | _type_ | peer-free structural runtime-operation subset accepted by manifests |
7943
+ | `SurfaceToolDefinition` | _type_ | plain service/runtime selection used by CLI and finite named MCP surfaces |
7944
+ | `SurfaceAgentProjection` | _type_ | Agent selection plus its reachable extend/flatten presentation policy |
7945
+ | `SurfaceMcpPreparation` | _type_ | one global MCP extend/flatten/schema-validation/multi-round preparation policy |
7946
+ | `SurfaceRuntimeToolDefinition` | _type_ | peer-free non-executable runtime-operation descriptor for manifest projection |
7947
+ | `SurfaceToolExtension` | _type_ | canonical structural tool extension including schema, resolver and optional filter |
7948
+ | `McpSchemaValidationConfig` / `IncompatibleSchemaPolicy` | _type_ | canonical MCP schema-preparation policy used by real mounts and manifests |
7949
+ | `SurfaceRealtimeSchemaPairSchema` | schema | input/output digest pair for args or acknowledgements |
7591
7950
  | `SurfaceSchemaDigestsSchema` | schema | params/input/output/multipart digest object |
7592
7951
  | `serializeSurfaceValue` | function | canonical versioned serialization used for deterministic digests |
7593
7952
 
@@ -7600,16 +7959,16 @@ available from `stitchkit/contract`.
7600
7959
 
7601
7960
  | Export | Kind | Summary |
7602
7961
  |--------|------|---------|
7603
- | `createManagedFileBoundary` | function | bind one existing application-owned root for capped reads and atomic writes |
7962
+ | `createManagedFileBoundary` | function | bind an application-owned root, optionally creating one final directory under a trusted existing parent |
7604
7963
  | `ManagedFileBoundary` | _type_ | non-reopenable `read`/`write` capability over canonical relative paths |
7605
- | `ManagedFileBoundaryConfig` | _type_ | bound root, finite limits, inspector and cleanup observer |
7964
+ | `ManagedFileBoundaryConfig` | _type_ | bound root, optional `createRoot`, finite read/write/inspection limits, inspector and cleanup observer |
7606
7965
  | `ManagedFileRefSchema` / `ManagedFileRef` | schema / _type_ | transport-safe relative path, measured size and optional media metadata |
7607
7966
  | `ManagedFilePathSchema` / `ManagedFilePath` | schema / _type_ | canonical POSIX relative managed-file path |
7608
7967
  | `ManagedFileSource` | _type_ | bounded immutable bytes plus validated ref passed to upload callbacks |
7609
7968
  | `ManagedFileReadOptions` / `ManagedFileWriteOptions` | _type_ | per-operation byte cap, signal and atomic write policy |
7610
- | `ManagedFileError` / `ManagedFileErrorCode` | class / _type_ | stable caller-safe boundary failures |
7611
- | `ManagedFileInspector` | _type_ | bounded-prefix content inspection callback that cannot own path or size |
7612
- | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector input and normalized metadata-only result |
7969
+ | `ManagedFileError` / `ManagedFileErrorCode` | class / _type_ | stable boundary failures; registered `FILE_*` mistakes are caller-safe while `FILE_IO_ERROR` remains internal |
7970
+ | `ManagedFileInspector` | _type_ | bounded-prefix read/write inspection callback with a finite cancellation signal that cannot own path or size |
7971
+ | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector prefix/name/declared media/signal input and validated metadata-only result |
7613
7972
 
7614
7973
  ---
7615
7974