refactor(layout): items 33+35 — module homes and workspace/config placement

Block E items 33 (T3-3 module homes) and 35 (T3-2 workspace/config
placement) from the cleanup plan. Item 34 (T3-1 runtime layout) was in
scope but is already closed on main; the verification is in the PR body.

Layout parity gate: 286 -> 269 unexcused divergences, baseline lowered.

Item 33 — module homes:
  - py utils/fingerprint.py -> watch/fingerprint.py (all six consumers
    are watch machinery; TS is already at watch/fingerprint.ts)
  - py workspace/provision/rollup.py -> provision/rollup.py (no workspace
    imports; TS is at the leaf provision/rollup.ts)
  - py workspace/provision/builtins.py deleted (one function, one caller;
    TS keeps it module-level in provision_node.ts)
  - py 9 snapshot key enums -> new workspace/snapshot/keys.py
  - py IndexType -> cache/index/config.py, CacheType -> cache/file/config.py
  - ts node/cache/redis/file.ts -> node/cache/file/redis.ts, taking
    add.lua with it (read relatively by its only consumer and copied by
    an explicit tsup path, so tsup.config.ts moves with them)
  - ts new core/src/concurrency/limiter.ts, replacing the private
    Semaphore in accessor/dify.ts

NodeMetaKey deliberately stays beside NodeMeta: it names NodeMeta's own
fields, and importing the snapshot package from mount/namespace closes a
real cycle (namespace -> snapshot.keys -> snapshot/__init__ -> api ->
state -> namespace).

Item 35 — workspace/config placement:
  - ts core/workspace/workspace.ts -> core/workspace/workspace/workspace.ts
  - ts node/workspace/mount_spec.ts -> core/workspace/mount/spec.ts
  - ts new core/workspace/workspace/guard.ts (three call sites converged
    onto it from their own typeof === 'string' tests)
  - ts new node/workspace/workspace/kernel_mounts.ts (NodeWorkspace
    211 -> 169 lines, now delegations)
  - ts server/src/config.ts -> node/src/config.ts, exported from the node
    barrel so a mirage-node consumer can load a YAML workspace config

Three bugs fixed in the dify semaphore on the way, each pinned by a test
verified to fail against the old code: release() returned the permit to
the pool before waking the waiter (two holders at capacity 1), double
release inflated the count, and maxConcurrency < 1 was silently clamped
where python raises.

Also drops `cache: pnpm` from the ts-audit job added in #831. That job
runs `pnpm audit` and never installs, so the pnpm store does not exist;
setup-node's post-run save then fails path validation on every cache
miss and silently skips on a hit, which is why it was green on main and
red here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
bytecii
2026-08-16 21:29:43 -07:00
parent c35fb1cf0e
commit 0e749345e5
182 changed files with 870 additions and 503 deletions
+4 -3
View File
@@ -165,13 +165,14 @@ jobs:
# 24 rather than the 22 the test job uses: this step runs no project
# code, so it is free to track the newer line the integ workflow is
# already on.
# already on. No `cache: pnpm` here on purpose: the audit step below
# resolves straight from the lockfile and never installs, so the pnpm
# store does not exist and the post-run save fails path validation on
# every cache miss.
- name: Set up Node
uses: actions/setup-node@v7
with:
node-version: "24"
cache: pnpm
cache-dependency-path: typescript/pnpm-lock.yaml
# Advisories read against the resolved lockfile, which is what the 27
# hand-written pnpm.overrides in typescript/package.json pin. Those
+6 -1
View File
@@ -12,9 +12,14 @@
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from enum import Enum
from pydantic import BaseModel
from mirage.types import CacheType
class CacheType(str, Enum):
RAM = "ram"
REDIS = "redis"
class CacheConfig(BaseModel):
+5 -2
View File
@@ -17,14 +17,17 @@ from typing import Any
from pydantic import BaseModel, Field
from mirage.types import IndexType
class ResourceType(str, Enum):
FILE = "file"
FOLDER = "folder"
class IndexType(str, Enum):
RAM = "ram"
REDIS = "redis"
class LookupStatus(str, Enum):
EXPIRED = "expired"
NOT_FOUND = "not_found"
+1 -1
View File
@@ -20,10 +20,10 @@ from pathlib import Path
from mirage.accessor.disk import DiskAccessor
from mirage.core.timeutil import epoch_to_iso
from mirage.types import PathSpec, WalkEntry
from mirage.utils.fingerprint import stat_fingerprint
from mirage.utils.key_prefix import mount_prefix_of
from mirage.watch.base import DeltaHook
from mirage.watch.delta import ListingDeltaHook
from mirage.watch.fingerprint import stat_fingerprint
def resolve(root: Path, path: str) -> Path:
+1 -1
View File
@@ -19,10 +19,10 @@ from mirage.core.dropbox.api import list_folder
from mirage.core.dropbox.client import DropboxApiError
from mirage.core.dropbox.paths import dropbox_path_of
from mirage.types import PathSpec, WalkEntry
from mirage.utils.fingerprint import stat_fingerprint
from mirage.utils.key_prefix import mount_prefix_of
from mirage.watch.base import DeltaHook
from mirage.watch.delta import ListingDeltaHook
from mirage.watch.fingerprint import stat_fingerprint
class DropboxWalk:
+1 -1
View File
@@ -20,10 +20,10 @@ from opendal.types import Metadata
from mirage.core.opendal.types import OperatorAccessor
from mirage.types import PathSpec, WalkEntry
from mirage.utils.fingerprint import stat_fingerprint
from mirage.utils.key_prefix import mount_prefix_of
from mirage.watch.base import DeltaHook
from mirage.watch.delta import ListingDeltaHook
from mirage.watch.fingerprint import stat_fingerprint
class OpendalWalk:
+1 -1
View File
@@ -19,10 +19,10 @@ from mirage.core.s3.client import (_client_kwargs, _key, _strip_prefix,
async_session)
from mirage.core.timeutil import to_iso_z
from mirage.types import PathSpec, WalkEntry
from mirage.utils.fingerprint import stat_fingerprint
from mirage.utils.key_prefix import mount_prefix_of
from mirage.watch.base import DeltaHook
from mirage.watch.delta import ListingDeltaHook
from mirage.watch.fingerprint import stat_fingerprint
from mirage.watch.walk import synth_dirs
+1 -1
View File
@@ -21,10 +21,10 @@ from mirage.core.ssh.client import _abs
from mirage.core.ssh.config import SSHConfig
from mirage.core.timeutil import epoch_to_iso
from mirage.types import PathSpec, WalkEntry
from mirage.utils.fingerprint import stat_fingerprint
from mirage.utils.key_prefix import mount_prefix_of
from mirage.watch.base import DeltaHook
from mirage.watch.delta import ListingDeltaHook
from mirage.watch.fingerprint import stat_fingerprint
async def _descend(
@@ -12,8 +12,8 @@
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.provision import (Precision, ProvisionResult, combine_alternative,
combine_sum)
from mirage.provision.types import (Precision, ProvisionResult,
combine_alternative, combine_sum)
def rollup_pipe(children: list[ProvisionResult]) -> ProvisionResult:
+2 -1
View File
@@ -19,9 +19,10 @@ from mirage.server.version.state_tree import (CONTROL_PREFIX, META_PATH,
blob_to_meta, meta_to_blob,
to_state, tree_inputs_from_state)
from mirage.server.version.store import VersionStore
from mirage.types import DriftPolicy, StateKey
from mirage.types import DriftPolicy
from mirage.workspace.snapshot import (apply_state_dict, install_fingerprints,
to_state_dict)
from mirage.workspace.snapshot.keys import StateKey
if TYPE_CHECKING:
from mirage.workspace.workspace import Workspace
+2 -1
View File
@@ -17,10 +17,11 @@ from typing import TYPE_CHECKING, Any
from mirage.server.version.api import read_version, resolve_ref
from mirage.server.version.state_tree import CATEGORIES, to_state
from mirage.server.version.store import VersionStore
from mirage.types import DriftPolicy, MountKey, ResourceStateKey, StateKey
from mirage.types import DriftPolicy
from mirage.utils.path import norm
from mirage.workspace.snapshot import (apply_state_dict, install_fingerprints,
to_state_dict)
from mirage.workspace.snapshot.keys import MountKey, ResourceStateKey, StateKey
if TYPE_CHECKING:
from mirage.workspace.workspace import Workspace
+1 -1
View File
@@ -17,7 +17,7 @@ from typing import Any
from mirage.server.version.api import read_version, resolve_ref, version_diff
from mirage.server.version.state_tree import to_state
from mirage.server.version.store import VersionStore
from mirage.types import SessionKey, StateKey
from mirage.workspace.snapshot.keys import SessionKey, StateKey
def _dict_delta(before: dict[str, Any], after: dict[str,
+2 -1
View File
@@ -15,7 +15,8 @@
import json
from typing import Any
from mirage.types import CacheKey, MountKey, ResourceStateKey, StateKey
from mirage.workspace.snapshot.keys import (CacheKey, MountKey,
ResourceStateKey, StateKey)
from mirage.workspace.snapshot.tar_io import _json_default
from mirage.workspace.snapshot.utils import FORMAT_VERSION
+1 -109
View File
@@ -564,33 +564,6 @@ def word_text(word: "str | PathSpec") -> str:
return word
class IndexType(str, Enum):
RAM = "ram"
REDIS = "redis"
class CacheType(str, Enum):
RAM = "ram"
REDIS = "redis"
class StateKey(StrEnum):
VERSION = "version"
MIRAGE_VERSION = "mirage_version"
MOUNTS = "mounts"
SESSIONS = "sessions"
DEFAULT_SESSION_ID = "default_session_id"
DEFAULT_AGENT_ID = "default_agent_id"
CURRENT_AGENT_ID = "current_agent_id"
CACHE = "cache"
HISTORY = "history"
JOBS = "jobs"
FINGERPRINTS = "fingerprints"
LIVE_ONLY_MOUNTS = "live_only_mounts"
NODES = "nodes"
CLIS = "clis"
class FileChangeKind(StrEnum):
"""Kind of an externally observed file change.
@@ -685,7 +658,7 @@ class WalkEntry:
virtual (str): Workspace-virtual path of the entry.
is_dir (bool): Whether the entry is a directory.
fingerprint (str | None): Content fingerprint (see
``mirage.utils.fingerprint.stat_fingerprint``). None means
``mirage.watch.fingerprint.stat_fingerprint``). None means
only create/delete are detectable for this entry.
size (int | None): Content size in bytes, when the listing
carries it.
@@ -727,84 +700,3 @@ class DriftPolicy(StrEnum):
"""
STRICT = "strict"
OFF = "off"
class FingerprintKey(StrEnum):
PATH = "path"
MOUNT_PREFIX = "mount_prefix"
FINGERPRINT = "fingerprint"
REVISION = "revision"
class MountKey(StrEnum):
INDEX = "index"
PREFIX = "prefix"
MODE = "mode"
CONSISTENCY = "consistency"
RESOURCE_CLASS = "resource_class"
RESOURCE_STATE = "resource_state"
class CLIKey(StrEnum):
NAME = "name"
SPEC = "spec"
CONFIG = "config"
SCRIPT = "script"
RUNTIME = "runtime"
class ScriptKey(StrEnum):
SOURCE = "source"
LANGUAGE = "language"
MODULE = "module"
class CacheKey(StrEnum):
LIMIT = "limit"
MAX_DRAIN_BYTES = "max_drain_bytes"
ENTRIES = "entries"
KEY = "key"
DATA = "data"
FINGERPRINT = "fingerprint"
TTL = "ttl"
CACHED_AT = "cached_at"
SIZE = "size"
class JobKey(StrEnum):
ID = "id"
COMMAND = "command"
CWD = "cwd"
STATUS = "status"
STDOUT = "stdout"
STDERR = "stderr"
EXIT_CODE = "exit_code"
CREATED_AT = "created_at"
AGENT = "agent"
SESSION_ID = "session_id"
class NodeMetaKey(StrEnum):
TARGET = "target"
MTIME = "mtime"
MODE = "mode"
UID = "uid"
GID = "gid"
ATIME = "atime"
OBSERVED_MTIME = "observed_mtime"
class SessionKey(StrEnum):
SESSION_ID = "session_id"
CWD = "cwd"
ENV = "env"
LAST_EXIT_CODE = "last_exit_code"
class ResourceStateKey(StrEnum):
TYPE = "type"
CONFIG = "config"
FILES = "files"
DIRS = "dirs"
MODIFIED = "modified"
KEY_PREFIX = "key_prefix"
+1 -1
View File
@@ -14,9 +14,9 @@
from mirage.types import (Delta, FileChangeKind, FileEvent, FileMetadata,
WalkEntry, WalkFn)
from mirage.utils.fingerprint import stat_fingerprint
from mirage.watch.base import DeltaHook, WatchRuntime
from mirage.watch.delta import ListingDeltaHook
from mirage.watch.fingerprint import stat_fingerprint
from mirage.watch.queue import (OverflowPolicy, QueueClosed, QueueFactory,
QueueOverflowError, RAMWatchQueue, WatchQueue)
from mirage.watch.watcher import Watcher
+1 -1
View File
@@ -18,8 +18,8 @@ from collections.abc import (AsyncIterator, Awaitable, Callable, Iterable,
from mirage.cache.index import IndexCacheStore
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.types import FileStat, FileType, PathSpec, WalkEntry
from mirage.utils.fingerprint import stat_fingerprint
from mirage.utils.key_prefix import mount_key, mount_prefix_of
from mirage.watch.fingerprint import stat_fingerprint
ReaddirFn = Callable[[PathSpec, IndexCacheStore], Awaitable[list[str]]]
StatFn = Callable[[PathSpec, IndexCacheStore], Awaitable[FileStat]]
@@ -15,11 +15,11 @@
import asyncio
from collections.abc import Iterable
from dataclasses import dataclass
from enum import StrEnum
from mirage.core.timeutil import epoch_to_iso
from mirage.resource.base import BaseResource
from mirage.types import (LINK_TARGET_KEY, FileStat, FileType, MountMode,
NodeMetaKey)
from mirage.types import LINK_TARGET_KEY, FileStat, FileType, MountMode
from mirage.utils.path import glob_prefix_match, resolve_symlinks
from mirage.workspace.mount.mount import MountEntry
from mirage.workspace.mount.namespace.ram import RAMNamespaceStore
@@ -27,6 +27,16 @@ from mirage.workspace.mount.namespace.store import NamespaceStore, NodeFields
from mirage.workspace.mount.registry import MountRegistry
class NodeMetaKey(StrEnum):
TARGET = "target"
MTIME = "mtime"
MODE = "mode"
UID = "uid"
GID = "gid"
ATIME = "atime"
OBSERVED_MTIME = "observed_mtime"
@dataclass(slots=True)
class NodeMeta:
"""Per-path namespace metadata.
@@ -17,6 +17,7 @@ from functools import partial
from typing import Any, Callable
from mirage.provision import Precision, ProvisionResult
from mirage.provision.rollup import rollup_list, rollup_pipe
from mirage.runtime.types import DispatchFn
from mirage.shell.node_kind import NodeKind, node_kind
from mirage.shell.types import FunctionBody
@@ -28,7 +29,6 @@ from mirage.workspace.expand import (classify_parts, expand_and_classify,
expand_parts, expand_redirects)
from mirage.workspace.mount import MountRegistry
from mirage.workspace.mount.namespace import Namespace
from mirage.workspace.provision.builtins import handle_builtin_provision
from mirage.workspace.provision.command import handle_command_provision
from mirage.workspace.provision.control import (handle_for_provision,
handle_function_provision,
@@ -37,7 +37,6 @@ from mirage.workspace.provision.control import (handle_for_provision,
from mirage.workspace.provision.pipes import (handle_connection_provision,
handle_pipe_provision)
from mirage.workspace.provision.redirect import handle_redirect_provision
from mirage.workspace.provision.rollup import rollup_list, rollup_pipe
from mirage.workspace.session import Session
from mirage.shell.helpers import ( # isort: skip
@@ -92,6 +91,11 @@ class PlanScope:
planning: set[str] = field(default_factory=set)
async def handle_builtin_provision() -> ProvisionResult:
"""Plan for shell builtins (cd, export, etc.): zero cost."""
return ProvisionResult(precision=Precision.EXACT)
async def _provision_redirected(
recurse: Callable[..., Any],
registry: MountRegistry,
@@ -1,20 +0,0 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.provision import Precision, ProvisionResult
async def handle_builtin_provision() -> ProvisionResult:
"""Plan for shell builtins (cd, export, etc.): zero cost."""
return ProvisionResult(precision=Precision.EXACT)
+1 -1
View File
@@ -15,7 +15,7 @@
from typing import Any
from mirage.provision import Precision, ProvisionResult
from mirage.workspace.provision.rollup import rollup_list
from mirage.provision.rollup import rollup_list
from mirage.workspace.session import Session
+1 -1
View File
@@ -15,7 +15,7 @@
from typing import Any
from mirage.provision import ProvisionResult
from mirage.workspace.provision.rollup import rollup_list, rollup_pipe
from mirage.provision.rollup import rollup_list, rollup_pipe
from mirage.workspace.session import Session
@@ -15,12 +15,12 @@
from typing import Any
from mirage.provision import Precision, ProvisionResult
from mirage.provision.rollup import rollup_list
from mirage.shell.types import RedirectKind
from mirage.types import PathSpec
from mirage.workspace.mount import MountRegistry
from mirage.workspace.mount.namespace import Namespace
from mirage.workspace.provision.command import handle_command_provision
from mirage.workspace.provision.rollup import rollup_list
from mirage.workspace.session import Session
+2 -1
View File
@@ -16,8 +16,9 @@ import asyncio
import logging
from typing import TYPE_CHECKING, Any, Callable
from mirage.types import DriftPolicy, FingerprintKey
from mirage.types import DriftPolicy
from mirage.workspace.mount.mount import MountEntry
from mirage.workspace.snapshot.keys import FingerprintKey
if TYPE_CHECKING:
from mirage.workspace.workspace import Workspace
+111
View File
@@ -0,0 +1,111 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from enum import StrEnum
class StateKey(StrEnum):
VERSION = "version"
MIRAGE_VERSION = "mirage_version"
MOUNTS = "mounts"
SESSIONS = "sessions"
DEFAULT_SESSION_ID = "default_session_id"
DEFAULT_AGENT_ID = "default_agent_id"
CURRENT_AGENT_ID = "current_agent_id"
CACHE = "cache"
HISTORY = "history"
JOBS = "jobs"
FINGERPRINTS = "fingerprints"
LIVE_ONLY_MOUNTS = "live_only_mounts"
NODES = "nodes"
CLIS = "clis"
class MountKey(StrEnum):
INDEX = "index"
PREFIX = "prefix"
MODE = "mode"
CONSISTENCY = "consistency"
RESOURCE_CLASS = "resource_class"
RESOURCE_STATE = "resource_state"
class CacheKey(StrEnum):
LIMIT = "limit"
MAX_DRAIN_BYTES = "max_drain_bytes"
ENTRIES = "entries"
KEY = "key"
DATA = "data"
FINGERPRINT = "fingerprint"
TTL = "ttl"
CACHED_AT = "cached_at"
SIZE = "size"
class JobKey(StrEnum):
ID = "id"
COMMAND = "command"
CWD = "cwd"
STATUS = "status"
STDOUT = "stdout"
STDERR = "stderr"
EXIT_CODE = "exit_code"
CREATED_AT = "created_at"
AGENT = "agent"
SESSION_ID = "session_id"
class ResourceStateKey(StrEnum):
TYPE = "type"
CONFIG = "config"
FILES = "files"
DIRS = "dirs"
MODIFIED = "modified"
KEY_PREFIX = "key_prefix"
# The four below name sub-shapes that typescript spells as literals at
# the point of use rather than as a frozen table (`keys.ts` stops at
# ResourceStateKey). They belong to the same snapshot vocabulary, so
# they live here too instead of staying behind in `types.py`.
# NodeMetaKey is the exception: it names NodeMeta's own fields, so it
# lives beside NodeMeta in `mount/namespace/namespace.py`. Snapshot
# serializes the namespace, not the other way round, and importing
# this package from there closes an import cycle.
class FingerprintKey(StrEnum):
PATH = "path"
MOUNT_PREFIX = "mount_prefix"
FINGERPRINT = "fingerprint"
REVISION = "revision"
class CLIKey(StrEnum):
NAME = "name"
SPEC = "spec"
CONFIG = "config"
SCRIPT = "script"
RUNTIME = "runtime"
class ScriptKey(StrEnum):
SOURCE = "source"
LANGUAGE = "language"
MODULE = "module"
class SessionKey(StrEnum):
SESSION_ID = "session_id"
CWD = "cwd"
ENV = "env"
LAST_EXIT_CODE = "last_exit_code"
+3 -2
View File
@@ -14,8 +14,9 @@
from typing import Any
from mirage.types import (CacheKey, JobKey, MountKey, ResourceName,
ResourceStateKey, StateKey)
from mirage.types import ResourceName
from mirage.workspace.snapshot.keys import (CacheKey, JobKey, MountKey,
ResourceStateKey, StateKey)
from mirage.workspace.snapshot.utils import BLOB_REF_KEY, is_safe_blob_path
+4 -3
View File
@@ -28,9 +28,7 @@ from mirage.runtime.types import Language, ScriptSource
from mirage.shell.console import (KILLED_OUTCOME, Channel, ConsoleChunk,
JobConsole, RAMConsoleStore, exit_outcome)
from mirage.shell.job_table import Job, JobStatus
from mirage.types import (CacheKey, CLIKey, ConsistencyPolicy, JobKey,
JsonValue, MountKey, MountMode, ResourceName,
ResourceStateKey, ScriptKey, SessionKey, StateKey)
from mirage.types import ConsistencyPolicy, JsonValue, MountMode, ResourceName
from mirage.version import __version__
from mirage.workspace.mount.namespace import NodeMeta
from mirage.workspace.session.session import Session
@@ -38,6 +36,9 @@ from mirage.workspace.session.shell_dirs import set_cwd
from mirage.workspace.snapshot.config import MountArgs
from mirage.workspace.snapshot.drift import (capture_fingerprints,
live_only_mount_prefixes)
from mirage.workspace.snapshot.keys import (CacheKey, CLIKey, JobKey, MountKey,
ResourceStateKey, ScriptKey,
SessionKey, StateKey)
from mirage.workspace.snapshot.utils import FORMAT_VERSION, norm_mount_prefix
# A per-name override for restoring installed CLIs: a plain mapping is a
@@ -40,7 +40,7 @@ from mirage.runtime.resolver import PrefixResolver
from mirage.shell.job_table import ConsoleFactory, JobTable
from mirage.types import (ConsistencyPolicy, DriftPolicy, FileEvent, FileStat,
JsonValue, MountBackend, MountMode, PathSpec,
StateKey, parse_mount_mode)
parse_mount_mode)
from mirage.utils.ids import new_session_id, new_workspace_id
from mirage.workspace.cli import CLIInstall
from mirage.workspace.dispatcher import Dispatcher
@@ -56,6 +56,7 @@ from mirage.workspace.snapshot import (DriftQueue, apply_state_dict,
read_tar)
from mirage.workspace.snapshot import snapshot as _write_snapshot
from mirage.workspace.snapshot import to_state_dict
from mirage.workspace.snapshot.keys import StateKey
from mirage.workspace.snapshot.state import (CLIOverrides, reusable_clis,
reusable_resources)
from mirage.workspace.store import WorkspaceStateStore
+1 -1
View File
@@ -15,7 +15,7 @@
from mirage.cache.index import (IndexConfig, IndexEntry, ListResult,
LookupResult, LookupStatus, RedisIndexConfig,
ResourceType)
from mirage.types import IndexType
from mirage.cache.index.config import IndexType
def test_index_entry_defaults():
+1 -1
View File
@@ -13,7 +13,7 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.provision import Precision, ProvisionResult
from mirage.workspace.provision.rollup import rollup_list, rollup_pipe
from mirage.provision.rollup import rollup_list, rollup_pipe
def test_rollup_pipe_sums():
+2 -1
View File
@@ -23,10 +23,11 @@ from mirage.server.version.backend import LocalBackend
from mirage.server.version.errors import NoSuchBranchError
from mirage.server.version.state_tree import META_PATH
from mirage.server.version.store import VersionStore
from mirage.types import CacheKey, MountMode, StateKey
from mirage.types import MountMode
from mirage.workspace import Workspace
from mirage.workspace.session.state import seed_var
from mirage.workspace.snapshot import to_state_dict
from mirage.workspace.snapshot.keys import CacheKey, StateKey
async def status(store, ws, branch="main"):
@@ -19,9 +19,10 @@ import pytest
from mirage.resource.ram import RAMResource
from mirage.server.version.state_tree import (blob_to_meta, meta_to_blob,
to_state, tree_inputs_from_state)
from mirage.types import (CacheKey, FingerprintKey, MountKey, MountMode,
SessionKey, StateKey)
from mirage.types import MountMode
from mirage.workspace import Workspace
from mirage.workspace.snapshot.keys import (CacheKey, FingerprintKey, MountKey,
SessionKey, StateKey)
from mirage.workspace.snapshot.manifest import split_manifest_and_blobs
from mirage.workspace.snapshot.state import to_state_dict
from mirage.workspace.snapshot.tar_io import read_tar, write_tar
@@ -1,4 +1,4 @@
from mirage.utils.fingerprint import stat_fingerprint
from mirage.watch.fingerprint import stat_fingerprint
def test_stat_fingerprint_prefers_etag():
@@ -0,0 +1,57 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import re
from enum import StrEnum
from pathlib import Path
import pytest
from mirage.workspace.snapshot import keys
REPO_ROOT = Path(__file__).resolve().parents[4]
KEYS_TS = (REPO_ROOT / "typescript" / "packages" / "core" / "src" /
"workspace" / "snapshot" / "keys.ts")
# These five have a typescript twin in `keys.ts`; the rest of the module
# is spelled as literals over there, so only these can be diffed.
SHARED = ["StateKey", "MountKey", "CacheKey", "JobKey", "ResourceStateKey"]
def _typescript_tables() -> dict[str, dict[str, str]]:
source = KEYS_TS.read_text()
tables: dict[str, dict[str, str]] = {}
pattern = r"export const (\w+) = Object\.freeze\(\{(.*?)\}\s*as const\)"
for name, body in re.findall(pattern, source, re.DOTALL):
tables[name] = dict(re.findall(r"(\w+): '([^']*)'", body))
return tables
@pytest.mark.parametrize("name", SHARED)
def test_shared_tables_match_typescript(name: str):
"""A snapshot key is a wire name, so a one-sided rename breaks restore."""
table = _typescript_tables()[name]
enum: type[StrEnum] = getattr(keys, name)
assert {member.name: member.value for member in enum} == table
def test_every_key_is_a_lowercase_wire_name():
enums = [
value for value in vars(keys).values() if isinstance(value, type)
and issubclass(value, StrEnum) and value is not StrEnum
]
assert len(enums) == 9
for enum in enums:
for member in enum:
assert member.value == member.name.lower()
@@ -15,7 +15,7 @@
import io
from typing import Any
from mirage.types import CacheKey, JobKey, MountKey, StateKey
from mirage.workspace.snapshot.keys import CacheKey, JobKey, MountKey, StateKey
from mirage.workspace.snapshot.manifest import split_manifest_and_blobs
from mirage.workspace.snapshot.tar_io import read_tar, write_tar
from mirage.workspace.snapshot.utils import BLOB_REF_KEY
+2 -1
View File
@@ -28,9 +28,10 @@ from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.resource.secrets import REDACTED_SECRET
from mirage.runtime.types import ScriptSource
from mirage.types import CLIKey, MountMode, ScriptKey, StateKey
from mirage.types import MountMode
from mirage.workspace import Workspace
from mirage.workspace.snapshot import to_state_dict
from mirage.workspace.snapshot.keys import CLIKey, ScriptKey, StateKey
from mirage.workspace.snapshot.utils import FORMAT_VERSION
+1 -1
View File
@@ -1,5 +1,5 @@
{
"baseline": 286,
"baseline": 269,
"baseline_reason": "Every divergence below the excused ones predates the gate and each needs its own decision, so --strict fails on a rise rather than demanding zero. Lower this number whenever a divergence is closed; the gate fails on a drop too, so an improvement cannot be silently spent. Items 31-37 of the cleanup plan are scoped from this report. The unit is one module, never one directory: a one-sided directory counts once per module inside it, so it cannot absorb new modules without moving the number. An excused directory is the deliberate exception -- it excuses its whole subtree, because the excuse is that there is no counterpart to mirror, which makes growth inside it expected rather than drift.",
"directories": {
"python_only": {
@@ -12,7 +12,7 @@
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import type { Options } from '@anthropic-ai/claude-agent-sdk'
import { buildSystemPrompt } from '../prompt.ts'
import { MirageServer } from './server.ts'
@@ -13,7 +13,7 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { VERSION } from '@struktoai/mirage-core/version'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
import { z } from 'zod'
import {
@@ -13,7 +13,7 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { createHash } from 'node:crypto'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
export class StaleMirageFileError extends Error {
readonly path: string
@@ -13,7 +13,7 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { describe, expect, it } from 'vitest'
import { ExecuteResult } from '@struktoai/mirage-core/workspace/workspace'
import { ExecuteResult } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { decode, ioToStr } from './io-text.ts'
const enc = (s: string): Uint8Array => new TextEncoder().encode(s)
+1 -1
View File
@@ -12,7 +12,7 @@
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import type { ExecuteResult } from '@struktoai/mirage-core/workspace/workspace'
import type { ExecuteResult } from '@struktoai/mirage-core/workspace/workspace/workspace'
export function decode(value: Uint8Array | null | undefined): string {
if (value === null || value === undefined) return ''
@@ -13,7 +13,7 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { gnuDirname } from '@struktoai/mirage-core/utils/path'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
+1 -1
View File
@@ -13,7 +13,7 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { VERSION } from '@struktoai/mirage-core/version'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
@@ -13,7 +13,7 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { gnuDirname } from '@struktoai/mirage-core/utils/path'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { applyDiff } from '@openai/agents'
import type { ApplyPatchOperation, ApplyPatchResult, Editor } from '@openai/agents'
@@ -12,7 +12,7 @@
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { tool } from '@openai/agents'
import { z } from 'zod'
@@ -1,4 +1,4 @@
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import {
tool,
type ToolOutputFileContent,
@@ -12,7 +12,7 @@
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import type { Shell, ShellAction, ShellResult, ShellOutputResult } from '@openai/agents'
export class MirageShell implements Shell {
@@ -12,7 +12,7 @@
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import {
createBashToolDefinition,
createEditToolDefinition,
@@ -13,7 +13,7 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { rstripSlash } from '@struktoai/mirage-core/utils/slash'
import type { ExecuteResult, Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { ExecuteResult, Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import type {
BashOperations,
EditOperations,
@@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest'
import { OpsRegistry } from '@struktoai/mirage-core/ops/registry'
import { RAMResource } from '@struktoai/mirage-core/resource/ram/ram'
import { MountMode } from '@struktoai/mirage-core/types'
import { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { MIRAGE_SYSTEM_PROMPT, buildSystemPrompt } from './prompt.ts'
function mkWs(): Workspace {
+1 -1
View File
@@ -12,7 +12,7 @@
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
export const MIRAGE_SYSTEM_PROMPT = `Your filesystem is powered by Mirage — a virtual filesystem that mounts cloud storage, local files, and in-memory data as a unified file tree.
+1 -1
View File
@@ -1,7 +1,7 @@
import { detectFileType } from '@struktoai/mirage-core/commands/builtin/file_helper'
import { FileType } from '@struktoai/mirage-core/types'
import type { FileStat } from '@struktoai/mirage-core/types'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import {
MIME_FOR_EXTENSION,
MIME_FOR_FILE_TYPE,
@@ -13,7 +13,7 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { gnuDirname } from '@struktoai/mirage-core/utils/path'
import type { ExecuteResult, Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { ExecuteResult, Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { FileVersionTracker, StaleMirageFileError } from './file-version.ts'
import { decode, ioToStr } from './io-text.ts'
@@ -14,7 +14,7 @@
import { encodeBase64 } from '@struktoai/mirage-core/utils/base64'
import { gnuDirname } from '@struktoai/mirage-core/utils/path'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace'
import type { Workspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { tool, type ToolSet } from 'ai'
import { z } from 'zod'
import { readWorkspaceFile } from '../read-file.ts'
+2 -2
View File
@@ -15,8 +15,8 @@
import type { Resource } from '@struktoai/mirage-core/resource/base'
import { createShellParser } from '@struktoai/mirage-core/shell/parse'
import type { ShellParser } from '@struktoai/mirage-core/shell/parse'
import { Workspace as CoreWorkspace } from '@struktoai/mirage-core/workspace/workspace'
import type { WorkspaceOptions } from '@struktoai/mirage-core/workspace/workspace'
import { Workspace as CoreWorkspace } from '@struktoai/mirage-core/workspace/workspace/workspace'
import type { WorkspaceOptions } from '@struktoai/mirage-core/workspace/workspace/workspace'
import { ENGINE_WASM_BASE64, GRAMMAR_WASM_BASE64 } from './generated/wasm.ts'
let cachedParser: Promise<ShellParser> | null = null
+3 -32
View File
@@ -12,6 +12,7 @@
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { ConcurrencyLimiter } from '../concurrency/limiter.ts'
import { Accessor } from './base.ts'
import type { DifyConfigResolved } from '../resource/dify/config.ts'
@@ -20,44 +21,14 @@ export interface DifyRequestOptions {
json?: unknown
}
class Semaphore {
private available: number
private readonly waiters: (() => void)[] = []
constructor(max: number) {
this.available = Math.max(1, max)
}
async acquire(): Promise<() => void> {
const release = (): void => {
this.release()
}
if (this.available > 0) {
this.available -= 1
return release
}
await new Promise<void>((resolve) => {
this.waiters.push(resolve)
})
this.available -= 1
return release
}
private release(): void {
this.available += 1
const next = this.waiters.shift()
if (next !== undefined) next()
}
}
export class DifyAccessor extends Accessor {
readonly config: DifyConfigResolved
private readonly limiter: Semaphore
private readonly limiter: ConcurrencyLimiter
constructor(config: DifyConfigResolved) {
super()
this.config = config
this.limiter = new Semaphore(config.maxConcurrency)
this.limiter = new ConcurrencyLimiter(config.maxConcurrency)
}
async request(
@@ -18,7 +18,7 @@ import { BaseResource } from '../../../resource/base.ts'
import { RAMResource } from '../../../resource/ram/ram.ts'
import { MountMode } from '../../../types.ts'
import { getTestParser } from '../../../workspace/fixtures/workspace_fixture.ts'
import { Workspace } from '../../../workspace/workspace.ts'
import { Workspace } from '../../../workspace/workspace/workspace.ts'
const ENC = new TextEncoder()
const DEC = new TextDecoder()
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../../../ops/registry.ts'
import { RAMResource } from '../../../resource/ram/ram.ts'
import { MountMode } from '../../../types.ts'
import { getTestParser } from '../../../workspace/fixtures/workspace_fixture.ts'
import { Workspace } from '../../../workspace/workspace.ts'
import { Workspace } from '../../../workspace/workspace/workspace.ts'
async function makeWs(): Promise<Workspace> {
const parser = await getTestParser()
@@ -26,7 +26,7 @@ import {
PathSpec,
} from '../../../types.ts'
import { getTestParser } from '../../../workspace/fixtures/workspace_fixture.ts'
import { Workspace } from '../../../workspace/workspace.ts'
import { Workspace } from '../../../workspace/workspace/workspace.ts'
import { statGeneric } from './stat.ts'
const MTIME = '2026-01-02T15:30:45Z'
@@ -24,7 +24,7 @@ import { IOResult } from '../../../../io/types.ts'
import { OpsRegistry } from '../../../../ops/registry.ts'
import { RAMResource } from '../../../../resource/ram/ram.ts'
import { MountMode } from '../../../../types.ts'
import { Workspace } from '../../../../workspace/workspace.ts'
import { Workspace } from '../../../../workspace/workspace/workspace.ts'
import { gitFs } from './fs.ts'
import { ensureDir } from './io.ts'
import type { Dispatch } from './types.ts'
@@ -25,7 +25,7 @@ import { OpsRegistry } from '../../../../ops/registry.ts'
import { RAMResource } from '../../../../resource/ram/ram.ts'
import { createShellParser, type ShellParser } from '../../../../shell/parse.ts'
import { MountMode } from '../../../../types.ts'
import { Workspace } from '../../../../workspace/workspace.ts'
import { Workspace } from '../../../../workspace/workspace/workspace.ts'
import { GIT } from './index.ts'
import { ensureDir } from './io.ts'
import type { Dispatch } from './types.ts'
@@ -33,7 +33,7 @@ import { OpsRegistry } from '../../../../ops/registry.ts'
import { RAMResource } from '../../../../resource/ram/ram.ts'
import { createShellParser, type ShellParser } from '../../../../shell/parse.ts'
import { MountMode } from '../../../../types.ts'
import { Workspace } from '../../../../workspace/workspace.ts'
import { Workspace } from '../../../../workspace/workspace/workspace.ts'
import { GIT } from './index.ts'
import { ensureDir, readNames, readOptional } from './io.ts'
import type { Dispatch } from './types.ts'
@@ -33,7 +33,7 @@ import { OpsRegistry } from '../../../../ops/registry.ts'
import { RAMResource } from '../../../../resource/ram/ram.ts'
import { createShellParser, type ShellParser } from '../../../../shell/parse.ts'
import { MountMode } from '../../../../types.ts'
import { Workspace } from '../../../../workspace/workspace.ts'
import { Workspace } from '../../../../workspace/workspace/workspace.ts'
import { GIT } from './index.ts'
import { ensureDir } from './io.ts'
import type { Dispatch } from './types.ts'
@@ -0,0 +1,122 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { describe, expect, it } from 'vitest'
import { ConcurrencyLimiter } from './limiter.ts'
interface ConcurrencyState {
active: number
peak: number
}
async function holdPermit(
limiter: ConcurrencyLimiter,
state: ConcurrencyState,
entered: string[],
gate: Promise<void>,
): Promise<void> {
const release = await limiter.acquire()
state.active += 1
state.peak = Math.max(state.peak, state.active)
entered.push('in')
try {
await gate
} finally {
state.active -= 1
release()
}
}
describe('ConcurrencyLimiter', () => {
it.each([[0], [-1]])('rejects a non-positive capacity (%i)', (capacity) => {
expect(() => new ConcurrencyLimiter(capacity)).toThrow('at least 1')
})
it('limits concurrent operations to the capacity', async () => {
const limiter = new ConcurrencyLimiter(2)
const state: ConcurrencyState = { active: 0, peak: 0 }
const entered: string[] = []
let open = (): void => undefined
const gate = new Promise<void>((resolve) => {
open = resolve
})
const held = Array.from({ length: 5 }, () => holdPermit(limiter, state, entered, gate))
await Promise.resolve()
await Promise.resolve()
expect(entered.length).toBe(2)
open()
await Promise.all(held)
expect(state.peak).toBe(2)
})
it('a throwing holder still returns its permit', async () => {
const limiter = new ConcurrencyLimiter(1)
const release = await limiter.acquire()
try {
throw new Error('boom')
} catch {
release()
}
const second = await limiter.acquire()
expect(typeof second).toBe('function')
})
// The bug the hand-off in `release` exists to prevent. Returning the
// permit to the pool before waking the waiter leaves it visible for the
// rest of that tick, so a caller arriving in the same tick takes it --
// and then the woken waiter decrements too, and both run at once. The
// late acquire has to happen in the same tick as the release, which is
// why this drives the limiter directly instead of through holdPermit.
it('a caller arriving during a release cannot barge past the queue', async () => {
const limiter = new ConcurrencyLimiter(1)
const first = await limiter.acquire()
let queuedGranted = false
const queued = limiter.acquire().then((release) => {
queuedGranted = true
return release
})
first()
let lateGranted = false
const late = limiter.acquire().then((release) => {
lateGranted = true
return release
})
await Promise.resolve()
await Promise.resolve()
expect(queuedGranted).toBe(true)
expect(lateGranted).toBe(false)
;(await queued)()
;(await late)()
})
it('releasing twice does not inflate the permit count', async () => {
const limiter = new ConcurrencyLimiter(1)
const release = await limiter.acquire()
release()
release()
const state: ConcurrencyState = { active: 0, peak: 0 }
const entered: string[] = []
await Promise.all([
holdPermit(limiter, state, entered, Promise.resolve()),
holdPermit(limiter, state, entered, Promise.resolve()),
])
expect(state.peak).toBe(1)
})
})
@@ -0,0 +1,66 @@
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
/**
* Limit concurrent async operations within one process.
*
* Twin of python's `concurrency/limiter.py`, which wraps
* `asyncio.Semaphore`. Node has no built-in semaphore, so the queue is
* explicit -- but the two contracts match: a permit count below one is
* rejected rather than clamped, and a release hands its permit straight
* to the longest-waiting caller instead of returning it to the pool.
* Handing off matters: returning it first would let a caller arriving in
* the same tick barge ahead of the queue and drive the count negative.
*/
export class ConcurrencyLimiter {
private available: number
private readonly waiters: (() => void)[] = []
constructor(maxConcurrency: number) {
if (maxConcurrency < 1) throw new Error('maxConcurrency must be at least 1')
this.available = maxConcurrency
}
/**
* Take one permit, waiting for a free one, and return its release.
*
* The release is idempotent so a caller that unwinds twice (a `finally`
* plus an explicit call) cannot inflate the permit count -- python gets
* that from `async with` and the shape here has to supply it.
*/
async acquire(): Promise<() => void> {
if (this.available > 0) {
this.available -= 1
} else {
await new Promise<void>((resolve) => {
this.waiters.push(resolve)
})
}
let released = false
return () => {
if (released) return
released = true
this.release()
}
}
private release(): void {
const next = this.waiters.shift()
if (next !== undefined) {
next()
return
}
this.available += 1
}
}
+2 -2
View File
@@ -95,5 +95,5 @@ export { SessionStore } from './workspace/session/store.ts'
export { ContentDriftError } from './workspace/snapshot/drift.ts'
export { toStateDict } from './workspace/snapshot/state.ts'
export { S3WorkspaceStateStore } from './workspace/store/s3.ts'
export { Workspace } from './workspace/workspace.ts'
export type { MountSpec } from './workspace/workspace.ts'
export { Workspace } from './workspace/workspace/workspace.ts'
export type { MountSpec } from './workspace/workspace/workspace.ts'
@@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest'
import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { MountMode, PathSpec, ResourceName } from '../types.ts'
import { Workspace } from '../workspace/workspace.ts'
import { Workspace } from '../workspace/workspace/workspace.ts'
import { runWithRecording } from './context.ts'
function call(
+1 -1
View File
@@ -22,7 +22,7 @@ import type { Action, OpsContext, OpsResultContext } from '../policy/types.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { FileType, Limit, MountMode, OnExceed } from '../types.ts'
import { enotdir } from '../utils/errors.ts'
import { Workspace } from '../workspace/workspace.ts'
import { Workspace } from '../workspace/workspace/workspace.ts'
const DEC = new TextDecoder()
@@ -21,7 +21,7 @@ import { RAMResource } from '../resource/ram/ram.ts'
import { createShellParser, type ShellParser } from '../shell/parse.ts'
import { Limit, MountMode, OnExceed, PathSpec } from '../types.ts'
import { MountRegistry } from '../workspace/mount/registry.ts'
import { Workspace } from '../workspace/workspace.ts'
import { Workspace } from '../workspace/workspace/workspace.ts'
import type { Policy } from './base.ts'
import { MountRootPolicy } from './builtin/mount_root.ts'
import { PolicyDenied } from './errors.ts'
@@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest'
import { OpsRegistry } from '../../ops/registry.ts'
import { MountMode, PathSpec, ResourceName } from '../../types.ts'
import { getTestParser } from '../../workspace/fixtures/workspace_fixture.ts'
import { Workspace } from '../../workspace/workspace.ts'
import { Workspace } from '../../workspace/workspace/workspace.ts'
import { RAMResource } from '../ram/ram.ts'
import { DevResource } from './dev.ts'
@@ -17,7 +17,7 @@ import type { Accessor } from '../../accessor/base.ts'
import { mkdir as coreMkdir } from '../../core/ram/mkdir.ts'
import { OpsRegistry } from '../../ops/registry.ts'
import { FileType, MountMode, PathSpec, ResourceName } from '../../types.ts'
import { Workspace } from '../../workspace/workspace.ts'
import { Workspace } from '../../workspace/workspace/workspace.ts'
import { RAMResource } from './ram.ts'
function setup(): { ram: RAMResource; registry: OpsRegistry; ws: Workspace } {
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { MountMode } from '../types.ts'
import { getTestParser, stderrStr, stdoutStr } from '../workspace/fixtures/workspace_fixture.ts'
import { Workspace } from '../workspace/workspace.ts'
import { Workspace } from '../workspace/workspace/workspace.ts'
import { MontyRuntime } from './python/monty/index.ts'
import { PyodideRuntime } from './python/pyodide.ts'
import { QuickJsRuntime } from './js/quickjs.ts'
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { FileType, MountMode } from '../types.ts'
import { getTestParser, stderrStr, stdoutStr } from '../workspace/fixtures/workspace_fixture.ts'
import { Workspace } from '../workspace/workspace.ts'
import { Workspace } from '../workspace/workspace/workspace.ts'
import { QuickJsRuntime } from './js/quickjs.ts'
import { MontyRuntime } from './python/monty/index.ts'
@@ -20,7 +20,7 @@ import { buildRuntime } from '../../table.ts'
import { getTestParser } from '../../../workspace/fixtures/workspace_fixture.ts'
import { RAMResource } from '../../../resource/ram/ram.ts'
import { MountMode } from '../../../types.ts'
import { Workspace } from '../../../workspace/workspace.ts'
import { Workspace } from '../../../workspace/workspace/workspace.ts'
import { PrefixResolver } from '../../resolver.ts'
function makeBridge(seed: Record<string, Uint8Array>): {
@@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest'
import { getTestParser } from '../../workspace/fixtures/workspace_fixture.ts'
import { RAMResource } from '../../resource/ram/ram.ts'
import { Limit, MountMode } from '../../types.ts'
import { Workspace } from '../../workspace/workspace.ts'
import { Workspace } from '../../workspace/workspace/workspace.ts'
import { RemoteSandbox } from './base.ts'
import { isLineExecutor } from '../mixin.ts'
import type { RunResult, RuntimeOptions } from '../types.ts'
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { MountMode } from '../types.ts'
import { getTestParser, stdoutStr } from '../workspace/fixtures/workspace_fixture.ts'
import { Workspace } from '../workspace/workspace.ts'
import { Workspace } from '../workspace/workspace/workspace.ts'
// Port of tests/shell/test_background_jobs.py::test_background_does_not_consume_stdin.
// A backgrounded command must NOT read from the shell's stdin — otherwise the
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { MountMode } from '../types.ts'
import { getTestParser, stderrStr, stdoutStr } from '../workspace/fixtures/workspace_fixture.ts'
import { Workspace } from '../workspace/workspace.ts'
import { Workspace } from '../workspace/workspace/workspace.ts'
// Direct port of tests/shell/test_quoting_coverage.py.
// Each test is one realistic agent pattern — failures surface as parser,
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { MountMode } from '../types.ts'
import { getTestParser, stdoutStr } from './fixtures/workspace_fixture.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
// GNU find/tree/du/ls typed bare behave exactly as if `.` had been
// typed: ./-prefixed walk lines, `.:` ls -R headers, du rows ending in
@@ -18,7 +18,7 @@ import { RAMResource } from '../resource/ram/ram.ts'
import type { BridgeDispatchFn } from '../runtime/types.ts'
import type { VFSEntry } from '../runtime/vfs.ts'
import { MountMode } from '../types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
function bridgeOn(ws: Workspace): BridgeDispatchFn {
return (ws as unknown as { buildWorkspaceBridge(): BridgeDispatchFn }).buildWorkspaceBridge()
@@ -24,7 +24,7 @@ import { describe, expect, it } from 'vitest'
import { RAMResource } from '../resource/ram/ram.ts'
import { createShellParser } from '../shell/parse.ts'
import { MountMode } from '../types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
const DEC = new TextDecoder()
const require = createRequire(import.meta.url)
@@ -19,7 +19,7 @@ import { cachesReads } from '../resource/base.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { createShellParser } from '../shell/parse.ts'
import { ConsistencyPolicy, MountMode, PathSpec } from '../types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
const ENC = new TextEncoder()
const DEC = new TextDecoder()
@@ -27,7 +27,7 @@ import { createShellParser, type ShellParser } from '../shell/parse.ts'
import { MountMode } from '../types.ts'
import { ScriptSource } from '../runtime/policy/types.ts'
import type { RuntimeLanguage } from '../runtime/types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
// Mirrors python/tests/e2e/test_cli_dispatch.py.
@@ -17,7 +17,7 @@ import { RAMResource } from '../resource/ram/ram.ts'
import { PathSpec } from '../types.ts'
import { RAMSessionStore } from './session/ram.ts'
import { toStateDict } from './snapshot/state.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
class ProbeRAMResource extends RAMResource {
closeCalls = 0
@@ -19,7 +19,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { createShellParser, type ShellParser } from '../shell/parse.ts'
import { Limit, MountMode, OnExceed } from '../types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
const require = createRequire(import.meta.url)
const engineWasm = readFileSync(require.resolve('web-tree-sitter/web-tree-sitter.wasm'))
@@ -22,7 +22,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { createShellParser, type ShellParser } from '../shell/parse.ts'
import { Limit, MountMode } from '../types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
class SignalProbeRuntime extends LanguageRuntime {
readonly language = 'python'
@@ -19,7 +19,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { createShellParser, type ShellParser } from '../shell/parse.ts'
import { Limit, MountMode, OnExceed } from '../types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
const require = createRequire(import.meta.url)
const engineWasm = readFileSync(require.resolve('web-tree-sitter/web-tree-sitter.wasm'))
@@ -21,8 +21,8 @@ import { ProvisionResult } from '../provision/types.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { MountMode, ResourceName } from '../types.ts'
import { getTestParser, stderrStr } from './fixtures/workspace_fixture.ts'
import type { ExecuteResult } from './workspace.ts'
import { Workspace } from './workspace.ts'
import type { ExecuteResult } from './workspace/workspace.ts'
import { Workspace } from './workspace/workspace.ts'
const ENC = new TextEncoder()
const SPEC = new CommandSpec({ rest: new Operand({ type: 'path' }) })
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { MountMode } from '../types.ts'
import { getTestParser, stderrStr, stdoutStr } from './fixtures/workspace_fixture.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
// Direct port of tests/workspace/test_cwd_integration.py.
// Exercises cwd tracking + `cd`/`pwd`/`ls` across quoting/escaping of
@@ -19,7 +19,7 @@ import type { RAMFileCacheStore } from '../cache/file/ram.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { createShellParser } from '../shell/parse.ts'
import { MountMode, PathSpec } from '../types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
const require = createRequire(import.meta.url)
const engineWasm = readFileSync(require.resolve('web-tree-sitter/web-tree-sitter.wasm'))
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../../ops/registry.ts'
import { RAMResource } from '../../resource/ram/ram.ts'
import { Limit, MountMode, PathSpec } from '../../types.ts'
import { getTestParser } from '../fixtures/workspace_fixture.ts'
import { Workspace } from '../workspace.ts'
import { Workspace } from '../workspace/workspace.ts'
const ENC = new TextEncoder()
const DEC = new TextDecoder()
@@ -19,7 +19,7 @@ import { OpsRegistry } from '../ops/registry.ts'
import { RAMResource } from '../resource/ram/ram.ts'
import { createShellParser, type ShellParser } from '../shell/parse.ts'
import { MountMode } from '../types.ts'
import { Workspace } from './workspace.ts'
import { Workspace } from './workspace/workspace.ts'
const require = createRequire(import.meta.url)
const engineWasm = readFileSync(require.resolve('web-tree-sitter/web-tree-sitter.wasm'))
@@ -18,8 +18,8 @@ import { RAMResource } from '../resource/ram/ram.ts'
import { MountMode } from '../types.ts'
import { Channel, JobConsole } from '../shell/console/index.ts'
import { getTestParser, stdoutStr } from './fixtures/workspace_fixture.ts'
import type { ExecuteResult } from './workspace.ts'
import { Workspace } from './workspace.ts'
import type { ExecuteResult } from './workspace/workspace.ts'
import { Workspace } from './workspace/workspace.ts'
const DEC = new TextDecoder()
@@ -19,7 +19,7 @@ import { RAMResource } from '../../resource/ram/ram.ts'
import { MountMode } from '../../types.ts'
import { gzip } from '../../utils/compress.ts'
import { getTestParser, stderrStr, stdoutStr } from '../fixtures/workspace_fixture.ts'
import { Workspace } from '../workspace.ts'
import { Workspace } from '../workspace/workspace.ts'
// Direct port of tests/workspace/executor/test_archive_relay.py: member
// selectors stay off routing, extraction lands in the cwd or -C across
@@ -17,7 +17,7 @@ import { RAMResource } from '../../../resource/ram/ram.ts'
import { CapacityState, MountMode } from '../../../types.ts'
import type { CapacityResult } from '../../../types.ts'
import { getTestParser } from '../../fixtures/workspace_fixture.ts'
import { Workspace } from '../../workspace.ts'
import { Workspace } from '../../workspace/workspace.ts'
// RAM backend that reports a fixed quota, standing in for a real filesystem
// / a provider that exposes storage numbers (real disk free space is
@@ -18,7 +18,7 @@ import { RAMResource } from '../../../resource/ram/ram.ts'
import type { FileStat } from '../../../types.ts'
import { MountMode } from '../../../types.ts'
import { getTestParser } from '../../fixtures/workspace_fixture.ts'
import { Workspace } from '../../workspace.ts'
import { Workspace } from '../../workspace/workspace.ts'
import { parseGroup, parseOwner, parseTouchStamp } from './metadata.ts'
describe('parseOwner', () => {
@@ -17,7 +17,7 @@ import { IOResult } from '../../../io/types.ts'
import { RAMResource } from '../../../resource/ram/ram.ts'
import { MountMode, PathSpec } from '../../../types.ts'
import { getTestParser } from '../../fixtures/workspace_fixture.ts'
import { Workspace } from '../../workspace.ts'
import { Workspace } from '../../workspace/workspace.ts'
import {
absPath,
expandOperands,
@@ -18,7 +18,7 @@ import { describe, expect, it } from 'vitest'
import { RAMResource } from '../../../resource/ram/ram.ts'
import { createShellParser } from '../../../shell/parse.ts'
import { ConsistencyPolicy, MountMode, PathSpec, ResourceName } from '../../../types.ts'
import { Workspace } from '../../workspace.ts'
import { Workspace } from '../../workspace/workspace.ts'
import { dropServiceCaches } from './run.ts'
const ENC = new TextEncoder()
@@ -17,7 +17,7 @@ import { OpsRegistry } from '../../ops/registry.ts'
import { RAMResource } from '../../resource/ram/ram.ts'
import { MountMode } from '../../types.ts'
import { getTestParser, stderrStr, stdoutStr } from '../fixtures/workspace_fixture.ts'
import { Workspace } from '../workspace.ts'
import { Workspace } from '../workspace/workspace.ts'
// Direct port of tests/workspace/test_cross_mount_errors.py.
// Exercises error paths for cross-mount head/tail — cross_mount.test.ts
@@ -24,7 +24,7 @@ import { describe, expect, it } from 'vitest'
import { RAMResource } from '../../resource/ram/ram.ts'
import { createShellParser } from '../../shell/parse.ts'
import { MountMode } from '../../types.ts'
import { Workspace } from '../workspace.ts'
import { Workspace } from '../workspace/workspace.ts'
const DEC = new TextDecoder()
const require = createRequire(import.meta.url)

Some files were not shown because too many files have changed in this diff Show More