agent-resurrect-btw 0.1.0__tar.gz
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.
- agent_resurrect_btw-0.1.0/LICENSE +21 -0
- agent_resurrect_btw-0.1.0/PKG-INFO +63 -0
- agent_resurrect_btw-0.1.0/README.md +51 -0
- agent_resurrect_btw-0.1.0/agent_resurrect_btw.egg-info/PKG-INFO +63 -0
- agent_resurrect_btw-0.1.0/agent_resurrect_btw.egg-info/SOURCES.txt +22 -0
- agent_resurrect_btw-0.1.0/agent_resurrect_btw.egg-info/dependency_links.txt +1 -0
- agent_resurrect_btw-0.1.0/agent_resurrect_btw.egg-info/entry_points.txt +2 -0
- agent_resurrect_btw-0.1.0/agent_resurrect_btw.egg-info/requires.txt +3 -0
- agent_resurrect_btw-0.1.0/agent_resurrect_btw.egg-info/top_level.txt +1 -0
- agent_resurrect_btw-0.1.0/pyproject.toml +25 -0
- agent_resurrect_btw-0.1.0/resurrect/__init__.py +2 -0
- agent_resurrect_btw-0.1.0/resurrect/cli.py +89 -0
- agent_resurrect_btw-0.1.0/resurrect/config.py +51 -0
- agent_resurrect_btw-0.1.0/resurrect/core/__init__.py +2 -0
- agent_resurrect_btw-0.1.0/resurrect/core/databricks_client.py +100 -0
- agent_resurrect_btw-0.1.0/resurrect/core/entire_bridge.py +110 -0
- agent_resurrect_btw-0.1.0/resurrect/handoff.py +47 -0
- agent_resurrect_btw-0.1.0/resurrect/parser.py +132 -0
- agent_resurrect_btw-0.1.0/resurrect/ui/__init__.py +2 -0
- agent_resurrect_btw-0.1.0/resurrect/ui/console.py +34 -0
- agent_resurrect_btw-0.1.0/setup.cfg +4 -0
- agent_resurrect_btw-0.1.0/tests/test_config.py +35 -0
- agent_resurrect_btw-0.1.0/tests/test_connectivity.py +78 -0
- agent_resurrect_btw-0.1.0/tests/test_privacy_boundary.py +50 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Entire Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-resurrect-btw
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Resurrect SDK
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: typer>=0.9.0
|
|
9
|
+
Requires-Dist: rich>=13.0.0
|
|
10
|
+
Requires-Dist: requests>=2.31.0
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# Agent Resurrect SDK
|
|
14
|
+
|
|
15
|
+
Agent Resurrect is a real-time loop-breaker and telemetry co-pilot for agentic workflows. It is designed to sit alongside a Databricks-backed failure trap table, observe agent activity, and provide the control surface needed to inspect, diagnose, and recover from runaway loops.
|
|
16
|
+
|
|
17
|
+
This repository currently contains the foundational SDK scaffold only. The business logic, Databricks integration, and runtime loop-breaking behavior will be added incrementally on top of this structure.
|
|
18
|
+
|
|
19
|
+
## Project Layout
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
resurrect/
|
|
23
|
+
__init__.py
|
|
24
|
+
cli.py
|
|
25
|
+
config.py
|
|
26
|
+
core/
|
|
27
|
+
__init__.py
|
|
28
|
+
entire_bridge.py
|
|
29
|
+
databricks_client.py
|
|
30
|
+
ui/
|
|
31
|
+
__init__.py
|
|
32
|
+
console.py
|
|
33
|
+
tests/
|
|
34
|
+
__init__.py
|
|
35
|
+
test_config.py
|
|
36
|
+
test_connectivity.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Planned CLI
|
|
40
|
+
|
|
41
|
+
The `resurrect` command will eventually expose these entry points:
|
|
42
|
+
|
|
43
|
+
- `resurrect init`
|
|
44
|
+
- `resurrect doctor`
|
|
45
|
+
- `resurrect run`
|
|
46
|
+
|
|
47
|
+
Those commands are not implemented yet. The current CLI module only provides a minimal Typer application stub so the package can be installed and extended safely.
|
|
48
|
+
|
|
49
|
+
## Environment Variables
|
|
50
|
+
|
|
51
|
+
Copy `.env.example` to `.env` and populate the Databricks connection settings:
|
|
52
|
+
|
|
53
|
+
- `DATABRICKS_HOST`
|
|
54
|
+
- `DATABRICKS_TOKEN`
|
|
55
|
+
- `DATABRICKS_WAREHOUSE_ID`
|
|
56
|
+
- `DELTA_TABLE_NAME`
|
|
57
|
+
|
|
58
|
+
## Build Metadata
|
|
59
|
+
|
|
60
|
+
- Package name: `agent-resurrect`
|
|
61
|
+
- Version: `0.1.0`
|
|
62
|
+
- Entry point: `resurrect`
|
|
63
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Agent Resurrect SDK
|
|
2
|
+
|
|
3
|
+
Agent Resurrect is a real-time loop-breaker and telemetry co-pilot for agentic workflows. It is designed to sit alongside a Databricks-backed failure trap table, observe agent activity, and provide the control surface needed to inspect, diagnose, and recover from runaway loops.
|
|
4
|
+
|
|
5
|
+
This repository currently contains the foundational SDK scaffold only. The business logic, Databricks integration, and runtime loop-breaking behavior will be added incrementally on top of this structure.
|
|
6
|
+
|
|
7
|
+
## Project Layout
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
resurrect/
|
|
11
|
+
__init__.py
|
|
12
|
+
cli.py
|
|
13
|
+
config.py
|
|
14
|
+
core/
|
|
15
|
+
__init__.py
|
|
16
|
+
entire_bridge.py
|
|
17
|
+
databricks_client.py
|
|
18
|
+
ui/
|
|
19
|
+
__init__.py
|
|
20
|
+
console.py
|
|
21
|
+
tests/
|
|
22
|
+
__init__.py
|
|
23
|
+
test_config.py
|
|
24
|
+
test_connectivity.py
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Planned CLI
|
|
28
|
+
|
|
29
|
+
The `resurrect` command will eventually expose these entry points:
|
|
30
|
+
|
|
31
|
+
- `resurrect init`
|
|
32
|
+
- `resurrect doctor`
|
|
33
|
+
- `resurrect run`
|
|
34
|
+
|
|
35
|
+
Those commands are not implemented yet. The current CLI module only provides a minimal Typer application stub so the package can be installed and extended safely.
|
|
36
|
+
|
|
37
|
+
## Environment Variables
|
|
38
|
+
|
|
39
|
+
Copy `.env.example` to `.env` and populate the Databricks connection settings:
|
|
40
|
+
|
|
41
|
+
- `DATABRICKS_HOST`
|
|
42
|
+
- `DATABRICKS_TOKEN`
|
|
43
|
+
- `DATABRICKS_WAREHOUSE_ID`
|
|
44
|
+
- `DELTA_TABLE_NAME`
|
|
45
|
+
|
|
46
|
+
## Build Metadata
|
|
47
|
+
|
|
48
|
+
- Package name: `agent-resurrect`
|
|
49
|
+
- Version: `0.1.0`
|
|
50
|
+
- Entry point: `resurrect`
|
|
51
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-resurrect-btw
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Resurrect SDK
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: typer>=0.9.0
|
|
9
|
+
Requires-Dist: rich>=13.0.0
|
|
10
|
+
Requires-Dist: requests>=2.31.0
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# Agent Resurrect SDK
|
|
14
|
+
|
|
15
|
+
Agent Resurrect is a real-time loop-breaker and telemetry co-pilot for agentic workflows. It is designed to sit alongside a Databricks-backed failure trap table, observe agent activity, and provide the control surface needed to inspect, diagnose, and recover from runaway loops.
|
|
16
|
+
|
|
17
|
+
This repository currently contains the foundational SDK scaffold only. The business logic, Databricks integration, and runtime loop-breaking behavior will be added incrementally on top of this structure.
|
|
18
|
+
|
|
19
|
+
## Project Layout
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
resurrect/
|
|
23
|
+
__init__.py
|
|
24
|
+
cli.py
|
|
25
|
+
config.py
|
|
26
|
+
core/
|
|
27
|
+
__init__.py
|
|
28
|
+
entire_bridge.py
|
|
29
|
+
databricks_client.py
|
|
30
|
+
ui/
|
|
31
|
+
__init__.py
|
|
32
|
+
console.py
|
|
33
|
+
tests/
|
|
34
|
+
__init__.py
|
|
35
|
+
test_config.py
|
|
36
|
+
test_connectivity.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Planned CLI
|
|
40
|
+
|
|
41
|
+
The `resurrect` command will eventually expose these entry points:
|
|
42
|
+
|
|
43
|
+
- `resurrect init`
|
|
44
|
+
- `resurrect doctor`
|
|
45
|
+
- `resurrect run`
|
|
46
|
+
|
|
47
|
+
Those commands are not implemented yet. The current CLI module only provides a minimal Typer application stub so the package can be installed and extended safely.
|
|
48
|
+
|
|
49
|
+
## Environment Variables
|
|
50
|
+
|
|
51
|
+
Copy `.env.example` to `.env` and populate the Databricks connection settings:
|
|
52
|
+
|
|
53
|
+
- `DATABRICKS_HOST`
|
|
54
|
+
- `DATABRICKS_TOKEN`
|
|
55
|
+
- `DATABRICKS_WAREHOUSE_ID`
|
|
56
|
+
- `DELTA_TABLE_NAME`
|
|
57
|
+
|
|
58
|
+
## Build Metadata
|
|
59
|
+
|
|
60
|
+
- Package name: `agent-resurrect`
|
|
61
|
+
- Version: `0.1.0`
|
|
62
|
+
- Entry point: `resurrect`
|
|
63
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
agent_resurrect_btw.egg-info/PKG-INFO
|
|
5
|
+
agent_resurrect_btw.egg-info/SOURCES.txt
|
|
6
|
+
agent_resurrect_btw.egg-info/dependency_links.txt
|
|
7
|
+
agent_resurrect_btw.egg-info/entry_points.txt
|
|
8
|
+
agent_resurrect_btw.egg-info/requires.txt
|
|
9
|
+
agent_resurrect_btw.egg-info/top_level.txt
|
|
10
|
+
resurrect/__init__.py
|
|
11
|
+
resurrect/cli.py
|
|
12
|
+
resurrect/config.py
|
|
13
|
+
resurrect/handoff.py
|
|
14
|
+
resurrect/parser.py
|
|
15
|
+
resurrect/core/__init__.py
|
|
16
|
+
resurrect/core/databricks_client.py
|
|
17
|
+
resurrect/core/entire_bridge.py
|
|
18
|
+
resurrect/ui/__init__.py
|
|
19
|
+
resurrect/ui/console.py
|
|
20
|
+
tests/test_config.py
|
|
21
|
+
tests/test_connectivity.py
|
|
22
|
+
tests/test_privacy_boundary.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
resurrect
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-resurrect-btw"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agent Resurrect SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer>=0.9.0",
|
|
13
|
+
"rich>=13.0.0",
|
|
14
|
+
"requests>=2.31.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
resurrect = "resurrect.cli:app"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
include-package-data = true
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
include = ["resurrect*"]
|
|
25
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from resurrect.config import ResurrectConfig
|
|
11
|
+
from resurrect.core.databricks_client import DatabricksClient
|
|
12
|
+
from resurrect.core.entire_bridge import CommandResult, check_entire_status, setup_entire
|
|
13
|
+
from resurrect.handoff import build_handoff_manifest, write_handoff_manifest
|
|
14
|
+
from resurrect.parser import ParsedTelemetry, parse_telemetry
|
|
15
|
+
from resurrect.ui.console import StatusItem, render_status_board
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Agent Resurrect command-line interface.")
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.callback(invoke_without_command=True)
|
|
23
|
+
def main() -> None:
|
|
24
|
+
"""Entry point for the resurrect CLI."""
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def init(
|
|
30
|
+
agent: str = "codex",
|
|
31
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show debug details."),
|
|
32
|
+
cwd: Path = typer.Argument(Path.cwd(), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
33
|
+
) -> None:
|
|
34
|
+
result = setup_entire(agent=agent, cwd=cwd)
|
|
35
|
+
config = ResurrectConfig().save(cwd)
|
|
36
|
+
if verbose or not result.ok:
|
|
37
|
+
_render_debug("entire enable", result, cwd)
|
|
38
|
+
if result.ok:
|
|
39
|
+
typer.echo(f"Initialized Entire hooks and created {config}")
|
|
40
|
+
else:
|
|
41
|
+
raise typer.Exit(code=1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def doctor(
|
|
46
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show debug details."),
|
|
47
|
+
cwd: Path = typer.Argument(Path.cwd(), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
48
|
+
) -> None:
|
|
49
|
+
config = ResurrectConfig.load(cwd)
|
|
50
|
+
items: list[StatusItem] = []
|
|
51
|
+
|
|
52
|
+
missing = config.missing_fields()
|
|
53
|
+
items.append(StatusItem("Config", not missing, "All required values loaded." if not missing else f"Missing: {', '.join(missing)}"))
|
|
54
|
+
|
|
55
|
+
entire_status = check_entire_status(cwd)
|
|
56
|
+
items.append(StatusItem("Entire", entire_status.ok, entire_status.stderr or entire_status.stdout))
|
|
57
|
+
if verbose or not entire_status.ok:
|
|
58
|
+
_render_debug("entire status", entire_status, cwd)
|
|
59
|
+
|
|
60
|
+
client = DatabricksClient(config.databricks_host, config.databricks_token, config.databricks_warehouse_id)
|
|
61
|
+
ping = client.ping_connection()
|
|
62
|
+
items.append(StatusItem("Databricks ping", ping.ok, ping.error or (f"{ping.latency_ms:.1f} ms" if ping.latency_ms is not None else "")))
|
|
63
|
+
|
|
64
|
+
table = client.verify_table_exists(config.delta_table_name)
|
|
65
|
+
items.append(StatusItem("Delta table", table.ok, table.error or f"{config.delta_table_name} is queryable"))
|
|
66
|
+
|
|
67
|
+
telemetry = parse_telemetry(cwd / "refs" / "entire" / "checkpoints")
|
|
68
|
+
query_result = client.query_failure_traps(telemetry, config.delta_table_name)
|
|
69
|
+
context_label = "[PARTIAL / REDACTED CONTEXT]" if not telemetry.status.is_complete else "[COMPLETE CONTEXT]"
|
|
70
|
+
items.append(StatusItem("Failure traps", query_result.ok, query_result.error or telemetry.error_signature, context_label=context_label))
|
|
71
|
+
|
|
72
|
+
render_status_board(items)
|
|
73
|
+
manifest_dir = cwd / ".resurrect"
|
|
74
|
+
manifest_dir.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
write_handoff_manifest(manifest_dir / "handoff_manifest.json", telemetry, verified_match=query_result.ok and telemetry.status.is_complete)
|
|
76
|
+
if not all(item.ok for item in items):
|
|
77
|
+
raise typer.Exit(code=1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _render_debug(label: str, result: CommandResult, cwd: Path) -> None:
|
|
81
|
+
body = Text()
|
|
82
|
+
body.append(f"Running: {result.command}\n", style="dim")
|
|
83
|
+
body.append(f"cwd: {cwd}\n", style="dim")
|
|
84
|
+
body.append(f"returncode: {result.returncode}\n", style="dim")
|
|
85
|
+
body.append("stdout:\n", style="bold")
|
|
86
|
+
body.append(f"{result.stdout or '<empty>'}\n")
|
|
87
|
+
body.append("stderr:\n", style="bold")
|
|
88
|
+
body.append(f"{result.stderr or '<empty>'}")
|
|
89
|
+
console.print(Panel(body, title=f"Debug: {label}", border_style="yellow"))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Configuration loading and persistence for Agent Resurrect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass, asdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Mapping
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_DELTA_TABLE_NAME = "dev_failure_traps"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class ResurrectConfig:
|
|
17
|
+
databricks_host: str | None = None
|
|
18
|
+
databricks_token: str | None = None
|
|
19
|
+
databricks_warehouse_id: str | None = None
|
|
20
|
+
delta_table_name: str = DEFAULT_DELTA_TABLE_NAME
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def load(cls, base_dir: Path | str = Path.cwd(), env: Mapping[str, str] | None = None) -> "ResurrectConfig":
|
|
24
|
+
env_map = os.environ if env is None else env
|
|
25
|
+
config_path = Path(base_dir) / ".resurrect" / "config.json"
|
|
26
|
+
data: dict[str, Any] = {}
|
|
27
|
+
if config_path.exists():
|
|
28
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
29
|
+
return cls(
|
|
30
|
+
databricks_host=data.get("databricks_host") or env_map.get("DATABRICKS_HOST"),
|
|
31
|
+
databricks_token=data.get("databricks_token") or env_map.get("DATABRICKS_TOKEN"),
|
|
32
|
+
databricks_warehouse_id=data.get("databricks_warehouse_id") or env_map.get("DATABRICKS_WAREHOUSE_ID"),
|
|
33
|
+
delta_table_name=data.get("delta_table_name") or env_map.get("DELTA_TABLE_NAME") or DEFAULT_DELTA_TABLE_NAME,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def save(self, base_dir: Path | str = Path.cwd()) -> Path:
|
|
37
|
+
config_dir = Path(base_dir) / ".resurrect"
|
|
38
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
config_path = config_dir / "config.json"
|
|
40
|
+
config_path.write_text(json.dumps(asdict(self), indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
41
|
+
return config_path
|
|
42
|
+
|
|
43
|
+
def missing_fields(self) -> list[str]:
|
|
44
|
+
missing = []
|
|
45
|
+
if not self.databricks_host:
|
|
46
|
+
missing.append("databricks_host")
|
|
47
|
+
if not self.databricks_token:
|
|
48
|
+
missing.append("databricks_token")
|
|
49
|
+
if not self.databricks_warehouse_id:
|
|
50
|
+
missing.append("databricks_warehouse_id")
|
|
51
|
+
return missing
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Databricks connectivity helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from time import perf_counter
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from resurrect.parser import ParsedTelemetry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class DiagnosticResult:
|
|
16
|
+
ok: bool
|
|
17
|
+
latency_ms: float | None = None
|
|
18
|
+
error: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DatabricksClient:
|
|
22
|
+
def __init__(self, host: str | None, token: str | None, warehouse_id: str | None, session: requests.Session | None = None) -> None:
|
|
23
|
+
self.host = host.rstrip("/") if host else None
|
|
24
|
+
self.token = token
|
|
25
|
+
self.warehouse_id = warehouse_id
|
|
26
|
+
self.session = session or requests.Session()
|
|
27
|
+
|
|
28
|
+
def _headers(self) -> dict[str, str]:
|
|
29
|
+
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
|
|
30
|
+
|
|
31
|
+
def _post_statement(self, sql: str, parameters: list[dict[str, Any]] | None = None) -> requests.Response:
|
|
32
|
+
url = f"{self.host}/api/2.0/sql/statements"
|
|
33
|
+
payload: dict[str, Any] = {
|
|
34
|
+
"warehouse_id": self.warehouse_id,
|
|
35
|
+
"statement": sql,
|
|
36
|
+
"wait_timeout": "10s",
|
|
37
|
+
}
|
|
38
|
+
if parameters:
|
|
39
|
+
payload["parameters"] = parameters
|
|
40
|
+
return self.session.post(url, json=payload, headers=self._headers(), timeout=15)
|
|
41
|
+
|
|
42
|
+
def ping_connection(self) -> DiagnosticResult:
|
|
43
|
+
if not self.host:
|
|
44
|
+
return DiagnosticResult(ok=False, error="Missing DATABRICKS_HOST.")
|
|
45
|
+
if not self.token:
|
|
46
|
+
return DiagnosticResult(ok=False, error="Missing DATABRICKS_TOKEN.")
|
|
47
|
+
if not self.warehouse_id:
|
|
48
|
+
return DiagnosticResult(ok=False, error="Missing DATABRICKS_WAREHOUSE_ID.")
|
|
49
|
+
|
|
50
|
+
start = perf_counter()
|
|
51
|
+
try:
|
|
52
|
+
response = self._post_statement("SELECT 1")
|
|
53
|
+
response.raise_for_status()
|
|
54
|
+
except requests.RequestException as exc:
|
|
55
|
+
return DiagnosticResult(ok=False, error=str(exc))
|
|
56
|
+
latency_ms = (perf_counter() - start) * 1000
|
|
57
|
+
return DiagnosticResult(ok=True, latency_ms=latency_ms)
|
|
58
|
+
|
|
59
|
+
def verify_table_exists(self, table_name: str = "dev_failure_traps") -> DiagnosticResult:
|
|
60
|
+
if not self.host or not self.token or not self.warehouse_id:
|
|
61
|
+
return DiagnosticResult(ok=False, error="Databricks configuration is incomplete.")
|
|
62
|
+
query = f"SELECT 1 FROM {table_name} LIMIT 1"
|
|
63
|
+
try:
|
|
64
|
+
response = self._post_statement(query)
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
except requests.RequestException as exc:
|
|
67
|
+
return DiagnosticResult(ok=False, error=str(exc))
|
|
68
|
+
return DiagnosticResult(ok=True)
|
|
69
|
+
|
|
70
|
+
def query_failure_traps(self, telemetry: ParsedTelemetry, table_name: str = "dev_failure_traps") -> DiagnosticResult:
|
|
71
|
+
if not self.host or not self.token or not self.warehouse_id:
|
|
72
|
+
return DiagnosticResult(ok=False, error="Databricks configuration is incomplete.")
|
|
73
|
+
if not telemetry.error_signature:
|
|
74
|
+
return DiagnosticResult(ok=False, error="Missing sanitized error signature.")
|
|
75
|
+
|
|
76
|
+
if telemetry.status.is_complete:
|
|
77
|
+
statement = (
|
|
78
|
+
f"SELECT * FROM {table_name} "
|
|
79
|
+
"WHERE error_signature = :error_signature "
|
|
80
|
+
"AND module_name IN (:module_0, :module_1, :module_2) "
|
|
81
|
+
"LIMIT 5"
|
|
82
|
+
)
|
|
83
|
+
parameters = [{"name": "error_signature", "value": telemetry.error_signature}]
|
|
84
|
+
parameters.extend({"name": f"module_{idx}", "value": module} for idx, module in enumerate(telemetry.module_names[:3]))
|
|
85
|
+
else:
|
|
86
|
+
statement = (
|
|
87
|
+
f"SELECT * FROM {table_name} "
|
|
88
|
+
"WHERE error_signature LIKE :error_signature "
|
|
89
|
+
"OR module_name IN (:module_0, :module_1, :module_2) "
|
|
90
|
+
"LIMIT 5"
|
|
91
|
+
)
|
|
92
|
+
parameters = [{"name": "error_signature", "value": f"%{telemetry.error_signature.split(':', 1)[0]}%"}]
|
|
93
|
+
parameters.extend({"name": f"module_{idx}", "value": module} for idx, module in enumerate(telemetry.module_names[:3]))
|
|
94
|
+
try:
|
|
95
|
+
response = self._post_statement(statement, parameters=parameters)
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
except requests.RequestException as exc:
|
|
98
|
+
return DiagnosticResult(ok=False, error=str(exc))
|
|
99
|
+
suffix = "heuristic/partial" if not telemetry.status.is_complete else "verified"
|
|
100
|
+
return DiagnosticResult(ok=True, error=suffix)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Helpers for Entire CLI integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class CommandResult:
|
|
12
|
+
ok: bool
|
|
13
|
+
command: str
|
|
14
|
+
stdout: str = ""
|
|
15
|
+
stderr: str = ""
|
|
16
|
+
returncode: int | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run(command: list[str], cwd: Path | str | None = None) -> CommandResult:
|
|
20
|
+
try:
|
|
21
|
+
completed = subprocess.run(
|
|
22
|
+
command,
|
|
23
|
+
cwd=cwd,
|
|
24
|
+
stdin=subprocess.DEVNULL,
|
|
25
|
+
capture_output=True,
|
|
26
|
+
text=True,
|
|
27
|
+
encoding="utf-8",
|
|
28
|
+
errors="replace",
|
|
29
|
+
check=False,
|
|
30
|
+
timeout=15,
|
|
31
|
+
)
|
|
32
|
+
stdout = (completed.stdout or "").strip()
|
|
33
|
+
stderr = (completed.stderr or "").strip()
|
|
34
|
+
return CommandResult(
|
|
35
|
+
ok=completed.returncode == 0,
|
|
36
|
+
command=" ".join(command),
|
|
37
|
+
stdout=stdout,
|
|
38
|
+
stderr=stderr,
|
|
39
|
+
returncode=completed.returncode,
|
|
40
|
+
)
|
|
41
|
+
except subprocess.TimeoutExpired as exc:
|
|
42
|
+
return CommandResult(
|
|
43
|
+
ok=False,
|
|
44
|
+
command=" ".join(command),
|
|
45
|
+
stdout=(exc.stdout or "").strip() if isinstance(exc.stdout, str) else "",
|
|
46
|
+
stderr=f"Command timed out after 15 seconds while running: {' '.join(command)}",
|
|
47
|
+
returncode=None,
|
|
48
|
+
)
|
|
49
|
+
except FileNotFoundError as exc:
|
|
50
|
+
return CommandResult(
|
|
51
|
+
ok=False,
|
|
52
|
+
command=" ".join(command),
|
|
53
|
+
stderr=f"Command not found: {command[0]}. Install it and ensure it is on PATH. Details: {exc}",
|
|
54
|
+
returncode=None,
|
|
55
|
+
)
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
return CommandResult(
|
|
58
|
+
ok=False,
|
|
59
|
+
command=" ".join(command),
|
|
60
|
+
stderr=f"Failed to run {' '.join(command)} in {cwd}: {exc}",
|
|
61
|
+
returncode=None,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def setup_entire(agent: str = "codex", cwd: Path | str = Path.cwd()) -> CommandResult:
|
|
66
|
+
git_dir = Path(cwd) / ".git"
|
|
67
|
+
if not git_dir.exists():
|
|
68
|
+
return CommandResult(ok=False, command="git rev-parse --is-inside-work-tree", stderr="Git repository not initialized.")
|
|
69
|
+
|
|
70
|
+
enable = _run(["entire", "enable", "--force"], cwd=cwd)
|
|
71
|
+
if not enable.ok:
|
|
72
|
+
return enable
|
|
73
|
+
|
|
74
|
+
hooks = _run(["entire", "hooks", "configure", "--agent", agent], cwd=cwd)
|
|
75
|
+
if not hooks.ok:
|
|
76
|
+
return hooks
|
|
77
|
+
|
|
78
|
+
return CommandResult(
|
|
79
|
+
ok=True,
|
|
80
|
+
command=f"entire enable --force && entire hooks configure --agent {agent}",
|
|
81
|
+
stdout="\n".join(part for part in [enable.stdout, hooks.stdout] if part),
|
|
82
|
+
stderr="\n".join(part for part in [enable.stderr, hooks.stderr] if part),
|
|
83
|
+
returncode=0,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_entire_status(cwd: Path | str = Path.cwd()) -> CommandResult:
|
|
88
|
+
status = _run(["entire", "status"], cwd=cwd)
|
|
89
|
+
text = f"{status.stdout}\n{status.stderr}".lower()
|
|
90
|
+
if status.returncode == 0 and ("enabled" in text or "checkpoints active" in text):
|
|
91
|
+
first_line = (status.stdout or "").splitlines()[0] if (status.stdout or "").splitlines() else ""
|
|
92
|
+
detail = f"Entire active ({first_line})" if first_line else "Entire active"
|
|
93
|
+
return CommandResult(
|
|
94
|
+
ok=True,
|
|
95
|
+
command=status.command,
|
|
96
|
+
stdout=status.stdout,
|
|
97
|
+
stderr=detail,
|
|
98
|
+
returncode=status.returncode,
|
|
99
|
+
)
|
|
100
|
+
if not status.ok:
|
|
101
|
+
return status
|
|
102
|
+
if "checkpoint" not in text or ("active" not in text and "enabled" not in text):
|
|
103
|
+
return CommandResult(
|
|
104
|
+
ok=False,
|
|
105
|
+
command=status.command,
|
|
106
|
+
stdout=status.stdout,
|
|
107
|
+
stderr=status.stderr or "Entire is not enabled on this repository.",
|
|
108
|
+
returncode=status.returncode,
|
|
109
|
+
)
|
|
110
|
+
return status
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Handoff manifest assembly for privacy-aware checkpoint resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from resurrect.parser import ParsedTelemetry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class HandoffManifest:
|
|
15
|
+
status_label: str
|
|
16
|
+
resolution_kind: str
|
|
17
|
+
privacy_provenance: str
|
|
18
|
+
confidence: str
|
|
19
|
+
error_signature: str
|
|
20
|
+
module_names: list[str] = field(default_factory=list)
|
|
21
|
+
is_complete: bool = False
|
|
22
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
def to_json(self) -> str:
|
|
25
|
+
return json.dumps(asdict(self), indent=2, sort_keys=True) + "\n"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_handoff_manifest(telemetry: ParsedTelemetry, verified_match: bool) -> HandoffManifest:
|
|
29
|
+
partial = not telemetry.status.is_complete or telemetry.status.confidence != "complete" or not verified_match
|
|
30
|
+
return HandoffManifest(
|
|
31
|
+
status_label="[PARTIAL / REDACTED CONTEXT]" if partial else "[COMPLETE CONTEXT]",
|
|
32
|
+
resolution_kind="heuristic" if partial else "verified",
|
|
33
|
+
privacy_provenance=telemetry.privacy_provenance,
|
|
34
|
+
confidence=telemetry.status.confidence,
|
|
35
|
+
error_signature=telemetry.error_signature,
|
|
36
|
+
module_names=telemetry.module_names,
|
|
37
|
+
is_complete=telemetry.status.is_complete and verified_match,
|
|
38
|
+
metadata={
|
|
39
|
+
"missing_fields": telemetry.status.missing_fields,
|
|
40
|
+
"source_path": str(telemetry.source_path) if telemetry.source_path else None,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def write_handoff_manifest(path: Path, telemetry: ParsedTelemetry, verified_match: bool) -> Path:
|
|
46
|
+
path.write_text(build_handoff_manifest(telemetry, verified_match).to_json(), encoding="utf-8")
|
|
47
|
+
return path
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Telemetry parsing and privacy sanitization for checkpoint context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REDACTION_MARKERS = ("[REDACTED]", "***")
|
|
13
|
+
DEFAULT_CHECKPOINT_DIR = Path("refs/entire/checkpoints")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class ContextStatus:
|
|
18
|
+
is_complete: bool
|
|
19
|
+
confidence: str
|
|
20
|
+
missing_fields: list[str] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class ParsedTelemetry:
|
|
25
|
+
error_signature: str = ""
|
|
26
|
+
module_names: list[str] = field(default_factory=list)
|
|
27
|
+
privacy_provenance: str = "sanitized"
|
|
28
|
+
status: ContextStatus = field(default_factory=lambda: ContextStatus(is_complete=False, confidence="partial"))
|
|
29
|
+
source_path: Path | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _looks_redacted(text: str) -> bool:
|
|
33
|
+
return any(marker in text for marker in REDACTION_MARKERS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sanitize_text(text: str) -> str:
|
|
37
|
+
text = re.sub(r"(?i)\b[A-Z]:\\[^ \n\r\t]+|/[^ \n\r\t]+", "<path>", text)
|
|
38
|
+
text = re.sub(r"(?i)\b(token|password|secret|api[_-]?key)\s*[:=]\s*[^ \n\r\t]+", r"\1=<redacted>", text)
|
|
39
|
+
text = re.sub(r"(?i)\b[0-9a-f]{16,}\b", "<token>", text)
|
|
40
|
+
text = re.sub(r"`[^`]+`", "<redacted>", text)
|
|
41
|
+
return text.strip()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _extract_exception_signature(text: str) -> str:
|
|
45
|
+
if not text:
|
|
46
|
+
return "UnknownError: missing checkpoint content"
|
|
47
|
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
48
|
+
if not lines:
|
|
49
|
+
return "UnknownError: empty checkpoint content"
|
|
50
|
+
for line in reversed(lines):
|
|
51
|
+
if ":" in line:
|
|
52
|
+
return _sanitize_text(line)
|
|
53
|
+
return _sanitize_text(lines[-1])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _extract_module_names(events: list[dict[str, Any]]) -> list[str]:
|
|
57
|
+
modules: set[str] = set()
|
|
58
|
+
for event in events:
|
|
59
|
+
module = event.get("module") or event.get("tool") or event.get("source")
|
|
60
|
+
if isinstance(module, str) and module:
|
|
61
|
+
modules.add(module.split(".")[0])
|
|
62
|
+
return sorted(modules)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _read_checkpoint_records(checkpoint_dir: Path) -> tuple[list[dict[str, Any]], list[str]]:
|
|
66
|
+
records: list[dict[str, Any]] = []
|
|
67
|
+
missing_fields: list[str] = []
|
|
68
|
+
if not checkpoint_dir.exists():
|
|
69
|
+
missing_fields.append("checkpoint_dir")
|
|
70
|
+
return records, missing_fields
|
|
71
|
+
for path in sorted(checkpoint_dir.glob("**/*.jsonl")):
|
|
72
|
+
try:
|
|
73
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
74
|
+
if not line.strip():
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
record = json.loads(line)
|
|
78
|
+
except json.JSONDecodeError:
|
|
79
|
+
missing_fields.append(f"truncated_line:{path.name}")
|
|
80
|
+
continue
|
|
81
|
+
if isinstance(record, dict):
|
|
82
|
+
records.append(record)
|
|
83
|
+
except OSError:
|
|
84
|
+
missing_fields.append(f"unreadable:{path.name}")
|
|
85
|
+
return records, missing_fields
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def parse_telemetry(checkpoint_dir: Path | None = None, fixture_path: Path | None = None) -> ParsedTelemetry:
|
|
89
|
+
checkpoint_root = checkpoint_dir or DEFAULT_CHECKPOINT_DIR
|
|
90
|
+
records, missing_fields = _read_checkpoint_records(checkpoint_root)
|
|
91
|
+
source_path: Path | None = checkpoint_root if records else None
|
|
92
|
+
|
|
93
|
+
if not records and fixture_path and fixture_path.exists():
|
|
94
|
+
source_path = fixture_path
|
|
95
|
+
try:
|
|
96
|
+
for line in fixture_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
97
|
+
if line.strip():
|
|
98
|
+
obj = json.loads(line)
|
|
99
|
+
if isinstance(obj, dict):
|
|
100
|
+
records.append(obj)
|
|
101
|
+
except (OSError, json.JSONDecodeError):
|
|
102
|
+
missing_fields.append("fixture_parse_failed")
|
|
103
|
+
|
|
104
|
+
transcript_parts: list[str] = []
|
|
105
|
+
redacted = False
|
|
106
|
+
for record in records:
|
|
107
|
+
if "prompt" not in record:
|
|
108
|
+
missing_fields.append("prompt")
|
|
109
|
+
if "stderr" not in record and "stdout" not in record:
|
|
110
|
+
missing_fields.append("tool_output")
|
|
111
|
+
for field in ("prompt", "transcript", "stderr", "stdout", "error", "message"):
|
|
112
|
+
value = record.get(field)
|
|
113
|
+
if isinstance(value, str) and value:
|
|
114
|
+
if _looks_redacted(value):
|
|
115
|
+
redacted = True
|
|
116
|
+
transcript_parts.append(value)
|
|
117
|
+
transcript = "\n".join(transcript_parts)
|
|
118
|
+
if _looks_redacted(transcript):
|
|
119
|
+
redacted = True
|
|
120
|
+
signature = _extract_exception_signature(transcript)
|
|
121
|
+
status = ContextStatus(
|
|
122
|
+
is_complete=not redacted and not missing_fields,
|
|
123
|
+
confidence="complete" if not redacted and not missing_fields else "partial",
|
|
124
|
+
missing_fields=sorted(set(missing_fields)),
|
|
125
|
+
)
|
|
126
|
+
return ParsedTelemetry(
|
|
127
|
+
error_signature=signature,
|
|
128
|
+
module_names=_extract_module_names(records),
|
|
129
|
+
privacy_provenance="checkpoint" if source_path == checkpoint_root else "fixture",
|
|
130
|
+
status=status,
|
|
131
|
+
source_path=source_path,
|
|
132
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Rich console rendering for diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class StatusItem:
|
|
14
|
+
name: str
|
|
15
|
+
ok: bool
|
|
16
|
+
detail: str = ""
|
|
17
|
+
context_label: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def render_status_board(items: list[StatusItem], title: str = "Resurrect Doctor") -> Console:
|
|
21
|
+
console = Console()
|
|
22
|
+
table = Table(show_header=True, header_style="bold")
|
|
23
|
+
table.add_column("Check")
|
|
24
|
+
table.add_column("Status")
|
|
25
|
+
table.add_column("Details")
|
|
26
|
+
for item in items:
|
|
27
|
+
status = "[green]PASS[/green]" if item.ok else "[red]FAIL[/red]"
|
|
28
|
+
check = "[green]✓[/green]" if item.ok else "[yellow]![/yellow]"
|
|
29
|
+
detail = item.detail or ""
|
|
30
|
+
if item.context_label:
|
|
31
|
+
detail = f"{item.context_label} {detail}".strip()
|
|
32
|
+
table.add_row(f"{check} {item.name}", status, detail)
|
|
33
|
+
console.print(Panel(table, title=title, border_style="blue"))
|
|
34
|
+
return console
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from resurrect.config import DEFAULT_DELTA_TABLE_NAME, ResurrectConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_loads_from_env_when_config_missing(tmp_path: Path) -> None:
|
|
10
|
+
env = {
|
|
11
|
+
"DATABRICKS_HOST": "https://example.databricks.com",
|
|
12
|
+
"DATABRICKS_TOKEN": "token",
|
|
13
|
+
"DATABRICKS_WAREHOUSE_ID": "wh-1",
|
|
14
|
+
}
|
|
15
|
+
config = ResurrectConfig.load(tmp_path, env=env)
|
|
16
|
+
assert config.databricks_host == env["DATABRICKS_HOST"]
|
|
17
|
+
assert config.databricks_token == env["DATABRICKS_TOKEN"]
|
|
18
|
+
assert config.databricks_warehouse_id == env["DATABRICKS_WAREHOUSE_ID"]
|
|
19
|
+
assert config.delta_table_name == DEFAULT_DELTA_TABLE_NAME
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_save_and_load_round_trip(tmp_path: Path) -> None:
|
|
23
|
+
config = ResurrectConfig(
|
|
24
|
+
databricks_host="https://example.databricks.com",
|
|
25
|
+
databricks_token="token",
|
|
26
|
+
databricks_warehouse_id="wh-1",
|
|
27
|
+
delta_table_name="custom_table",
|
|
28
|
+
)
|
|
29
|
+
path = config.save(tmp_path)
|
|
30
|
+
assert path.exists()
|
|
31
|
+
saved = json.loads(path.read_text(encoding="utf-8"))
|
|
32
|
+
assert saved["delta_table_name"] == "custom_table"
|
|
33
|
+
|
|
34
|
+
loaded = ResurrectConfig.load(tmp_path, env={})
|
|
35
|
+
assert loaded == config
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from unittest.mock import Mock
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from resurrect.core.databricks_client import DatabricksClient
|
|
10
|
+
from resurrect.core.entire_bridge import _run, check_entire_status, setup_entire
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_setup_entire_runs_commands(monkeypatch, tmp_path: Path) -> None:
|
|
14
|
+
(tmp_path / ".git").mkdir()
|
|
15
|
+
calls: list[list[str]] = []
|
|
16
|
+
|
|
17
|
+
def fake_run(command, cwd=None, stdin=None, capture_output=None, text=None, encoding=None, errors=None, check=None, timeout=None):
|
|
18
|
+
calls.append(command)
|
|
19
|
+
return SimpleNamespace(returncode=0, stdout="ok", stderr="")
|
|
20
|
+
|
|
21
|
+
monkeypatch.setattr("resurrect.core.entire_bridge.subprocess.run", fake_run)
|
|
22
|
+
result = setup_entire(cwd=tmp_path)
|
|
23
|
+
assert result.ok
|
|
24
|
+
assert calls == [["entire", "enable", "--force"], ["entire", "hooks", "configure", "--agent", "codex"]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_check_entire_status_detects_active_checkpoints(monkeypatch, tmp_path: Path) -> None:
|
|
28
|
+
def fake_run(command, cwd=None, stdin=None, capture_output=None, text=None, encoding=None, errors=None, check=None, timeout=None):
|
|
29
|
+
return SimpleNamespace(returncode=0, stdout="checkpoints active", stderr="")
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr("resurrect.core.entire_bridge.subprocess.run", fake_run)
|
|
32
|
+
result = check_entire_status(cwd=tmp_path)
|
|
33
|
+
assert result.ok
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_ping_connection_and_table_query(monkeypatch) -> None:
|
|
37
|
+
session = Mock(spec=requests.Session)
|
|
38
|
+
response = Mock()
|
|
39
|
+
response.raise_for_status.return_value = None
|
|
40
|
+
session.post.return_value = response
|
|
41
|
+
client = DatabricksClient("https://example.databricks.com", "token", "wh-1", session=session)
|
|
42
|
+
|
|
43
|
+
ping = client.ping_connection()
|
|
44
|
+
table = client.verify_table_exists()
|
|
45
|
+
|
|
46
|
+
assert ping.ok
|
|
47
|
+
assert table.ok
|
|
48
|
+
assert session.post.call_count == 2
|
|
49
|
+
assert session.post.call_args_list[0].args[0].endswith("/api/2.0/sql/statements")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_ping_connection_reports_missing_fields() -> None:
|
|
53
|
+
client = DatabricksClient(None, None, None)
|
|
54
|
+
result = client.ping_connection()
|
|
55
|
+
assert not result.ok
|
|
56
|
+
assert "Missing DATABRICKS_HOST" in result.error
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_run_handles_none_output(monkeypatch, tmp_path: Path) -> None:
|
|
60
|
+
def fake_run(command, cwd=None, stdin=None, capture_output=None, text=None, encoding=None, errors=None, check=None, timeout=None):
|
|
61
|
+
return SimpleNamespace(returncode=0, stdout=None, stderr=None)
|
|
62
|
+
|
|
63
|
+
monkeypatch.setattr("resurrect.core.entire_bridge.subprocess.run", fake_run)
|
|
64
|
+
result = _run(["entire", "status"], cwd=tmp_path)
|
|
65
|
+
assert result.ok
|
|
66
|
+
assert result.stdout == ""
|
|
67
|
+
assert result.stderr == ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_run_decodes_non_ascii_output(monkeypatch, tmp_path: Path) -> None:
|
|
71
|
+
def fake_run(command, cwd=None, stdin=None, capture_output=None, text=None, encoding=None, errors=None, check=None, timeout=None):
|
|
72
|
+
return SimpleNamespace(returncode=0, stdout="naïve ✓", stderr="错误")
|
|
73
|
+
|
|
74
|
+
monkeypatch.setattr("resurrect.core.entire_bridge.subprocess.run", fake_run)
|
|
75
|
+
result = _run(["entire", "status"], cwd=tmp_path)
|
|
76
|
+
assert result.ok
|
|
77
|
+
assert result.stdout == "naïve ✓"
|
|
78
|
+
assert result.stderr == "错误"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from unittest.mock import Mock
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from resurrect.core.databricks_client import DatabricksClient
|
|
10
|
+
from resurrect.handoff import build_handoff_manifest
|
|
11
|
+
from resurrect.parser import parse_telemetry
|
|
12
|
+
from resurrect.ui.console import StatusItem
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_privacy_boundary_with_redacted_checkpoint(monkeypatch, tmp_path: Path) -> None:
|
|
16
|
+
checkpoint_dir = tmp_path / "refs" / "entire" / "checkpoints"
|
|
17
|
+
checkpoint_dir.mkdir(parents=True)
|
|
18
|
+
fixture = Path(__file__).parent / "fixtures" / "redacted_checkpoint.jsonl"
|
|
19
|
+
(checkpoint_dir / "checkpoint.jsonl").write_text(fixture.read_text(encoding="utf-8"), encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
telemetry = parse_telemetry(checkpoint_dir)
|
|
22
|
+
assert telemetry.error_signature
|
|
23
|
+
assert not telemetry.status.is_complete
|
|
24
|
+
assert telemetry.status.confidence == "partial"
|
|
25
|
+
assert "prompt" in telemetry.status.missing_fields
|
|
26
|
+
|
|
27
|
+
session = Mock(spec=requests.Session)
|
|
28
|
+
response = Mock()
|
|
29
|
+
response.raise_for_status.return_value = None
|
|
30
|
+
session.post.return_value = response
|
|
31
|
+
client = DatabricksClient("https://example.databricks.com", "token", "wh-1", session=session)
|
|
32
|
+
|
|
33
|
+
result = client.query_failure_traps(telemetry)
|
|
34
|
+
assert result.ok
|
|
35
|
+
|
|
36
|
+
payload = session.post.call_args.kwargs["json"]
|
|
37
|
+
assert "prompt" not in str(payload).lower()
|
|
38
|
+
assert "[redacted]" not in str(payload).lower()
|
|
39
|
+
assert "***" not in str(payload)
|
|
40
|
+
assert "statement" in payload
|
|
41
|
+
assert payload["statement"].startswith("SELECT * FROM dev_failure_traps")
|
|
42
|
+
assert "parameters" in payload
|
|
43
|
+
|
|
44
|
+
manifest = build_handoff_manifest(telemetry, verified_match=result.ok)
|
|
45
|
+
assert manifest.status_label == "[PARTIAL / REDACTED CONTEXT]"
|
|
46
|
+
assert manifest.confidence == "partial"
|
|
47
|
+
assert manifest.is_complete is False
|
|
48
|
+
|
|
49
|
+
item = StatusItem("Failure traps", result.ok, result.error or "", context_label=manifest.status_label)
|
|
50
|
+
assert item.context_label == "[PARTIAL / REDACTED CONTEXT]"
|