@struktoai/mirage-dsh 0.0.1 → 0.0.6

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.
@@ -0,0 +1,258 @@
1
+ import { Context } from '@deepseek-ai/cordis';
2
+ import { ShellExecutor, ShellExecRequest, ShellExecSpec, ShellRunResult, ShellProcess } from '@deepseek-ai/dsh-shell';
3
+
4
+ /** Configuration for the mirage shell executor. */
5
+ interface MirageShellConfig {
6
+ /**
7
+ * Default working directory for commands. Defaults to `/`. With
8
+ * `sessionId` it instead seeds the bound session's initial cwd, and the
9
+ * session's own cwd is the default from then on.
10
+ */
11
+ workdir?: string;
12
+ /** Default foreground timeout in milliseconds. Defaults to 120000. */
13
+ defaultTimeoutMs?: number;
14
+ /** Upper cap on any requested timeout. Defaults to 600000. */
15
+ maxTimeoutMs?: number;
16
+ /** Default stdout capture budget in bytes. Defaults to 200000. */
17
+ stdoutMaxBytes?: number;
18
+ /** stderr capture budget in bytes. Defaults to 64000. */
19
+ stderrMaxBytes?: number;
20
+ /**
21
+ * Bind every command to this named workspace session. By default each
22
+ * command runs in an ephemeral fork of the workspace's default session,
23
+ * so nothing persists between calls, which is the one-shot contract of
24
+ * dsh's bash tool. With a session bound, `cd`, `export`, and function
25
+ * definitions persist across calls, the persistent-shell contract. The
26
+ * session is created on first use if the workspace does not have it; an
27
+ * existing session is adopted as is.
28
+ *
29
+ * A spec carrying an explicit `env`, or a `workdir` that names a real
30
+ * directory in this world, still runs as a one-call subshell of the
31
+ * bound session, per mirage's `ExecuteOptions` semantics: both say
32
+ * "just for this command". The two things dsh injects on every call
33
+ * are deliberately not read that way, since neither carries that
34
+ * intent and either would fork every command and leave the binding
35
+ * with nothing to persist. A workdir resolved on the harness's own
36
+ * machine names nothing here and is dropped; the managed `DSH_*`
37
+ * snapshot is seeded into the session instead.
38
+ */
39
+ sessionId?: string;
40
+ /**
41
+ * When set, a background command whose streamed output overruns its
42
+ * delta budget spills its full stdout and stderr to files under this
43
+ * workspace directory, and `readOutput()` points at them so a reader
44
+ * can recover what the delta dropped. The directory is a workspace
45
+ * path (e.g. `/tmp` on a ram mount), so the agent reads the spill
46
+ * through the same VFS as everything else; the writes go through the
47
+ * workspace, so they appear in history like any other write. Unset
48
+ * (the default) means no spill: output that overruns is simply
49
+ * flagged `lossy`, the honest "no safe path available" answer.
50
+ */
51
+ spillDir?: string;
52
+ }
53
+ /**
54
+ * Mirage-backed implementation of `ctx.shell`: `run` executes the command
55
+ * line with mirage's own shell (coreutils-faithful commands, installed
56
+ * CLIs, the policy layer) against the shared `ctx.mirage` workspace, so a
57
+ * path from `ctx.fs` means the same file here. There is no OS process
58
+ * behind a command: `signal` in results is a compatibility value for kills,
59
+ * and abort/timeout act cooperatively at the executor's own boundaries.
60
+ *
61
+ * Every command runs in an ephemeral fork of the workspace's default
62
+ * session, so no shell state survives from one call to the next, matching
63
+ * the one-shot contract of dsh's bash tool. Configuring a `sessionId`
64
+ * binds all commands to one named session instead, whose cwd, exports,
65
+ * and functions persist across calls.
66
+ */
67
+ declare class MirageShellExecutor extends ShellExecutor {
68
+ static readonly inject: string[];
69
+ private readonly workdir;
70
+ private readonly defaultTimeoutMs;
71
+ private readonly maxTimeoutMs;
72
+ private readonly stdoutMaxBytes;
73
+ private readonly stderrMaxBytes;
74
+ private readonly sessionId;
75
+ private readonly spillDir;
76
+ private sessionReady;
77
+ private readOnlyReady;
78
+ constructor(ctx: Context, config?: MirageShellConfig);
79
+ private workspace;
80
+ /**
81
+ * The directory this command actually runs in.
82
+ *
83
+ * dsh fills an unspecified workdir from the calling session's cwd (by
84
+ * way of the sandbox policy's workspace root), and that is a directory
85
+ * on the harness's own machine, which names nothing here. Running
86
+ * there leaves `pwd` reporting a path the agent cannot reach and every
87
+ * relative path failing, and, because a per-call cwd forks a subshell,
88
+ * it also defeats a bound session on every call. So a workdir that is
89
+ * not a directory in this world is treated as unset: the configured
90
+ * default when unbound, the session's own cwd when bound.
91
+ *
92
+ * @param spec the resolved spec whose workdir is being placed.
93
+ * @returns the workdir to execute under, `''` meaning the session's own.
94
+ */
95
+ private worldWorkdir;
96
+ private newSpill;
97
+ /**
98
+ * With every runtime in the world reaching only the vfs
99
+ * (`ctx.mirage.vfsOnly`), the workspace dispatch is the single gate
100
+ * for anything a command can do, so this executor behaves like a
101
+ * workspace-write sandbox: reads and writes land only where mounts
102
+ * (and their modes) allow. Declaring it lets sandbox-aware plugins
103
+ * (dsh's permission presets) compose over this executor. A world
104
+ * holding a runtime with doors around the gate (the host `local`
105
+ * python, a remote sandbox) voids that claim, so this answers
106
+ * undefined then (the base contract's "does not sandbox") and those
107
+ * plugins refuse to compose instead of trusting a lie.
108
+ */
109
+ get sandboxMode(): ShellExecutor['sandboxMode'];
110
+ /**
111
+ * The mode this one call runs under: the policy the caller resolved
112
+ * for it, or this executor's own default when the call carried none.
113
+ * Undefined keeps the "no claim" answer for a world some runtime can
114
+ * act outside of, where no mode would be true.
115
+ *
116
+ * @param spec the resolved spec whose policy is being read.
117
+ * @returns the effective mode, or undefined when nothing is claimed.
118
+ */
119
+ private modeFor;
120
+ /**
121
+ * The session this call runs in: the read-only twin when the policy
122
+ * confines it to reads, else this executor's own binding.
123
+ *
124
+ * `workspace-write` and `danger-full-access` both run in the ordinary
125
+ * session, because the mounts and their modes already are the
126
+ * workspace boundary and mirage has nothing wider to grant.
127
+ *
128
+ * @param spec the resolved spec whose policy selects the session.
129
+ * @returns the session id to execute under, or undefined for the default.
130
+ */
131
+ private sessionFor;
132
+ /**
133
+ * The sandbox facts to stamp on this run's result and process handle,
134
+ * or undefined when the world is not fully workspace-bound (no claim).
135
+ *
136
+ * `enforcement` is 'full': when every runtime reaches only the vfs, the
137
+ * workspace gate cannot be bypassed, so unlike an OS sandbox on an older
138
+ * kernel there is no promised effect it fails to govern. `runnerFailed`
139
+ * is false because the workspace executor is the runner and a failure to
140
+ * run surfaces as a rejected/aborted execution, not a runner that never
141
+ * started.
142
+ *
143
+ * @param spec the resolved spec this run was built from.
144
+ * @param denied whether the run was refused a write by the narrowing.
145
+ * @returns the facts to stamp, or undefined when nothing is claimed.
146
+ */
147
+ private sandboxInfo;
148
+ /**
149
+ * The retention budget of a background command's console.
150
+ *
151
+ * Only the background path caps retention: there a follow loop drains
152
+ * the console while the command still runs, so the budget bounds what
153
+ * the loop has not reached yet, and a chunk lost to it is reported as
154
+ * a gap in the sequence. A foreground console is read once, after the
155
+ * fact, with nothing to bound.
156
+ *
157
+ * @param spec the resolved spec carrying the stdout budget.
158
+ * @returns the retention budget in bytes.
159
+ */
160
+ private retentionFor;
161
+ /**
162
+ * Take a finished run's two streams off its console, capped, and spill
163
+ * whichever one lost bytes.
164
+ *
165
+ * A foreground run streams into a console rather than returning its
166
+ * output whole, because mirage throws on abort: bytes a killed command
167
+ * had already printed are recoverable from a sink and nowhere else.
168
+ * Only a truncated stream spills, since an untruncated one is already
169
+ * whole in `text` and writing a file for it would put a copy of every
170
+ * command's output on a mount. The console this reads is untrimmed, so
171
+ * a spill is the whole stream rather than the tail of one.
172
+ *
173
+ * @param console_ the console this run streamed into.
174
+ * @param spec the resolved spec carrying the stdout budget.
175
+ * @returns the capped streams, each with a spill path when it lost bytes.
176
+ */
177
+ private collectFrom;
178
+ /**
179
+ * Whether this run was refused, by the session's permission document
180
+ * or by the read-only narrowing.
181
+ *
182
+ * The document's refusals ride the result itself: a `Deny`, an
183
+ * unanswered ask and a policy that raised all leave `refusal` on the
184
+ * `ExecuteResult`, whatever the line did with its streams (`2>&1`, a
185
+ * trailing command that owns the status). That record is read for
186
+ * every call, because a role's `commands.deny` and `commands.ask`
187
+ * rules bind under `workspace-write` and `danger-full-access` alike:
188
+ * a mode says what the mounts allow, and says nothing about whether a
189
+ * rule forbids the line. The narrowing has no record, since it is
190
+ * EROFS/EACCES from the mounts, so its signatures are still read off
191
+ * stderr, and only for a call that ran read-only, where this executor
192
+ * is what imposed it.
193
+ *
194
+ * @param spec the resolved spec this run was built from.
195
+ * @param result what the workspace answered.
196
+ * @param stderr the run's captured standard error.
197
+ * @returns true when something refused the run.
198
+ */
199
+ private wasDenied;
200
+ resolve(request: ShellExecRequest): ShellExecSpec;
201
+ private ensureSession;
202
+ /**
203
+ * Seed this call's managed `DSH_*` snapshot into the bound session.
204
+ *
205
+ * These are harness facts about the session (its home, its id), not
206
+ * overrides for one command, and on a bound session they have to live
207
+ * in the session: handed over as a per-call `env` they would fork a
208
+ * subshell on every call, since dsh never sends an empty snapshot.
209
+ *
210
+ * The snapshot replaces rather than merges, per the seam's own rule
211
+ * that a fact absent from the current snapshot must not inherit a
212
+ * stale value from an earlier one. Only the managed namespace is
213
+ * touched, so a variable the agent exported itself is left alone.
214
+ *
215
+ * @param ws the live workspace holding the session.
216
+ * @param sessionId the session this call runs in.
217
+ * @param spec the resolved spec carrying the snapshot.
218
+ */
219
+ private applyManagedEnv;
220
+ private readOnlySession;
221
+ /**
222
+ * Create (once) the session a read-only call runs in: a twin of the
223
+ * session this executor would otherwise use, with every grant it holds
224
+ * narrowed to `read`, so mirage's own dispatch is what refuses the
225
+ * write rather than a second permission layer bolted on here.
226
+ *
227
+ * The twin narrows, never widens, and that takes every part of the
228
+ * source's view, which is what `narrow` in core stamps: modes, hidden
229
+ * paths, hidden variables, command rules. Its modes cover every mount
230
+ * at `read` (the one exception is the null sink, per
231
+ * {@link SINK_PREFIX}), which is at least as narrow as whatever the
232
+ * source held, since `read` is the weakest mode there is; naming a
233
+ * mount only narrows it, so a prefix the map omits would keep its own
234
+ * mode rather than disappear. The other three are copied from the
235
+ * source session rather than recompiled, because the profile it was
236
+ * created under is not something a session records.
237
+ *
238
+ * Leaving any of them behind widens. Hides are the obvious one: a
239
+ * binding confined to `/allowed` would read `/secret` in read-only
240
+ * mode although the same command is refused outside it. Command rules
241
+ * are the one modes cannot stand in for, because a mode bounds a
242
+ * mount and an account CLI reaches a service: a profile that denies
243
+ * `slack message send` or `git push` still denies it here, where
244
+ * every mount being `read` says nothing at all about it.
245
+ *
246
+ * The policy's `workspaceRoot` is deliberately not consulted anywhere:
247
+ * it is a directory on the harness's machine, so containment against
248
+ * it says nothing about this world. The mounts are the boundary.
249
+ *
250
+ * @returns the id of the read-only session.
251
+ */
252
+ private provisionReadOnly;
253
+ private provisionSession;
254
+ run(spec: ShellExecSpec): Promise<ShellRunResult>;
255
+ start(spec: ShellExecSpec): ShellProcess;
256
+ }
257
+
258
+ export { type MirageShellConfig as M, MirageShellExecutor as a };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@struktoai/mirage-dsh",
3
- "version": "0.0.1",
3
+ "version": "0.0.6",
4
4
  "description": "DeepSeek Harness (dsh) providers backed by a mirage workspace: ctx.fs and ctx.shell over mounted resources",
5
5
  "homepage": "https://github.com/strukto-ai/mirage#readme",
6
6
  "bugs": {
@@ -17,6 +17,9 @@
17
17
  "access": "public"
18
18
  },
19
19
  "type": "module",
20
+ "engines": {
21
+ "node": ">=20.10.0"
22
+ },
20
23
  "main": "./dist/index.js",
21
24
  "types": "./dist/index.d.ts",
22
25
  "exports": {
@@ -47,18 +50,18 @@
47
50
  "cordis.patch.yml"
48
51
  ],
49
52
  "dependencies": {
50
- "@struktoai/mirage-core": "0.0.5",
51
- "@struktoai/mirage-node": "0.0.5"
53
+ "@struktoai/mirage-core": "0.0.6",
54
+ "@struktoai/mirage-node": "0.0.6"
52
55
  },
53
56
  "peerDependencies": {
54
57
  "@deepseek-ai/cordis": "^4.0.1",
55
- "@deepseek-ai/dsh-fs": "0.0.1-rc.1",
56
- "@deepseek-ai/dsh-shell": "0.0.1-rc.5"
58
+ "@deepseek-ai/dsh-fs": "0.1.1-rc.2",
59
+ "@deepseek-ai/dsh-shell": "0.1.1-rc.2"
57
60
  },
58
61
  "devDependencies": {
59
62
  "@deepseek-ai/cordis": "^4.0.1",
60
- "@deepseek-ai/dsh-fs": "0.0.1-rc.1",
61
- "@deepseek-ai/dsh-shell": "0.0.1-rc.5",
63
+ "@deepseek-ai/dsh-fs": "0.1.1-rc.2",
64
+ "@deepseek-ai/dsh-shell": "0.1.1-rc.2",
62
65
  "tsup": "^8.5.0",
63
66
  "typescript": "^6.0.0",
64
67
  "vitest": "^3.2.6"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/fs.ts","../src/errors.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { Buffer } from 'node:buffer'\nimport { posix } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'\nimport type {\n FsDirEntry,\n FsEditOutcome,\n FsEditRequest,\n FsInfo,\n FsPathInfo,\n FsTarget,\n FsWriteIntent,\n FsWriteOutcome,\n} from '@deepseek-ai/dsh-fs'\nimport { compareCodePoints, FileType, isMissingPath } from '@struktoai/mirage-core'\nimport type { FileStat, Ops } from '@struktoai/mirage-core'\nimport { assertNotAborted, mapMirageError } from './errors.ts'\nimport {\n applyLiteralEdit,\n decodeStrictText,\n detectsCrlf,\n normalizeLineEndings,\n restoreLineEndings,\n} from './text.ts'\nimport type {} from './service.ts'\n\ntype LinksSeam = NonNullable<Ops['links']>\n\nconst DEFAULT_DIFF_BASIS_MAX_BYTES = 10 * 1024 * 1024\n\n/** Configuration for the mirage filesystem backend. */\nexport interface MirageFsConfig {\n /** Virtual base directory for relative paths. Defaults to `/`. */\n cwd?: string\n /** Exclusive byte limit on each overwrite-diff side. Defaults to 10 MiB. */\n diffBasisMaxBytes?: number\n}\n\n// The freshness token behind FsVersion. Backends that report a fingerprint\n// or revision get a content-derived token; the rest fall back to metadata\n// (modified + size), and a backend reporting neither yields a constant, so\n// stale guards degrade to always-fresh there instead of always-stale.\nfunction versionOf(stat: FileStat): FsVersion {\n if (stat.fingerprint !== null) return FsVersion(`fp:${stat.fingerprint}`)\n if (stat.revision !== null) return FsVersion(`rev:${stat.revision}`)\n if (stat.modified !== null || stat.size !== null) {\n return FsVersion(`meta:${stat.modified ?? ''}:${String(stat.size ?? '')}`)\n }\n return FsVersion('unversioned')\n}\n\nfunction typeOf(stat: FileStat): FsInfo['type'] {\n switch (stat.type) {\n case FileType.DIRECTORY:\n return 'directory'\n case FileType.SYMLINK:\n return 'other'\n default:\n return 'file'\n }\n}\n\nasync function* singleChunk(text: string): AsyncGenerator<string, void, void> {\n yield await Promise.resolve(text)\n}\n\n/**\n * Mirage-backed implementation of `ctx.fs`. Targets are canonical virtual\n * paths (namespace symlinks followed), every operation walks the workspace\n * op door — session grants, admission policies, cache read-through and\n * post-write invalidation all fire exactly as they do for a shell command —\n * and `processPath` answers in the same virtual path space the mirage shell\n * executes in, so the two providers share one execution world.\n */\nexport class MirageFileSystem extends FileSystem {\n static readonly inject = ['mirage']\n\n private fsOps: Ops | null = null\n private readonly cwd: string\n private readonly diffBasisMaxBytes: number\n // Per-targetKey tail promise: serializes mutating ops so the\n // read -> guard -> write window cannot interleave (one concurrent writer\n // wins, the rest observe the new version and reject as stale).\n private readonly locks = new Map<string, Promise<void>>()\n\n constructor(ctx: Context, config: MirageFsConfig = {}) {\n super(ctx)\n this.cwd = config.cwd ?? '/'\n this.diffBasisMaxBytes = config.diffBasisMaxBytes ?? DEFAULT_DIFF_BASIS_MAX_BYTES\n }\n\n // The workspace may still be building (declarative mounts resolve\n // asynchronously), so every entry point awaits the service's `ready`\n // once and caches the op door. The caller's signal can fire during\n // that wait, after its entry assertion passed, so it is asserted\n // again here, before the op it guards dispatches.\n private async ops(signal?: AbortSignal, operation = 'ready'): Promise<Ops> {\n this.fsOps ??= (await this.ctx.mirage.ready).fs\n assertNotAborted(signal, operation)\n return this.fsOps\n }\n\n private get links(): LinksSeam | null {\n if (this.fsOps === null) {\n throw new Error('mirage: filesystem used before the workspace is ready')\n }\n return this.fsOps.links\n }\n\n private normalize(path: string, cwd?: string): string {\n return posix.resolve(cwd ?? this.cwd, path)\n }\n\n private follow(path: string): string {\n const links = this.links\n if (links === null) return path\n try {\n return links.follow(path)\n } catch (err) {\n throw mapMirageError(err, 'resolve', path)\n }\n }\n\n private withLock<T>(key: FsTargetKey, body: () => Promise<T>): Promise<T> {\n const mapKey = String(key)\n const previous = this.locks.get(mapKey) ?? Promise.resolve()\n const next = previous.then(body, body)\n // The stored tail is void (an outcome would pin whole file contents in\n // the map) and drops on settlement unless a newer op queued behind it.\n const tail = next.then(\n () => undefined,\n () => undefined,\n )\n this.locks.set(mapKey, tail)\n void tail.then(() => {\n if (this.locks.get(mapKey) === tail) this.locks.delete(mapKey)\n })\n return next\n }\n\n async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {\n assertNotAborted(opts?.signal, 'resolve')\n await this.ops(opts?.signal, 'resolve')\n const followed = this.follow(this.normalize(path, opts?.cwd))\n return { targetKey: FsTargetKey(followed), displayPath: followed }\n }\n\n processPath(target: FsTarget): string {\n return String(target.targetKey)\n }\n\n fileUrl(target: FsTarget): string {\n const encoded = String(target.targetKey).split('/').map(encodeURIComponent).join('/')\n return `file://${encoded}`\n }\n\n contains(parent: FsTarget, child: FsTarget): boolean {\n const parentKey = String(parent.targetKey)\n const childKey = String(child.targetKey)\n if (parentKey === childKey) return true\n const prefix = parentKey === '/' ? '/' : `${parentKey}/`\n return childKey.startsWith(prefix)\n }\n\n async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {\n assertNotAborted(signal, 'stat')\n const key = String(target.targetKey)\n let stat: FileStat\n try {\n stat = await (await this.ops(signal, 'stat')).stat(key)\n } catch (err) {\n if (isMissingPath(err)) return undefined\n throw mapMirageError(err, 'stat', target.displayPath)\n }\n return {\n version: versionOf(stat),\n type: typeOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async lstat(\n path: string,\n opts?: { cwd?: string },\n signal?: AbortSignal,\n ): Promise<FsPathInfo | undefined> {\n assertNotAborted(signal, 'lstat')\n await this.ops(signal, 'lstat')\n const normalized = this.normalize(path, opts?.cwd)\n // Follow every component except the last: the probe is about the path\n // entry itself, so a link at the leaf must report as one.\n const parentFollowed =\n normalized === '/'\n ? '/'\n : posix.join(this.follow(posix.dirname(normalized)), posix.basename(normalized))\n const links = this.links\n if (links?.isLink(parentFollowed) === true) {\n const linkTarget = links.readlink(parentFollowed) ?? ''\n return {\n version: FsVersion(`link:${linkTarget}`),\n type: 'symlink',\n size: new TextEncoder().encode(linkTarget).byteLength,\n }\n }\n let stat: FileStat\n try {\n stat = await (await this.ops(signal, 'lstat')).stat(parentFollowed)\n } catch (err) {\n if (isMissingPath(err)) return undefined\n throw mapMirageError(err, 'lstat', normalized)\n }\n return {\n version: versionOf(stat),\n type: typeOf(stat) === 'directory' ? 'directory' : typeOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async readText(target: FsTarget, signal?: AbortSignal): Promise<string> {\n assertNotAborted(signal, 'read')\n const key = String(target.targetKey)\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops(signal, 'read')).readFile(key)\n } catch (err) {\n throw mapMirageError(err, 'read', target.displayPath)\n }\n return decodeStrictText(bytes, target.displayPath)\n }\n\n async streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>> {\n // mirage reads land as one rendered buffer, so streaming is a single\n // chunk with readText's exact text semantics.\n const text = await this.readText(target, signal)\n return singleChunk(text)\n }\n\n async listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]> {\n assertNotAborted(signal, 'list')\n const key = String(target.targetKey)\n const info = await this.stat(target, signal)\n if (info === undefined) {\n throw new FsError(\n `cannot list \"${target.displayPath}\": no such file or directory`,\n 'FS_NOT_FOUND',\n )\n }\n if (info.type !== 'directory') {\n throw new FsError(`cannot list \"${target.displayPath}\": not a directory`, 'FS_NOT_DIRECTORY')\n }\n let children: string[]\n try {\n children = await (await this.ops(signal, 'list')).readdir(key)\n } catch (err) {\n throw mapMirageError(err, 'list', target.displayPath)\n }\n const names = new Set<string>()\n for (const childPath of children) {\n names.add(posix.basename(childPath))\n }\n // Namespace symlinks live above every backend, so a listing merges\n // them in here exactly as mirage's own ls does.\n const links = this.links\n if (links !== null) {\n const base = key === '/' ? '/' : `${key}/`\n for (const linkPath of links.symlinkTargets().keys()) {\n if (linkPath.startsWith(base) && !linkPath.slice(base.length).includes('/')) {\n names.add(linkPath.slice(base.length))\n }\n }\n }\n const entries: FsDirEntry[] = []\n for (const name of [...names].sort(compareCodePoints)) {\n entries.push(await this.dirEntry(key, name))\n }\n return entries\n }\n\n private async dirEntry(parentKey: string, name: string): Promise<FsDirEntry> {\n const childPath = parentKey === '/' ? `/${name}` : `${parentKey}/${name}`\n let followed: string\n try {\n followed = this.follow(childPath)\n } catch {\n // A cyclic link must not reject the whole listing; it lists as an\n // entry of unknown kind keyed on its own path.\n return {\n name,\n type: 'other',\n target: { targetKey: FsTargetKey(childPath), displayPath: childPath },\n }\n }\n const target: FsTarget = { targetKey: FsTargetKey(followed), displayPath: childPath }\n let stat: FileStat\n try {\n stat = await (await this.ops()).stat(followed)\n } catch {\n // A child the listing named but stat cannot classify (a broken link,\n // a race with a delete) still lists, as an entry of unknown kind.\n return { name, type: 'other', target }\n }\n return {\n name,\n type: typeOf(stat),\n target,\n version: versionOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async writeText(\n target: FsTarget,\n content: string,\n expected?: FsWriteIntent,\n signal?: AbortSignal,\n ): Promise<FsWriteOutcome> {\n return this.withLock(target.targetKey, async () => {\n assertNotAborted(signal, 'write')\n const key = String(target.targetKey)\n const existing = await this.stat(target, signal)\n if (existing !== undefined && existing.type !== 'file') {\n throw new FsError(\n `cannot write \"${target.displayPath}\": not a regular file`,\n 'FS_NOT_REGULAR_FILE',\n )\n }\n if (expected?.kind === 'createIfAbsent' && existing !== undefined) {\n throw new FsError(\n `cannot write \"${target.displayPath}\": file already exists`,\n 'FS_NOT_OBSERVED',\n )\n }\n if (expected?.kind === 'replaceIfVersion') {\n if (existing?.version !== expected.version) {\n throw new FsError(\n `cannot write \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n }\n const before = existing === undefined ? null : await this.diffBasis(target, content)\n const crlf = before !== null && detectsCrlf(before)\n try {\n await (\n await this.ops(signal, 'write')\n ).writeFile(key, restoreLineEndings(normalizeLineEndings(content), crlf))\n } catch (err) {\n throw mapMirageError(err, 'write', target.displayPath)\n }\n return {\n operation: existing === undefined ? ('create' as const) : ('update' as const),\n version: await this.versionAfterWrite(target),\n before: before === null ? null : normalizeLineEndings(before),\n after: normalizeLineEndings(content),\n }\n })\n }\n\n // The overwrite-diff basis: the prior content when it is text and both\n // sides fit the exclusive limit, else null (dsh renders a whole-file\n // diff then). Never fails the write itself.\n private async diffBasis(target: FsTarget, content: string): Promise<string | null> {\n if (Buffer.byteLength(content, 'utf8') >= this.diffBasisMaxBytes) return null\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops()).readFile(String(target.targetKey))\n } catch {\n return null\n }\n if (bytes.byteLength >= this.diffBasisMaxBytes) return null\n try {\n return decodeStrictText(bytes, target.displayPath)\n } catch {\n return null\n }\n }\n\n private async versionAfterWrite(target: FsTarget): Promise<FsVersion> {\n const after = await this.stat(target)\n return after?.version ?? FsVersion(`missing:${String(target.targetKey)}`)\n }\n\n async editText(\n target: FsTarget,\n edit: FsEditRequest,\n expected?: { version: FsVersion },\n signal?: AbortSignal,\n ): Promise<FsEditOutcome> {\n return this.withLock(target.targetKey, async () => {\n assertNotAborted(signal, 'edit')\n const key = String(target.targetKey)\n const existing = await this.stat(target, signal)\n // Stale guard before literal matching: an edit based on an old read\n // reports FS_STALE_VERSION, not a match failure against newer content.\n if (existing === undefined) {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n if (existing.type !== 'file') {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": not a regular file`,\n 'FS_NOT_REGULAR_FILE',\n )\n }\n if (expected !== undefined && existing.version !== expected.version) {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops(signal, 'edit')).readFile(key)\n } catch (err) {\n throw mapMirageError(err, 'edit', target.displayPath)\n }\n const raw = decodeStrictText(bytes, target.displayPath)\n const original = normalizeLineEndings(raw)\n const edited = applyLiteralEdit(\n original,\n normalizeLineEndings(edit.oldString),\n normalizeLineEndings(edit.newString),\n edit.replaceAll,\n target.displayPath,\n )\n try {\n await (\n await this.ops(signal, 'edit')\n ).writeFile(key, restoreLineEndings(edited, detectsCrlf(raw)))\n } catch (err) {\n throw mapMirageError(err, 'edit', target.displayPath)\n }\n return {\n version: await this.versionAfterWrite(target),\n before: original,\n after: edited,\n }\n })\n }\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { FsError } from '@deepseek-ai/dsh-fs'\nimport type { FsErrorCode } from '@deepseek-ai/dsh-fs'\nimport { isMissingPath } from '@struktoai/mirage-core'\n\nexport function assertNotAborted(signal: AbortSignal | undefined, operation: string): void {\n if (signal?.aborted === true) {\n throw new FsError(`${operation} aborted`, 'FS_ABORTED')\n }\n}\n\nfunction codeOf(err: unknown): string | null {\n if (err === null || typeof err !== 'object') return null\n const stamped = err as { code?: unknown }\n return typeof stamped.code === 'string' ? stamped.code : null\n}\n\n// mirage stamps POSIX codes on its errors (ENOENT, EISDIR, ENOTDIR, EACCES,\n// EXDEV; PolicyDenied carries EACCES too), so the dsh taxonomy is derived\n// from the stamp rather than from message sniffing or class checks that\n// would couple this adapter to mirage internals.\nfunction codeFor(err: unknown): FsErrorCode {\n if (isMissingPath(err)) return 'FS_NOT_FOUND'\n switch (codeOf(err)) {\n case 'EISDIR':\n return 'FS_NOT_REGULAR_FILE'\n case 'ENOTDIR':\n return 'FS_NOT_DIRECTORY'\n case 'EACCES':\n case 'EPERM':\n return 'FS_PERMISSION_DENIED'\n default:\n return 'FS_IO_ERROR'\n }\n}\n\nexport function mapMirageError(err: unknown, operation: string, displayPath: string): FsError {\n if (err instanceof FsError) return err\n if (err instanceof DOMException && err.name === 'AbortError') {\n return new FsError(`${operation} aborted`, 'FS_ABORTED', { cause: err })\n }\n const message = err instanceof Error ? err.message : String(err)\n return new FsError(`cannot ${operation} \"${displayPath}\": ${message}`, codeFor(err), {\n cause: err,\n })\n}\n"],"mappings":"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","names":["FsError","isMissingPath","isMissingPath","FsError"]}
@@ -1,102 +0,0 @@
1
- // src/service.ts
2
- import { Service } from "@deepseek-ai/cordis";
3
- import { buildRuntime, parseMountMode } from "@struktoai/mirage-core";
4
- import { buildResource, Workspace } from "@struktoai/mirage-node";
5
- function isMountBlock(entry) {
6
- if (Array.isArray(entry)) return false;
7
- return typeof entry.resource === "string";
8
- }
9
- async function resolveMount(entry) {
10
- const resource = await buildResource(entry.resource, entry.config ?? {});
11
- if (entry.mode === void 0) return resource;
12
- return [resource, parseMountMode(entry.mode)];
13
- }
14
- function toRuntimeEntry(entry) {
15
- if (typeof entry === "string") return entry;
16
- return buildRuntime(entry.name, entry.captures === void 0 ? {} : { captures: entry.captures });
17
- }
18
- function builtEntry(entry) {
19
- return typeof entry === "string" ? buildRuntime(entry) : entry;
20
- }
21
- var MirageService = class extends Service {
22
- static provide = "mirage";
23
- /** Resolves to the shared workspace once every mount is built. */
24
- ready;
25
- built = null;
26
- plannedRuntimes;
27
- constructor(ctx, config = {}) {
28
- super(ctx, "mirage");
29
- if (config.workspace !== void 0 && config.mounts !== void 0) {
30
- throw new Error("mirage: pass either workspace or mounts, not both");
31
- }
32
- if (config.workspace !== void 0) {
33
- this.built = config.workspace;
34
- this.ready = Promise.resolve(config.workspace);
35
- return;
36
- }
37
- if (config.mounts === void 0) {
38
- throw new Error("mirage: one of workspace or mounts is required");
39
- }
40
- const options = { ...config.workspaceOptions ?? {} };
41
- if (config.runtimes !== void 0) {
42
- if (options.runtimes !== void 0) {
43
- throw new Error("mirage: pass runtimes or workspaceOptions.runtimes, not both");
44
- }
45
- options.runtimes = config.runtimes.map(toRuntimeEntry);
46
- }
47
- this.plannedRuntimes = options.runtimes?.map(builtEntry);
48
- if (this.plannedRuntimes !== void 0) options.runtimes = [...this.plannedRuntimes];
49
- const blocks = Object.values(config.mounts).some(isMountBlock);
50
- if (blocks) {
51
- this.ready = this.open(config.mounts, options);
52
- } else {
53
- const owned = new Workspace(config.mounts, options);
54
- this.built = owned;
55
- this.ready = Promise.resolve(owned);
56
- }
57
- ctx.effect(
58
- () => async () => {
59
- const ws = await this.ready.catch(() => null);
60
- if (ws !== null) await ws.close();
61
- },
62
- "mirage workspace"
63
- );
64
- }
65
- /** The live workspace; throws until `ready` resolves. */
66
- get workspace() {
67
- if (this.built === null) {
68
- throw new Error("mirage: workspace is not ready yet; await ctx.mirage.ready");
69
- }
70
- return this.built;
71
- }
72
- /**
73
- * True while every runtime in the world reaches only the vfs
74
- * (`Runtime.reach`), meaning the workspace dispatch is the single
75
- * gate for anything the agent can execute: every effect passes
76
- * mount modes, session grants, and policy before touching the real
77
- * backend behind a mount. The default world qualifies, and so do
78
- * the bridged engines, while the host `local` python or a remote
79
- * sandbox can act around the gate. Answers before `ready` from the
80
- * constructor-resolved entries (mounts resolve asynchronously,
81
- * runtimes never do) and from the live workspace afterwards, so a
82
- * later `addRuntime` is seen.
83
- */
84
- get vfsOnly() {
85
- const entries = this.built === null ? this.plannedRuntimes ?? [] : this.built.runtimeEntries;
86
- return entries.every((entry) => entry.reach === "vfs");
87
- }
88
- async open(mounts, options) {
89
- const resolved = {};
90
- for (const [prefix, entry] of Object.entries(mounts)) {
91
- resolved[prefix] = isMountBlock(entry) ? await resolveMount(entry) : entry;
92
- }
93
- const ws = new Workspace(resolved, options);
94
- this.built = ws;
95
- return ws;
96
- }
97
- };
98
-
99
- export {
100
- MirageService
101
- };
102
- //# sourceMappingURL=chunk-J6F4OX6Z.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/service.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { Service } from '@deepseek-ai/cordis'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { MountSpec, Runtime, RuntimeEntry } from '@struktoai/mirage-core'\nimport { buildRuntime, parseMountMode } from '@struktoai/mirage-core'\nimport { buildResource, Workspace } from '@struktoai/mirage-node'\nimport type { Mount, NodeWorkspaceOptions } from '@struktoai/mirage-node'\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n mirage: MirageService\n }\n}\n\n/**\n * A declarative mount, YAML-friendly: the resource is named by its\n * registry type and built through `buildResource`, so a dsh bundle\n * patch can mount anything without holding a live instance. Told apart\n * from a live `Mount` by the type of `resource`: a string names a\n * registry entry, an object is the instance itself.\n */\nexport interface MirageMountBlock {\n /** Resource registry name (`ram`, `s3`, `slack`, `redis`, ...). */\n resource: string\n /** Mount mode: `read`, `write`, or `exec`. Omitted = the workspace default. */\n mode?: string\n /** Resource constructor config, passed to the registry factory. */\n config?: Record<string, unknown>\n}\n\n/** One mount entry: a live spec or instance, or a declarative block. */\nexport type MirageMount = MountSpec | Mount | MirageMountBlock\n\n/** A runtime entry, YAML-friendly: a name, or a name with captures. */\nexport interface MirageRuntimeBlock {\n /** Runtime registry name (`monty`, `pyodide`, `quickjs`, ...). */\n name: string\n /** Command words this runtime captures (e.g. `['python', 'python3']`). */\n captures?: string[]\n}\n\n/**\n * Configuration for the shared mirage execution world. Exactly one of\n * `workspace` (adopt a live workspace the embedder owns and closes) or\n * `mounts` (construct a workspace here; the service closes it when its\n * plugin unloads) must be given.\n */\nexport interface MirageConfig {\n /** A live workspace to adopt; its lifecycle stays with the embedder. */\n workspace?: Workspace\n /** Mounts for a service-owned workspace, keyed by mount prefix. */\n mounts?: Record<string, MirageMount>\n /**\n * Runtimes for a service-owned workspace, built via `buildRuntime`.\n * The YAML-friendly twin of `workspaceOptions.runtimes`; pass one or\n * the other, not both.\n */\n runtimes?: (string | MirageRuntimeBlock)[]\n /** Options forwarded to the service-owned workspace's constructor. */\n workspaceOptions?: NodeWorkspaceOptions\n}\n\nfunction isMountBlock(entry: MirageMount): entry is MirageMountBlock {\n if (Array.isArray(entry)) return false\n return typeof (entry as MirageMountBlock).resource === 'string'\n}\n\nasync function resolveMount(entry: MirageMountBlock): Promise<MountSpec> {\n const resource = await buildResource(entry.resource, entry.config ?? {})\n if (entry.mode === undefined) return resource\n return [resource, parseMountMode(entry.mode)]\n}\n\nfunction toRuntimeEntry(entry: string | MirageRuntimeBlock): RuntimeEntry {\n if (typeof entry === 'string') return entry\n return buildRuntime(entry.name, entry.captures === undefined ? {} : { captures: entry.captures })\n}\n\n// A name shorthand builds exactly as the workspace would build it; holding\n// the instance early is what lets `vfsOnly` classify the world before the\n// (asynchronous) mounts finish resolving.\nfunction builtEntry(entry: RuntimeEntry): Runtime {\n return typeof entry === 'string' ? buildRuntime(entry) : entry\n}\n\n/**\n * The one mirage `Workspace` behind every mirage-backed dsh provider\n * (`ctx.mirage`, mirroring how `ctx.e2b` owns one sandbox for the E2B\n * adapters). `MirageFileSystem` and `MirageShellExecutor` both inject it,\n * which is what keeps `ctx.fs` targets and `ctx.shell` commands in one\n * execution world: a `processPath` handed to the shell resolves there.\n *\n * Construction with live resources is synchronous; a declarative mount\n * block defers to `buildResource`, so consumers await `ready` (the\n * adapters do) and `workspace` throws until it resolves, mirroring how\n * the E2B service owner exposes its sandbox.\n */\nexport class MirageService extends Service {\n static readonly provide = 'mirage'\n\n /** Resolves to the shared workspace once every mount is built. */\n readonly ready: Promise<Workspace>\n\n private built: Workspace | null = null\n private readonly plannedRuntimes: readonly Runtime[] | undefined\n\n constructor(ctx: Context, config: MirageConfig = {}) {\n super(ctx, 'mirage')\n if (config.workspace !== undefined && config.mounts !== undefined) {\n throw new Error('mirage: pass either workspace or mounts, not both')\n }\n if (config.workspace !== undefined) {\n this.built = config.workspace\n this.ready = Promise.resolve(config.workspace)\n return\n }\n if (config.mounts === undefined) {\n throw new Error('mirage: one of workspace or mounts is required')\n }\n const options = { ...(config.workspaceOptions ?? {}) }\n if (config.runtimes !== undefined) {\n if (options.runtimes !== undefined) {\n throw new Error('mirage: pass runtimes or workspaceOptions.runtimes, not both')\n }\n options.runtimes = config.runtimes.map(toRuntimeEntry)\n }\n this.plannedRuntimes = options.runtimes?.map(builtEntry)\n if (this.plannedRuntimes !== undefined) options.runtimes = [...this.plannedRuntimes]\n const blocks = Object.values(config.mounts).some(isMountBlock)\n if (blocks) {\n this.ready = this.open(config.mounts, options)\n } else {\n const owned = new Workspace(config.mounts as Record<string, MountSpec | Mount>, options)\n this.built = owned\n this.ready = Promise.resolve(owned)\n }\n ctx.effect(\n () => async () => {\n const ws = await this.ready.catch(() => null)\n if (ws !== null) await ws.close()\n },\n 'mirage workspace',\n )\n }\n\n /** The live workspace; throws until `ready` resolves. */\n get workspace(): Workspace {\n if (this.built === null) {\n throw new Error('mirage: workspace is not ready yet; await ctx.mirage.ready')\n }\n return this.built\n }\n\n /**\n * True while every runtime in the world reaches only the vfs\n * (`Runtime.reach`), meaning the workspace dispatch is the single\n * gate for anything the agent can execute: every effect passes\n * mount modes, session grants, and policy before touching the real\n * backend behind a mount. The default world qualifies, and so do\n * the bridged engines, while the host `local` python or a remote\n * sandbox can act around the gate. Answers before `ready` from the\n * constructor-resolved entries (mounts resolve asynchronously,\n * runtimes never do) and from the live workspace afterwards, so a\n * later `addRuntime` is seen.\n */\n get vfsOnly(): boolean {\n const entries = this.built === null ? (this.plannedRuntimes ?? []) : this.built.runtimeEntries\n return entries.every((entry) => entry.reach === 'vfs')\n }\n\n private async open(\n mounts: Record<string, MirageMount>,\n options: NodeWorkspaceOptions,\n ): Promise<Workspace> {\n const resolved: Record<string, MountSpec | Mount> = {}\n for (const [prefix, entry] of Object.entries(mounts)) {\n resolved[prefix] = isMountBlock(entry) ? await resolveMount(entry) : entry\n }\n const ws = new Workspace(resolved, options)\n this.built = ws\n return ws\n }\n}\n"],"mappings":";AAcA,SAAS,eAAe;AAGxB,SAAS,cAAc,sBAAsB;AAC7C,SAAS,eAAe,iBAAiB;AAyDzC,SAAS,aAAa,OAA+C;AACnE,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,SAAO,OAAQ,MAA2B,aAAa;AACzD;AAEA,eAAe,aAAa,OAA6C;AACvE,QAAM,WAAW,MAAM,cAAc,MAAM,UAAU,MAAM,UAAU,CAAC,CAAC;AACvE,MAAI,MAAM,SAAS,OAAW,QAAO;AACrC,SAAO,CAAC,UAAU,eAAe,MAAM,IAAI,CAAC;AAC9C;AAEA,SAAS,eAAe,OAAkD;AACxE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,SAAO,aAAa,MAAM,MAAM,MAAM,aAAa,SAAY,CAAC,IAAI,EAAE,UAAU,MAAM,SAAS,CAAC;AAClG;AAKA,SAAS,WAAW,OAA8B;AAChD,SAAO,OAAO,UAAU,WAAW,aAAa,KAAK,IAAI;AAC3D;AAcO,IAAM,gBAAN,cAA4B,QAAQ;AAAA,EACzC,OAAgB,UAAU;AAAA;AAAA,EAGjB;AAAA,EAED,QAA0B;AAAA,EACjB;AAAA,EAEjB,YAAY,KAAc,SAAuB,CAAC,GAAG;AACnD,UAAM,KAAK,QAAQ;AACnB,QAAI,OAAO,cAAc,UAAa,OAAO,WAAW,QAAW;AACjE,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AACA,QAAI,OAAO,cAAc,QAAW;AAClC,WAAK,QAAQ,OAAO;AACpB,WAAK,QAAQ,QAAQ,QAAQ,OAAO,SAAS;AAC7C;AAAA,IACF;AACA,QAAI,OAAO,WAAW,QAAW;AAC/B,YAAM,IAAI,MAAM,gDAAgD;AAAA,IAClE;AACA,UAAM,UAAU,EAAE,GAAI,OAAO,oBAAoB,CAAC,EAAG;AACrD,QAAI,OAAO,aAAa,QAAW;AACjC,UAAI,QAAQ,aAAa,QAAW;AAClC,cAAM,IAAI,MAAM,8DAA8D;AAAA,MAChF;AACA,cAAQ,WAAW,OAAO,SAAS,IAAI,cAAc;AAAA,IACvD;AACA,SAAK,kBAAkB,QAAQ,UAAU,IAAI,UAAU;AACvD,QAAI,KAAK,oBAAoB,OAAW,SAAQ,WAAW,CAAC,GAAG,KAAK,eAAe;AACnF,UAAM,SAAS,OAAO,OAAO,OAAO,MAAM,EAAE,KAAK,YAAY;AAC7D,QAAI,QAAQ;AACV,WAAK,QAAQ,KAAK,KAAK,OAAO,QAAQ,OAAO;AAAA,IAC/C,OAAO;AACL,YAAM,QAAQ,IAAI,UAAU,OAAO,QAA6C,OAAO;AACvF,WAAK,QAAQ;AACb,WAAK,QAAQ,QAAQ,QAAQ,KAAK;AAAA,IACpC;AACA,QAAI;AAAA,MACF,MAAM,YAAY;AAChB,cAAM,KAAK,MAAM,KAAK,MAAM,MAAM,MAAM,IAAI;AAC5C,YAAI,OAAO,KAAM,OAAM,GAAG,MAAM;AAAA,MAClC;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,YAAuB;AACzB,QAAI,KAAK,UAAU,MAAM;AACvB,YAAM,IAAI,MAAM,4DAA4D;AAAA,IAC9E;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,IAAI,UAAmB;AACrB,UAAM,UAAU,KAAK,UAAU,OAAQ,KAAK,mBAAmB,CAAC,IAAK,KAAK,MAAM;AAChF,WAAO,QAAQ,MAAM,CAAC,UAAU,MAAM,UAAU,KAAK;AAAA,EACvD;AAAA,EAEA,MAAc,KACZ,QACA,SACoB;AACpB,UAAM,WAA8C,CAAC;AACrD,eAAW,CAAC,QAAQ,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACpD,eAAS,MAAM,IAAI,aAAa,KAAK,IAAI,MAAM,aAAa,KAAK,IAAI;AAAA,IACvE;AACA,UAAM,KAAK,IAAI,UAAU,UAAU,OAAO;AAC1C,SAAK,QAAQ;AACb,WAAO;AAAA,EACT;AACF;","names":[]}