pari-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pari/__init__.py +3 -0
- pari/agent/__init__.py +14 -0
- pari/agent/loop.py +35 -0
- pari/agent/reflection.py +39 -0
- pari/agent/runtime.py +91 -0
- pari/agent/subagent.py +34 -0
- pari/cli.py +132 -0
- pari/config.py +23 -0
- pari/context/__init__.py +15 -0
- pari/context/cache.py +15 -0
- pari/context/chunks.py +40 -0
- pari/context/compaction.py +21 -0
- pari/context/epoch.py +32 -0
- pari/context/warmer.py +32 -0
- pari/edit/__init__.py +20 -0
- pari/edit/cascade.py +22 -0
- pari/edit/diff.py +8 -0
- pari/edit/dmp.py +15 -0
- pari/edit/exact.py +37 -0
- pari/edit/git_osr.py +14 -0
- pari/edit/indenter.py +12 -0
- pari/eval/__init__.py +14 -0
- pari/eval/judge.py +21 -0
- pari/eval/logging.py +19 -0
- pari/eval/metrics.py +22 -0
- pari/eval/runner.py +34 -0
- pari/fs/__init__.py +16 -0
- pari/fs/atomic.py +53 -0
- pari/fs/path.py +28 -0
- pari/fs/queue.py +22 -0
- pari/intelligence/__init__.py +15 -0
- pari/intelligence/ast.py +25 -0
- pari/intelligence/cache.py +21 -0
- pari/intelligence/ppr.py +21 -0
- pari/intelligence/repomap.py +21 -0
- pari/intelligence/search.py +33 -0
- pari/output/__init__.py +14 -0
- pari/output/parser.py +14 -0
- pari/output/spooler.py +33 -0
- pari/output/truncation.py +20 -0
- pari/providers/__init__.py +24 -0
- pari/providers/anthropic.py +23 -0
- pari/providers/base.py +56 -0
- pari/providers/deepseek.py +24 -0
- pari/providers/mock.py +46 -0
- pari/providers/ollama.py +25 -0
- pari/providers/openai.py +23 -0
- pari/routing/__init__.py +16 -0
- pari/routing/anti_verbosity.py +13 -0
- pari/routing/classifier.py +24 -0
- pari/routing/router.py +23 -0
- pari/sandbox/__init__.py +16 -0
- pari/sandbox/base.py +74 -0
- pari/sandbox/bwrap.py +39 -0
- pari/sandbox/dispatcher.py +31 -0
- pari/sandbox/e2b.py +32 -0
- pari/sandbox/process.py +38 -0
- pari/sandbox/seatbelt.py +32 -0
- pari/session/__init__.py +15 -0
- pari/session/checkpoint.py +24 -0
- pari/session/dag.py +34 -0
- pari/session/events.py +50 -0
- pari/session/logger.py +41 -0
- pari/session/undo.py +99 -0
- pari/tools/__init__.py +14 -0
- pari/tools/base.py +35 -0
- pari/tools/bash.py +24 -0
- pari/tools/fs.py +53 -0
- pari/tools/registry.py +36 -0
- pari/tools/search.py +38 -0
- pari/tui/__init__.py +28 -0
- pari/tui/app.py +355 -0
- pari/tui/commands.py +45 -0
- pari/tui/diff_view.py +17 -0
- pari/tui/status.py +65 -0
- pari_cli-0.1.0.dist-info/METADATA +167 -0
- pari_cli-0.1.0.dist-info/RECORD +81 -0
- pari_cli-0.1.0.dist-info/WHEEL +5 -0
- pari_cli-0.1.0.dist-info/entry_points.txt +2 -0
- pari_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- pari_cli-0.1.0.dist-info/top_level.txt +1 -0
pari/providers/ollama.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""PARI Providers: Local Ollama Adapter.
|
|
2
|
+
|
|
3
|
+
Supports local models (Llama 3, Qwen 2.5 Coder, DeepSeek R1 distilled) via local HTTP endpoint.
|
|
4
|
+
Allows 100% offline, private coding agent execution without external cloud tokens.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OllamaProvider(LLMProvider):
|
|
12
|
+
"""Adapter for locally hosted Ollama instances."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, model_name: str = "qwen2.5-coder:7b", base_url: str = "http://localhost:11434/v1"):
|
|
15
|
+
super().__init__(model_name, api_key="ollama")
|
|
16
|
+
self.base_url = base_url
|
|
17
|
+
|
|
18
|
+
async def complete(
|
|
19
|
+
self,
|
|
20
|
+
messages: List[ProviderMessage],
|
|
21
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
22
|
+
temperature: float = 0.0,
|
|
23
|
+
) -> ModelResponse:
|
|
24
|
+
"""Call local Ollama endpoint."""
|
|
25
|
+
raise NotImplementedError
|
pari/providers/openai.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""PARI Providers: OpenAI Adapter.
|
|
2
|
+
|
|
3
|
+
Supports GPT-4o, o3-mini, and GPT-4.5 with function calling.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenAIProvider(LLMProvider):
|
|
11
|
+
"""Adapter for OpenAI Chat Completions API."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, model_name: str = "gpt-4o", api_key: Optional[str] = None):
|
|
14
|
+
super().__init__(model_name, api_key)
|
|
15
|
+
|
|
16
|
+
async def complete(
|
|
17
|
+
self,
|
|
18
|
+
messages: List[ProviderMessage],
|
|
19
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
20
|
+
temperature: float = 0.0,
|
|
21
|
+
) -> ModelResponse:
|
|
22
|
+
"""Call OpenAI Chat Completions API."""
|
|
23
|
+
raise NotImplementedError
|
pari/routing/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""PARI Layer 8: Dual-Model Routing Engine.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Harvested from Aider's architect_coder.py and closes Antigravity F1 (planning token burn).
|
|
5
|
+
Reduces per-turn output costs by ~85%:
|
|
6
|
+
- Architect Model (Claude 3.7 Sonnet / o3-mini): Ingests context and emits a dense,
|
|
7
|
+
high-signal 300-token plan.
|
|
8
|
+
- Editor Model (Claude 3.5 Haiku / DeepSeek V3): Ingests the plan and emits mechanical diffs.
|
|
9
|
+
|
|
10
|
+
Subsystems:
|
|
11
|
+
- router: Orchestrates the Architect-to-Editor pipeline
|
|
12
|
+
- classifier: Identifies simple/localized edits to bypass Architect overhead
|
|
13
|
+
- anti_verbosity: Enforces strict 4,096-token ceiling on reasoning to arrest planning loops
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Placeholder imports will be exported once implementations are added
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""PARI Layer 8: Anti-Verbosity Guard.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Google Antigravity F1 (200k-400k token burn on meta-planning).
|
|
5
|
+
Places a strict hard token ceiling (default 4,096 tokens) on the Architect model.
|
|
6
|
+
If the Architect fails to conclude its plan within 4k tokens, the output is cut
|
|
7
|
+
and dispatched immediately to the Editor model with instructions to execute
|
|
8
|
+
the partial plan, arresting recursive planning loops.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def enforce_planning_ceiling(plan_text: str, max_tokens: int = 4096) -> str:
|
|
12
|
+
"""Enforce token ceiling on architect reasoning output."""
|
|
13
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""PARI Layer 8: Task Classifier.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Evaluates incoming user prompts and active file scope.
|
|
5
|
+
If the task is simple (single file typo, localized test fix, doc update),
|
|
6
|
+
bypasses the Architect model entirely, saving both latency and token cost.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskComplexity(str, Enum):
|
|
14
|
+
SIMPLE_EDIT = "simple_edit" # Bypass architect -> route directly to editor
|
|
15
|
+
ARCHITECTURAL = "architectural" # Dual-model: Architect plan -> Editor diff
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaskClassifier:
|
|
19
|
+
"""Classifies task complexity to select optimal routing pipeline."""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def classify(prompt: str, active_files: List[str]) -> TaskComplexity:
|
|
23
|
+
"""Classify task complexity based on intent keywords and scope."""
|
|
24
|
+
raise NotImplementedError
|
pari/routing/router.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""PARI Layer 8: Dual-Model Router Pipeline.
|
|
2
|
+
|
|
3
|
+
Coordinates prompt classification, architect plan generation, and editor execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Callable, Coroutine, List, Optional
|
|
7
|
+
from pari.routing.classifier import TaskClassifier, TaskComplexity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DualModelRouter:
|
|
11
|
+
"""Dispatches tasks across Architect and Editor model tiers."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
architect_call: Optional[Callable[[str], Coroutine[Any, Any, str]]] = None,
|
|
16
|
+
editor_call: Optional[Callable[[str, Optional[str]], Coroutine[Any, Any, str]]] = None,
|
|
17
|
+
):
|
|
18
|
+
self.architect_call = architect_call
|
|
19
|
+
self.editor_call = editor_call
|
|
20
|
+
|
|
21
|
+
async def route_and_execute(self, prompt: str, active_files: List[str]) -> str:
|
|
22
|
+
"""Route prompt based on task complexity classification."""
|
|
23
|
+
raise NotImplementedError
|
pari/sandbox/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""PARI Layer 0: Multi-Platform Kernel Sandboxing Engine.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Claude Code F2, Pi F1, Aider F1, and OpenCode F1.
|
|
5
|
+
Prevents hallucinated or prompt-injected destructive commands (e.g., rm -rf /)
|
|
6
|
+
from executing with host-user privileges.
|
|
7
|
+
|
|
8
|
+
Subsystems:
|
|
9
|
+
- dispatcher: Automatically selects the most secure sandbox backend based on OS
|
|
10
|
+
- bwrap: Linux & Windows WSL2 bubblewrap container isolation (<5ms startup)
|
|
11
|
+
- seatbelt: macOS Apple Seatbelt (sandbox-exec + Scheme profile)
|
|
12
|
+
- e2b: Cloud microVM execution using Firecracker KVM
|
|
13
|
+
- process: Process-group supervision, die-with-parent lifecycle, and zombie reaping
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Placeholder imports will be exported once implementations are added
|
pari/sandbox/base.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""PARI Layer 0: Base Sandbox Interface.
|
|
2
|
+
|
|
3
|
+
Defines the contract for all platform sandbox implementations (Bubblewrap, Seatbelt, E2B, ProcessGroup).
|
|
4
|
+
All commands executed by PARI must run through an implementation of SandboxRunner.
|
|
5
|
+
|
|
6
|
+
Guarantees:
|
|
7
|
+
1. Working directory isolation: Writes restricted to authorized workspace tree.
|
|
8
|
+
2. Die-with-parent lifecycle: Child processes killed when agent process terminates.
|
|
9
|
+
3. Resource bounding: Execution constrained by timeout, max output bytes, and memory.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SandboxLimits:
|
|
20
|
+
"""Resource constraints for sandboxed command execution.
|
|
21
|
+
|
|
22
|
+
Fields:
|
|
23
|
+
timeout_seconds: Maximum allowed wall-clock run duration.
|
|
24
|
+
max_output_bytes: Ceiling for stdout/stderr spooling.
|
|
25
|
+
memory_limit_mb: cgroup ceiling on Linux.
|
|
26
|
+
allow_network: Policy toggle for network interface access.
|
|
27
|
+
"""
|
|
28
|
+
timeout_seconds: float = 30.0
|
|
29
|
+
max_output_bytes: int = 50 * 1024
|
|
30
|
+
memory_limit_mb: int = 2048
|
|
31
|
+
allow_network: bool = True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class SandboxResult:
|
|
36
|
+
"""Structured result from sandboxed command execution.
|
|
37
|
+
|
|
38
|
+
Fields:
|
|
39
|
+
exit_code: Process return code.
|
|
40
|
+
stdout: Standard output string (truncated if exceeding limits).
|
|
41
|
+
stderr: Standard error string.
|
|
42
|
+
duration_seconds: Elapsed execution time.
|
|
43
|
+
timed_out: True if terminated by timeout.
|
|
44
|
+
truncated: True if output exceeded max_output_bytes.
|
|
45
|
+
"""
|
|
46
|
+
exit_code: int
|
|
47
|
+
stdout: str
|
|
48
|
+
stderr: str
|
|
49
|
+
duration_seconds: float
|
|
50
|
+
timed_out: bool = False
|
|
51
|
+
truncated: bool = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SandboxRunner(ABC):
|
|
55
|
+
"""Abstract base class for all PARI kernel and cloud sandboxes."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None):
|
|
58
|
+
# TODO: Initialize workspace path and enforce resource limits
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def run_command(
|
|
63
|
+
self,
|
|
64
|
+
command: str,
|
|
65
|
+
env: Optional[Dict[str, str]] = None,
|
|
66
|
+
timeout: Optional[float] = None,
|
|
67
|
+
) -> SandboxResult:
|
|
68
|
+
"""Execute a shell command inside the sandbox."""
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def cleanup(self) -> None:
|
|
73
|
+
"""Terminate any lingering child processes, virtual mounts, or microVM instances."""
|
|
74
|
+
raise NotImplementedError
|
pari/sandbox/bwrap.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""PARI Layer 0: Linux / WSL2 Bubblewrap Sandbox Runner.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Harvested from Codex codex-bwrap.
|
|
5
|
+
Provides sub-5ms unprivileged user namespace isolation (zero root required).
|
|
6
|
+
Mounts root / as read-only, workspace as read-write, masks ~/.ssh, ~/.gnupg, and
|
|
7
|
+
drops network namespaces when network isolation is requested.
|
|
8
|
+
|
|
9
|
+
Kernel Namespaces:
|
|
10
|
+
- CLONE_NEWUSER, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWIPC, CLONE_NEWUTS.
|
|
11
|
+
- Die-with-parent via bwrap --die-with-parent flag.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
from pari.sandbox.base import SandboxLimits, SandboxResult, SandboxRunner
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BwrapSandboxRunner(SandboxRunner):
|
|
20
|
+
"""Runs commands in an unprivileged Linux Bubblewrap (bwrap) sandbox."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None):
|
|
23
|
+
super().__init__(workspace, limits)
|
|
24
|
+
# TODO: Verify 'bwrap' executable exists in PATH
|
|
25
|
+
|
|
26
|
+
def run_command(
|
|
27
|
+
self,
|
|
28
|
+
command: str,
|
|
29
|
+
env: Optional[Dict[str, str]] = None,
|
|
30
|
+
timeout: Optional[float] = None,
|
|
31
|
+
) -> SandboxResult:
|
|
32
|
+
# TODO: Assemble bwrap CLI arguments:
|
|
33
|
+
# --die-with-parent, --ro-bind / /, --bind workspace workspace,
|
|
34
|
+
# mask sensitive directories (~/.ssh, ~/.gnupg), and spawn child process
|
|
35
|
+
raise NotImplementedError("Bwrap sandbox runner placeholder.")
|
|
36
|
+
|
|
37
|
+
def cleanup(self) -> None:
|
|
38
|
+
# TODO: Clean up any ephemeral mounts
|
|
39
|
+
pass
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""PARI Layer 0: Sandbox Selection Dispatcher.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Intelligently inspects the host platform and environment configuration
|
|
5
|
+
to select the most secure available sandbox engine:
|
|
6
|
+
1. Cloud mode -> E2B Firecracker MicroVM
|
|
7
|
+
2. Linux / WSL2 with bwrap -> Bubblewrap runner
|
|
8
|
+
3. macOS -> Apple Seatbelt runner
|
|
9
|
+
4. Fallback / Native Windows -> Supervised Process Group runner
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from pari.sandbox.base import SandboxLimits, SandboxRunner
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_sandbox_runner(
|
|
18
|
+
workspace: Path,
|
|
19
|
+
limits: Optional[SandboxLimits] = None,
|
|
20
|
+
force_mode: Optional[str] = None,
|
|
21
|
+
) -> SandboxRunner:
|
|
22
|
+
"""Select and instantiate the appropriate sandbox runner for the current platform.
|
|
23
|
+
|
|
24
|
+
TODO:
|
|
25
|
+
- Check force_mode or PARI_SANDBOX_MODE env var.
|
|
26
|
+
- If cloud -> E2BCloudSandboxRunner
|
|
27
|
+
- If Linux and bwrap in PATH -> BwrapSandboxRunner
|
|
28
|
+
- If macOS and sandbox-exec in PATH -> MacOSSeatbeltSandboxRunner
|
|
29
|
+
- Otherwise -> SupervisedProcessRunner fallback
|
|
30
|
+
"""
|
|
31
|
+
raise NotImplementedError("Sandbox selection dispatcher placeholder.")
|
pari/sandbox/e2b.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""PARI Layer 0: Cloud E2B Firecracker Sandbox Runner.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Satisfies cloud microVM sandboxing using E2B Firecracker hardware KVM microVMs.
|
|
5
|
+
Bypasses weeks of custom hypervisor / vsock plumbing while guaranteeing full
|
|
6
|
+
kernel-level hardware virtualization for remote evaluation and cloud runs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
from pari.sandbox.base import SandboxLimits, SandboxResult, SandboxRunner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class E2BCloudSandboxRunner(SandboxRunner):
|
|
15
|
+
"""Executes commands inside an ephemeral E2B Firecracker Cloud microVM."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None, api_key: Optional[str] = None):
|
|
18
|
+
super().__init__(workspace, limits)
|
|
19
|
+
# TODO: Initialize E2B Sandbox client with api_key
|
|
20
|
+
|
|
21
|
+
def run_command(
|
|
22
|
+
self,
|
|
23
|
+
command: str,
|
|
24
|
+
env: Optional[Dict[str, str]] = None,
|
|
25
|
+
timeout: Optional[float] = None,
|
|
26
|
+
) -> SandboxResult:
|
|
27
|
+
# TODO: Execute command remotely inside Firecracker microVM
|
|
28
|
+
raise NotImplementedError("E2B cloud sandbox runner placeholder.")
|
|
29
|
+
|
|
30
|
+
def cleanup(self) -> None:
|
|
31
|
+
# TODO: Close remote microVM instance
|
|
32
|
+
pass
|
pari/sandbox/process.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""PARI Layer 0: Process Group Supervisor & Fallback Runner.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Codex F5 (Orphaned exec-server processes) and provides the supervised
|
|
5
|
+
fallback runner when kernel sandbox utilities (bwrap/seatbelt) are absent.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Creates independent process groups using os.setsid() so child process trees
|
|
9
|
+
can be terminated cleanly as a unit (killpg).
|
|
10
|
+
- Sets PR_SET_PDEATHSIG on Linux so child processes terminate if the parent crashes.
|
|
11
|
+
- Prevents zombie process accumulation via signal handling and reaping.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
from pari.sandbox.base import SandboxLimits, SandboxResult, SandboxRunner
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SupervisedProcessRunner(SandboxRunner):
|
|
20
|
+
"""Supervised process runner using process-group tracking and path-guard boundaries."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None):
|
|
23
|
+
super().__init__(workspace, limits)
|
|
24
|
+
# TODO: Initialize active PID tracking list
|
|
25
|
+
|
|
26
|
+
def run_command(
|
|
27
|
+
self,
|
|
28
|
+
command: str,
|
|
29
|
+
env: Optional[Dict[str, str]] = None,
|
|
30
|
+
timeout: Optional[float] = None,
|
|
31
|
+
) -> SandboxResult:
|
|
32
|
+
# TODO: Execute command using subprocess.Popen with preexec_fn=os.setsid
|
|
33
|
+
# and enforce die-with-parent lifecycle + timeout handling
|
|
34
|
+
raise NotImplementedError("Supervised process runner placeholder.")
|
|
35
|
+
|
|
36
|
+
def cleanup(self) -> None:
|
|
37
|
+
# TODO: Terminate all recorded active process groups
|
|
38
|
+
pass
|
pari/sandbox/seatbelt.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""PARI Layer 0: macOS Apple Seatbelt Sandbox Runner.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Uses macOS sandbox-exec with an explicit TinyScheme profile.
|
|
5
|
+
Confines filesystem write operations strictly to the workspace directory.
|
|
6
|
+
Note: Expands explicit user home path since Seatbelt TinyScheme does not expand tilde (~).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
from pari.sandbox.base import SandboxLimits, SandboxResult, SandboxRunner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MacOSSeatbeltSandboxRunner(SandboxRunner):
|
|
15
|
+
"""Runs commands in an Apple Seatbelt sandbox via sandbox-exec."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None):
|
|
18
|
+
super().__init__(workspace, limits)
|
|
19
|
+
# TODO: Verify 'sandbox-exec' exists in PATH
|
|
20
|
+
|
|
21
|
+
def run_command(
|
|
22
|
+
self,
|
|
23
|
+
command: str,
|
|
24
|
+
env: Optional[Dict[str, str]] = None,
|
|
25
|
+
timeout: Optional[float] = None,
|
|
26
|
+
) -> SandboxResult:
|
|
27
|
+
# TODO: Generate TinyScheme profile restricting writes to workspace and /tmp,
|
|
28
|
+
# denying ~/.ssh, ~/.aws, ~/.gnupg access, and execute via sandbox-exec -f
|
|
29
|
+
raise NotImplementedError("macOS Seatbelt sandbox runner placeholder.")
|
|
30
|
+
|
|
31
|
+
def cleanup(self) -> None:
|
|
32
|
+
pass
|
pari/session/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""PARI Layer 1: Durable Session Engine (Merkle DAG).
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Claude Code F1 (No ACID state durability; 0-byte corrupt session file on SIGINT)
|
|
5
|
+
and Pi F4 / F5 (O(N) JSONL replay and mid-append torn-write truncation).
|
|
6
|
+
|
|
7
|
+
Subsystems:
|
|
8
|
+
- events: Structured session event records (SESSION_START, USER_PROMPT, TOOL_CALL, etc.)
|
|
9
|
+
- logger: Append-only JSONL writer with CRC32 integrity check & torn-write recovery
|
|
10
|
+
- dag: Merkle DAG node tracking, parentId linking, and branch reconstruction
|
|
11
|
+
- checkpoint: Periodic index snapshots (every 50 turns) for O(1) state recovery
|
|
12
|
+
- undo: Bounded, guarded file rollback + child process termination + side effect report
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Placeholder imports will be exported once implementations are added
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""PARI Layer 1: Checkpoint Index Manager.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Pi F4 (O(N) JSONL replay latency on deep 500+ turn sessions).
|
|
5
|
+
Periodically writes a CHECKPOINT index event (every 50 turns) containing the
|
|
6
|
+
active context state, file snapshot manifests, and token metrics.
|
|
7
|
+
Allows resuming a session by reading backward to the most recent checkpoint.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
from pari.session.logger import SessionLogger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CheckpointManager:
|
|
15
|
+
"""Manages periodic checkpoint intervals for fast session resumption."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, logger: SessionLogger, interval_turns: int = 50):
|
|
18
|
+
self.logger = logger
|
|
19
|
+
self.interval_turns = interval_turns
|
|
20
|
+
# TODO: Track turn count
|
|
21
|
+
|
|
22
|
+
def record_turn(self, state_snapshot: Dict[str, Any]) -> Optional[str]:
|
|
23
|
+
"""Increment turn counter and trigger checkpoint write if interval is met."""
|
|
24
|
+
raise NotImplementedError
|
pari/session/dag.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""PARI Layer 1: Merkle DAG Session Graph.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Harvested from Pi's session-manager.ts.
|
|
5
|
+
Maintains session history as a Directed Acyclic Graph (DAG) using parentId references.
|
|
6
|
+
Enables non-linear branching, checkpoints, and causal traversal without in-memory state loss.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
from pari.session.events import SessionEvent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DAGNode:
|
|
14
|
+
"""Represents a node in the causal event Merkle DAG."""
|
|
15
|
+
def __init__(self, event: SessionEvent):
|
|
16
|
+
self.event = event
|
|
17
|
+
self.parent: Optional["DAGNode"] = None
|
|
18
|
+
self.children: List["DAGNode"] = []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SessionDAG:
|
|
22
|
+
"""In-memory DAG index over the immutable session event stream."""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
# TODO: Initialize node map and head/root pointers
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def add_event(self, event: SessionEvent) -> DAGNode:
|
|
29
|
+
"""Insert event into DAG and link parent/child pointers."""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def get_lineage(self, leaf_id: Optional[str] = None) -> List[SessionEvent]:
|
|
33
|
+
"""Traverse backwards from leaf to root to reconstruct the linear event thread."""
|
|
34
|
+
raise NotImplementedError
|
pari/session/events.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""PARI Layer 1: Structured Session Event Types.
|
|
2
|
+
|
|
3
|
+
Defines all immutable event records logged into the append-only Merkle DAG.
|
|
4
|
+
Each event possesses a unique record ID, timestamp, and an optional parent_id
|
|
5
|
+
forming an unbroken causal Merkle chain.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SessionEventType(str, Enum):
|
|
14
|
+
SESSION_START = "session_start"
|
|
15
|
+
USER_PROMPT = "user_prompt"
|
|
16
|
+
ASSISTANT_RESPONSE = "assistant_response"
|
|
17
|
+
TOOL_CALL = "tool_call"
|
|
18
|
+
TOOL_RESULT = "tool_result"
|
|
19
|
+
FILE_SNAPSHOT = "file_snapshot"
|
|
20
|
+
CHECKPOINT = "checkpoint"
|
|
21
|
+
SESSION_END = "session_end"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SessionEvent:
|
|
26
|
+
"""An individual event entry in the session Merkle DAG.
|
|
27
|
+
|
|
28
|
+
Fields:
|
|
29
|
+
type: Event category
|
|
30
|
+
payload: Event data dictionary
|
|
31
|
+
id: Unique UUID identifier
|
|
32
|
+
parent_id: ID of preceding causal event
|
|
33
|
+
timestamp: Epoch timestamp
|
|
34
|
+
crc32: Integrity checksum
|
|
35
|
+
"""
|
|
36
|
+
type: SessionEventType
|
|
37
|
+
payload: Dict[str, Any]
|
|
38
|
+
id: Optional[str] = None
|
|
39
|
+
parent_id: Optional[str] = None
|
|
40
|
+
timestamp: float = 0.0
|
|
41
|
+
crc32: Optional[int] = None
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
44
|
+
"""Convert event to serializable dictionary."""
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SessionEvent":
|
|
49
|
+
"""Reconstruct event from serialized dictionary."""
|
|
50
|
+
raise NotImplementedError
|
pari/session/logger.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""PARI Layer 1: Append-Only JSONL Session Logger.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Claude Code F1 & Pi F5.
|
|
5
|
+
Writes every session event with an explicit CRC32 checksum, flushes to OS buffer,
|
|
6
|
+
and calls os.fsync() on critical milestones.
|
|
7
|
+
On startup, scans backward from EOF to automatically detect and truncate torn writes
|
|
8
|
+
caused by power loss or sudden SIGKILL.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Iterator, List, Optional
|
|
13
|
+
from pari.session.events import SessionEvent, SessionEventType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SessionLogger:
|
|
17
|
+
"""Manages an append-only JSONL session log with CRC32 integrity verification."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, session_file: Path):
|
|
20
|
+
self.session_file = session_file
|
|
21
|
+
# TODO: Initialize file path and perform backward recovery scan for torn writes
|
|
22
|
+
|
|
23
|
+
def _recover_and_index(self) -> None:
|
|
24
|
+
"""Scan session file, recover from torn writes at EOF, and locate the last record ID."""
|
|
25
|
+
raise NotImplementedError("Torn write recovery placeholder.")
|
|
26
|
+
|
|
27
|
+
def log(self, event_type: SessionEventType, payload: Dict[str, Any]) -> SessionEvent:
|
|
28
|
+
"""Append a new verified session event to disk with CRC32 checksum."""
|
|
29
|
+
raise NotImplementedError("Session logging placeholder.")
|
|
30
|
+
|
|
31
|
+
def append_event(self, event_type: str, data: Dict[str, Any]) -> str:
|
|
32
|
+
"""Legacy append API."""
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
def read_events(self) -> List[Dict[str, Any]]:
|
|
36
|
+
"""Read all verified records from the session file."""
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
def iter_events(self) -> Iterator[SessionEvent]:
|
|
40
|
+
"""Iterate over all valid events in chronological order."""
|
|
41
|
+
raise NotImplementedError
|
pari/session/undo.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""PARI Layer 1: Guarded Undo Manager.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Implements a safe, bounded /undo mechanism:
|
|
5
|
+
1. Restores files modified by the most recent turn.
|
|
6
|
+
2. Guarded check: If the developer manually edited the file after PARI touched it,
|
|
7
|
+
aborts or prompts to prevent accidental overwrite of user work.
|
|
8
|
+
3. Terminates background processes started during the turn.
|
|
9
|
+
4. Explicit disclosure: Informs user that external side effects (API calls, remote DBs,
|
|
10
|
+
pip install) are logged but not automatically reversible.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class FileSnapshot:
|
|
20
|
+
"""Pre- and post-edit file hash snapshot."""
|
|
21
|
+
rel_path: str
|
|
22
|
+
pre_edit_content: str
|
|
23
|
+
pre_edit_hash: str
|
|
24
|
+
post_edit_hash: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class UndoResult:
|
|
29
|
+
"""Outcome of guarded undo execution."""
|
|
30
|
+
reverted_files: List[str]
|
|
31
|
+
conflicted_files: List[str]
|
|
32
|
+
terminated_processes: int
|
|
33
|
+
disclosed_side_effects: List[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
import hashlib
|
|
37
|
+
from typing import Dict, List
|
|
38
|
+
from pari.fs.atomic import atomic_write, read_file
|
|
39
|
+
from pari.fs.path import resolve_safe_path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GuardedUndoManager:
|
|
43
|
+
"""Provides guarded rollback for physical file changes and process termination."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, workspace: Path):
|
|
46
|
+
self.workspace = workspace.resolve()
|
|
47
|
+
self.snapshots: Dict[str, FileSnapshot] = {}
|
|
48
|
+
self.side_effects: List[str] = []
|
|
49
|
+
|
|
50
|
+
def _hash(self, content: str) -> str:
|
|
51
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
52
|
+
|
|
53
|
+
def record_snapshot(self, rel_path: str, pre_content: str, post_content: str):
|
|
54
|
+
"""Record file state prior to write mutation."""
|
|
55
|
+
self.snapshots[rel_path] = FileSnapshot(
|
|
56
|
+
rel_path=rel_path,
|
|
57
|
+
pre_edit_content=pre_content,
|
|
58
|
+
pre_edit_hash=self._hash(pre_content),
|
|
59
|
+
post_edit_hash=self._hash(post_content),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def record_side_effect(self, description: str):
|
|
63
|
+
"""Log external irreversible side effect (e.g. package install, DB migration)."""
|
|
64
|
+
self.side_effects.append(description)
|
|
65
|
+
|
|
66
|
+
def undo(self, force: bool = False) -> UndoResult:
|
|
67
|
+
"""Execute guarded rollback verifying files haven't drifted."""
|
|
68
|
+
reverted: List[str] = []
|
|
69
|
+
conflicts: List[str] = []
|
|
70
|
+
|
|
71
|
+
for rel_path, snapshot in list(self.snapshots.items()):
|
|
72
|
+
file_path = resolve_safe_path(self.workspace, rel_path)
|
|
73
|
+
if not file_path.exists():
|
|
74
|
+
atomic_write(file_path, snapshot.pre_edit_content)
|
|
75
|
+
reverted.append(rel_path)
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
current_content = read_file(file_path)
|
|
79
|
+
current_hash = self._hash(current_content)
|
|
80
|
+
|
|
81
|
+
if current_hash == snapshot.post_edit_hash or force:
|
|
82
|
+
atomic_write(file_path, snapshot.pre_edit_content)
|
|
83
|
+
reverted.append(rel_path)
|
|
84
|
+
else:
|
|
85
|
+
conflicts.append(rel_path)
|
|
86
|
+
|
|
87
|
+
# Clear reverted snapshots
|
|
88
|
+
for rel in reverted:
|
|
89
|
+
self.snapshots.pop(rel, None)
|
|
90
|
+
|
|
91
|
+
result = UndoResult(
|
|
92
|
+
reverted_files=reverted,
|
|
93
|
+
conflicted_files=conflicts,
|
|
94
|
+
terminated_processes=0,
|
|
95
|
+
disclosed_side_effects=list(self.side_effects),
|
|
96
|
+
)
|
|
97
|
+
self.side_effects.clear()
|
|
98
|
+
return result
|
|
99
|
+
|