Files
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

179 lines
6.7 KiB
Python

# ========= 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 logging
from mirage.types import DriftPolicy, FingerprintKey
logger = logging.getLogger(__name__)
class ContentDriftError(Exception):
"""Raised at load time when a remote resource's live fingerprint
differs from what was recorded in the snapshot.
Indicates the underlying source has been modified since the snapshot
was taken, so reading current bytes would silently diverge from what
the original agent saw. Surface to the caller rather than mask.
Attributes:
path (str): Virtual path that drifted.
snapshot_fingerprint (str): Recorded marker.
live_fingerprint (str | None): Marker observed at load time.
"""
def __init__(self, path: str, snapshot_fingerprint: str,
live_fingerprint: str | None) -> None:
self.path = path
self.snapshot_fingerprint = snapshot_fingerprint
self.live_fingerprint = live_fingerprint
live_repr = repr(
live_fingerprint) if live_fingerprint is not None else "<missing>"
super().__init__(
f"{path}: snapshot fingerprint {snapshot_fingerprint!r}, "
f"live {live_repr}; data on the underlying source has changed "
"since the snapshot was taken")
def capture_fingerprints(ws) -> list[dict]:
"""Walk session ops and emit one entry per distinct read on a
``SUPPORTS_SNAPSHOT`` mount.
Pure aggregation over ``ws._ops.records``. Each read ``OpRecord``
carries the ``fingerprint`` and/or ``revision`` the backend
returned at the moment the agent read the bytes (populated from
the GET response, not a fresh stat at snapshot time). This avoids
the race where the upstream changes between read and snapshot.
Skips paths whose owning mount has ``SUPPORTS_SNAPSHOT=False``
(live-only backends like Gmail/Slack/Linear) and reads where the
backend returned neither marker.
Args:
ws: Workspace whose ops log to walk.
Returns:
list[dict]: One entry per (recorded, fingerprinted) path, with
``PATH``, ``MOUNT_PREFIX`` and at least one of ``FINGERPRINT``
or ``REVISION``. Both may be present on versioned backends that
return ETag and VersionId on every GET.
"""
seen: set[str] = set()
out: list[dict] = []
for rec in ws._ops.records:
if rec.op != "read" or rec.path in seen:
continue
if rec.fingerprint is None and rec.revision is None:
continue
seen.add(rec.path)
try:
mount = ws._registry.mount_for(rec.path)
except ValueError:
continue
if not getattr(mount.resource, "SUPPORTS_SNAPSHOT", False):
continue
entry: dict = {
FingerprintKey.PATH: rec.path,
FingerprintKey.MOUNT_PREFIX: mount.prefix,
}
if rec.fingerprint is not None:
entry[FingerprintKey.FINGERPRINT] = rec.fingerprint
if rec.revision is not None:
entry[FingerprintKey.REVISION] = rec.revision
out.append(entry)
return out
def install_fingerprints(ws, fingerprint_entries: list[dict],
drift_policy: DriftPolicy) -> None:
"""Install snapshot fingerprints/revisions onto a reconstructed ws.
Revisions pin replay reads to exact backend versions; bare
fingerprints queue an eager drift check. OFF evicts the snapshot
cache for fingerprinted paths so reads serve current state.
Args:
ws: the reconstructed workspace to install onto.
fingerprint_entries: entries from a snapshot's FINGERPRINTS.
drift_policy: STRICT queues drift checks; OFF skips and evicts.
"""
ws._drift_policy = drift_policy
if drift_policy == DriftPolicy.OFF:
if fingerprint_entries:
ws._cache.evict_paths(f[FingerprintKey.PATH]
for f in fingerprint_entries)
return
for f in fingerprint_entries:
path = f[FingerprintKey.PATH]
try:
mount = ws._registry.mount_for(path)
except ValueError:
continue
revision = f.get(FingerprintKey.REVISION)
if revision is not None:
mount.revisions[path] = revision
continue
fingerprint = f.get(FingerprintKey.FINGERPRINT)
if fingerprint is not None:
ws._pending_drift.append((mount, path, fingerprint))
ws._drift_check_pending = bool(ws._pending_drift)
def live_only_mount_prefixes(ws) -> list[str]:
"""Return mount prefixes whose resource opts out of snapshot replay.
These mounts will serve current state at load time with no drift
detection. Surfaced in the snapshot manifest so the load layer can
log them and so users can audit which paths are non-replayable.
"""
out: list[str] = []
for m in ws._registry.mounts():
if m.prefix in {"/dev/", "/.bash_history/"}:
continue
if not getattr(m.resource, "SUPPORTS_SNAPSHOT", False):
out.append(m.prefix)
return out
async def check_drift(ws, path: str, recorded: str) -> None:
"""Stat `path` against its mount and raise ContentDriftError if the
live fingerprint does not match `recorded`.
No-op if the mount cannot be resolved or the resource cannot
fingerprint (raises only on a real, observable mismatch).
Args:
ws: Workspace whose registry to consult.
path (str): Virtual path to check.
recorded (str): Fingerprint recorded at snapshot time.
Raises:
ContentDriftError: live fingerprint differs from recorded.
"""
try:
mount = ws._registry.mount_for(path)
except ValueError:
return
if not getattr(mount.resource, "SUPPORTS_SNAPSHOT", False):
return
try:
stat = await mount.execute_op("stat", path)
except FileNotFoundError as exc:
raise ContentDriftError(path, recorded, None) from exc
live = getattr(stat, "fingerprint", None)
if live is None:
return
if live != recorded:
raise ContentDriftError(path, recorded, live)