stitchkit 0.103.8 → 0.103.9

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 (64) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/agent-runtime/sandbox-process.d.ts +2 -2
  3. package/dist/agent-runtime/sandbox-process.d.ts.map +1 -1
  4. package/dist/durability/contract.d.ts +3 -2
  5. package/dist/durability/contract.d.ts.map +1 -1
  6. package/dist/durability/effect.d.ts.map +1 -1
  7. package/dist/durability/engine.d.ts.map +1 -1
  8. package/dist/durability/json.d.ts.map +1 -1
  9. package/dist/entrypoints/agent-runtime/harness.js +2 -2
  10. package/dist/entrypoints/agent-runtime/sandbox.js +29 -81
  11. package/dist/entrypoints/agent-runtime.js +2 -2
  12. package/dist/entrypoints/cli.js +1 -1
  13. package/dist/entrypoints/files.d.ts +2 -0
  14. package/dist/entrypoints/files.d.ts.map +1 -1
  15. package/dist/entrypoints/files.js +6 -2
  16. package/dist/entrypoints/primitives.js +77 -57
  17. package/dist/entrypoints/process.d.ts +4 -0
  18. package/dist/entrypoints/process.d.ts.map +1 -0
  19. package/dist/entrypoints/process.js +13 -0
  20. package/dist/entrypoints/tools.js +12 -3
  21. package/dist/files/boundary.d.ts +19 -1
  22. package/dist/files/boundary.d.ts.map +1 -1
  23. package/dist/files/file-io.d.ts +5 -1
  24. package/dist/files/file-io.d.ts.map +1 -1
  25. package/dist/files/read.d.ts +13 -0
  26. package/dist/files/read.d.ts.map +1 -0
  27. package/dist/index-12m2v2a9.js +98 -0
  28. package/dist/index-8a9ksm91.js +95 -0
  29. package/dist/{index-90ppdx3q.js → index-9jb61j5a.js} +1 -1
  30. package/dist/{index-5vyd0fkb.js → index-ary5s7k0.js} +40 -61
  31. package/dist/index-c1jmtxa0.js +74 -0
  32. package/dist/index-eeqmg7qm.js +202 -0
  33. package/dist/index-mbnnw3f2.js +25 -0
  34. package/dist/{index-v4a2fjtk.js → index-sybedc8b.js} +124 -55
  35. package/dist/internal/abort-race.d.ts +3 -0
  36. package/dist/internal/abort-race.d.ts.map +1 -0
  37. package/dist/internal/atomic-file.d.ts +20 -2
  38. package/dist/internal/atomic-file.d.ts.map +1 -1
  39. package/dist/internal/atomic-publication.d.ts +19 -0
  40. package/dist/internal/atomic-publication.d.ts.map +1 -0
  41. package/dist/internal/json-data.d.ts +8 -0
  42. package/dist/internal/json-data.d.ts.map +1 -0
  43. package/dist/primitives/canonical-json.d.ts +11 -0
  44. package/dist/primitives/canonical-json.d.ts.map +1 -0
  45. package/dist/primitives/index.d.ts +1 -0
  46. package/dist/primitives/index.d.ts.map +1 -1
  47. package/dist/process/command-owner.d.ts +13 -0
  48. package/dist/process/command-owner.d.ts.map +1 -0
  49. package/dist/process/command.d.ts +3 -0
  50. package/dist/process/command.d.ts.map +1 -0
  51. package/dist/process/contract.d.ts +35 -0
  52. package/dist/process/contract.d.ts.map +1 -0
  53. package/dist/process/group.d.ts +6 -0
  54. package/dist/process/group.d.ts.map +1 -0
  55. package/dist/tools/transfer/managed-file-error.d.ts.map +1 -1
  56. package/llms/files.txt +8 -0
  57. package/llms/primitives.txt +31 -1
  58. package/llms/process.txt +167 -0
  59. package/llms/stitchkit.txt +1 -0
  60. package/llms/tools.txt +18 -0
  61. package/llms-full.txt +216 -1
  62. package/llms.txt +6 -4
  63. package/package.json +5 -1
  64. package/dist/index-aan6hqb9.js +0 -65
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ export declare const NativeCommandOptionsSchema: z.ZodObject<{
3
+ executable: z.ZodString;
4
+ args: z.ZodDefault<z.ZodArray<z.ZodString>>;
5
+ cwd: z.ZodOptional<z.ZodString>;
6
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
7
+ envPolicy: z.ZodDefault<z.ZodEnum<{
8
+ ambient: "ambient";
9
+ "declared-only": "declared-only";
10
+ }>>;
11
+ signal: z.ZodOptional<z.ZodCustom<AbortSignal, AbortSignal>>;
12
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
13
+ maxOutputBytes: z.ZodOptional<z.ZodNumber>;
14
+ capture: z.ZodDefault<z.ZodBoolean>;
15
+ stdin: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>>;
16
+ maxStdinBytes: z.ZodDefault<z.ZodNumber>;
17
+ onOutput: z.ZodOptional<z.ZodCustom<(bytes: Uint8Array, channel: 'stdout' | 'stderr', signal: AbortSignal) => void | Promise<void>, (bytes: Uint8Array, channel: 'stdout' | 'stderr', signal: AbortSignal) => void | Promise<void>>>;
18
+ killGraceMs: z.ZodDefault<z.ZodNumber>;
19
+ cleanupTimeoutMs: z.ZodDefault<z.ZodNumber>;
20
+ }, z.core.$strict>;
21
+ export type NativeCommandOptions = z.input<typeof NativeCommandOptionsSchema>;
22
+ export type ParsedNativeCommandOptions = z.output<typeof NativeCommandOptionsSchema>;
23
+ declare const NativeCommandResultSchema: z.ZodObject<{
24
+ exitCode: z.ZodNullable<z.ZodNumber>;
25
+ signal: z.ZodNullable<z.ZodString>;
26
+ stdout: z.ZodInstanceOf<Uint8Array<ArrayBuffer>>;
27
+ stderr: z.ZodInstanceOf<Uint8Array<ArrayBuffer>>;
28
+ }, z.core.$strip>;
29
+ export type NativeCommandResult = z.infer<typeof NativeCommandResultSchema>;
30
+ export declare class NativeCommandError extends Error {
31
+ readonly code: 'COMMAND_LIMIT' | 'COMMAND_UNAVAILABLE' | 'COMMAND_CLEANUP';
32
+ constructor(code: 'COMMAND_LIMIT' | 'COMMAND_UNAVAILABLE' | 'COMMAND_CLEANUP', message: string, options?: ErrorOptions);
33
+ }
34
+ export {};
35
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/process/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;gDAoBtB,UAAU,WACR,QAAQ,GAAG,QAAQ,UACpB,WAAW,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;kBAgB7B,CAAC;AACL,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,0BAA0B,CAAC,CAAC;AACrF,QAAA,MAAM,yBAAyB;;;;;iBAK7B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,qBAAa,kBAAmB,SAAQ,KAAK;aAEzB,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,iBAAiB;IADnF,YACkB,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,iBAAiB,EACjF,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY,EAIvB;CACF"}
@@ -0,0 +1,6 @@
1
+ import { NativeCommandError } from './contract.js';
2
+ export declare function signalGroup(pid: number | undefined, signal: NodeJS.Signals): void;
3
+ export declare function stopCommandGroup(pid: number | undefined, graceMs: number): Promise<void>;
4
+ export declare function commandCleanupError(cause: unknown): NativeCommandError;
5
+ export declare function waitForCommandClose<T>(closed: Promise<T>, timeoutMs: number): Promise<T>;
6
+ //# sourceMappingURL=group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group.d.ts","sourceRoot":"","sources":["../../src/process/group.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAOjF;AAYD,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAItE;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAExF"}
@@ -1 +1 @@
1
- {"version":3,"file":"managed-file-error.d.ts","sourceRoot":"","sources":["../../../src/tools/transfer/managed-file-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAuB,MAAM,uBAAuB,CAAC;AAgBtE,yFAAyF;AACzF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAkBnE;AAED,kGAAkG;AAClG,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAE/D"}
1
+ {"version":3,"file":"managed-file-error.d.ts","sourceRoot":"","sources":["../../../src/tools/transfer/managed-file-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAuB,MAAM,uBAAuB,CAAC;AAmBtE,yFAAyF;AACzF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAyBnE;AAED,kGAAkG;AAClG,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAE/D"}
package/llms/files.txt CHANGED
@@ -5,6 +5,9 @@
5
5
 
6
6
  Contains: API reference: `stitchkit/files` (docs/api/reference.md)
7
7
 
8
+ Also read:
9
+ - see `llms/process.txt` for the Native IO guide
10
+
8
11
  ==============================================================================
9
12
  # API reference: `stitchkit/files` (docs/api/reference.md)
10
13
  ==============================================================================
@@ -35,3 +38,8 @@ available from `stitchkit/contract`.
35
38
  | `CHUNK_SPOOL_ERROR_CODES` / `isChunkSpoolErrorCode` | const / function | every `ChunkSpoolErrorCode` as a frozen list, and the guard for one — map them onto the app's own error codes, or a spool refusal reaches its client as an unknown 500 |
36
39
 
37
40
  ---
41
+
42
+ | Export | Kind | Purpose |
43
+ |---|---|---|
44
+ | `AtomicFilePublicationError` | class | published=true with cleanup/directory-sync/directory-close phase |
45
+ | `FileObservation` | type | observed descriptor dev/ino/size/nlink/mtimeMs/ctimeMs |
@@ -15,7 +15,7 @@ description: Declare lifecycle, access, exact values, audit, delivery and export
15
15
  type: guide
16
16
  status: active
17
17
  created: 2026-09-01
18
- updated: 2026-09-01
18
+ updated: 2026-10-01 21:03 +07:00
19
19
  ---
20
20
 
21
21
  # Generic application primitives
@@ -108,6 +108,31 @@ of the application's existing async operation. The input, scope, metadata and to
108
108
  change. The application creates bytes and owns operation persistence; the managed-file boundary
109
109
  streams them.
110
110
 
111
+ ## Canonical JSON
112
+
113
+ ```ts
114
+ import { canonicalJson } from 'stitchkit/primitives'
115
+
116
+ const bytes = canonicalJson({ '2': 'two', '10': 'ten', optional: undefined, nested: [1, null] }, {
117
+ maxDepth: 20, maxNodes: 10_000, maxBytes: 64 * 1024,
118
+ })
119
+ // {"10":"ten","2":"two","nested":[1,null]}
120
+ // Native crypto or browser WebCrypto hashes these same UTF-8 bytes.
121
+ ```
122
+
123
+ `canonicalJson` использует единый serializer исторических digest: object keys в UTF-16 code-unit
124
+ order, включая integer-like keys; array order сохраняется. Optional object members с undefined
125
+ опускаются. Top-level/array undefined, sparse arrays, functions/symbols/BigInt, non-finite numbers,
126
+ negative zero, cycles, user prototypes, accessors, non-enumerable/symbol members отказаны до
127
+ serialization. Приложение валидирует свою schema первым; canonical boundary не заменяет её.
128
+
129
+ Defaults: depth 100 (maximum 100), nodes 100 000, UTF-8 bytes 1 MiB. Превышение или не-JSON data —
130
+ TypeError; неверные options — Zod validation error. Это browser-safe leaf без Node/Bun/crypto,
131
+ agent-runtime/testing или optional peers. Native и browser используют один algorithm; hashing
132
+ остаётся platform adapter. Переносите только serializer recursion, сохраняя proof schemas и
133
+ verification boundary. Существующие внутренние historical normalization/digest fixtures не меняются;
134
+ новый public boundary сознательно отказывает неоднозначным values вместо фиктивной совместимости.
135
+
111
136
  ==============================================================================
112
137
  # API reference: `stitchkit/primitives` (docs/api/reference.md)
113
138
  ==============================================================================
@@ -156,3 +181,8 @@ schedules, transports and file generation remain application-owned.
156
181
  | `SourceText` / `SourceRisk` | _types_ | migration scanner input and exact path/line evidence |
157
182
 
158
183
  ---
184
+
185
+ | Export | Kind | Purpose |
186
+ |---|---|---|
187
+ | `canonicalJson` | function | bounded plain JSON, UTF-16 key sort, optional object members omitted |
188
+ | `CanonicalJsonOptions` | type | inferred depth/node/UTF-8 bytes limits |
@@ -0,0 +1,167 @@
1
+ # stitchkit/process — stitchkit docs slice
2
+
3
+ > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
+ > `llms.txt` in this package.
5
+
6
+ Contains: Guide: Native IO (docs/guide/native-io.md) · API reference: `stitchkit/process` (docs/api/reference.md)
7
+
8
+ ==============================================================================
9
+ # Guide: Native IO (docs/guide/native-io.md)
10
+ ==============================================================================
11
+
12
+ ---
13
+ title: Конечные команды и файловые гарантии
14
+ description: Native IO для Bun и Node, с явно выбранной durability, безопасными bounds и caller lifetime.
15
+ type: guide
16
+ status: active
17
+ created: 2026-10-01 20:44 +07:00
18
+ updated: 2026-10-01 21:31 +07:00
19
+ participants:
20
+ - role: authored
21
+ harness: Codex
22
+ model: GPT-6
23
+ at: 2026-10-01 20:44 +07:00
24
+ ---
25
+
26
+ # Native IO
27
+
28
+ Примеры используют публичные imports и одинаково исполняются в Bun и Node ≥22.
29
+
30
+ ## Запись файла
31
+
32
+ ```ts
33
+ import { writeFileAtomic, AtomicFilePublicationError } from 'stitchkit/files'
34
+
35
+ try {
36
+ await writeFileAtomic('/srv/app/receipt.json', JSON.stringify({ id: 'receipt-1' }), {
37
+ replace: false,
38
+ mode: 0o600,
39
+ durability: 'directory',
40
+ })
41
+ } catch (error) {
42
+ if (error instanceof AtomicFilePublicationError) {
43
+ // target уже виден. Проверьте receipt, прежде чем решать о повторе.
44
+ console.error(error.phase, error.cause)
45
+ } else throw error
46
+ }
47
+ await writeFileAtomic('/srv/app/state.json', '{"version":2}', {
48
+ replace: true, durability: 'directory',
49
+ })
50
+ ```
51
+
52
+ По умолчанию `replace: true`, `durability: 'file'`, `mode: 0o600`: existing вызовы сохраняют
53
+ file-fsync и atomic replacement. `durability: 'none'` отключает sync; `'directory'` выполняет
54
+ полную запись → exact descriptor chmod → file fsync → rename (replace) либо link (create) →
55
+ удаление staging link → parent fsync. `writeFileAtomicSync` имеет те же options и порядок.
56
+ Create отказывает с native `EEXIST` при существующем файле, symlink или concurrent winner.
57
+ Никогда не переводите этот отказ в replace автоматически.
58
+
59
+ `AtomicFilePublicationError.published === true` отличает видимость от неподтверждённого
60
+ acknowledgement. `phase` — `cleanup`, `directory-sync` или `directory-close`; первичная причина
61
+ сохранена в `cause`. Ошибка до publish оставляет прежний target. Cleanup вторичен к первичной ошибке.
62
+ При create кратко существует второй hardlink; strict single-link reader вправе отказать в этом окне.
63
+
64
+ Directory fsync — native POSIX/filesystem capability. Windows явно отказывает; иной filesystem
65
+ может отказать при open/sync. Проверка на Linux не подтверждает поведение macOS, сетевой FS или
66
+ сохранность при отключении питания. Состояние диска определяется гарантиями самой FS/ОС.
67
+ `createManagedFileBoundary.write(..., { durable: true })` использует тот же publish owner и parent sync;
68
+ его create по умолчанию остаётся create, а `replace: true` явно разрешает replacement.
69
+
70
+ ## Строгое чтение
71
+
72
+ ```ts
73
+ import { createManagedFileBoundary } from 'stitchkit/files'
74
+
75
+ const files = await createManagedFileBoundary({ root: '/srv/app', maxReadBytes: 64 * 1024 })
76
+ const source = await files.read('receipt.json', {
77
+ rejectSymlinks: true, singleLink: true, stable: true, observe: true,
78
+ })
79
+ console.log(source.observation) // dev, ino, size, nlink, mtimeMs, ctimeMs
80
+ const receipt = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(source.bytes))
81
+ // schema.parse(receipt) и проверка digest принадлежат приложению.
82
+ ```
83
+
84
+ Без новых options ответ сохраняет `{ ref, bytes }`, допускает trusted in-root symlink и hardlink.
85
+ `rejectSymlinks` запрещает requested leaf, `singleLink` проверяет `nlink` открытого descriptor,
86
+ `stable` сравнивает dev/ino/size/mtime/ctime/nlink вокруг byte read; `observe` добавляет metadata.
87
+ `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`, `FILE_UNSAFE_LINK`, `FILE_TOO_LARGE`, `FILE_CHANGED`,
88
+ `FILE_UNSUPPORTED` и `FILE_IO_ERROR` различаются. Abort сохраняет caller reason.
89
+ Nonblocking open позволяет отказать FIFO без ожидания writer. Bytes ограничены во время чтения,
90
+ включая рост файла, не только initial stat. Chunk allocation ≤ maxBytes+1+64 KiB, flatten ≤ maxBytes;
91
+ FD закрывается при success, отказе, abort и IO error.
92
+ Ошибка close сохраняет первичную ошибку чтения; после успешного чтения ошибка close возвращается caller.
93
+
94
+ Root и ancestors принадлежат trusted actor. Эти options не дают descriptor-relative containment
95
+ при hostile concurrent ancestor replacement. Отдельного переключателя «считать ancestors безопасными»
96
+ нет: portable owner не предоставляет такую capability. Metadata stability не доказывает immutable
97
+ snapshot против произвольного hostile writer; используйте application lock/immutable publication.
98
+
99
+ ## Команды
100
+
101
+ ```ts
102
+ import { runNativeCommand } from 'stitchkit/process'
103
+
104
+ const captured = await runNativeCommand({
105
+ executable: '/usr/bin/git', args: ['rev-parse', 'HEAD'], cwd: '/srv/app',
106
+ envPolicy: 'declared-only', env: { PATH: '/usr/bin:/bin' },
107
+ capture: true, maxOutputBytes: 4096, timeoutMs: 5000,
108
+ })
109
+ console.log(captured.exitCode, new TextDecoder().decode(captured.stdout))
110
+
111
+ const lifetime = new AbortController()
112
+ await runNativeCommand({
113
+ executable: '/usr/bin/git', args: ['status'], signal: lifetime.signal,
114
+ onOutput: async (bytes, channel, outputSignal) => {
115
+ outputSignal.throwIfAborted()
116
+ await output.write(bytes, channel, outputSignal) // application-owned bounded sink
117
+ },
118
+ })
119
+ ```
120
+
121
+ Команда требует caller signal или finite `timeoutMs` (1–2147483647 ms: native timer range; большее значение отказывается, не превращается в 1 ms). Streaming имеет caller lifetime, без скрытого
122
+ 60-second timeout или 1-MiB output limit. `capture` по умолчанию false; true требует
123
+ `maxOutputBytes` для суммарных stdout+stderr. Лимит считает исходные bytes; API возвращает
124
+ `Uint8Array`, не преобразует invalid UTF-8. Result содержит наблюдаемый `exitCode: number | null`
125
+ и `signal: string | null`; signal termination не превращается в выдуманный exit code. `maxOutputBytes` может ограничивать и streaming.
126
+ `stdin` — bytes с `maxStdinBytes` (default 1 MiB). `envPolicy` default `declared-only`: отсутствие env
127
+ означает пустой environment; `ambient` явно разрешает наследование. Cwd/executable policy остаётся
128
+ приложению; этот primitive запускает native executable и сам не создаёт shell.
129
+
130
+ Вывод читается с backpressure отдельно для stdout/stderr: один выполняющийся sink на канал плюс
131
+ bounded native Readable buffer; накопления всей истории при streaming нет. Callback получает
132
+ lifetime signal. Abort освобождает ожидание даже игнорирующего signal callback, но не способен
133
+ отменить сторонние действия, которые сам callback продолжит после abort. Sink обязан отменять свои
134
+ IO, не сохранять бесконечную историю и не изменять shared descriptor flags. После abort новых
135
+ callbacks owner не запускает. Normal completion ждёт всех bytes и sinks.
136
+
137
+ Cleanup: POSIX process group, TERM, `killGraceMs` (default 100, maximum 10 000), затем KILL;
138
+ `cleanupTimeoutMs` (default 2000, maximum 30 000) ограничивает ожидание close после сигналов.
139
+ Сохраняется caller/sink причина; failure cleanup — `NativeCommandError` с `COMMAND_CLEANUP` и
140
+ AggregateError cause. Даже вышедший parent не исключает helper, удерживающий pipe. Windows
141
+ отказывает с `COMMAND_UNAVAILABLE`; потомок, самостоятельно покинувший группу через setsid,
142
+ лежит за границей этой capability. Это one-shot execution, не PTY, supervisor или daemon host.
143
+
144
+ ## Механическая адаптация
145
+
146
+ Atomic file wrapper заменяется одним вызовом `writeFileAtomic` с выбранными options. Bounded
147
+ regular-file read заменяется `files.read`; JSON/schema, receipt conflicts, journal rotation и
148
+ application locks сохраняются. Spawn/drain/capture/cancellation wrapper заменяется `runNativeCommand`;
149
+ shell policy, command admission, логирование, redaction, bytes→text decoder и operator sink сохраняются.
150
+ Длительные signal-only команды остаются streaming; bounded tar/capture получают свои явные limits.
151
+ Импорт leaf не делает установку всего npm package маленькой; выбирайте dependency после измерения
152
+ packed размера и dependency closure.
153
+
154
+ ==============================================================================
155
+ # API reference: `stitchkit/process` (docs/api/reference.md)
156
+ ==============================================================================
157
+
158
+ ## `stitchkit/process`
159
+
160
+ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/native-io.md).
161
+
162
+ | Export | Kind | Purpose |
163
+ |---|---|---|
164
+ | `runNativeCommand` | function | caller-lifetime streaming or explicitly bounded binary capture |
165
+ | `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink and finite cleanup limits |
166
+ | `NativeCommandResult` | type | observed nullable exit code/signal and captured stdout/stderr bytes |
167
+ | `NativeCommandError` | class | COMMAND_LIMIT, COMMAND_UNAVAILABLE or COMMAND_CLEANUP with internal cause |
@@ -68,6 +68,7 @@ own, recorded as an ADR.
68
68
  | `stitchkit/tools/connections` | server (Bun or Node) | evolving | consume external MCP servers and a bounded OpenAPI subset — typed reauthorization, a per-request host fence and per-call credentials |
69
69
  | `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
70
70
  | `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
71
+ | `stitchkit/process` | server (Bun or Node, POSIX) | evolving | finite native commands, byte sinks, bounded capture and process-group cancellation |
71
72
  | `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
72
73
  | `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification, send-failure classification, resumable broadcasts, the operator channel with dedupe, local Bot API files, webhook ownership and durable webhook update intake |
73
74
  | `stitchkit/telegram/html` | browser and server | evolving | Telegram's HTML as a tree — clean any markup to what Telegram accepts, cut long text into valid parts by what the reader sees, check markup before it is sent |
package/llms/tools.txt CHANGED
@@ -409,6 +409,10 @@ where both exist the runtime's wins, because the run is recorded in its ledger.
409
409
 
410
410
  #### An effect in another system — `effect`, at most once
411
411
 
412
+ Standalone callers must hold an application-owned exclusive execution lease for the conversation/run
413
+ through the whole effect call. Sharing a ledger or separate ledger adapters is not an atomic claim;
414
+ the runtime supplies its run lease. Process-local coalescing only covers the same ledger object.
415
+
412
416
  `step` records its result **after** the body, so a body cut short by a crash
413
417
  runs again. For sending a message or starting a turn at a provider, a repeat is
414
418
  worse than a loss. The engine's `effect` records the intent **before** it runs:
@@ -431,6 +435,20 @@ throws, or a `reconcile` that throws or overruns `reconcileTimeoutMs` (default
431
435
  call. The proof is JSON of at most 64 KiB — the identity the recipient assigned,
432
436
  not a payload. → [ADR 0200](../decisions/0200-an-effect-is-recorded-before-it-runs.md)
433
437
 
438
+ Caller abort during reconcile releases the wait with `StepAbortedError`, leaves the intent and ignores
439
+ a late result. A coalesced caller can cancel its own await without cancelling the shared owner;
440
+ the first caller owns reconciliation handlers/deadline. `reconcileTimeoutMs` cannot exceed the native
441
+ timer range (2147483647 ms). It does not turn cancellation into proof of absence. Neither `null` nor an absence digest
442
+ proves that an earlier recipient request cannot complete later. Safe retries require recipient-side
443
+ idempotency or a verified fence. See [ADR 0224](../decisions/0224-native-primitives-have-one-owner.md).
444
+
445
+ Ledger reads are paged at 10 000 and incremental in one engine; a fresh engine replays the conversation
446
+ and retains its durable keys. This is not a bounded outstanding index/direct terminal receipt store.
447
+ For application delivery, `defineDomainEventDelivery` in `stitchkit/primitives` composes a caller-owned
448
+ atomic outbox claim with delivered/retryable/terminal/unknown states, bounded dispatch count (default
449
+ 100). Storage retention, per-key lookup, recipient proof and transactions remain in that adapter;
450
+ it is not an automatic cutover for a different reconciliation protocol.
451
+
434
452
  ### Typed MCP call metadata
435
453
 
436
454
  Contract handlers, runtime-tool handlers/factories, lifecycle and tool hooks all
package/llms-full.txt CHANGED
@@ -59,6 +59,7 @@ own, recorded as an ADR.
59
59
  | `stitchkit/tools/connections` | server (Bun or Node) | evolving | consume external MCP servers and a bounded OpenAPI subset — typed reauthorization, a per-request host fence and per-call credentials |
60
60
  | `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
61
61
  | `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
62
+ | `stitchkit/process` | server (Bun or Node, POSIX) | evolving | finite native commands, byte sinks, bounded capture and process-group cancellation |
62
63
  | `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
63
64
  | `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification, send-failure classification, resumable broadcasts, the operator channel with dedupe, local Bot API files, webhook ownership and durable webhook update intake |
64
65
  | `stitchkit/telegram/html` | browser and server | evolving | Telegram's HTML as a tree — clean any markup to what Telegram accepts, cut long text into valid parts by what the reader sees, check markup before it is sent |
@@ -3872,6 +3873,10 @@ where both exist the runtime's wins, because the run is recorded in its ledger.
3872
3873
 
3873
3874
  #### An effect in another system — `effect`, at most once
3874
3875
 
3876
+ Standalone callers must hold an application-owned exclusive execution lease for the conversation/run
3877
+ through the whole effect call. Sharing a ledger or separate ledger adapters is not an atomic claim;
3878
+ the runtime supplies its run lease. Process-local coalescing only covers the same ledger object.
3879
+
3875
3880
  `step` records its result **after** the body, so a body cut short by a crash
3876
3881
  runs again. For sending a message or starting a turn at a provider, a repeat is
3877
3882
  worse than a loss. The engine's `effect` records the intent **before** it runs:
@@ -3894,6 +3899,20 @@ throws, or a `reconcile` that throws or overruns `reconcileTimeoutMs` (default
3894
3899
  call. The proof is JSON of at most 64 KiB — the identity the recipient assigned,
3895
3900
  not a payload. → [ADR 0200](../decisions/0200-an-effect-is-recorded-before-it-runs.md)
3896
3901
 
3902
+ Caller abort during reconcile releases the wait with `StepAbortedError`, leaves the intent and ignores
3903
+ a late result. A coalesced caller can cancel its own await without cancelling the shared owner;
3904
+ the first caller owns reconciliation handlers/deadline. `reconcileTimeoutMs` cannot exceed the native
3905
+ timer range (2147483647 ms). It does not turn cancellation into proof of absence. Neither `null` nor an absence digest
3906
+ proves that an earlier recipient request cannot complete later. Safe retries require recipient-side
3907
+ idempotency or a verified fence. See [ADR 0224](../decisions/0224-native-primitives-have-one-owner.md).
3908
+
3909
+ Ledger reads are paged at 10 000 and incremental in one engine; a fresh engine replays the conversation
3910
+ and retains its durable keys. This is not a bounded outstanding index/direct terminal receipt store.
3911
+ For application delivery, `defineDomainEventDelivery` in `stitchkit/primitives` composes a caller-owned
3912
+ atomic outbox claim with delivered/retryable/terminal/unknown states, bounded dispatch count (default
3913
+ 100). Storage retention, per-key lookup, recipient proof and transactions remain in that adapter;
3914
+ it is not an automatic cutover for a different reconciliation protocol.
3915
+
3897
3916
  ### Typed MCP call metadata
3898
3917
 
3899
3918
  Contract handlers, runtime-tool handlers/factories, lifecycle and tool hooks all
@@ -8002,6 +8021,153 @@ workspace-relative or canonical `/workspace/...` paths. Unsupported policies mus
8002
8021
  be refused explicitly by the backend.
8003
8022
 
8004
8023
 
8024
+ ==============================================================================
8025
+ # Guide: Native IO (docs/guide/native-io.md)
8026
+ ==============================================================================
8027
+
8028
+ ---
8029
+ title: Конечные команды и файловые гарантии
8030
+ description: Native IO для Bun и Node, с явно выбранной durability, безопасными bounds и caller lifetime.
8031
+ type: guide
8032
+ status: active
8033
+ created: 2026-10-01 20:44 +07:00
8034
+ updated: 2026-10-01 21:31 +07:00
8035
+ participants:
8036
+ - role: authored
8037
+ harness: Codex
8038
+ model: GPT-6
8039
+ at: 2026-10-01 20:44 +07:00
8040
+ ---
8041
+
8042
+ # Native IO
8043
+
8044
+ Примеры используют публичные imports и одинаково исполняются в Bun и Node ≥22.
8045
+
8046
+ ## Запись файла
8047
+
8048
+ ```ts
8049
+ import { writeFileAtomic, AtomicFilePublicationError } from 'stitchkit/files'
8050
+
8051
+ try {
8052
+ await writeFileAtomic('/srv/app/receipt.json', JSON.stringify({ id: 'receipt-1' }), {
8053
+ replace: false,
8054
+ mode: 0o600,
8055
+ durability: 'directory',
8056
+ })
8057
+ } catch (error) {
8058
+ if (error instanceof AtomicFilePublicationError) {
8059
+ // target уже виден. Проверьте receipt, прежде чем решать о повторе.
8060
+ console.error(error.phase, error.cause)
8061
+ } else throw error
8062
+ }
8063
+ await writeFileAtomic('/srv/app/state.json', '{"version":2}', {
8064
+ replace: true, durability: 'directory',
8065
+ })
8066
+ ```
8067
+
8068
+ По умолчанию `replace: true`, `durability: 'file'`, `mode: 0o600`: existing вызовы сохраняют
8069
+ file-fsync и atomic replacement. `durability: 'none'` отключает sync; `'directory'` выполняет
8070
+ полную запись → exact descriptor chmod → file fsync → rename (replace) либо link (create) →
8071
+ удаление staging link → parent fsync. `writeFileAtomicSync` имеет те же options и порядок.
8072
+ Create отказывает с native `EEXIST` при существующем файле, symlink или concurrent winner.
8073
+ Никогда не переводите этот отказ в replace автоматически.
8074
+
8075
+ `AtomicFilePublicationError.published === true` отличает видимость от неподтверждённого
8076
+ acknowledgement. `phase` — `cleanup`, `directory-sync` или `directory-close`; первичная причина
8077
+ сохранена в `cause`. Ошибка до publish оставляет прежний target. Cleanup вторичен к первичной ошибке.
8078
+ При create кратко существует второй hardlink; strict single-link reader вправе отказать в этом окне.
8079
+
8080
+ Directory fsync — native POSIX/filesystem capability. Windows явно отказывает; иной filesystem
8081
+ может отказать при open/sync. Проверка на Linux не подтверждает поведение macOS, сетевой FS или
8082
+ сохранность при отключении питания. Состояние диска определяется гарантиями самой FS/ОС.
8083
+ `createManagedFileBoundary.write(..., { durable: true })` использует тот же publish owner и parent sync;
8084
+ его create по умолчанию остаётся create, а `replace: true` явно разрешает replacement.
8085
+
8086
+ ## Строгое чтение
8087
+
8088
+ ```ts
8089
+ import { createManagedFileBoundary } from 'stitchkit/files'
8090
+
8091
+ const files = await createManagedFileBoundary({ root: '/srv/app', maxReadBytes: 64 * 1024 })
8092
+ const source = await files.read('receipt.json', {
8093
+ rejectSymlinks: true, singleLink: true, stable: true, observe: true,
8094
+ })
8095
+ console.log(source.observation) // dev, ino, size, nlink, mtimeMs, ctimeMs
8096
+ const receipt = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(source.bytes))
8097
+ // schema.parse(receipt) и проверка digest принадлежат приложению.
8098
+ ```
8099
+
8100
+ Без новых options ответ сохраняет `{ ref, bytes }`, допускает trusted in-root symlink и hardlink.
8101
+ `rejectSymlinks` запрещает requested leaf, `singleLink` проверяет `nlink` открытого descriptor,
8102
+ `stable` сравнивает dev/ino/size/mtime/ctime/nlink вокруг byte read; `observe` добавляет metadata.
8103
+ `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`, `FILE_UNSAFE_LINK`, `FILE_TOO_LARGE`, `FILE_CHANGED`,
8104
+ `FILE_UNSUPPORTED` и `FILE_IO_ERROR` различаются. Abort сохраняет caller reason.
8105
+ Nonblocking open позволяет отказать FIFO без ожидания writer. Bytes ограничены во время чтения,
8106
+ включая рост файла, не только initial stat. Chunk allocation ≤ maxBytes+1+64 KiB, flatten ≤ maxBytes;
8107
+ FD закрывается при success, отказе, abort и IO error.
8108
+ Ошибка close сохраняет первичную ошибку чтения; после успешного чтения ошибка close возвращается caller.
8109
+
8110
+ Root и ancestors принадлежат trusted actor. Эти options не дают descriptor-relative containment
8111
+ при hostile concurrent ancestor replacement. Отдельного переключателя «считать ancestors безопасными»
8112
+ нет: portable owner не предоставляет такую capability. Metadata stability не доказывает immutable
8113
+ snapshot против произвольного hostile writer; используйте application lock/immutable publication.
8114
+
8115
+ ## Команды
8116
+
8117
+ ```ts
8118
+ import { runNativeCommand } from 'stitchkit/process'
8119
+
8120
+ const captured = await runNativeCommand({
8121
+ executable: '/usr/bin/git', args: ['rev-parse', 'HEAD'], cwd: '/srv/app',
8122
+ envPolicy: 'declared-only', env: { PATH: '/usr/bin:/bin' },
8123
+ capture: true, maxOutputBytes: 4096, timeoutMs: 5000,
8124
+ })
8125
+ console.log(captured.exitCode, new TextDecoder().decode(captured.stdout))
8126
+
8127
+ const lifetime = new AbortController()
8128
+ await runNativeCommand({
8129
+ executable: '/usr/bin/git', args: ['status'], signal: lifetime.signal,
8130
+ onOutput: async (bytes, channel, outputSignal) => {
8131
+ outputSignal.throwIfAborted()
8132
+ await output.write(bytes, channel, outputSignal) // application-owned bounded sink
8133
+ },
8134
+ })
8135
+ ```
8136
+
8137
+ Команда требует caller signal или finite `timeoutMs` (1–2147483647 ms: native timer range; большее значение отказывается, не превращается в 1 ms). Streaming имеет caller lifetime, без скрытого
8138
+ 60-second timeout или 1-MiB output limit. `capture` по умолчанию false; true требует
8139
+ `maxOutputBytes` для суммарных stdout+stderr. Лимит считает исходные bytes; API возвращает
8140
+ `Uint8Array`, не преобразует invalid UTF-8. Result содержит наблюдаемый `exitCode: number | null`
8141
+ и `signal: string | null`; signal termination не превращается в выдуманный exit code. `maxOutputBytes` может ограничивать и streaming.
8142
+ `stdin` — bytes с `maxStdinBytes` (default 1 MiB). `envPolicy` default `declared-only`: отсутствие env
8143
+ означает пустой environment; `ambient` явно разрешает наследование. Cwd/executable policy остаётся
8144
+ приложению; этот primitive запускает native executable и сам не создаёт shell.
8145
+
8146
+ Вывод читается с backpressure отдельно для stdout/stderr: один выполняющийся sink на канал плюс
8147
+ bounded native Readable buffer; накопления всей истории при streaming нет. Callback получает
8148
+ lifetime signal. Abort освобождает ожидание даже игнорирующего signal callback, но не способен
8149
+ отменить сторонние действия, которые сам callback продолжит после abort. Sink обязан отменять свои
8150
+ IO, не сохранять бесконечную историю и не изменять shared descriptor flags. После abort новых
8151
+ callbacks owner не запускает. Normal completion ждёт всех bytes и sinks.
8152
+
8153
+ Cleanup: POSIX process group, TERM, `killGraceMs` (default 100, maximum 10 000), затем KILL;
8154
+ `cleanupTimeoutMs` (default 2000, maximum 30 000) ограничивает ожидание close после сигналов.
8155
+ Сохраняется caller/sink причина; failure cleanup — `NativeCommandError` с `COMMAND_CLEANUP` и
8156
+ AggregateError cause. Даже вышедший parent не исключает helper, удерживающий pipe. Windows
8157
+ отказывает с `COMMAND_UNAVAILABLE`; потомок, самостоятельно покинувший группу через setsid,
8158
+ лежит за границей этой capability. Это one-shot execution, не PTY, supervisor или daemon host.
8159
+
8160
+ ## Механическая адаптация
8161
+
8162
+ Atomic file wrapper заменяется одним вызовом `writeFileAtomic` с выбранными options. Bounded
8163
+ regular-file read заменяется `files.read`; JSON/schema, receipt conflicts, journal rotation и
8164
+ application locks сохраняются. Spawn/drain/capture/cancellation wrapper заменяется `runNativeCommand`;
8165
+ shell policy, command admission, логирование, redaction, bytes→text decoder и operator sink сохраняются.
8166
+ Длительные signal-only команды остаются streaming; bounded tar/capture получают свои явные limits.
8167
+ Импорт leaf не делает установку всего npm package маленькой; выбирайте dependency после измерения
8168
+ packed размера и dependency closure.
8169
+
8170
+
8005
8171
  ==============================================================================
8006
8172
  # Guide: Generic application primitives (docs/guide/primitives.md)
8007
8173
  ==============================================================================
@@ -8012,7 +8178,7 @@ description: Declare lifecycle, access, exact values, audit, delivery and export
8012
8178
  type: guide
8013
8179
  status: active
8014
8180
  created: 2026-09-01
8015
- updated: 2026-09-01
8181
+ updated: 2026-10-01 21:03 +07:00
8016
8182
  ---
8017
8183
 
8018
8184
  # Generic application primitives
@@ -8105,6 +8271,31 @@ of the application's existing async operation. The input, scope, metadata and to
8105
8271
  change. The application creates bytes and owns operation persistence; the managed-file boundary
8106
8272
  streams them.
8107
8273
 
8274
+ ## Canonical JSON
8275
+
8276
+ ```ts
8277
+ import { canonicalJson } from 'stitchkit/primitives'
8278
+
8279
+ const bytes = canonicalJson({ '2': 'two', '10': 'ten', optional: undefined, nested: [1, null] }, {
8280
+ maxDepth: 20, maxNodes: 10_000, maxBytes: 64 * 1024,
8281
+ })
8282
+ // {"10":"ten","2":"two","nested":[1,null]}
8283
+ // Native crypto or browser WebCrypto hashes these same UTF-8 bytes.
8284
+ ```
8285
+
8286
+ `canonicalJson` использует единый serializer исторических digest: object keys в UTF-16 code-unit
8287
+ order, включая integer-like keys; array order сохраняется. Optional object members с undefined
8288
+ опускаются. Top-level/array undefined, sparse arrays, functions/symbols/BigInt, non-finite numbers,
8289
+ negative zero, cycles, user prototypes, accessors, non-enumerable/symbol members отказаны до
8290
+ serialization. Приложение валидирует свою schema первым; canonical boundary не заменяет её.
8291
+
8292
+ Defaults: depth 100 (maximum 100), nodes 100 000, UTF-8 bytes 1 MiB. Превышение или не-JSON data —
8293
+ TypeError; неверные options — Zod validation error. Это browser-safe leaf без Node/Bun/crypto,
8294
+ agent-runtime/testing или optional peers. Native и browser используют один algorithm; hashing
8295
+ остаётся platform adapter. Переносите только serializer recursion, сохраняя proof schemas и
8296
+ verification boundary. Существующие внутренние historical normalization/digest fixtures не меняются;
8297
+ новый public boundary сознательно отказывает неоднозначным values вместо фиктивной совместимости.
8298
+
8108
8299
 
8109
8300
  ==============================================================================
8110
8301
  # Guide: Application migration recipes (docs/guide/application-migration-recipes.md)
@@ -18804,6 +18995,12 @@ schedules, transports and file generation remain application-owned.
18804
18995
 
18805
18996
  ---
18806
18997
 
18998
+ | Export | Kind | Purpose |
18999
+ |---|---|---|
19000
+ | `canonicalJson` | function | bounded plain JSON, UTF-16 key sort, optional object members omitted |
19001
+ | `CanonicalJsonOptions` | type | inferred depth/node/UTF-8 bytes limits |
19002
+
19003
+
18807
19004
  ## `stitchkit/server`
18808
19005
 
18809
19006
  Server-only. Builds and runs the HTTP server, and carries the server primitives.
@@ -20380,6 +20577,12 @@ available from `stitchkit/contract`.
20380
20577
 
20381
20578
  ---
20382
20579
 
20580
+ | Export | Kind | Purpose |
20581
+ |---|---|---|
20582
+ | `AtomicFilePublicationError` | class | published=true with cleanup/directory-sync/directory-close phase |
20583
+ | `FileObservation` | type | observed descriptor dev/ino/size/nlink/mtimeMs/ctimeMs |
20584
+
20585
+
20383
20586
  ## `stitchkit/telegram`
20384
20587
 
20385
20588
  Peer-free server-only Telegram platform primitives — the Bot API's own
@@ -20907,3 +21110,15 @@ AI-only peer boundary. Existing custom tool callbacks need no new import.
20907
21110
  | Export | Kind | Summary |
20908
21111
  |--------|------|---------|
20909
21112
  | `createAgentHarnessTools` / `AgentHarnessToolsConfig` | function / _type_ | async per-run full mount configuration (`services` plus all `AgentMountConfig` options); composes application lifecycle then runtime fence exactly once |
21113
+
21114
+
21115
+ ## `stitchkit/process`
21116
+
21117
+ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/native-io.md).
21118
+
21119
+ | Export | Kind | Purpose |
21120
+ |---|---|---|
21121
+ | `runNativeCommand` | function | caller-lifetime streaming or explicitly bounded binary capture |
21122
+ | `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink and finite cleanup limits |
21123
+ | `NativeCommandResult` | type | observed nullable exit code/signal and captured stdout/stderr bytes |
21124
+ | `NativeCommandError` | class | COMMAND_LIMIT, COMMAND_UNAVAILABLE or COMMAND_CLEANUP with internal cause |