@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
393 lines • 15.5 kB
JavaScript
// ========= 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 git, { Errors } from 'isomorphic-git';
import { getAdmission } from "../../../../context/session_context.js";
import { decodeText } from "../../../../shell/bytes.js";
import { concat } from "../../../../utils/bytes.js";
import { readerOf } from "../../../../workspace/files.js";
import { readIndex } from "./index_file.js";
import { visiblePath } from "./pathspec.js";
import { loadRefs } from "./refs.js";
import { objectType, rawObject, repoArgs } from "./repo.js";
// What a session is shown of a repository's history. A hide or a path rule
// names a place in the working tree, and git keeps every version of what stood
// there in its object store, which no file call reaches: so git asks the same
// two questions of the place an object stands at. A hidden place is one the
// history never had, and content at a place a rule keeps from the command is
// refused as the file is. Mirrors Python's `seen`.
/**
* Whether a hide or a rule in force could cover anything in the repository's
* working tree for the running command; when nothing can, every object is shown
* as stored and nothing is looked up.
*
* @param repo the opened repository
*/
export function guarded(repo) {
const vfs = repo.location.vfs;
if (vfs === undefined || vfs === null)
return false;
if (repo.guarded === null) {
const worktree = repo.location.worktree.virtual;
const gate = getAdmission();
repo.guarded = readerOf(vfs).hidesUnder(worktree) || gate?.scopes(worktree) === true;
}
return repo.guarded;
}
/**
* Refuse a place as the dispatcher refuses its file: a rule in force for the
* running command throws `PolicyDenied` naming it.
*
* @param location the discovered repository
* @param relative the repository-relative place
*/
export function checkRules(location, relative) {
const gate = getAdmission();
if (gate !== null && location.vfs !== undefined && location.vfs !== null)
gate.check(location.worktree.join(relative).virtual);
}
/**
* Whether a rule in force could refuse anything at or under a place for the
* running command; when none can, nothing there is asked.
*
* @param location the discovered repository
* @param relative the repository-relative place, the root by default
*/
export function ruledUnder(location, relative = '') {
const gate = getAdmission();
if (gate === null || location.vfs === undefined || location.vfs === null)
return false;
return gate.scopes(location.worktree.join(relative).virtual);
}
/**
* Whether no rule in force refuses a place for the running command, asked
* without refusing it.
*
* @param location the discovered repository
* @param relative the repository-relative place
*/
export function permitted(location, relative) {
const gate = getAdmission();
if (gate === null || location.vfs === undefined || location.vfs === null)
return true;
return gate.refusal(location.worktree.join(relative).virtual) === null;
}
/**
* An object a revision found at a place, which the session may be shown there:
* noted, so a tree prints what that place holds, and a blob's content refused
* where a rule keeps the place from the command. A rule guards content, not
* names, so a tree is listed.
*
* The caller has already refused a hidden place in its own words.
*
* @param repo the opened repository
* @param obj the object found
* @param relative the repository-relative place
* @param revision the operand that named it, whose place this is whatever other
* place holds the same object
*/
export function placed(repo, obj, relative, revision = null) {
if (obj.type === 'blob')
checkRules(repo.location, relative);
if (!repo.places.has(obj.oid))
repo.places.set(obj.oid, relative);
if (revision !== null)
repo.named.set(revision, relative);
return obj;
}
/**
* Note a commit's tree, which stands at the repository's root.
*
* @param repo the opened repository
* @param tree the root tree's id
* @param revision the operand that named it
*/
export function rooted(repo, tree, revision = null) {
if (!repo.places.has(tree))
repo.places.set(tree, '');
if (revision !== null)
repo.named.set(revision, '');
}
/**
* An object named by id, if the session may be shown it: one reachable at a
* place it sees and no rule keeps from the command; null when it is shown
* nowhere.
*
* A commit and a tag carry no place and are always shown. A tree or a blob is
* looked for where the history and the index hold it, so an id an agent
* computed for content it guessed answers exactly as an id nothing holds. A
* blob held only where a rule keeps it throws that rule's `PolicyDenied`.
*
* @param repo the opened repository
* @param obj the object the id names
*/
export async function located(repo, obj) {
if (!guarded(repo) || obj.type === 'commit' || obj.type === 'tag' || repo.places.has(obj.oid))
return obj;
const place = await find(repo, obj.oid, obj.type === 'blob');
if (place === null)
return null;
repo.places.set(obj.oid, place);
return obj;
}
/**
* Whether an id names an object the session may be shown: one the store holds,
* which `located` finds wherever it stands.
*
* @param repo the opened repository
* @param oid the object's id
*/
export async function visibleObject(repo, oid) {
const type = await objectType(repo, oid);
if (type === null)
return false;
if (!guarded(repo))
return true;
return (await located(repo, { oid, type })) !== null;
}
/**
* Where this command found an object, for a tree whose entries are shown
* relative to it; the root when nothing narrows what is shown.
*
* @param repo the opened repository
* @param oid the object's id
*/
export async function placeOf(repo, oid) {
let found = repo.places.get(oid) ?? null;
if (found === null && guarded(repo))
found = await find(repo, oid, false);
return found ?? '';
}
/**
* Where the object an operand names stands for that operand: the place its own
* path gave it (`HEAD:b` is `b` even where `HEAD:a` holds the same tree), else
* where this command found it.
*
* @param repo the opened repository
* @param name the operand as typed
* @param oid the object's id
*/
export async function placeNamed(repo, name, oid) {
const named = repo.named.get(name);
return named ?? (await placeOf(repo, oid));
}
/**
* A tree's entries the session sees, at the place it was found.
*
* @param repo the opened repository
* @param tree the tree's id
* @param base the place it stands at, null for where this command found it
*/
export async function shownItems(repo, tree, base = null) {
const items = (await git.readTree({ ...repoArgs(repo), oid: tree })).tree;
if (!guarded(repo))
return items;
const where = base ?? (await placeOf(repo, tree));
return items.filter((entry) => visiblePath(repo.location, where ? `${where}/${entry.path}` : entry.path));
}
/**
* An object's stored bytes as the session is shown them: a tree's with only the
* entries it sees, so neither the bytes nor their count name a hidden one. Only
* a tree is placed, so a commit's or a blob's bytes cost no lookup.
*
* A tree's entries stand in its bytes in git's order, each its mode, its name
* and its id, so dropping the ones the session does not see leaves exactly the
* tree git would store for the rest.
*
* @param repo the opened repository
* @param obj the object
* @param name the operand that named it, whose place a tree is listed at; null
* for where this command found it
*/
export async function shownRaw(repo, obj, name = null) {
const { raw } = await rawObject(repo, obj.oid);
if (obj.type !== 'tree' || !guarded(repo))
return raw;
const where = name === null ? await placeOf(repo, obj.oid) : await placeNamed(repo, name, obj.oid);
// Each entry ends in its id's twenty bytes, right after the NUL.
const idBytes = 20;
const kept = [];
for (let at = 0; at < raw.length;) {
const space = raw.indexOf(0x20, at);
const nul = space < 0 ? -1 : raw.indexOf(0, space);
if (nul < 0)
break;
const end = nul + 1 + idBytes;
const name = decodeText(raw.subarray(space + 1, nul));
if (visiblePath(repo.location, where ? `${where}/${name}` : name))
kept.push(raw.subarray(at, end));
at = end;
}
return concat(kept);
}
/**
* A tree's leaves the session sees, keyed relative to `base`.
*
* What a diff, a log or a search reads its sides from, so a hidden path is no
* change, no match and no count, as if never committed. A leaf at a place a
* rule keeps from the command stays listed, a rule guarding content rather
* than names, and its blob is noted so `readable` refuses to read it there.
*
* @param repo the opened repository
* @param entries leaves keyed by git name
* @param base the place the tree stands at
*/
export function shownEntries(repo, entries, base = '') {
if (!guarded(repo))
return new Map(entries);
const location = repo.location;
const shown = new Map();
for (const [path, entry] of entries) {
const relative = base ? `${base}/${path}` : path;
if (!visiblePath(location, relative))
continue;
shown.set(path, entry);
if (permitted(location, relative))
repo.free.add(entry.oid);
else if (!repo.sealed.has(entry.oid))
repo.sealed.set(entry.oid, relative);
}
return shown;
}
/**
* Refuse a blob's content where this command found it only at a place a rule
* keeps from it: the same bytes anywhere it may read are no secret, so those
* read.
*
* @param repo the opened repository
* @param sha the blob's id
* @throws PolicyDenied the rule's refusal, naming the place
*/
export function readable(repo, sha) {
if (repo.free.has(sha))
return;
const place = repo.sealed.get(sha);
if (place !== undefined)
checkRules(repo.location, place);
}
/**
* The first place the index or the history holds an object at that the session
* sees and, for content, no rule keeps from the command.
*
* Every lookup a command makes shares one walk (`sightings`), resumed where the
* last stopped and remembered as it goes, so a batch naming the same or many
* ids reads the history once. Content held only where rules refuse it is judged
* at the first such place, which throws that rule's `PolicyDenied`.
*
* @param repo the opened repository
* @param oid the object's id
* @param content whether it is a blob, whose bytes rules guard
*/
async function find(repo, oid, content) {
const location = repo.location;
const usable = (place) => !content || permitted(location, place);
for (const place of repo.seen.get(oid) ?? [])
if (usable(place))
return place;
repo.sightings ??= sightings(repo);
// Stepped by hand: leaving a `for await` early would close the walk the
// next lookup resumes.
for (let next = await repo.sightings.next(); next.done !== true;) {
const [found, place] = next.value;
const held = repo.seen.get(found);
if (held === undefined)
repo.seen.set(found, [place]);
else
held.push(place);
if (found === oid && usable(place))
return place;
next = await repo.sightings.next();
}
const first = repo.seen.get(oid)?.[0];
if (first === undefined)
return null;
checkRules(location, first);
return first;
}
/**
* Every object the index and the history hold, each with a place the session
* sees, in the order a lookup wants them: the index first, then each commit any
* ref reaches. A subtree is read once per place, however many commits share
* it, an object is sighted once per place, and a hidden place is never
* descended into: what stands under it is hidden too. An object only a ref
* names directly stands at no place and is never sighted.
*
* @param repo the opened repository
*/
async function* sightings(repo) {
const location = repo.location;
const state = await readIndex(repo, repo.vfs);
const staged = [...state.entries].map(([name, entry]) => [name, entry.oid]);
for (const [name, conflict] of state.conflicts) {
for (const side of [conflict.ancestor, conflict.this, conflict.other])
if (side !== null)
staged.push([name, side.oid]);
}
const sighted = new Set();
const fresh = (oid, place) => {
const key = `${oid}\0${place}`;
if (sighted.has(key))
return false;
sighted.add(key);
return true;
};
for (const [relative, sha] of staged)
if (relative && visiblePath(location, relative) && fresh(sha, relative))
yield [sha, relative];
const walk = async function* (treeId, base) {
if (!fresh(treeId, base))
return;
yield [treeId, base];
for (const entry of (await git.readTree({ ...repoArgs(repo), oid: treeId })).tree) {
const relative = base ? `${base}/${entry.path}` : entry.path;
if (!visiblePath(location, relative))
continue;
if (entry.type === 'tree')
yield* walk(entry.oid, relative);
else if (fresh(entry.oid, relative))
yield [entry.oid, relative];
}
};
const refs = await loadRefs(repo.vfs, location.gitdir, location.commondir);
// A symbolic ref names another ref the table already holds.
const pending = [...refs.values()].filter((value) => /^[0-9a-f]{40}$/.test(value));
const reached = new Set();
for (let sha = pending.pop(); sha !== undefined; sha = pending.pop()) {
if (reached.has(sha))
continue;
reached.add(sha);
let read;
try {
// Deprecated upstream for being general, but one read saying which kind
// an id names, its fields parsed, is what the walk needs.
// eslint-disable-next-line @typescript-eslint/no-deprecated
read = await git.readObject({ ...repoArgs(repo), oid: sha });
}
catch (err) {
if (err instanceof Errors.NotFoundError)
continue;
throw err;
}
if (read.format !== 'parsed')
continue;
if (read.type === 'tag') {
pending.push(read.object.object);
}
else if (read.type === 'commit') {
for (const item of read.object.parent)
pending.push(item);
yield* walk(read.object.tree, '');
}
}
}
//# sourceMappingURL=seen.js.map