stitchkit 0.70.0 → 0.70.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.
package/llms-full.txt CHANGED
@@ -953,21 +953,26 @@ const server = createServer({ services, socket })
953
953
 
954
954
  const result = await server.shutdown({
955
955
  gracePeriodMs: 30_000,
956
+ realtimeCloseTimeoutMs: 1_000,
956
957
  forceTimeoutMs: 5_000,
957
958
  retryAfterSeconds: 5,
958
959
  signal: shutdownController.signal,
959
960
  })
960
961
  ```
961
962
 
962
- The first call closes HTTP and Socket.IO admission, then gives the complete
963
- graceful request/realtime/runtime chain one `gracePeriodMs` budget. If that
964
- budget or the external signal forces destructive teardown, `forceTimeoutMs`
965
- bounds physical completion separately. Repeated calls return the same Promise;
963
+ The first call closes HTTP and Socket.IO admission, then gives admitted HTTP/application work the
964
+ full `gracePeriodMs` budget. Once that work drains, `realtimeCloseTimeoutMs` (default `1_000`)
965
+ bounds WebSocket close handshakes inside the same outer deadline. Any upgraded sockets still open
966
+ at that boundary are terminated without shortening HTTP grace, and graceful runtime shutdown
967
+ continues. If the outer grace budget or external signal forces destructive teardown,
968
+ `forceTimeoutMs` bounds physical completion separately. Repeated calls return the same Promise;
966
969
  the first options win. New
967
970
  ordinary HTTP work receives `503`, `Retry-After` and `Connection: close` outside
968
971
  `wrapFetch`. `result.outcome` is `clean` or `forced`; a forced result preserves
969
972
  the pending snapshot and reason while final pending counters describe the
970
- post-close transport state. A graceful phase error still runs forced cleanup and
973
+ post-close transport state. `forcedWebSockets` counts both sockets terminated at the dedicated
974
+ realtime bound and sockets terminated by outer force; `pendingWebSocketsAtForce` is only the latter
975
+ snapshot, so a clean result can truthfully report a bounded realtime termination. A graceful phase error still runs forced cleanup and
971
976
  then rejects with the original error; a forced transport that cannot confirm
972
977
  completion before `forceTimeoutMs` rejects instead of reporting a false zero.
973
978
  `runtime` is a diagnostics escape hatch, not a second canonical stop path.
@@ -4069,7 +4074,9 @@ required host authorization callback. File paths are relative, bounded and conta
4069
4074
  descriptor-relative resolution: each ancestor is opened without following symlinks and remains
4070
4075
  pinned through authorization and the filesystem effect. Reads revalidate the pinned file identity;
4071
4076
  writes and patches revalidate the pinned parent identity; search and resource discovery descend
4072
- only through opened directories. Linux uses `/proc/self/fd`; macOS and FreeBSD use `/dev/fd`.
4077
+ only through opened directories. Linux traverses `/proc/self/fd`; macOS uses the package's
4078
+ architecture-specific Node-API `openat` backend. Unsupported platforms, including FreeBSD until a
4079
+ real backend is qualified, fail closed. The package needs no install-time compiler or script.
4073
4080
  Other platforms fail these filesystem operations closed because Node exposes no equivalent
4074
4081
  portable directory-handle-relative API. Writes/edits reject symlink targets, content is strict
4075
4082
  UTF-8 and retained bytes are finite. Shell accepts a finite alias mapped by the host to an absolute executable plus an
@@ -8899,11 +8906,13 @@ current one *up to* your target, and apply each snippet.
8899
8906
  ### Descriptor-backed Agent filesystem containment
8900
8907
 
8901
8908
  Built-in coding file/search tools and `createAgentHarnessFileResources` now require a runtime
8902
- that can address an opened directory descriptor: Linux `/proc/self/fd`, or macOS/FreeBSD
8903
- `/dev/fd`. This is what keeps a mutable parent rename or outside-symlink replacement from changing
8909
+ that can address an opened directory descriptor: Linux `/proc/self/fd`, or the packaged macOS
8910
+ Node-API backend added in 0.70.1. This is what keeps a mutable parent rename or outside-symlink replacement from changing
8904
8911
  the authorized target between validation and the actual effect.
8905
8912
 
8906
- No call-site change is needed on those platforms. On Windows or another platform without that
8913
+ No call-site change is needed on those platforms. The 0.70.0 notes incorrectly inferred macOS and
8914
+ FreeBSD support from `/dev/fd`; macOS is restored by 0.70.1 and FreeBSD remains unsupported. On
8915
+ Windows or another platform without that
8907
8916
  boundary, move the built-in filesystem operations to a supported worker or replace them with
8908
8917
  application-owned tools backed by an equivalent native handle API. They fail closed rather than
8909
8918
  falling back to path spelling. `run_command` remains separately available under its explicit
@@ -11697,9 +11706,9 @@ Also re-exports the error helpers from `stitchkit/contract`.
11697
11706
  | `UnixListenConfig` | _type_ | unix listener — a socket path, or `{ path, mode }` to tighten the file mode after listen |
11698
11707
  | `BunServerHandle` | _type_ | managed Bun handle (`url`, `port`, `runtime`, `status`, `shutdown`) |
11699
11708
  | `ManagedServerHandle` | _type_ | shared lifecycle shape generic over the runtime escape hatch |
11700
- | `ShutdownOptionsSchema` / `ShutdownOptions` | schema / _type_ | one graceful budget, bounded forced-completion timeout, retry hint and optional external abort signal |
11709
+ | `ShutdownOptionsSchema` / `ShutdownOptions` | schema / _type_ | HTTP/application grace, a separate WebSocket close-handshake bound, bounded forced-completion timeout, retry hint and optional external abort signal |
11701
11710
  | `ShutdownStatusSchema` / `ShutdownStatus` | schema / _type_ | live state and request/WebSocket counters |
11702
- | `ShutdownResultSchema` / `ShutdownResult` | schema / _type_ | clean/forced result with final counters and at-force snapshots |
11711
+ | `ShutdownResultSchema` / `ShutdownResult` | schema / _type_ | clean/forced result with final counters, outer-force snapshots and `forcedWebSockets` including bounded realtime terminations |
11703
11712
  | `ShutdownStateSchema` / `ShutdownState` | schema / _type_ | managed lifecycle state machine |
11704
11713
  | `ServiceDef` | _type_ | the result of `implement` |
11705
11714
  | `MethodDef` | _type_ | one resolved endpoint inside a service |
@@ -11887,7 +11896,7 @@ cutovers are covered by the executable
11887
11896
  | `ManagedResourceDependency` | _type_ | a dependency named by id or given as the resource itself — the second form is what `context.use(...)` can type |
11888
11897
  | `ManagedResourcePublished` | _type_ | the value type `context.use(resource)` returns, recovered from that resource's own `start` |
11889
11898
  | `ManagedResourcePublishesNoValue` | _type_ | what `context.use(...)` returns for a resource that publishes nothing — a branded refusal rather than `never`, so reading it does not silently compile |
11890
- | `ManagedServerResourceConfig` | _type_ | the server or a sync/async factory receiving `ManagedResourceContext`, plus stable ID, dependencies and shutdown policy for `managedServerResource` |
11899
+ | `ManagedServerResourceConfig` | _type_ | the server or a sync/async factory receiving `ManagedResourceContext`, plus stable ID, dependencies and server shutdown policy (`retryAfterSeconds`, `realtimeCloseTimeoutMs`) |
11891
11900
  | `ManagedServerResource` | _type_ | the resource `managedServerResource` returns, whose `start` publishes the `ManagedServerHandle` |
11892
11901
  | `ApplicationHealthHandlerOptions` / `ApplicationHealthHandlerOptionsSchema` | _type_ / schema | liveness/readiness selection and sanitized `Retry-After` policy |
11893
11902
  | `ApplicationOperationalHandlers` / `ApplicationOperationalHandlersOptions` / `ApplicationOperationalHandlersOptionsSchema` | _type_ / schema | conventional status/readiness/liveness route surface and shared retry policy |
@@ -12244,7 +12253,7 @@ artifact store is supplied, `read_output`.
12244
12253
 
12245
12254
  | Export | Kind | Summary |
12246
12255
  |--------|------|---------|
12247
- | `createAgentCodingTools` | function | construct direct host-authorized bounded file, search, guarded patch, shell and artifact runtime-tool definitions; filesystem operations require Linux `/proc/self/fd` or macOS/FreeBSD `/dev/fd` descriptor paths and otherwise fail closed |
12256
+ | `createAgentCodingTools` | function | construct direct host-authorized bounded file, search, guarded patch, shell and artifact runtime-tool definitions; filesystem operations use Linux `/proc/self/fd` or the packaged macOS Node-API backend and otherwise fail closed |
12248
12257
  | `AgentCodingToolDefinition` | _type_ | peer-free structural direct-tool shape accepted by the canonical runtime-tool surface |
12249
12258
  | `AgentCodingToolConfig` | _type_ | absolute root, required authorization callback, finite executable alias map, exact child environment and optional limits |
12250
12259
  | `AgentCodingToolAuthorizationSchema` / `AgentCodingToolAuthorization` | schema / _type_ | discriminated read/write/search/patch/shell/artifact decision presented to host policy before effect |
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.70.0",
3
+ "version": "0.70.1",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",
@@ -38,6 +38,7 @@
38
38
  "types": "./dist/index.d.ts",
39
39
  "files": [
40
40
  "dist",
41
+ "native/*.node",
41
42
  "llms.txt",
42
43
  "llms-full.txt",
43
44
  "README.md",
@@ -141,6 +142,7 @@
141
142
  "access": "public"
142
143
  },
143
144
  "scripts": {
145
+ "build:native-contained-files": "node scripts/build-contained-files-native.mjs",
144
146
  "check": "bun x tsc --noEmit",
145
147
  "build:browser": "bun build src/index.ts src/react.ts src/contract/index.ts src/declaration.ts src/agent-runtime-browser.ts --outdir dist --target node --packages external --splitting --root src",
146
148
  "build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/tool-invoker.ts src/cli.ts src/remote.ts src/files.ts src/testing.ts src/observability/index.ts src/agent-runtime.ts src/agent-runtime-harness.ts src/agent-runtime-coding-tools.ts src/agent-runtime-openrouter.ts src/agent-runtime-sqlite-bun.ts src/agent-runtime-sqlite-node.ts src/application.ts src/application-grammy.ts src/application-opentelemetry.ts --outdir dist --target node --packages external --splitting --root src",
@@ -1,202 +0,0 @@
1
- // src/agent-runtime/contained-files.ts
2
- import { constants } from "node:fs";
3
- import { open, readdir, realpath } from "node:fs/promises";
4
- import path from "node:path";
5
- function descriptorPath(handle) {
6
- if (process.platform === "linux")
7
- return `/proc/self/fd/${handle.fd}`;
8
- if (process.platform === "darwin" || process.platform === "freebsd") {
9
- return `/dev/fd/${handle.fd}`;
10
- }
11
- throw new Error("Contained filesystem operations require descriptor paths on this platform");
12
- }
13
- async function openPinnedDirectory(absolute) {
14
- const expected = await realpath(absolute);
15
- const handle = await open(expected, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
16
- try {
17
- if (await realpath(descriptorPath(handle)) !== expected) {
18
- throw new Error("Contained directory identity changed while opening");
19
- }
20
- return handle;
21
- } catch (error) {
22
- await handle.close();
23
- throw error;
24
- }
25
- }
26
- function safeSegments(relative) {
27
- if (path.isAbsolute(relative))
28
- throw new Error("Contained paths must be relative");
29
- const segments = relative.split(/[\\/]/u);
30
- if (segments.length === 0 || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
31
- throw new Error("Contained path has invalid segments");
32
- }
33
- return segments;
34
- }
35
- async function openContainedParent(root, relative) {
36
- const segments = safeSegments(relative);
37
- const basename = segments.pop();
38
- if (!basename)
39
- throw new Error("Contained path is missing a basename");
40
- let current = await openPinnedDirectory(root);
41
- try {
42
- for (const segment of segments) {
43
- const next = await open(path.join(descriptorPath(current), segment), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
44
- const metadata = await next.stat();
45
- if (!metadata.isDirectory()) {
46
- await next.close();
47
- throw new Error("Contained ancestor is not a directory");
48
- }
49
- await current.close();
50
- current = next;
51
- }
52
- return { handle: current, path: descriptorPath(current), basename };
53
- } catch (error) {
54
- await current.close();
55
- throw error;
56
- }
57
- }
58
- async function openContainedFile(root, relative) {
59
- const parent = await openContainedParent(root, relative);
60
- try {
61
- const handle = await open(path.join(parent.path, parent.basename), constants.O_RDONLY | constants.O_NOFOLLOW);
62
- const metadata = await handle.stat();
63
- if (!metadata.isFile()) {
64
- await handle.close();
65
- throw new Error("Contained path is not a regular file");
66
- }
67
- return handle;
68
- } finally {
69
- await parent.handle.close();
70
- }
71
- }
72
- function sameIdentity(left, right) {
73
- return left.dev === right.dev && left.ino === right.ino;
74
- }
75
- async function assertContainedFileCurrent(root, relative, expected) {
76
- const current = await openContainedFile(root, relative);
77
- try {
78
- if (!sameIdentity(await expected.stat(), await current.stat())) {
79
- throw new Error("Contained file identity changed during authorization");
80
- }
81
- } finally {
82
- await current.close();
83
- }
84
- }
85
- async function assertContainedParentCurrent(root, relative, expected) {
86
- const current = await openContainedParent(root, relative);
87
- try {
88
- if (!sameIdentity(await expected.stat(), await current.handle.stat())) {
89
- throw new Error("Contained parent identity changed during authorization");
90
- }
91
- } finally {
92
- await current.handle.close();
93
- }
94
- }
95
- async function readContainedUtf8Handle(handle, maxBytes) {
96
- const metadata = await handle.stat();
97
- if (!metadata.isFile())
98
- throw new Error("Contained path is not a regular file");
99
- if (metadata.size > maxBytes)
100
- throw new Error("Contained file exceeds byte budget");
101
- const buffer = Buffer.alloc(metadata.size);
102
- let offset = 0;
103
- while (offset < buffer.byteLength) {
104
- const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
105
- if (bytesRead === 0)
106
- break;
107
- offset += bytesRead;
108
- }
109
- if (offset !== metadata.size)
110
- throw new Error("Contained file changed while being read");
111
- return {
112
- text: new TextDecoder("utf-8", { fatal: true }).decode(buffer),
113
- bytes: offset,
114
- mode: metadata.mode
115
- };
116
- }
117
- async function walkContainedFiles(input) {
118
- const scan = await scanContainedFiles({ ...input, symlinks: "refuse" });
119
- if (scan.truncated)
120
- throw new Error("Contained file traversal exceeded its bounds");
121
- return scan.files;
122
- }
123
- async function scanContainedFiles(input) {
124
- const files = [];
125
- let truncated = false;
126
- let skippedDirectories = 0;
127
- let skippedSymlinks = 0;
128
- const root = await openPinnedDirectory(input.root);
129
- const visit = async (directory, relativeDirectory, depth) => {
130
- if (truncated)
131
- return;
132
- if (depth > input.maxDepth) {
133
- truncated = true;
134
- return;
135
- }
136
- const entries = await readdir(descriptorPath(directory), { withFileTypes: true });
137
- entries.sort((left, right) => left.name.localeCompare(right.name));
138
- for (const entry of entries) {
139
- const relative = relativeDirectory ? path.join(relativeDirectory, entry.name) : entry.name;
140
- const anchored = path.join(descriptorPath(directory), entry.name);
141
- if (entry.isSymbolicLink()) {
142
- if (input.symlinks === "refuse") {
143
- throw new Error(`Contained file traversal refuses symlink: ${relative}`);
144
- }
145
- skippedSymlinks += 1;
146
- continue;
147
- }
148
- if (entry.isDirectory()) {
149
- if (input.excludeDirectory?.(relative)) {
150
- skippedDirectories += 1;
151
- continue;
152
- }
153
- let child;
154
- try {
155
- child = await open(anchored, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
156
- } catch (error) {
157
- if (input.symlinks === "skip") {
158
- skippedSymlinks += 1;
159
- continue;
160
- }
161
- throw error;
162
- }
163
- try {
164
- await visit(child, relative, depth + 1);
165
- } finally {
166
- await child.close();
167
- }
168
- continue;
169
- }
170
- if (!entry.isFile())
171
- continue;
172
- if (files.length >= input.maxFiles) {
173
- truncated = true;
174
- return;
175
- }
176
- let content;
177
- if (input.readMaxBytes !== undefined) {
178
- try {
179
- const handle = await open(anchored, constants.O_RDONLY | constants.O_NOFOLLOW);
180
- try {
181
- content = await readContainedUtf8Handle(handle, input.readMaxBytes);
182
- } finally {
183
- await handle.close();
184
- }
185
- } catch (error) {
186
- if (input.skipUnreadable)
187
- continue;
188
- throw error;
189
- }
190
- }
191
- files.push({ absolute: relative, relative, ...content && { content } });
192
- }
193
- };
194
- try {
195
- await visit(root, "", 0);
196
- } finally {
197
- await root.close();
198
- }
199
- return { files, truncated, skippedDirectories, skippedSymlinks };
200
- }
201
-
202
- export { openContainedParent, openContainedFile, assertContainedFileCurrent, assertContainedParentCurrent, readContainedUtf8Handle, walkContainedFiles, scanContainedFiles };