@zaalipro/dsh-workflows 0.1.0-rc.12

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 (229) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +224 -0
  3. package/README.i18n.yaml +4 -0
  4. package/README.md +111 -0
  5. package/README.zh.md +111 -0
  6. package/cordis.patch.yml +10 -0
  7. package/docs/architecture.i18n.yaml +4 -0
  8. package/docs/architecture.md +177 -0
  9. package/docs/architecture.zh.md +177 -0
  10. package/docs/testing.i18n.yaml +4 -0
  11. package/docs/testing.md +125 -0
  12. package/docs/testing.zh.md +125 -0
  13. package/docs/user-guide.i18n.yaml +4 -0
  14. package/docs/user-guide.md +351 -0
  15. package/docs/user-guide.zh.md +351 -0
  16. package/lib/client-types/WorkflowMemberInspector.d.ts +28 -0
  17. package/lib/client-types/WorkflowMemberInspector.d.ts.map +1 -0
  18. package/lib/client-types/WorkflowMemberInspector.js +181 -0
  19. package/lib/client-types/WorkflowMemberInspector.js.map +1 -0
  20. package/lib/client-types/WorkflowRunPanel.d.ts +43 -0
  21. package/lib/client-types/WorkflowRunPanel.d.ts.map +1 -0
  22. package/lib/client-types/WorkflowRunPanel.js +130 -0
  23. package/lib/client-types/WorkflowRunPanel.js.map +1 -0
  24. package/lib/client-types/WorkflowsDashboard.d.ts +49 -0
  25. package/lib/client-types/WorkflowsDashboard.d.ts.map +1 -0
  26. package/lib/client-types/WorkflowsDashboard.js +942 -0
  27. package/lib/client-types/WorkflowsDashboard.js.map +1 -0
  28. package/lib/client-types/adapter.d.ts +60 -0
  29. package/lib/client-types/adapter.d.ts.map +1 -0
  30. package/lib/client-types/adapter.js +117 -0
  31. package/lib/client-types/adapter.js.map +1 -0
  32. package/lib/client-types/chat-renderer.d.ts +10 -0
  33. package/lib/client-types/chat-renderer.d.ts.map +1 -0
  34. package/lib/client-types/chat-renderer.js +11 -0
  35. package/lib/client-types/chat-renderer.js.map +1 -0
  36. package/lib/client-types/contract.d.ts +210 -0
  37. package/lib/client-types/contract.d.ts.map +1 -0
  38. package/lib/client-types/contract.js +37 -0
  39. package/lib/client-types/contract.js.map +1 -0
  40. package/lib/client-types/controller.d.ts +83 -0
  41. package/lib/client-types/controller.d.ts.map +1 -0
  42. package/lib/client-types/controller.js +705 -0
  43. package/lib/client-types/controller.js.map +1 -0
  44. package/lib/client-types/index.d.ts +19 -0
  45. package/lib/client-types/index.d.ts.map +1 -0
  46. package/lib/client-types/index.js +731 -0
  47. package/lib/client-types/index.js.map +1 -0
  48. package/lib/client-types/locales.d.ts +257 -0
  49. package/lib/client-types/locales.d.ts.map +1 -0
  50. package/lib/client-types/locales.js +270 -0
  51. package/lib/client-types/locales.js.map +1 -0
  52. package/lib/client-types/slot-components.d.ts +38 -0
  53. package/lib/client-types/slot-components.d.ts.map +1 -0
  54. package/lib/client-types/slot-components.js +22 -0
  55. package/lib/client-types/slot-components.js.map +1 -0
  56. package/lib/client-types/store.d.ts +69 -0
  57. package/lib/client-types/store.d.ts.map +1 -0
  58. package/lib/client-types/store.js +135 -0
  59. package/lib/client-types/store.js.map +1 -0
  60. package/lib/client-types/workflow-definition.d.ts +52 -0
  61. package/lib/client-types/workflow-definition.d.ts.map +1 -0
  62. package/lib/client-types/workflow-definition.js +135 -0
  63. package/lib/client-types/workflow-definition.js.map +1 -0
  64. package/lib/client.js +11065 -0
  65. package/lib/client.js.map +1 -0
  66. package/lib/compat-engine/index.js +1926 -0
  67. package/lib/compat-engine/index.js.map +1 -0
  68. package/lib/compat-engine/worker.cjs +1500 -0
  69. package/lib/compat-engine/worker.cjs.map +1 -0
  70. package/lib/typert.host.d.ts +3 -0
  71. package/lib/typert.host.js +1709 -0
  72. package/lib/typert.remote-client.d.ts +57 -0
  73. package/lib/typert.remote-client.js +1662 -0
  74. package/lib/types/commands/aliases.d.ts +4 -0
  75. package/lib/types/commands/aliases.d.ts.map +1 -0
  76. package/lib/types/commands/aliases.js +25 -0
  77. package/lib/types/commands/aliases.js.map +1 -0
  78. package/lib/types/commands/index.d.ts +66 -0
  79. package/lib/types/commands/index.d.ts.map +1 -0
  80. package/lib/types/commands/index.js +550 -0
  81. package/lib/types/commands/index.js.map +1 -0
  82. package/lib/types/commands/parser.d.ts +17 -0
  83. package/lib/types/commands/parser.d.ts.map +1 -0
  84. package/lib/types/commands/parser.js +45 -0
  85. package/lib/types/commands/parser.js.map +1 -0
  86. package/lib/types/config.d.ts +71 -0
  87. package/lib/types/config.d.ts.map +1 -0
  88. package/lib/types/config.js +109 -0
  89. package/lib/types/config.js.map +1 -0
  90. package/lib/types/index.d.ts +41 -0
  91. package/lib/types/index.d.ts.map +1 -0
  92. package/lib/types/index.js +483 -0
  93. package/lib/types/index.js.map +1 -0
  94. package/lib/types/invariant.d.ts +21 -0
  95. package/lib/types/invariant.d.ts.map +1 -0
  96. package/lib/types/invariant.js +85 -0
  97. package/lib/types/invariant.js.map +1 -0
  98. package/lib/types/registry/definition.d.ts +24 -0
  99. package/lib/types/registry/definition.d.ts.map +1 -0
  100. package/lib/types/registry/definition.js +189 -0
  101. package/lib/types/registry/definition.js.map +1 -0
  102. package/lib/types/registry/index.d.ts +100 -0
  103. package/lib/types/registry/index.d.ts.map +1 -0
  104. package/lib/types/registry/index.js +899 -0
  105. package/lib/types/registry/index.js.map +1 -0
  106. package/lib/types/registry/names.d.ts +19 -0
  107. package/lib/types/registry/names.d.ts.map +1 -0
  108. package/lib/types/registry/names.js +69 -0
  109. package/lib/types/registry/names.js.map +1 -0
  110. package/lib/types/registry/remote.d.ts +15 -0
  111. package/lib/types/registry/remote.d.ts.map +1 -0
  112. package/lib/types/registry/remote.js +128 -0
  113. package/lib/types/registry/remote.js.map +1 -0
  114. package/lib/types/registry/roots.d.ts +44 -0
  115. package/lib/types/registry/roots.d.ts.map +1 -0
  116. package/lib/types/registry/roots.js +118 -0
  117. package/lib/types/registry/roots.js.map +1 -0
  118. package/lib/types/registry/types.d.ts +62 -0
  119. package/lib/types/registry/types.d.ts.map +1 -0
  120. package/lib/types/registry/types.js +2 -0
  121. package/lib/types/registry/types.js.map +1 -0
  122. package/lib/types/registry/watchers.d.ts +70 -0
  123. package/lib/types/registry/watchers.d.ts.map +1 -0
  124. package/lib/types/registry/watchers.js +152 -0
  125. package/lib/types/registry/watchers.js.map +1 -0
  126. package/lib/types/remote-events.d.ts +42 -0
  127. package/lib/types/remote-events.d.ts.map +1 -0
  128. package/lib/types/remote-events.js +21 -0
  129. package/lib/types/remote-events.js.map +1 -0
  130. package/lib/types/run-recorder.d.ts +44 -0
  131. package/lib/types/run-recorder.d.ts.map +1 -0
  132. package/lib/types/run-recorder.js +356 -0
  133. package/lib/types/run-recorder.js.map +1 -0
  134. package/lib/types/supervisor/canned-validate.d.ts +25 -0
  135. package/lib/types/supervisor/canned-validate.d.ts.map +1 -0
  136. package/lib/types/supervisor/canned-validate.js +122 -0
  137. package/lib/types/supervisor/canned-validate.js.map +1 -0
  138. package/lib/types/supervisor/completion-notice.d.ts +49 -0
  139. package/lib/types/supervisor/completion-notice.d.ts.map +1 -0
  140. package/lib/types/supervisor/completion-notice.js +281 -0
  141. package/lib/types/supervisor/completion-notice.js.map +1 -0
  142. package/lib/types/supervisor/cursors.d.ts +68 -0
  143. package/lib/types/supervisor/cursors.d.ts.map +1 -0
  144. package/lib/types/supervisor/cursors.js +158 -0
  145. package/lib/types/supervisor/cursors.js.map +1 -0
  146. package/lib/types/supervisor/engine-compat.d.ts +48 -0
  147. package/lib/types/supervisor/engine-compat.d.ts.map +1 -0
  148. package/lib/types/supervisor/engine-compat.js +116 -0
  149. package/lib/types/supervisor/engine-compat.js.map +1 -0
  150. package/lib/types/supervisor/index.d.ts +183 -0
  151. package/lib/types/supervisor/index.d.ts.map +1 -0
  152. package/lib/types/supervisor/index.js +2040 -0
  153. package/lib/types/supervisor/index.js.map +1 -0
  154. package/lib/types/supervisor/parallel-compat.d.ts +36 -0
  155. package/lib/types/supervisor/parallel-compat.d.ts.map +1 -0
  156. package/lib/types/supervisor/parallel-compat.js +180 -0
  157. package/lib/types/supervisor/parallel-compat.js.map +1 -0
  158. package/lib/types/supervisor/remote.d.ts +25 -0
  159. package/lib/types/supervisor/remote.d.ts.map +1 -0
  160. package/lib/types/supervisor/remote.js +478 -0
  161. package/lib/types/supervisor/remote.js.map +1 -0
  162. package/lib/types/supervisor/storage/bounded-file.d.ts +41 -0
  163. package/lib/types/supervisor/storage/bounded-file.d.ts.map +1 -0
  164. package/lib/types/supervisor/storage/bounded-file.js +186 -0
  165. package/lib/types/supervisor/storage/bounded-file.js.map +1 -0
  166. package/lib/types/supervisor/storage/details-codec.d.ts +14 -0
  167. package/lib/types/supervisor/storage/details-codec.d.ts.map +1 -0
  168. package/lib/types/supervisor/storage/details-codec.js +359 -0
  169. package/lib/types/supervisor/storage/details-codec.js.map +1 -0
  170. package/lib/types/supervisor/storage/index.d.ts +26 -0
  171. package/lib/types/supervisor/storage/index.d.ts.map +1 -0
  172. package/lib/types/supervisor/storage/index.js +100 -0
  173. package/lib/types/supervisor/storage/index.js.map +1 -0
  174. package/lib/types/supervisor/storage/lease.d.ts +34 -0
  175. package/lib/types/supervisor/storage/lease.d.ts.map +1 -0
  176. package/lib/types/supervisor/storage/lease.js +289 -0
  177. package/lib/types/supervisor/storage/lease.js.map +1 -0
  178. package/lib/types/supervisor/storage/manifest-codec.d.ts +10 -0
  179. package/lib/types/supervisor/storage/manifest-codec.d.ts.map +1 -0
  180. package/lib/types/supervisor/storage/manifest-codec.js +335 -0
  181. package/lib/types/supervisor/storage/manifest-codec.js.map +1 -0
  182. package/lib/types/supervisor/storage/manifest-store.d.ts +99 -0
  183. package/lib/types/supervisor/storage/manifest-store.d.ts.map +1 -0
  184. package/lib/types/supervisor/storage/manifest-store.js +1606 -0
  185. package/lib/types/supervisor/storage/manifest-store.js.map +1 -0
  186. package/lib/types/supervisor/storage/manifest-types.d.ts +269 -0
  187. package/lib/types/supervisor/storage/manifest-types.d.ts.map +1 -0
  188. package/lib/types/supervisor/storage/manifest-types.js +2 -0
  189. package/lib/types/supervisor/storage/manifest-types.js.map +1 -0
  190. package/lib/types/supervisor/storage/private-root.d.ts +143 -0
  191. package/lib/types/supervisor/storage/private-root.d.ts.map +1 -0
  192. package/lib/types/supervisor/storage/private-root.js +860 -0
  193. package/lib/types/supervisor/storage/private-root.js.map +1 -0
  194. package/lib/types/supervisor/storage/recovery.d.ts +14 -0
  195. package/lib/types/supervisor/storage/recovery.d.ts.map +1 -0
  196. package/lib/types/supervisor/storage/recovery.js +126 -0
  197. package/lib/types/supervisor/storage/recovery.js.map +1 -0
  198. package/lib/types/supervisor/storage/run-files.d.ts +67 -0
  199. package/lib/types/supervisor/storage/run-files.d.ts.map +1 -0
  200. package/lib/types/supervisor/storage/run-files.js +346 -0
  201. package/lib/types/supervisor/storage/run-files.js.map +1 -0
  202. package/lib/types/supervisor/types.d.ts +295 -0
  203. package/lib/types/supervisor/types.d.ts.map +1 -0
  204. package/lib/types/supervisor/types.js +3 -0
  205. package/lib/types/supervisor/types.js.map +1 -0
  206. package/lib/types/supervisor/value-view.d.ts +19 -0
  207. package/lib/types/supervisor/value-view.d.ts.map +1 -0
  208. package/lib/types/supervisor/value-view.js +183 -0
  209. package/lib/types/supervisor/value-view.js.map +1 -0
  210. package/lib/types/tool/index.d.ts +86 -0
  211. package/lib/types/tool/index.d.ts.map +1 -0
  212. package/lib/types/tool/index.js +810 -0
  213. package/lib/types/tool/index.js.map +1 -0
  214. package/lib/types/tool/schema.d.ts +158 -0
  215. package/lib/types/tool/schema.d.ts.map +1 -0
  216. package/lib/types/tool/schema.js +137 -0
  217. package/lib/types/tool/schema.js.map +1 -0
  218. package/lib/types/types.d.ts +9 -0
  219. package/lib/types/types.d.ts.map +1 -0
  220. package/lib/types/types.js +3 -0
  221. package/lib/types/types.js.map +1 -0
  222. package/lib/types/user-questions.d.ts +9 -0
  223. package/lib/types/user-questions.d.ts.map +1 -0
  224. package/lib/types/user-questions.js +91 -0
  225. package/lib/types/user-questions.js.map +1 -0
  226. package/package.json +291 -0
  227. package/skills/create-workflow/SKILL.md +166 -0
  228. package/vendor/workflow-engine/LICENSE +21 -0
  229. package/vendor/workflow-engine/README.md +1 -0
@@ -0,0 +1,860 @@
1
+ import { constants } from 'node:fs';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { chmod, link, lstat, mkdir, open, readdir, realpath, rename, rmdir, unlink } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, join, parse } from 'node:path';
5
+ import { BoundedFileError, assertSafeComponent } from './bounded-file.js';
6
+ /** Close every layout descriptor, retaining the first failure. */
7
+ export async function closeWorkflowStorageLayout(layout) {
8
+ let first;
9
+ // Close children before the lease. The order is deliberately stable so a
10
+ // teardown cannot race a still-open descriptor while the root is released.
11
+ for (const directory of [layout.sessions, layout.runs, layout.staging, layout.quarantine]) {
12
+ try {
13
+ await directory.close();
14
+ }
15
+ catch (error) {
16
+ first ??= error;
17
+ }
18
+ }
19
+ try {
20
+ await layout.root.close();
21
+ }
22
+ catch (error) {
23
+ first ??= error;
24
+ }
25
+ if (first !== undefined)
26
+ throw first;
27
+ }
28
+ const O_DIRECTORY = constants.O_DIRECTORY ?? 0;
29
+ const O_NOFOLLOW = constants.O_NOFOLLOW;
30
+ const encoder = new TextEncoder();
31
+ function failUnsupported() {
32
+ throw new BoundedFileError(`safe workflow storage is unavailable on ${process.platform}`, 'WORKFLOW_STORAGE_UNSUPPORTED');
33
+ }
34
+ function unsupportedCapability(detail) {
35
+ return new BoundedFileError(`descriptor-rooted workflow storage ${detail} is unavailable`, 'WORKFLOW_STORAGE_UNSUPPORTED');
36
+ }
37
+ /** Refuse malformed or incomplete Host capabilities before the package creates
38
+ * any category directory. A structural cast is not a security boundary: all
39
+ * required descriptor operations must be callable at runtime. */
40
+ function assertHostDirectoryDelegate(value) {
41
+ if (typeof value !== 'object' || value === null)
42
+ throw unsupportedCapability('directory capability');
43
+ for (const member of ['openDirectory', 'readBytes', 'writeText', 'assertIdentity', 'close']) {
44
+ if (typeof value[member] !== 'function')
45
+ throw unsupportedCapability(`directory ${member}`);
46
+ }
47
+ }
48
+ function assertHostStorageCapabilities(provider, root) {
49
+ const canList = typeof root.listEntries === 'function'
50
+ || typeof provider.listDir === 'function' && root.target !== undefined;
51
+ if (!canList)
52
+ throw unsupportedCapability('directory listing');
53
+ if (typeof root.fileInfo !== 'function' && !(root.target !== undefined && typeof provider.fileInfoChild === 'function'))
54
+ throw unsupportedCapability('file metadata');
55
+ if (typeof root.publishDirectory !== 'function' && !(root.target !== undefined && typeof provider.publishDirectory === 'function'))
56
+ throw unsupportedCapability('directory publication');
57
+ if (typeof root.removeFile !== 'function' && !(root.target !== undefined && typeof provider.removeFile === 'function'))
58
+ throw unsupportedCapability('file removal');
59
+ if (typeof root.removeDirectory !== 'function' && !(root.target !== undefined && typeof provider.removeDirectory === 'function'))
60
+ throw unsupportedCapability('directory removal');
61
+ }
62
+ function component(name) {
63
+ return assertSafeComponent(name, 'private path component');
64
+ }
65
+ function ownerOk(info) {
66
+ return typeof process.getuid !== 'function' || info.uid === process.getuid();
67
+ }
68
+ function assertDirectoryInfo(info, path, allowSystemRoot = false) {
69
+ if (!info.isDirectory() || info.nlink < 1 || (!allowSystemRoot && !ownerOk(info)) || (!allowSystemRoot && (info.mode & 0o777) !== 0o700)) {
70
+ throw new BoundedFileError(`workflow storage path "${path}" is unsafe: expected owner-only directory`, 'WORKFLOW_STORAGE_UNSAFE');
71
+ }
72
+ }
73
+ function assertAncestorDirectoryInfo(info, path) {
74
+ if (!info.isDirectory() || info.nlink < 1) {
75
+ throw new BoundedFileError(`workflow storage path "${path}" is unsafe: expected directory`, 'WORKFLOW_STORAGE_UNSAFE');
76
+ }
77
+ }
78
+ function assertFileInfo(info, path) {
79
+ if (!info.isFile() || info.nlink !== 1 || !ownerOk(info) || (info.mode & 0o777) !== 0o600) {
80
+ throw new BoundedFileError(`workflow storage path "${path}" is unsafe: expected owner-only regular file`, 'WORKFLOW_STORAGE_UNSAFE');
81
+ }
82
+ }
83
+ function identityOf(info) {
84
+ return {
85
+ dev: Number(info.dev), ino: Number(info.ino), size: Number(info.size),
86
+ mtimeMs: Number(info.mtimeMs), ctimeMs: Number(info.ctimeMs), mode: Number(info.mode & 0o777),
87
+ nlink: Number(info.nlink), ...(info.uid === undefined ? {} : { uid: Number(info.uid) }),
88
+ };
89
+ }
90
+ function sameIdentity(a, b, content = false) {
91
+ if (Number(a.dev) !== Number(b.dev) || Number(a.ino) !== Number(b.ino))
92
+ return false;
93
+ if (!content)
94
+ return true;
95
+ return Number(a.size) === Number(b.size)
96
+ && Number(a.mtimeMs) === Number(b.mtimeMs)
97
+ && Number(a.ctimeMs) === Number(b.ctimeMs)
98
+ && Number(a.mode) === Number(b.mode)
99
+ && Number(a.nlink) === Number(b.nlink);
100
+ }
101
+ /** A retained directory descriptor. Relative operations revalidate its identity. */
102
+ class LocalPrivateDirectory {
103
+ path;
104
+ handle;
105
+ identity;
106
+ parent;
107
+ systemRoot;
108
+ strict;
109
+ closed = false;
110
+ handleClosed = false;
111
+ children = new Set();
112
+ closePromise;
113
+ constructor(path, handle, identity, parent, systemRoot = false, strict = true) {
114
+ this.path = path;
115
+ this.handle = handle;
116
+ this.identity = identity;
117
+ this.parent = parent;
118
+ this.systemRoot = systemRoot;
119
+ this.strict = strict;
120
+ parent?.children.add(this);
121
+ }
122
+ ensureOpen(signal) {
123
+ if (this.closed)
124
+ throw new BoundedFileError('private directory is closed');
125
+ signal?.throwIfAborted();
126
+ }
127
+ /** Check this descriptor and all retained ancestors without requiring them
128
+ * to remain open for new operations. A child capability keeps its parent
129
+ * object alive, so closing a parent merely releases the caller's reference;
130
+ * the descriptor is closed after the last child releases it. */
131
+ async assertRetainedIdentity(signal) {
132
+ signal?.throwIfAborted();
133
+ if (this.handleClosed)
134
+ throw new BoundedFileError('private directory is closed', 'WORKFLOW_STORAGE_UNSAFE');
135
+ const retained = await this.handle.stat();
136
+ const current = await lstat(this.path);
137
+ if (!retained.isDirectory() || !current.isDirectory() || !sameIdentity(retained, current)
138
+ || retained.dev !== this.identity.dev || retained.ino !== this.identity.ino) {
139
+ throw new BoundedFileError(`workflow storage path "${this.path}" is unsafe: directory identity changed`, 'WORKFLOW_STORAGE_UNSAFE');
140
+ }
141
+ if (this.strict)
142
+ assertDirectoryInfo(current, this.path, this.systemRoot);
143
+ else
144
+ assertAncestorDirectoryInfo(current, this.path);
145
+ if (this.parent !== undefined)
146
+ await this.parent.assertRetainedIdentity(signal);
147
+ }
148
+ async assertIdentity(signal) {
149
+ this.ensureOpen(signal);
150
+ await this.assertRetainedIdentity(signal);
151
+ }
152
+ async openDirectory(name, signal, options = { create: false }) {
153
+ component(name);
154
+ this.ensureOpen(signal);
155
+ await this.assertIdentity(signal);
156
+ if (O_NOFOLLOW === undefined || O_NOFOLLOW === 0 || process.platform === 'win32')
157
+ failUnsupported();
158
+ const childPath = join(this.path, name);
159
+ let created = false;
160
+ try {
161
+ const existing = await lstat(childPath);
162
+ if (existing.isSymbolicLink() || !existing.isDirectory()) {
163
+ throw new BoundedFileError(`workflow storage path "${childPath}" is unsafe: expected directory`, 'WORKFLOW_STORAGE_UNSAFE');
164
+ }
165
+ assertDirectoryInfo(existing, childPath);
166
+ }
167
+ catch (error) {
168
+ if (error.code !== 'ENOENT' || options.create === false)
169
+ throw error;
170
+ await mkdir(childPath, { mode: 0o700 });
171
+ created = true;
172
+ }
173
+ try {
174
+ await chmod(childPath, 0o700);
175
+ const childHandle = await open(childPath, constants.O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
176
+ const childInfo = await childHandle.stat();
177
+ assertDirectoryInfo(childInfo, childPath);
178
+ const pathInfo = await lstat(childPath);
179
+ if (!sameIdentity(childInfo, pathInfo))
180
+ throw new BoundedFileError(`workflow storage path "${childPath}" is unsafe: identity changed`, 'WORKFLOW_STORAGE_UNSAFE');
181
+ await this.assertRetainedIdentity(signal);
182
+ return new LocalPrivateDirectory(childPath, childHandle, { dev: Number(childInfo.dev), ino: Number(childInfo.ino) }, this);
183
+ }
184
+ catch (error) {
185
+ if (created) {
186
+ // Remove only the directory just created; never recursively remove. A
187
+ // failed identity check leaves pre-existing data untouched.
188
+ await import('node:fs/promises').then(fs => fs.rmdir(childPath)).catch(() => undefined);
189
+ }
190
+ throw error;
191
+ }
192
+ }
193
+ async listEntries(signal) {
194
+ this.ensureOpen(signal);
195
+ await this.assertRetainedIdentity(signal);
196
+ const entries = await readdir(this.path, { withFileTypes: true });
197
+ const result = [];
198
+ for (const entry of entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
199
+ signal?.throwIfAborted();
200
+ component(entry.name);
201
+ const info = await lstat(join(this.path, entry.name));
202
+ const type = info.isSymbolicLink() ? 'symlink' : info.isDirectory() ? 'directory' : info.isFile() ? 'file' : 'other';
203
+ result.push({ name: entry.name, type, ...(type === 'file' || type === 'directory' ? { identity: identityOf(info) } : {}) });
204
+ }
205
+ await this.assertRetainedIdentity(signal);
206
+ return result;
207
+ }
208
+ async fileInfo(name, signal) {
209
+ component(name);
210
+ this.ensureOpen(signal);
211
+ await this.assertRetainedIdentity(signal);
212
+ try {
213
+ const info = await lstat(join(this.path, name));
214
+ if (info.isSymbolicLink())
215
+ throw new BoundedFileError(`workflow storage path "${join(this.path, name)}" is unsafe: symbolic link`, 'WORKFLOW_STORAGE_UNSAFE');
216
+ await this.assertRetainedIdentity(signal);
217
+ return identityOf(info);
218
+ }
219
+ catch (error) {
220
+ if (error.code === 'ENOENT') {
221
+ // An ENOENT is not proof of absence when the retained directory may
222
+ // have been replaced concurrently. Revalidate before returning the
223
+ // negative observation so callers never turn an ancestor swap into a
224
+ // guarded create.
225
+ await this.assertRetainedIdentity(signal);
226
+ return undefined;
227
+ }
228
+ throw error;
229
+ }
230
+ }
231
+ async removeFile(name, expected, signal) {
232
+ component(name);
233
+ this.ensureOpen(signal);
234
+ await this.assertRetainedIdentity(signal);
235
+ const target = join(this.path, name);
236
+ const before = await lstat(target);
237
+ if (before.isSymbolicLink() || !before.isFile())
238
+ throw new BoundedFileError(`workflow storage path "${target}" is unsafe: destination is not a regular file`, 'WORKFLOW_STORAGE_UNSAFE');
239
+ assertFileInfo(before, target);
240
+ if (expected !== undefined && !sameVersion(expected, before))
241
+ throw new BoundedFileError(`private file "${name}" has changed`, 'WORKFLOW_STALE_REVISION');
242
+ await unlink(target);
243
+ await this.assertRetainedIdentity(signal);
244
+ }
245
+ async removeDirectory(name, expected, signal) {
246
+ component(name);
247
+ this.ensureOpen(signal);
248
+ await this.assertRetainedIdentity(signal);
249
+ const target = join(this.path, name);
250
+ const before = await lstat(target);
251
+ if (!before.isDirectory() || before.isSymbolicLink() || before.nlink < 1)
252
+ throw new BoundedFileError(`workflow storage path "${target}" is unsafe: destination is not an empty directory`, 'WORKFLOW_STORAGE_UNSAFE');
253
+ if (expected !== undefined && !sameVersion(expected, before))
254
+ throw new BoundedFileError(`private directory "${name}" has changed`, 'WORKFLOW_STALE_REVISION');
255
+ try {
256
+ await rmdir(target);
257
+ }
258
+ catch (error) {
259
+ if (error.code === 'ENOTEMPTY')
260
+ throw new BoundedFileError(`private directory "${name}" is not empty`, 'WORKFLOW_STORAGE_UNSAFE', { cause: error });
261
+ throw error;
262
+ }
263
+ await this.assertRetainedIdentity(signal);
264
+ }
265
+ async publishDirectory(name, target, targetName, signal) {
266
+ component(name);
267
+ component(targetName);
268
+ this.ensureOpen(signal);
269
+ await this.assertRetainedIdentity(signal);
270
+ await target.assertIdentity(signal);
271
+ if (!(target instanceof LocalPrivateDirectory))
272
+ throw new BoundedFileError('private directory publication providers do not match', 'WORKFLOW_STORAGE_UNSUPPORTED');
273
+ const sourcePath = join(this.path, name);
274
+ const destinationPath = join(target.path, targetName);
275
+ const source = await lstat(sourcePath);
276
+ if (!source.isDirectory() || source.isSymbolicLink())
277
+ throw new BoundedFileError(`workflow storage path "${sourcePath}" is unsafe: staging entry is not a directory`, 'WORKFLOW_STORAGE_UNSAFE');
278
+ const destination = await lstat(destinationPath).catch(error => error.code === 'ENOENT' ? undefined : Promise.reject(error));
279
+ if (destination !== undefined)
280
+ throw new BoundedFileError(`workflow storage path "${destinationPath}" already exists`, 'WORKFLOW_STORAGE_UNSAFE');
281
+ signal?.throwIfAborted();
282
+ await rename(sourcePath, destinationPath);
283
+ await this.assertRetainedIdentity(signal);
284
+ await target.assertIdentity(signal);
285
+ }
286
+ async readBytes(name, signal, maxBytes) {
287
+ component(name);
288
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
289
+ throw new RangeError('maxBytes must be a non-negative safe integer');
290
+ this.ensureOpen(signal);
291
+ await this.assertIdentity(signal);
292
+ if (O_NOFOLLOW === undefined || O_NOFOLLOW === 0 || process.platform === 'win32')
293
+ failUnsupported();
294
+ const filePath = join(this.path, name);
295
+ const file = await open(filePath, constants.O_RDONLY | O_NOFOLLOW);
296
+ try {
297
+ signal?.throwIfAborted();
298
+ const info = await file.stat();
299
+ assertFileInfo(info, filePath);
300
+ if (info.size > maxBytes)
301
+ throw new BoundedFileError(`workflow storage file "${filePath}" exceeds ${maxBytes} bytes`, 'WORKFLOW_STORAGE_LIMIT');
302
+ const chunks = [];
303
+ let total = 0;
304
+ while (total <= maxBytes) {
305
+ signal?.throwIfAborted();
306
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes - total + 1));
307
+ const { bytesRead } = await file.read(chunk, 0, chunk.length, null);
308
+ if (bytesRead === 0)
309
+ break;
310
+ total += bytesRead;
311
+ if (total > maxBytes)
312
+ throw new BoundedFileError(`workflow storage file "${filePath}" exceeds ${maxBytes} bytes`, 'WORKFLOW_STORAGE_LIMIT');
313
+ chunks.push(chunk.subarray(0, bytesRead));
314
+ }
315
+ const after = await file.stat();
316
+ if (!sameIdentity(info, after, true))
317
+ throw new BoundedFileError(`workflow storage file "${filePath}" changed while reading`, 'WORKFLOW_STORAGE_UNSAFE');
318
+ return new Uint8Array(Buffer.concat(chunks, total));
319
+ }
320
+ finally {
321
+ await file.close();
322
+ await this.assertIdentity(signal);
323
+ }
324
+ }
325
+ async writeText(name, content, expected, signal) {
326
+ component(name);
327
+ this.ensureOpen(signal);
328
+ await this.assertIdentity(signal);
329
+ if (expected.kind !== 'createIfAbsent' && expected.kind !== 'replaceIfVersion')
330
+ throw new BoundedFileError('unguarded private write');
331
+ const target = join(this.path, name);
332
+ let before;
333
+ try {
334
+ before = await lstat(target);
335
+ if (before.isSymbolicLink() || !before.isFile())
336
+ throw new BoundedFileError(`workflow storage path "${target}" is unsafe: destination is not a regular file`, 'WORKFLOW_STORAGE_UNSAFE');
337
+ assertFileInfo(before, target);
338
+ }
339
+ catch (error) {
340
+ if (error.code !== 'ENOENT')
341
+ throw error;
342
+ }
343
+ if (expected.kind === 'createIfAbsent' && before !== undefined)
344
+ throw new BoundedFileError(`private file "${name}" already exists`, 'WORKFLOW_STORAGE_UNSAFE');
345
+ if (expected.kind === 'replaceIfVersion' && !before)
346
+ throw new BoundedFileError(`private file "${name}" does not exist`, 'WORKFLOW_STALE_REVISION');
347
+ if (expected.kind === 'replaceIfVersion' && expected.version !== undefined) {
348
+ const candidate = expected.version;
349
+ if (typeof candidate !== 'object' || candidate === null || !sameVersion(candidate, before)) {
350
+ throw new BoundedFileError(`private file "${name}" has changed`, 'WORKFLOW_STALE_REVISION');
351
+ }
352
+ }
353
+ signal?.throwIfAborted();
354
+ const temp = join(this.path, `.${name}.${randomBytes(16).toString('hex')}.tmp`);
355
+ const bytes = encoder.encode(content);
356
+ let tempHandle;
357
+ try {
358
+ tempHandle = await open(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | O_NOFOLLOW, 0o600);
359
+ let offset = 0;
360
+ while (offset < bytes.byteLength) {
361
+ const written = await tempHandle.write(bytes, offset, bytes.byteLength - offset, offset);
362
+ if (written.bytesWritten <= 0)
363
+ throw new BoundedFileError(`workflow storage file "${temp}" was short-written`, 'WORKFLOW_STORAGE_UNSAFE');
364
+ offset += written.bytesWritten;
365
+ }
366
+ await tempHandle.sync();
367
+ await tempHandle.close();
368
+ tempHandle = undefined;
369
+ await this.assertIdentity(signal);
370
+ const latest = await lstat(target).catch(error => {
371
+ if (error.code === 'ENOENT')
372
+ return undefined;
373
+ throw error;
374
+ });
375
+ if (expected.kind === 'createIfAbsent' && latest !== undefined)
376
+ throw new BoundedFileError(`private file "${name}" already exists`, 'WORKFLOW_STORAGE_UNSAFE');
377
+ if (expected.kind === 'replaceIfVersion' && (!latest || (before && !sameIdentity(before, latest, true))))
378
+ throw new BoundedFileError(`private file "${name}" has changed`, 'WORKFLOW_STALE_REVISION');
379
+ if (expected.kind === 'createIfAbsent') {
380
+ // `link` is the POSIX no-replace publication primitive. It fails with
381
+ // EEXIST instead of overwriting a destination created after our check.
382
+ try {
383
+ await link(temp, target);
384
+ }
385
+ catch (error) {
386
+ if (error.code === 'EEXIST') {
387
+ throw new BoundedFileError(`private file "${name}" already exists`, 'WORKFLOW_STORAGE_UNSAFE', { cause: error });
388
+ }
389
+ throw error;
390
+ }
391
+ await unlink(temp);
392
+ }
393
+ else {
394
+ await rename(temp, target);
395
+ }
396
+ const after = await lstat(target);
397
+ assertFileInfo(after, target);
398
+ const parent = await open(this.path, constants.O_RDONLY | O_DIRECTORY);
399
+ try {
400
+ await parent.sync();
401
+ }
402
+ finally {
403
+ await parent.close();
404
+ }
405
+ await this.assertRetainedIdentity(signal);
406
+ return { operation: expected.kind, version: identityOf(after), before: before ? identityOf(before) : null, after: content };
407
+ }
408
+ finally {
409
+ await tempHandle?.close().catch(() => undefined);
410
+ await unlink(temp).catch(() => undefined);
411
+ }
412
+ }
413
+ async close() {
414
+ if (this.closePromise !== undefined)
415
+ return this.closePromise;
416
+ this.closed = true;
417
+ this.closePromise = new Promise((resolve, reject) => {
418
+ this.closeWaiters.push({ resolve, reject });
419
+ void this.closeWhenUnreferenced();
420
+ });
421
+ return this.closePromise;
422
+ }
423
+ closeWaiters = [];
424
+ async closeWhenUnreferenced() {
425
+ if (!this.handleClosed && this.children.size === 0) {
426
+ this.handleClosed = true;
427
+ let error;
428
+ try {
429
+ await this.handle.close();
430
+ }
431
+ catch (cause) {
432
+ error = cause;
433
+ }
434
+ this.parent?.children.delete(this);
435
+ // A child keeps its ancestor descriptor alive, but does not implicitly
436
+ // close an ancestor that the caller still owns. If the parent was
437
+ // explicitly closed while this child was retained, complete that
438
+ // deferred close now.
439
+ if (this.parent?.closed)
440
+ await this.parent.closeWhenUnreferenced();
441
+ for (const waiter of this.closeWaiters.splice(0))
442
+ error === undefined ? waiter.resolve() : waiter.reject(error);
443
+ }
444
+ }
445
+ }
446
+ /** Adapter for an official descriptor capability. No operation below
447
+ * opens a path with Node; the path is retained only for diagnostics and for
448
+ * the provider's own `openPrivateDirectory` bootstrap seam. */
449
+ class HostPrivateDirectory {
450
+ path;
451
+ provider;
452
+ delegate;
453
+ closed = false;
454
+ constructor(path, provider, delegate) {
455
+ this.path = path;
456
+ this.provider = provider;
457
+ this.delegate = delegate;
458
+ assertHostDirectoryDelegate(delegate);
459
+ }
460
+ ensure(signal) {
461
+ if (this.closed)
462
+ throw new BoundedFileError('private directory is closed', 'WORKFLOW_STORAGE_UNSAFE');
463
+ signal?.throwIfAborted();
464
+ }
465
+ async openDirectory(name, signal, options = {}) {
466
+ component(name);
467
+ this.ensure(signal);
468
+ await this.delegate.assertIdentity(signal);
469
+ try {
470
+ const child = await this.delegate.openDirectory(name, signal, options);
471
+ assertHostDirectoryDelegate(child);
472
+ await this.delegate.assertIdentity(signal);
473
+ return new HostPrivateDirectory(join(this.path, name), this.provider, child);
474
+ }
475
+ catch (error) {
476
+ // Only the provider's explicit not-found result may enter the create
477
+ // fallback. Treating an arbitrary delegate failure (including an
478
+ // identity/link failure) as absence would silently reopen a mutable path
479
+ // and defeat the descriptor guarantee.
480
+ const code = typeof error === 'object' && error !== null && 'code' in error ? String(error.code) : '';
481
+ const missing = code === 'ENOENT' || code === 'FS_NOT_FOUND';
482
+ if (options.create !== true || !missing)
483
+ throw error;
484
+ await this.delegate.assertIdentity(signal);
485
+ let child;
486
+ if (this.delegate.target !== undefined && typeof this.provider.openPrivateDirectoryChild === 'function') {
487
+ child = await this.provider.openPrivateDirectoryChild(this.delegate.target, name, { create: true }, signal);
488
+ }
489
+ else if (this.provider.allowLegacyPathFallback === true) {
490
+ // This path-shaped adapter is retained only for explicitly marked
491
+ // legacy test fixtures. A production Host provider must expose an opaque
492
+ // child primitive; never infer fixture status from the shape of a
493
+ // target or silently reopen a lexical path after an absent result.
494
+ child = await this.provider.openPrivateDirectory(join(this.path, name), { create: true }, signal);
495
+ }
496
+ else {
497
+ throw unsupportedCapability('relative child creation');
498
+ }
499
+ try {
500
+ assertHostDirectoryDelegate(child);
501
+ await this.delegate.assertIdentity(signal);
502
+ return new HostPrivateDirectory(join(this.path, name), this.provider, child);
503
+ }
504
+ catch (creationError) {
505
+ if (typeof child.close === 'function')
506
+ await child.close().catch(() => undefined);
507
+ throw creationError;
508
+ }
509
+ }
510
+ }
511
+ async listEntries(signal) {
512
+ this.ensure(signal);
513
+ await this.delegate.assertIdentity(signal);
514
+ const direct = this.delegate.listEntries;
515
+ const list = this.provider.listDir;
516
+ let entries;
517
+ if (direct !== undefined)
518
+ entries = await direct.call(this.delegate, signal);
519
+ else if (list !== undefined && this.delegate.target !== undefined) {
520
+ // The retained opaque target is consumed directly; no lexical path is
521
+ // resolved or reopened here.
522
+ entries = await list(this.delegate.target, signal);
523
+ }
524
+ else
525
+ throw new BoundedFileError('descriptor-rooted directory listing is unavailable', 'WORKFLOW_STORAGE_UNSUPPORTED');
526
+ const result = [];
527
+ for (const entry of entries) {
528
+ signal?.throwIfAborted();
529
+ component(entry.name);
530
+ const type = entry.type === 'file' ? 'file' : entry.type === 'directory' ? 'directory' : entry.type === 'symlink' ? 'symlink' : 'other';
531
+ // Provider versions are opaque. Do not manufacture dev/ino values for
532
+ // Provider entries: a zero identity is indistinguishable from a real identity
533
+ // and would turn an unverified cleanup into a false capability claim.
534
+ // A provider version is opaque metadata, not a POSIX dev/ino pair. Do
535
+ // not manufacture NaN/zero fields and present them as an identity: the
536
+ // caller must obtain a real guarded fileInfo observation (or fail
537
+ // closed) before it can read, account, or remove the entry.
538
+ const hasPosixIdentity = Number.isSafeInteger(Number(entry.dev))
539
+ && Number.isSafeInteger(Number(entry.ino))
540
+ && Number.isSafeInteger(Number(entry.size))
541
+ && Number(entry.size) >= 0;
542
+ result.push({ name: entry.name, type, ...(hasPosixIdentity ? {
543
+ identity: {
544
+ dev: Number(entry.dev), ino: Number(entry.ino), size: Number(entry.size),
545
+ mtimeMs: Number(entry.mtimeMs ?? 0), ctimeMs: Number(entry.ctimeMs ?? 0),
546
+ mode: Number(entry.mode ?? 0), nlink: Number(entry.nlink ?? 1),
547
+ ...(entry.version === undefined ? {} : { version: entry.version }),
548
+ },
549
+ } : {}) });
550
+ }
551
+ await this.delegate.assertIdentity(signal);
552
+ return result;
553
+ }
554
+ async fileInfo(name, signal) {
555
+ component(name);
556
+ this.ensure(signal);
557
+ const direct = this.delegate.fileInfo;
558
+ await this.delegate.assertIdentity(signal);
559
+ if (direct !== undefined) {
560
+ const result = await direct.call(this.delegate, name, signal);
561
+ await this.delegate.assertIdentity(signal);
562
+ return result;
563
+ }
564
+ if (this.delegate.target === undefined || typeof this.provider.fileInfoChild !== 'function')
565
+ throw new BoundedFileError('descriptor-rooted file metadata is unavailable', 'WORKFLOW_STORAGE_UNSUPPORTED');
566
+ const result = await this.provider.fileInfoChild(this.delegate.target, name, signal);
567
+ await this.delegate.assertIdentity(signal);
568
+ return result;
569
+ }
570
+ async removeFile(name, expected, signal) {
571
+ component(name);
572
+ this.ensure(signal);
573
+ const remover = this.delegate.removeFile;
574
+ if (typeof remover === 'function')
575
+ await remover.call(this.delegate, name, expected, signal);
576
+ else if (typeof this.provider.removeFile === 'function' && this.delegate.target !== undefined) {
577
+ await this.provider.removeFile(this.delegate.target, name, expected, signal);
578
+ }
579
+ else
580
+ throw new BoundedFileError('descriptor-rooted file removal is unavailable', 'WORKFLOW_STORAGE_UNSUPPORTED');
581
+ await this.delegate.assertIdentity(signal);
582
+ }
583
+ async removeDirectory(name, expected, signal) {
584
+ component(name);
585
+ this.ensure(signal);
586
+ const remover = this.delegate.removeDirectory;
587
+ if (typeof remover === 'function')
588
+ await remover.call(this.delegate, name, expected, signal);
589
+ else if (typeof this.provider.removeDirectory === 'function' && this.delegate.target !== undefined) {
590
+ await this.provider.removeDirectory(this.delegate.target, name, expected, signal);
591
+ }
592
+ else
593
+ throw new BoundedFileError('descriptor-rooted directory removal is unavailable', 'WORKFLOW_STORAGE_UNSUPPORTED');
594
+ await this.delegate.assertIdentity(signal);
595
+ }
596
+ async publishDirectory(name, target, targetName, signal) {
597
+ component(name);
598
+ component(targetName);
599
+ this.ensure(signal);
600
+ await this.delegate.assertIdentity(signal);
601
+ await target.assertIdentity(signal);
602
+ if (!(target instanceof HostPrivateDirectory))
603
+ throw new BoundedFileError('private directory publication providers do not match', 'WORKFLOW_STORAGE_UNSUPPORTED');
604
+ const direct = this.delegate.publishDirectory;
605
+ if (typeof direct === 'function') {
606
+ await direct.call(this.delegate, name, target.delegate, targetName, signal);
607
+ }
608
+ else if (typeof this.provider.publishDirectory === 'function' && this.delegate.target !== undefined && target.delegate.target !== undefined) {
609
+ await this.provider.publishDirectory(this.delegate.target, name, target.delegate.target, targetName, signal);
610
+ }
611
+ else
612
+ throw new BoundedFileError('descriptor-rooted directory publication is unavailable', 'WORKFLOW_STORAGE_UNSUPPORTED');
613
+ await this.delegate.assertIdentity(signal);
614
+ await target.delegate.assertIdentity(signal);
615
+ }
616
+ async readBytes(name, signal, maxBytes) {
617
+ component(name);
618
+ this.ensure(signal);
619
+ return this.delegate.readBytes(name, signal, maxBytes);
620
+ }
621
+ async writeText(name, content, expected, signal) {
622
+ component(name);
623
+ this.ensure(signal);
624
+ const guarded = expected.kind === 'replaceIfVersion' && expected.version !== undefined
625
+ && typeof expected.version === 'object' && expected.version !== null && 'version' in expected.version
626
+ ? { ...expected, version: expected.version.version }
627
+ : expected;
628
+ const outcome = await this.delegate.writeText(name, content, guarded, signal);
629
+ return outcome;
630
+ }
631
+ async assertIdentity(signal) { this.ensure(signal); await this.delegate.assertIdentity(signal); }
632
+ async close() { if (this.closed)
633
+ return; this.closed = true; await this.delegate.close(); }
634
+ }
635
+ function sameVersion(candidate, before) {
636
+ const expected = identityOf(before);
637
+ let compared = false;
638
+ // A local guarded write must carry the stable device/inode pair. Accepting
639
+ // an empty or unrelated object here would turn replaceIfVersion into an
640
+ // unconditional rename. Host-backed directories do their opaque-version
641
+ // comparison in the provider adapter and do not call this helper.
642
+ for (const key of ['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs', 'mode', 'nlink']) {
643
+ if (candidate[key] === undefined)
644
+ continue;
645
+ compared = true;
646
+ const value = Number(candidate[key]);
647
+ if (!Number.isFinite(value) || value !== expected[key])
648
+ return false;
649
+ }
650
+ if (candidate.version !== undefined) {
651
+ compared = true;
652
+ const beforeVersion = before.version;
653
+ if (beforeVersion === undefined || !isSameOpaque(candidate.version, beforeVersion))
654
+ return false;
655
+ }
656
+ return compared && candidate.dev !== undefined && candidate.ino !== undefined;
657
+ }
658
+ function isSameOpaque(left, right) {
659
+ if (Object.is(left, right))
660
+ return true;
661
+ try {
662
+ return JSON.stringify(left) === JSON.stringify(right);
663
+ }
664
+ catch {
665
+ return false;
666
+ }
667
+ }
668
+ async function canonicalPrivatePath(path) {
669
+ const parsed = parse(path);
670
+ const allowedAliases = process.platform === 'darwin' ? new Set(['/var', '/tmp', '/etc']) : new Set();
671
+ let lexical = parsed.root;
672
+ for (const part of path.slice(parsed.root.length).split(/[\\/]+/u).filter(Boolean)) {
673
+ lexical = join(lexical, part);
674
+ try {
675
+ const entry = await lstat(lexical);
676
+ if (entry.isSymbolicLink() && !allowedAliases.has(lexical))
677
+ throw new BoundedFileError(`workflow storage path "${path}" is unsafe: ancestor "${lexical}" is a symbolic link`, 'WORKFLOW_STORAGE_UNSAFE');
678
+ }
679
+ catch (error) {
680
+ if (error.code === 'ENOENT')
681
+ break;
682
+ throw error;
683
+ }
684
+ }
685
+ let current = path;
686
+ const missing = [];
687
+ while (true) {
688
+ try {
689
+ const info = await lstat(current);
690
+ if (info.isSymbolicLink() && missing.length === 0)
691
+ throw new BoundedFileError(`workflow storage path "${path}" is unsafe: symbolic-link directory`, 'WORKFLOW_STORAGE_UNSAFE');
692
+ if (!info.isDirectory() && !info.isSymbolicLink() && missing.length > 0)
693
+ throw new BoundedFileError(`workflow storage path "${current}" is unsafe: ancestor is not a directory`, 'WORKFLOW_STORAGE_UNSAFE');
694
+ const resolved = await realpath(current);
695
+ return missing.length === 0 ? resolved : join(resolved, ...missing);
696
+ }
697
+ catch (error) {
698
+ if (error.code !== 'ENOENT')
699
+ throw error;
700
+ const parent = dirname(current);
701
+ if (parent.length === 0 || parent === current || current === parsed.root)
702
+ throw error;
703
+ const componentName = current.slice(parent.length + 1);
704
+ missing.unshift(componentName);
705
+ current = parent;
706
+ }
707
+ }
708
+ }
709
+ /** Open one owner-only private directory and pin its descriptor identity. */
710
+ export async function openPrivateDirectory(path, create = true) {
711
+ if (O_NOFOLLOW === undefined || O_NOFOLLOW === 0 || process.platform === 'win32')
712
+ failUnsupported();
713
+ if (!isAbsolute(path))
714
+ throw new BoundedFileError(`workflow storage path "${path}" is unsafe: path must be absolute`, 'WORKFLOW_STORAGE_UNSAFE');
715
+ const normalized = await canonicalPrivatePath(path.replace(/[\\/]+$/u, '') || parse(path).root);
716
+ const parsed = parse(normalized);
717
+ const parts = normalized.slice(parsed.root.length).split(/[\\/]+/u).filter(Boolean);
718
+ let currentPath = parsed.root;
719
+ let parent;
720
+ let current;
721
+ const opened = [];
722
+ try {
723
+ // Open every ancestor one component at a time. This retains the full
724
+ // descriptor chain instead of opening a deep path after a string-only
725
+ // lstat walk, which is vulnerable to an ancestor substitution race.
726
+ const rootHandle = await open(parsed.root, constants.O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
727
+ const rootInfo = await rootHandle.stat();
728
+ assertDirectoryInfo(rootInfo, parsed.root, true);
729
+ current = new LocalPrivateDirectory(parsed.root, rootHandle, { dev: Number(rootInfo.dev), ino: Number(rootInfo.ino) }, undefined, true);
730
+ opened.push(current);
731
+ parent = current;
732
+ for (const [partIndex, part] of parts.entries()) {
733
+ const childPath = join(currentPath, part);
734
+ let childInfo;
735
+ try {
736
+ childInfo = await lstat(childPath);
737
+ if (childInfo.isSymbolicLink() || !childInfo.isDirectory())
738
+ throw new BoundedFileError(`workflow storage path "${childPath}" is unsafe: expected directory`, 'WORKFLOW_STORAGE_UNSAFE');
739
+ if (partIndex === parts.length - 1)
740
+ assertDirectoryInfo(childInfo, childPath);
741
+ else
742
+ assertAncestorDirectoryInfo(childInfo, childPath);
743
+ }
744
+ catch (error) {
745
+ if (error.code !== 'ENOENT' || !create)
746
+ throw error;
747
+ await mkdir(childPath, { mode: 0o700 });
748
+ await chmod(childPath, 0o700);
749
+ childInfo = await lstat(childPath);
750
+ if (partIndex === parts.length - 1)
751
+ assertDirectoryInfo(childInfo, childPath);
752
+ else
753
+ assertAncestorDirectoryInfo(childInfo, childPath);
754
+ }
755
+ const childHandle = await open(childPath, constants.O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
756
+ try {
757
+ const retained = await childHandle.stat();
758
+ if (partIndex === parts.length - 1)
759
+ assertDirectoryInfo(retained, childPath);
760
+ else
761
+ assertAncestorDirectoryInfo(retained, childPath);
762
+ const pathInfo = await lstat(childPath);
763
+ if (!sameIdentity(retained, pathInfo))
764
+ throw new BoundedFileError(`workflow storage path "${childPath}" is unsafe: identity changed`, 'WORKFLOW_STORAGE_UNSAFE');
765
+ const previousParent = parent;
766
+ current = new LocalPrivateDirectory(childPath, childHandle, { dev: Number(retained.dev), ino: Number(retained.ino) }, previousParent, false, partIndex === parts.length - 1);
767
+ opened.push(current);
768
+ if (previousParent !== undefined)
769
+ void previousParent.close();
770
+ parent = current;
771
+ currentPath = childPath;
772
+ }
773
+ catch (error) {
774
+ await childHandle.close().catch(() => undefined);
775
+ throw error;
776
+ }
777
+ }
778
+ return current;
779
+ }
780
+ catch (error) {
781
+ // Close leaf-to-root so each retained ancestor can release immediately.
782
+ for (const directory of opened.reverse())
783
+ await directory.close().catch(() => undefined);
784
+ throw error;
785
+ }
786
+ }
787
+ export async function openVerifiedRunDirectory(layout, runDirectory) {
788
+ component(runDirectory);
789
+ const directory = await layout.runs.openDirectory(runDirectory);
790
+ return { id: runDirectory, directory, scriptPath: join(layout.runs.path, runDirectory, 'script.js'), assertIdentity: () => directory.assertIdentity() };
791
+ }
792
+ export async function initializePrivateLayout(anchor, lease, hostFileSystem) {
793
+ await anchor.assertCurrent();
794
+ await lease.assertCurrent();
795
+ if (hostFileSystem !== undefined) {
796
+ if (typeof hostFileSystem.openPrivateDirectory !== 'function')
797
+ failUnsupported();
798
+ const rootDelegate = await hostFileSystem.openPrivateDirectory(anchor.root, { create: false });
799
+ try {
800
+ assertHostDirectoryDelegate(rootDelegate);
801
+ assertHostStorageCapabilities(hostFileSystem, rootDelegate);
802
+ await rootDelegate.assertIdentity();
803
+ }
804
+ catch (error) {
805
+ if (typeof rootDelegate.close === 'function')
806
+ await rootDelegate.close().catch(() => undefined);
807
+ throw error;
808
+ }
809
+ const root = new HostPrivateDirectory(anchor.root, hostFileSystem, rootDelegate);
810
+ const opened = [];
811
+ try {
812
+ // Create/open categories through the retained root capability. Do not
813
+ // reopen an absolute child path after the anchor has been pinned: that
814
+ // would reintroduce an ancestor substitution window in Host adapters.
815
+ const sessions = await root.openDirectory('sessions', undefined, { create: true });
816
+ opened.push(sessions);
817
+ const runs = await root.openDirectory('runs', undefined, { create: true });
818
+ opened.push(runs);
819
+ const staging = await root.openDirectory('staging', undefined, { create: true });
820
+ opened.push(staging);
821
+ const quarantine = await root.openDirectory('quarantine', undefined, { create: true });
822
+ opened.push(quarantine);
823
+ return { anchor, lease, root, rootDirectory: root, sessions, runs, staging, quarantine };
824
+ }
825
+ catch (error) {
826
+ for (const directory of opened.reverse())
827
+ await directory.close().catch(() => undefined);
828
+ await root.close().catch(() => undefined);
829
+ throw error;
830
+ }
831
+ }
832
+ const root = await openPrivateDirectory(anchor.root, false);
833
+ const opened = [];
834
+ try {
835
+ const sessions = await root.openDirectory('sessions', undefined, { create: true });
836
+ opened.push(sessions);
837
+ const runs = await root.openDirectory('runs', undefined, { create: true });
838
+ opened.push(runs);
839
+ const staging = await root.openDirectory('staging', undefined, { create: true });
840
+ opened.push(staging);
841
+ const quarantine = await root.openDirectory('quarantine', undefined, { create: true });
842
+ opened.push(quarantine);
843
+ // Keep the root capability alive for the lifetime of every child. The
844
+ // child capabilities also retain this parent, but making it explicit in
845
+ // the layout gives teardown and Host implementations one authoritative
846
+ // owner and prevents accidental use-after-close during cleanup.
847
+ return { anchor, lease, root, rootDirectory: root, sessions, runs, staging, quarantine };
848
+ }
849
+ catch (error) {
850
+ // A partially-created layout must not leave descriptors behind. Keep the
851
+ // original bootstrap error authoritative while making best-effort cleanup.
852
+ for (const directory of opened.reverse())
853
+ await directory.close().catch(() => undefined);
854
+ await root.close().catch(() => undefined);
855
+ throw error;
856
+ }
857
+ }
858
+ /** Alias using the name from the storage design. */
859
+ export const initializeLeasedWorkflowStorage = initializePrivateLayout;
860
+ //# sourceMappingURL=private-root.js.map