naxe 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.
- naxe/__init__.py +0 -0
- naxe/auth.py +19 -0
- naxe/cli_config.py +193 -0
- naxe/config.py +61 -0
- naxe/handlers/__init__.py +81 -0
- naxe/handlers/_common.py +11 -0
- naxe/handlers/approval.py +47 -0
- naxe/handlers/audit.py +48 -0
- naxe/handlers/dependencies.py +24 -0
- naxe/handlers/jobs.py +114 -0
- naxe/handlers/tasks.py +135 -0
- naxe/handlers/templates.py +26 -0
- naxe/init_cmd.py +237 -0
- naxe/resolver.py +181 -0
- naxe/schema.py +216 -0
- naxe/server.py +126 -0
- naxe/store/__init__.py +61 -0
- naxe/store/agents.py +49 -0
- naxe/store/approval.py +120 -0
- naxe/store/comments.py +79 -0
- naxe/store/core.py +152 -0
- naxe/store/jobs.py +147 -0
- naxe/store/tasks.py +513 -0
- naxe/store/templates.py +51 -0
- naxe/store.py +941 -0
- naxe/tools.py +565 -0
- naxe/tui/__init__.py +3 -0
- naxe/tui/app.py +451 -0
- naxe/tui/screens.py +1068 -0
- naxe/tui/theme.py +204 -0
- naxe/tui/widgets.py +550 -0
- naxe/tui.py +2238 -0
- naxe/watch.py +187 -0
- naxe-0.1.0.dist-info/METADATA +254 -0
- naxe-0.1.0.dist-info/RECORD +39 -0
- naxe-0.1.0.dist-info/WHEEL +5 -0
- naxe-0.1.0.dist-info/entry_points.txt +5 -0
- naxe-0.1.0.dist-info/licenses/LICENSE +661 -0
- naxe-0.1.0.dist-info/top_level.txt +1 -0
naxe/__init__.py
ADDED
|
File without changes
|
naxe/auth.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
KEY_PREFIX = "naxe_sk_"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_key() -> str:
|
|
8
|
+
"""Generate a random API key. Returns the raw key — shown once, never stored."""
|
|
9
|
+
return KEY_PREFIX + os.urandom(32).hex()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def hash_key(raw_key: str) -> str:
|
|
13
|
+
"""SHA-256 hash of the raw key. This is what gets stored in the DB."""
|
|
14
|
+
return hashlib.sha256(raw_key.encode()).hexdigest()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_key_format(key: str) -> bool:
|
|
18
|
+
"""Check key has correct prefix and length (prefix + 64 hex chars)."""
|
|
19
|
+
return key.startswith(KEY_PREFIX) and len(key) == len(KEY_PREFIX) + 64
|
naxe/cli_config.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from naxe.config import (
|
|
5
|
+
resolve_db_url, resolve_db_url_with_source, write_config_url, _CONFIG_FILE,
|
|
6
|
+
resolve_theme_with_source, write_theme, _THEME_FILE,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
args = sys.argv[1:]
|
|
12
|
+
|
|
13
|
+
if not args:
|
|
14
|
+
print("Usage: naxe-config <command> [args]")
|
|
15
|
+
print()
|
|
16
|
+
print("Commands:")
|
|
17
|
+
print(" status Show DB connection, auth mode, and job summary")
|
|
18
|
+
print(" set-url <url> Save a DB URL to ~/.config/naxe/config")
|
|
19
|
+
print(" get-url Print the currently resolved DB URL and its source")
|
|
20
|
+
print(" set-theme <name> Save a default theme to ~/.config/naxe/theme")
|
|
21
|
+
print(" get-theme Print the currently resolved theme and its source")
|
|
22
|
+
print()
|
|
23
|
+
print("Agent commands:")
|
|
24
|
+
print(" register-agent <name> Register a new agent and print its API key (shown once)")
|
|
25
|
+
print(" revoke-agent <name> Revoke an agent's API key")
|
|
26
|
+
print(" list-agents List all registered agents")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
command = args[0]
|
|
30
|
+
|
|
31
|
+
if command == "status":
|
|
32
|
+
_status()
|
|
33
|
+
|
|
34
|
+
elif command == "set-url":
|
|
35
|
+
if len(args) < 2:
|
|
36
|
+
print("Usage: naxe-config set-url <url>", file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
write_config_url(args[1])
|
|
39
|
+
print(f"Saved to {_CONFIG_FILE}")
|
|
40
|
+
|
|
41
|
+
elif command == "get-url":
|
|
42
|
+
url, source = resolve_db_url_with_source()
|
|
43
|
+
print(f"{url} ({source})")
|
|
44
|
+
|
|
45
|
+
elif command == "set-theme":
|
|
46
|
+
if len(args) < 2:
|
|
47
|
+
print("Usage: naxe-config set-theme <name>", file=sys.stderr)
|
|
48
|
+
print("Built-in naxe themes: naxe, naxe-bold")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
write_theme(args[1])
|
|
51
|
+
print(f"Saved to {_THEME_FILE}")
|
|
52
|
+
|
|
53
|
+
elif command == "get-theme":
|
|
54
|
+
theme, source = resolve_theme_with_source()
|
|
55
|
+
print(f"{theme} ({source})")
|
|
56
|
+
|
|
57
|
+
elif command == "register-agent":
|
|
58
|
+
if len(args) < 2:
|
|
59
|
+
print("Usage: naxe-config register-agent <name>", file=sys.stderr)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
_register_agent(args[1])
|
|
62
|
+
|
|
63
|
+
elif command == "revoke-agent":
|
|
64
|
+
if len(args) < 2:
|
|
65
|
+
print("Usage: naxe-config revoke-agent <name>", file=sys.stderr)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
_revoke_agent(args[1])
|
|
68
|
+
|
|
69
|
+
elif command == "list-agents":
|
|
70
|
+
_list_agents()
|
|
71
|
+
|
|
72
|
+
else:
|
|
73
|
+
print(f"Unknown command: {command}", file=sys.stderr)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _status() -> None:
|
|
78
|
+
from naxe.schema import get_connection
|
|
79
|
+
from naxe import store
|
|
80
|
+
from naxe.config import resolve_theme_with_source
|
|
81
|
+
|
|
82
|
+
url, url_source = resolve_db_url_with_source()
|
|
83
|
+
theme, theme_source = resolve_theme_with_source()
|
|
84
|
+
|
|
85
|
+
print(f"Database: {url}")
|
|
86
|
+
print(f" ({url_source})")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
conn = get_connection(url, readonly=True)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"Status: ✗ Cannot connect — {e}")
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
print(f"Status: ✓ Connected")
|
|
95
|
+
|
|
96
|
+
# Auth mode
|
|
97
|
+
try:
|
|
98
|
+
total_agents = conn.execute("SELECT COUNT(*) AS cnt FROM agents").fetchone()["cnt"]
|
|
99
|
+
active_agents = store.count_active_agents(conn)
|
|
100
|
+
if total_agents == 0:
|
|
101
|
+
print(f"Auth: Open mode (no agents registered)")
|
|
102
|
+
else:
|
|
103
|
+
print(f"Auth: Locked — {active_agents} active agent{'s' if active_agents != 1 else ''}, {total_agents - active_agents} revoked")
|
|
104
|
+
except Exception:
|
|
105
|
+
print(f"Auth: Unknown (agents table not found — run CREATE TABLE manually)")
|
|
106
|
+
|
|
107
|
+
# Job summary
|
|
108
|
+
try:
|
|
109
|
+
total_jobs = conn.execute("SELECT COUNT(*) AS cnt FROM jobs").fetchone()["cnt"]
|
|
110
|
+
active_jobs = conn.execute(
|
|
111
|
+
"SELECT COUNT(*) AS cnt FROM jobs WHERE status NOT IN ('completed', 'cancelled')"
|
|
112
|
+
).fetchone()["cnt"]
|
|
113
|
+
in_progress_tasks = conn.execute(
|
|
114
|
+
"SELECT COUNT(*) AS cnt FROM tasks WHERE status = 'in_progress'"
|
|
115
|
+
).fetchone()["cnt"]
|
|
116
|
+
print(f"Jobs: {active_jobs} active, {total_jobs} total")
|
|
117
|
+
if in_progress_tasks:
|
|
118
|
+
print(f"Tasks: {in_progress_tasks} currently in progress")
|
|
119
|
+
except Exception:
|
|
120
|
+
print(f"Jobs: Unknown (schema not initialised)")
|
|
121
|
+
|
|
122
|
+
print(f"Theme: {theme} ({theme_source})")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _register_agent(name: str) -> None:
|
|
126
|
+
from naxe.schema import get_connection
|
|
127
|
+
from naxe import store, auth
|
|
128
|
+
from naxe.auth import validate_key_format
|
|
129
|
+
|
|
130
|
+
conn = get_connection(resolve_db_url())
|
|
131
|
+
|
|
132
|
+
# If agents are already registered, caller must present a valid key
|
|
133
|
+
# (only an existing agent can register new ones on a locked DB)
|
|
134
|
+
n = store.count_active_agents(conn)
|
|
135
|
+
if n > 0:
|
|
136
|
+
raw_key = os.environ.get("NAXE_API_KEY", "")
|
|
137
|
+
if not raw_key:
|
|
138
|
+
print(
|
|
139
|
+
"naxe-config: NAXE_API_KEY is required to register a new agent when agents are already registered.",
|
|
140
|
+
file=sys.stderr,
|
|
141
|
+
)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
if not validate_key_format(raw_key):
|
|
144
|
+
print("naxe-config: NAXE_API_KEY has invalid format.", file=sys.stderr)
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
if store.get_agent_by_key_hash(conn, auth.hash_key(raw_key)) is None:
|
|
147
|
+
print("naxe-config: Invalid or revoked API key.", file=sys.stderr)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
raw_key = auth.generate_key()
|
|
151
|
+
try:
|
|
152
|
+
store.register_agent(conn, name, auth.hash_key(raw_key))
|
|
153
|
+
conn.commit()
|
|
154
|
+
except ValueError as e:
|
|
155
|
+
print(f"naxe-config: {e}", file=sys.stderr)
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
158
|
+
print(f"Agent '{name}' registered.")
|
|
159
|
+
print(f"Key: {raw_key}")
|
|
160
|
+
print("Store this securely — it will not be shown again.")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _revoke_agent(name: str) -> None:
|
|
164
|
+
from naxe.schema import get_connection
|
|
165
|
+
from naxe import store
|
|
166
|
+
|
|
167
|
+
conn = get_connection(resolve_db_url())
|
|
168
|
+
revoked = store.revoke_agent(conn, name)
|
|
169
|
+
if revoked:
|
|
170
|
+
conn.commit()
|
|
171
|
+
print(f"Agent '{name}' revoked.")
|
|
172
|
+
else:
|
|
173
|
+
print(f"naxe-config: Agent '{name}' not found or already revoked.", file=sys.stderr)
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _list_agents() -> None:
|
|
178
|
+
from naxe.schema import get_connection
|
|
179
|
+
from naxe import store
|
|
180
|
+
|
|
181
|
+
conn = get_connection(resolve_db_url())
|
|
182
|
+
agents = store.list_agents(conn)
|
|
183
|
+
|
|
184
|
+
if not agents:
|
|
185
|
+
print("No agents registered. (Open mode — any caller is accepted.)")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
print(f"{'Name':<24} {'Created':<20} Status")
|
|
189
|
+
print("-" * 56)
|
|
190
|
+
for a in agents:
|
|
191
|
+
status = "active" if a["active"] else "revoked"
|
|
192
|
+
created = str(a["created_at"])[:19]
|
|
193
|
+
print(f"{a['name']:<24} {created:<20} {status}")
|
naxe/config.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
_CONFIG_FILE = Path.home() / ".config" / "naxe" / "config"
|
|
5
|
+
_THEME_FILE = Path.home() / ".config" / "naxe" / "theme"
|
|
6
|
+
|
|
7
|
+
DEFAULT_THEME = "naxe"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_db_url() -> str:
|
|
11
|
+
if url := os.environ.get("NAXE_DB_URL"):
|
|
12
|
+
return url
|
|
13
|
+
if _CONFIG_FILE.exists():
|
|
14
|
+
url = _CONFIG_FILE.read_text().strip()
|
|
15
|
+
if url:
|
|
16
|
+
return url
|
|
17
|
+
if path := os.environ.get("NAXE_DB_PATH"):
|
|
18
|
+
return path
|
|
19
|
+
return "./naxe.db"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_db_url_with_source() -> tuple[str, str]:
|
|
23
|
+
if url := os.environ.get("NAXE_DB_URL"):
|
|
24
|
+
return url, "env:NAXE_DB_URL"
|
|
25
|
+
if _CONFIG_FILE.exists():
|
|
26
|
+
url = _CONFIG_FILE.read_text().strip()
|
|
27
|
+
if url:
|
|
28
|
+
return url, f"config:{_CONFIG_FILE}"
|
|
29
|
+
if path := os.environ.get("NAXE_DB_PATH"):
|
|
30
|
+
return path, "env:NAXE_DB_PATH"
|
|
31
|
+
return "./naxe.db", "default"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_config_url(url: str) -> None:
|
|
35
|
+
_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
_CONFIG_FILE.write_text(url + "\n")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_theme() -> str:
|
|
40
|
+
if theme := os.environ.get("NAXE_THEME"):
|
|
41
|
+
return theme.strip()
|
|
42
|
+
if _THEME_FILE.exists():
|
|
43
|
+
theme = _THEME_FILE.read_text().strip()
|
|
44
|
+
if theme:
|
|
45
|
+
return theme
|
|
46
|
+
return DEFAULT_THEME
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_theme_with_source() -> tuple[str, str]:
|
|
50
|
+
if theme := os.environ.get("NAXE_THEME"):
|
|
51
|
+
return theme.strip(), "env:NAXE_THEME"
|
|
52
|
+
if _THEME_FILE.exists():
|
|
53
|
+
theme = _THEME_FILE.read_text().strip()
|
|
54
|
+
if theme:
|
|
55
|
+
return theme, f"config:{_THEME_FILE}"
|
|
56
|
+
return DEFAULT_THEME, "default"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_theme(theme: str) -> None:
|
|
60
|
+
_THEME_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
_THEME_FILE.write_text(theme + "\n")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from naxe.handlers.jobs import (
|
|
2
|
+
handle_create_job,
|
|
3
|
+
handle_list_jobs,
|
|
4
|
+
handle_edit_job,
|
|
5
|
+
handle_get_job_status,
|
|
6
|
+
handle_cancel_job,
|
|
7
|
+
handle_pause_job,
|
|
8
|
+
handle_resume_job,
|
|
9
|
+
)
|
|
10
|
+
from naxe.handlers.tasks import (
|
|
11
|
+
handle_add_tasks,
|
|
12
|
+
handle_get_next_actions,
|
|
13
|
+
handle_claim_task,
|
|
14
|
+
handle_claim_next_action,
|
|
15
|
+
handle_complete_task,
|
|
16
|
+
handle_fail_task,
|
|
17
|
+
handle_heartbeat_task,
|
|
18
|
+
handle_update_task_progress,
|
|
19
|
+
handle_cancel_task,
|
|
20
|
+
handle_edit_task,
|
|
21
|
+
handle_requeue_task,
|
|
22
|
+
)
|
|
23
|
+
from naxe.handlers.dependencies import (
|
|
24
|
+
handle_add_job_dependency,
|
|
25
|
+
handle_set_job_concurrency,
|
|
26
|
+
handle_set_worktree_paths,
|
|
27
|
+
)
|
|
28
|
+
from naxe.handlers.approval import (
|
|
29
|
+
handle_request_approval,
|
|
30
|
+
handle_approve_task,
|
|
31
|
+
handle_reject_task,
|
|
32
|
+
handle_return_task,
|
|
33
|
+
)
|
|
34
|
+
from naxe.handlers.audit import (
|
|
35
|
+
handle_add_task_comment,
|
|
36
|
+
handle_get_task_comments,
|
|
37
|
+
handle_get_task_events,
|
|
38
|
+
handle_get_job_audit_trail,
|
|
39
|
+
handle_get_blocked_tasks,
|
|
40
|
+
)
|
|
41
|
+
from naxe.handlers.templates import (
|
|
42
|
+
handle_create_job_template,
|
|
43
|
+
handle_list_templates,
|
|
44
|
+
handle_instantiate_template,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
DISPATCH = {
|
|
48
|
+
"create_job": handle_create_job,
|
|
49
|
+
"list_jobs": handle_list_jobs,
|
|
50
|
+
"edit_job": handle_edit_job,
|
|
51
|
+
"get_job_status": handle_get_job_status,
|
|
52
|
+
"cancel_job": handle_cancel_job,
|
|
53
|
+
"pause_job": handle_pause_job,
|
|
54
|
+
"resume_job": handle_resume_job,
|
|
55
|
+
"add_tasks": handle_add_tasks,
|
|
56
|
+
"get_next_actions": handle_get_next_actions,
|
|
57
|
+
"claim_task": handle_claim_task,
|
|
58
|
+
"claim_next_action": handle_claim_next_action,
|
|
59
|
+
"complete_task": handle_complete_task,
|
|
60
|
+
"fail_task": handle_fail_task,
|
|
61
|
+
"heartbeat_task": handle_heartbeat_task,
|
|
62
|
+
"update_task_progress": handle_update_task_progress,
|
|
63
|
+
"cancel_task": handle_cancel_task,
|
|
64
|
+
"edit_task": handle_edit_task,
|
|
65
|
+
"requeue_task": handle_requeue_task,
|
|
66
|
+
"add_job_dependency": handle_add_job_dependency,
|
|
67
|
+
"set_job_concurrency": handle_set_job_concurrency,
|
|
68
|
+
"set_worktree_paths": handle_set_worktree_paths,
|
|
69
|
+
"request_approval": handle_request_approval,
|
|
70
|
+
"approve_task": handle_approve_task,
|
|
71
|
+
"reject_task": handle_reject_task,
|
|
72
|
+
"return_task": handle_return_task,
|
|
73
|
+
"add_task_comment": handle_add_task_comment,
|
|
74
|
+
"get_task_comments": handle_get_task_comments,
|
|
75
|
+
"get_task_events": handle_get_task_events,
|
|
76
|
+
"get_job_audit_trail": handle_get_job_audit_trail,
|
|
77
|
+
"get_blocked_tasks": handle_get_blocked_tasks,
|
|
78
|
+
"create_job_template": handle_create_job_template,
|
|
79
|
+
"list_templates": handle_list_templates,
|
|
80
|
+
"instantiate_template": handle_instantiate_template,
|
|
81
|
+
}
|
naxe/handlers/_common.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from mcp.types import TextContent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _ok(**kwargs) -> list[TextContent]:
|
|
7
|
+
return [TextContent(type="text", text=json.dumps(kwargs, default=str))]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _err(msg: str) -> list[TextContent]:
|
|
11
|
+
return [TextContent(type="text", text=json.dumps({"error": msg}))]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from naxe.handlers._common import _ok, _err
|
|
4
|
+
from naxe import store
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def handle_request_approval(conn, arguments: dict) -> list:
|
|
8
|
+
task_id = arguments["task_id"]
|
|
9
|
+
agent_id = arguments["agent_id"]
|
|
10
|
+
task = store.request_approval(conn, task_id, agent_id, arguments.get("notes"))
|
|
11
|
+
if task is None:
|
|
12
|
+
return _err("Task not found or not eligible for approval request (must be in_progress and owned by this agent)")
|
|
13
|
+
return _ok(success=True, task=task)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def handle_approve_task(conn, arguments: dict) -> list:
|
|
17
|
+
task_id = arguments["task_id"]
|
|
18
|
+
approver_id = arguments["approver_id"]
|
|
19
|
+
result = store.approve_task(conn, task_id, approver_id, arguments.get("notes"))
|
|
20
|
+
if result is None:
|
|
21
|
+
return _err("Task not found or not awaiting approval")
|
|
22
|
+
task = result["task"]
|
|
23
|
+
newly_unblocked = result["newly_unblocked"]
|
|
24
|
+
ret: dict[str, Any] = {"success": True, "task": task, "newly_unblocked": newly_unblocked}
|
|
25
|
+
if task and "_newly_unblocked_jobs" in task:
|
|
26
|
+
ret["newly_unblocked_jobs"] = task.pop("_newly_unblocked_jobs")
|
|
27
|
+
return _ok(**ret)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def handle_reject_task(conn, arguments: dict) -> list:
|
|
31
|
+
task_id = arguments["task_id"]
|
|
32
|
+
approver_id = arguments["approver_id"]
|
|
33
|
+
reason = arguments["reason"]
|
|
34
|
+
task = store.reject_task(conn, task_id, approver_id, reason)
|
|
35
|
+
if task is None:
|
|
36
|
+
return _err("Task not found or not awaiting approval")
|
|
37
|
+
return _ok(success=True, task=task)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def handle_return_task(conn, arguments: dict) -> list:
|
|
41
|
+
task_id = arguments["task_id"]
|
|
42
|
+
approver_id = arguments["approver_id"]
|
|
43
|
+
feedback = arguments["feedback"]
|
|
44
|
+
task = store.return_task(conn, task_id, approver_id, feedback)
|
|
45
|
+
if task is None:
|
|
46
|
+
return _err("Task not found or not awaiting approval")
|
|
47
|
+
return _ok(success=True, task=task)
|
naxe/handlers/audit.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from naxe.handlers._common import _ok, _err
|
|
2
|
+
from naxe import store, resolver
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_add_task_comment(conn, arguments: dict) -> list:
|
|
6
|
+
task_id = arguments["task_id"]
|
|
7
|
+
author_type = arguments["author_type"]
|
|
8
|
+
if author_type not in ("agent", "human"):
|
|
9
|
+
return _err("author_type must be 'agent' or 'human'")
|
|
10
|
+
comment = store.add_task_comment(
|
|
11
|
+
conn, task_id, arguments["author_id"], author_type, arguments["content"]
|
|
12
|
+
)
|
|
13
|
+
if comment is None:
|
|
14
|
+
return _err(f"Task '{task_id}' not found")
|
|
15
|
+
return _ok(success=True, comment=comment)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handle_get_task_comments(conn, arguments: dict) -> list:
|
|
19
|
+
task_id = arguments["task_id"]
|
|
20
|
+
task = store.get_task(conn, task_id)
|
|
21
|
+
if not task:
|
|
22
|
+
return _err(f"Task '{task_id}' not found")
|
|
23
|
+
comments = store.get_task_comments(conn, task_id, arguments.get("approval_round"))
|
|
24
|
+
return _ok(task_id=task_id, comments=comments)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def handle_get_task_events(conn, arguments: dict) -> list:
|
|
28
|
+
task_id = arguments["task_id"]
|
|
29
|
+
if not store.get_task(conn, task_id):
|
|
30
|
+
return _err(f"Task '{task_id}' not found")
|
|
31
|
+
events = store.get_task_events(conn, task_id)
|
|
32
|
+
return _ok(task_id=task_id, events=events)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def handle_get_job_audit_trail(conn, arguments: dict) -> list:
|
|
36
|
+
job = store.get_job(conn, arguments["job_id"])
|
|
37
|
+
if not job:
|
|
38
|
+
return _err(f"Job '{arguments['job_id']}' not found")
|
|
39
|
+
events = store.get_job_events(conn, job["id"])
|
|
40
|
+
return _ok(job_id=job["id"], events=events)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def handle_get_blocked_tasks(conn, arguments: dict) -> list:
|
|
44
|
+
job = store.get_job(conn, arguments["job_id"])
|
|
45
|
+
if not job:
|
|
46
|
+
return _err(f"Job '{arguments['job_id']}' not found")
|
|
47
|
+
blocked = resolver.get_blocking_reasons(conn, job["id"])
|
|
48
|
+
return _ok(job_id=job["id"], blocked_tasks=blocked)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from naxe.handlers._common import _ok, _err
|
|
2
|
+
from naxe import store
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_add_job_dependency(conn, arguments: dict) -> list:
|
|
6
|
+
try:
|
|
7
|
+
store.add_job_dependency(conn, arguments["job_id"], arguments["depends_on_job_id"])
|
|
8
|
+
except ValueError as e:
|
|
9
|
+
return _err(str(e))
|
|
10
|
+
return _ok(success=True, job_id=arguments["job_id"], depends_on_job_id=arguments["depends_on_job_id"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_set_job_concurrency(conn, arguments: dict) -> list:
|
|
14
|
+
updated = store.set_job_concurrency(conn, arguments["job_id"], arguments.get("max_workers"))
|
|
15
|
+
if updated is None:
|
|
16
|
+
return _err(f"Job '{arguments['job_id']}' not found")
|
|
17
|
+
return _ok(success=True, job=updated)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handle_set_worktree_paths(conn, arguments: dict) -> list:
|
|
21
|
+
updated = store.set_worktree_paths(conn, arguments["job_id"], arguments.get("paths", {}))
|
|
22
|
+
if updated is None:
|
|
23
|
+
return _err(f"Job '{arguments['job_id']}' not found")
|
|
24
|
+
return _ok(success=True, job=updated)
|
naxe/handlers/jobs.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from naxe.handlers._common import _ok, _err
|
|
5
|
+
from naxe import store, resolver
|
|
6
|
+
from naxe.schema import TaskStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def handle_create_job(conn, arguments: dict) -> list:
|
|
10
|
+
job = store.create_job(
|
|
11
|
+
conn, arguments["name"],
|
|
12
|
+
arguments.get("max_workers"),
|
|
13
|
+
worktree=arguments.get("worktree", False),
|
|
14
|
+
)
|
|
15
|
+
return _ok(job_id=job["id"], job=job)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handle_list_jobs(conn, arguments: dict) -> list:
|
|
19
|
+
limit = arguments.get("limit", 50)
|
|
20
|
+
offset = arguments.get("offset", 0)
|
|
21
|
+
id_prefix = arguments.get("id_prefix")
|
|
22
|
+
page = store.list_jobs(conn, limit=limit, offset=offset, id_prefix=id_prefix)
|
|
23
|
+
result = []
|
|
24
|
+
for job in page["jobs"]:
|
|
25
|
+
tasks = store.get_tasks_for_job(conn, job["id"])
|
|
26
|
+
result.append({
|
|
27
|
+
**job,
|
|
28
|
+
"progress": {
|
|
29
|
+
"total": len(tasks),
|
|
30
|
+
"completed": sum(1 for t in tasks if t["status"] == TaskStatus.COMPLETED),
|
|
31
|
+
"in_progress": sum(1 for t in tasks if t["status"] == TaskStatus.IN_PROGRESS),
|
|
32
|
+
"pending": sum(1 for t in tasks if t["status"] == TaskStatus.PENDING),
|
|
33
|
+
"failed": sum(1 for t in tasks if t["status"] == TaskStatus.FAILED),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
return _ok(jobs=result, total=page["total"], has_more=page["has_more"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def handle_edit_job(conn, arguments: dict) -> list:
|
|
40
|
+
updated = store.edit_job(conn, arguments["job_id"], arguments["name"])
|
|
41
|
+
if updated is None:
|
|
42
|
+
return _err(f"Job '{arguments['job_id']}' not found")
|
|
43
|
+
return _ok(success=True, job=updated)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def handle_get_job_status(conn, arguments: dict) -> list:
|
|
47
|
+
job = store.get_job(conn, arguments["job_id"])
|
|
48
|
+
if not job:
|
|
49
|
+
return _err(f"Job '{arguments['job_id']}' not found")
|
|
50
|
+
job_id = job["id"]
|
|
51
|
+
tasks = store.get_tasks_for_job(conn, job_id)
|
|
52
|
+
blocked_tasks = resolver.get_blocking_reasons(conn, job_id)
|
|
53
|
+
blocked_map = {b["id"]: b["blocked_by"] for b in blocked_tasks}
|
|
54
|
+
for t in tasks:
|
|
55
|
+
if t["status"] == TaskStatus.PENDING and t["id"] in blocked_map:
|
|
56
|
+
t["blocked_by"] = blocked_map[t["id"]]
|
|
57
|
+
else:
|
|
58
|
+
t["blocked_by"] = []
|
|
59
|
+
now_iso = datetime.now(timezone.utc).isoformat()
|
|
60
|
+
for t in tasks:
|
|
61
|
+
status = t["status"]
|
|
62
|
+
if status == TaskStatus.PENDING and t["id"] in blocked_map:
|
|
63
|
+
t["display_status"] = "waiting_on"
|
|
64
|
+
elif status == TaskStatus.PENDING and t.get("start_date") and t["start_date"] > now_iso:
|
|
65
|
+
t["display_status"] = "scheduled"
|
|
66
|
+
elif status == TaskStatus.PENDING:
|
|
67
|
+
t["display_status"] = "next_action"
|
|
68
|
+
else:
|
|
69
|
+
t["display_status"] = status
|
|
70
|
+
progress = {
|
|
71
|
+
"total": len(tasks),
|
|
72
|
+
"completed": sum(1 for t in tasks if t["status"] == TaskStatus.COMPLETED),
|
|
73
|
+
"in_progress": sum(1 for t in tasks if t["status"] == TaskStatus.IN_PROGRESS),
|
|
74
|
+
"pending": sum(1 for t in tasks if t["status"] == TaskStatus.PENDING),
|
|
75
|
+
"failed": sum(1 for t in tasks if t["status"] == TaskStatus.FAILED),
|
|
76
|
+
}
|
|
77
|
+
dep_rows = conn.execute(
|
|
78
|
+
"""SELECT jd.depends_on_job_id, j.name, j.status
|
|
79
|
+
FROM job_dependencies jd
|
|
80
|
+
JOIN jobs j ON j.id = jd.depends_on_job_id
|
|
81
|
+
WHERE jd.job_id = %s""",
|
|
82
|
+
(job_id,),
|
|
83
|
+
).fetchall()
|
|
84
|
+
blocking_jobs = [
|
|
85
|
+
{"id": r["depends_on_job_id"], "name": r["name"], "status": r["status"]}
|
|
86
|
+
for r in dep_rows
|
|
87
|
+
if r["status"] != TaskStatus.COMPLETED
|
|
88
|
+
]
|
|
89
|
+
active_workers = store.count_active_workers(conn, job_id)
|
|
90
|
+
return _ok(job=job, tasks=tasks, progress=progress, blocking_jobs=blocking_jobs, active_workers=active_workers)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def handle_cancel_job(conn, arguments: dict) -> list:
|
|
94
|
+
job_id = arguments["job_id"]
|
|
95
|
+
if not store.get_job(conn, job_id):
|
|
96
|
+
return _err(f"Job '{job_id}' not found")
|
|
97
|
+
result = store.cancel_job(conn, job_id)
|
|
98
|
+
return _ok(success=True, **result)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def handle_pause_job(conn, arguments: dict) -> list:
|
|
102
|
+
job_id = arguments["job_id"]
|
|
103
|
+
job = store.pause_job(conn, job_id, reason=arguments.get("reason"))
|
|
104
|
+
if job is None:
|
|
105
|
+
return _err(f"Job '{job_id}' not found")
|
|
106
|
+
return _ok(success=True, job=job)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def handle_resume_job(conn, arguments: dict) -> list:
|
|
110
|
+
job_id = arguments["job_id"]
|
|
111
|
+
job = store.resume_job(conn, job_id)
|
|
112
|
+
if job is None:
|
|
113
|
+
return _err(f"Job '{job_id}' not found")
|
|
114
|
+
return _ok(success=True, job=job)
|