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
316 lines
13 KiB
Python
316 lines
13 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. =========
|
|
|
|
from dataclasses import dataclass, field
|
|
from functools import partial
|
|
from typing import Any, Callable
|
|
|
|
from mirage.provision import Precision, ProvisionResult
|
|
from mirage.shell.node_kind import NodeKind, node_kind
|
|
from mirage.shell.types import NodeType as NT
|
|
from mirage.shell.types import RedirectKind
|
|
from mirage.shell.types import ShellBuiltin as SB
|
|
from mirage.types import PathSpec
|
|
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,
|
|
handle_if_provision,
|
|
handle_while_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
|
|
get_case_items, get_command_name, get_for_parts, get_function_body,
|
|
get_function_name, get_if_branches, get_list_parts, get_negated_command,
|
|
get_parts, get_pipeline_commands, get_redirects, get_subshell_body,
|
|
get_text, get_while_parts, has_command_substitution, split_env_prefix)
|
|
|
|
# eval / source execute their payload, so they are NOT free builtins:
|
|
# leaving them out lets them fall through to command resolution, which
|
|
# honestly reports UNKNOWN instead of a zero-cost EXACT.
|
|
_BUILTIN_NAMES = frozenset({
|
|
SB.CD,
|
|
SB.TRUE,
|
|
SB.FALSE,
|
|
SB.EXPORT,
|
|
SB.UNSET,
|
|
SB.LOCAL,
|
|
SB.PRINTENV,
|
|
SB.READ,
|
|
SB.SET,
|
|
SB.SHIFT,
|
|
SB.TRAP,
|
|
SB.TEST,
|
|
SB.BRACKET,
|
|
SB.DOUBLE_BRACKET,
|
|
SB.WAIT,
|
|
SB.FG,
|
|
SB.KILL,
|
|
SB.JOBS,
|
|
SB.PS,
|
|
SB.ECHO,
|
|
SB.PRINTF,
|
|
SB.SLEEP,
|
|
"return",
|
|
"break",
|
|
"continue",
|
|
})
|
|
|
|
|
|
@dataclass
|
|
class PlanScope:
|
|
"""Walk-local planner state.
|
|
|
|
Function definitions seen during this plan are recorded here (not
|
|
on the session: planning must not mutate shell state), and
|
|
`planning` guards recursive functions from looping the planner.
|
|
"""
|
|
functions: dict[str, list] = field(default_factory=dict)
|
|
planning: set[str] = field(default_factory=set)
|
|
|
|
|
|
async def _provision_redirected(
|
|
recurse: Callable,
|
|
registry: MountRegistry,
|
|
namespace: Namespace | None,
|
|
execute_fn: Callable,
|
|
command: Any,
|
|
redirects: list,
|
|
session: Session,
|
|
) -> ProvisionResult:
|
|
"""Plan one redirected command: expand targets, cost, degrade.
|
|
|
|
Args:
|
|
recurse (Callable): the provision recursion.
|
|
registry (MountRegistry): mount registry.
|
|
namespace (Namespace | None): addressing authority.
|
|
execute_fn (Callable): recursive execute (for expansions).
|
|
command (Any): the redirected command node.
|
|
redirects (list): parsed redirects.
|
|
session (Session): shell session state.
|
|
"""
|
|
expanded, pipe_node = await expand_redirects(redirects, session,
|
|
execute_fn, registry)
|
|
# A cmdsub target expands empty under provision, so its
|
|
# classification is garbage; the precision degrade below keeps
|
|
# the plan honest without costing a phantom write (mirrors TS).
|
|
targets = [
|
|
(r.kind, r.target) for r in expanded
|
|
if r.kind in (RedirectKind.STDIN, RedirectKind.STDOUT) and isinstance(
|
|
r.target, PathSpec) and not r.target.virtual.startswith("/dev/")
|
|
and not (r.target_node is not None
|
|
and has_command_substitution(r.target_node))
|
|
]
|
|
result = await handle_redirect_provision(recurse, registry, command,
|
|
targets, session, namespace)
|
|
if any(r.target_node is not None
|
|
and has_command_substitution(r.target_node) for r in redirects):
|
|
# A suppressed substitution hid the real redirect target.
|
|
result.precision = Precision.UNKNOWN
|
|
if pipe_node is not None:
|
|
return rollup_pipe([result, await recurse(pipe_node, session)])
|
|
return result
|
|
|
|
|
|
async def _provision_reassociated(
|
|
recurse: Callable,
|
|
registry: MountRegistry,
|
|
namespace: Namespace | None,
|
|
execute_fn: Callable,
|
|
redirects: list,
|
|
right: Any,
|
|
node: Any,
|
|
session: Session,
|
|
) -> ProvisionResult:
|
|
"""Provision recurse wrapper for a re-associated trailing redirect.
|
|
|
|
Mirrors the executor: the list's last command carries the hoisted
|
|
redirects; every other node provisions normally.
|
|
|
|
Args:
|
|
recurse (Callable): the provision recursion.
|
|
registry (MountRegistry): mount registry.
|
|
namespace (Namespace | None): addressing authority.
|
|
execute_fn (Callable): recursive execute (for expansions).
|
|
redirects (list): parsed redirects hoisted off the list.
|
|
right (Any): the list's last command node.
|
|
node (Any): node being provisioned by the connection handler.
|
|
session (Session): shell session state.
|
|
"""
|
|
if node is not right:
|
|
return await recurse(node, session)
|
|
return await _provision_redirected(recurse, registry, namespace,
|
|
execute_fn, right, redirects, session)
|
|
|
|
|
|
async def provision_node(
|
|
registry: MountRegistry,
|
|
dispatch: Callable,
|
|
execute_fn: Callable,
|
|
namespace: Namespace | None,
|
|
node: Any,
|
|
session: Session,
|
|
scope: PlanScope | None = None,
|
|
) -> ProvisionResult:
|
|
"""Walk tree-sitter AST and estimate execution cost.
|
|
|
|
Dispatches on the same NodeKind classification as the executor
|
|
(`mirage.shell.classify`), so every construct the executor runs
|
|
has a planner branch; kinds neither walker supports fall through
|
|
to an honest UNKNOWN.
|
|
|
|
Args:
|
|
registry (MountRegistry): mount registry for path resolution.
|
|
dispatch (Callable): VFS op dispatcher (op, path, **kw).
|
|
execute_fn (Callable): recursive execute (for expansions).
|
|
node (Any): tree-sitter node to plan.
|
|
session (Session): shell session state.
|
|
scope (PlanScope | None): walk-local planner state; created at
|
|
the root and threaded through recursion.
|
|
"""
|
|
plan_scope = scope if scope is not None else PlanScope()
|
|
recurse = partial(provision_node,
|
|
registry,
|
|
dispatch,
|
|
execute_fn,
|
|
namespace,
|
|
scope=plan_scope)
|
|
kind = node_kind(node)
|
|
|
|
if kind == NodeKind.COMMENT:
|
|
return ProvisionResult(precision=Precision.EXACT)
|
|
|
|
if kind in (NodeKind.PROGRAM, NodeKind.SUBSHELL, NodeKind.COMPOUND):
|
|
if kind == NodeKind.SUBSHELL:
|
|
body = get_subshell_body(node)
|
|
else:
|
|
body = [c for c in node.named_children if c.type != NT.COMMENT]
|
|
children = []
|
|
for child in body:
|
|
children.append(await recurse(child, session))
|
|
if not children:
|
|
return ProvisionResult(precision=Precision.EXACT)
|
|
return rollup_list(";", children)
|
|
|
|
if kind == NodeKind.COMMAND:
|
|
name = get_command_name(node)
|
|
func_body = plan_scope.functions.get(name)
|
|
if func_body is None:
|
|
func_body = session.functions.get(name)
|
|
if func_body is not None:
|
|
return await handle_function_provision(recurse, name, func_body,
|
|
plan_scope.planning,
|
|
session)
|
|
if name in _BUILTIN_NAMES:
|
|
return await handle_builtin_provision()
|
|
_, parts = split_env_prefix(get_parts(node))
|
|
if not parts:
|
|
return ProvisionResult(precision=Precision.EXACT)
|
|
expanded = await expand_parts(parts, session, execute_fn)
|
|
classified = classify_parts(expanded, registry, session.cwd)
|
|
result = await handle_command_provision(registry, classified, session,
|
|
namespace)
|
|
if any(has_command_substitution(p) for p in parts):
|
|
# The plan walk suppressed the substitution, so the operand
|
|
# list is incomplete: the totals are floors, not answers.
|
|
result.precision = Precision.UNKNOWN
|
|
return result
|
|
|
|
if kind == NodeKind.PIPELINE:
|
|
commands, _ = get_pipeline_commands(node)
|
|
return await handle_pipe_provision(recurse, commands, session)
|
|
|
|
if kind == NodeKind.LIST:
|
|
left, op, right = get_list_parts(node)
|
|
return await handle_connection_provision(recurse, left, op, right,
|
|
session)
|
|
|
|
if kind == NodeKind.REDIRECT:
|
|
command, redirects = get_redirects(node)
|
|
if command.type == NT.LIST:
|
|
# Mirror the executor: a trailing redirect hoisted over an
|
|
# &&/|| list binds to the last command.
|
|
left, op, right = get_list_parts(command)
|
|
wrapped = partial(_provision_reassociated, recurse, registry,
|
|
namespace, execute_fn, redirects, right)
|
|
return await handle_connection_provision(wrapped, left, op, right,
|
|
session)
|
|
return await _provision_redirected(recurse, registry, namespace,
|
|
execute_fn, command, redirects,
|
|
session)
|
|
|
|
if kind == NodeKind.IF:
|
|
branches, else_body = get_if_branches(node)
|
|
return await handle_if_provision(recurse, branches, else_body, session)
|
|
|
|
if kind == NodeKind.FOR:
|
|
_, values, body = get_for_parts(node)
|
|
if any(has_command_substitution(v) for v in values):
|
|
# The iteration count comes from a suppressed substitution:
|
|
# plan one pass as a floor and degrade.
|
|
result = await handle_for_provision(recurse, body, 1, session)
|
|
result.precision = Precision.UNKNOWN
|
|
return result
|
|
classified = await expand_and_classify(values, session, execute_fn,
|
|
registry, session.cwd)
|
|
n = len(classified) or 1
|
|
return await handle_for_provision(recurse, body, n, session)
|
|
|
|
if kind == NodeKind.SELECT:
|
|
# select re-prompts until break: unbounded like while.
|
|
_, _, body = get_for_parts(node)
|
|
return await handle_while_provision(recurse, body, session)
|
|
|
|
if kind in (NodeKind.WHILE, NodeKind.UNTIL):
|
|
_, body = get_while_parts(node)
|
|
return await handle_while_provision(recurse, body, session)
|
|
|
|
if kind == NodeKind.CASE:
|
|
items = get_case_items(node)
|
|
children = []
|
|
for _, body in items:
|
|
if not body:
|
|
continue
|
|
stmts = [await recurse(stmt, session) for stmt in body]
|
|
children.append(stmts[0] if len(stmts) ==
|
|
1 else rollup_list(";", stmts))
|
|
if children:
|
|
return rollup_list("||", children)
|
|
return ProvisionResult(precision=Precision.EXACT)
|
|
|
|
if kind == NodeKind.FUNCTION_DEF:
|
|
name = get_function_name(node)
|
|
body = get_function_body(node)
|
|
if name and body is not None:
|
|
plan_scope.functions[name] = body
|
|
return await handle_builtin_provision()
|
|
|
|
if kind in (NodeKind.DECLARATION, NodeKind.UNSET, NodeKind.TEST,
|
|
NodeKind.VAR_ASSIGN):
|
|
return await handle_builtin_provision()
|
|
|
|
if kind == NodeKind.NEGATED:
|
|
inner = get_negated_command(node)
|
|
return await recurse(inner, session)
|
|
|
|
return ProvisionResult(command=get_text(node), precision=Precision.UNKNOWN)
|