antahkarana-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.
ant/__init__.py
ADDED
ant/cli.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Author - Deepak Soni
|
|
2
|
+
"""ant — Antaḥkaraṇa governed-AI operator CLI.
|
|
3
|
+
|
|
4
|
+
`pip install antahkarana-cli` → run `ant` → connect to your copilot → prompt it. It investigates a machine,
|
|
5
|
+
reasons, runs read-only commands immediately, and asks y/N before any state-changing action (catastrophic
|
|
6
|
+
commands are hard-blocked server-side). Thin client over the copilot's /api/chat (the model + control ring).
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
ant interactive REPL (banner + prompt)
|
|
10
|
+
ant "check open ports" one-shot
|
|
11
|
+
ant connect <url> save the copilot endpoint
|
|
12
|
+
ant login <token> save an auth token (sent as Bearer)
|
|
13
|
+
ant status show connection + model health
|
|
14
|
+
In the REPL, slash commands: /connect /login /model /status /help /clear /exit
|
|
15
|
+
"""
|
|
16
|
+
import json, os, sys, urllib.request, urllib.error
|
|
17
|
+
|
|
18
|
+
# ── cross-OS terminal setup (Windows / macOS / Linux) ──
|
|
19
|
+
try: # never crash on non-UTF-8 consoles (e.g. Windows cmd)
|
|
20
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
if os.name == "nt":
|
|
24
|
+
os.system("") # enable ANSI escape processing on Windows 10+
|
|
25
|
+
_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
|
26
|
+
_uni = (getattr(sys.stdout, "encoding", "") or "").lower().startswith("utf")
|
|
27
|
+
TOOL_ICON, WARN_ICON = ("🔧", "⚠") if _uni else ("[*]", "[!]")
|
|
28
|
+
|
|
29
|
+
CFG_DIR = os.path.expanduser("~/.ant")
|
|
30
|
+
CFG = os.path.join(CFG_DIR, "config.json")
|
|
31
|
+
DEFAULT_URL = "http://129.146.3.254:8090"
|
|
32
|
+
_RAW = {"g": "\033[32m", "y": "\033[33m", "r": "\033[31m", "c": "\033[36m", "b": "\033[1m", "d": "\033[2m", "x": "\033[0m"}
|
|
33
|
+
C = {k: (v if _COLOR else "") for k, v in _RAW.items()}
|
|
34
|
+
|
|
35
|
+
if _uni:
|
|
36
|
+
_ART = " ▄▀█ █▄░█ ▀█▀ · Antaḥkaraṇa\n █▀█ █░▀█ ░█░ · governed AI operator"
|
|
37
|
+
else:
|
|
38
|
+
_ART = " a n t : Antahkarana\n governed AI operator"
|
|
39
|
+
BANNER = f"{C['c']}{C['b']}\n{_ART}{C['x']}{C['d']}\n prompt a machine - read-only runs - changes need y/N - catastrophic blocked{C['x']}\n"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load():
|
|
43
|
+
try:
|
|
44
|
+
return json.load(open(CFG))
|
|
45
|
+
except Exception:
|
|
46
|
+
return {"url": DEFAULT_URL, "token": "", "model": ""}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def save(cfg):
|
|
50
|
+
os.makedirs(CFG_DIR, exist_ok=True)
|
|
51
|
+
json.dump(cfg, open(CFG, "w"), indent=2)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _headers(cfg):
|
|
55
|
+
h = {"Content-Type": "application/json"}
|
|
56
|
+
if cfg.get("token"):
|
|
57
|
+
h["Authorization"] = "Bearer " + cfg["token"]
|
|
58
|
+
return h
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def health(cfg):
|
|
62
|
+
try:
|
|
63
|
+
req = urllib.request.Request(cfg["url"].rstrip("/") + "/api/health", headers=_headers(cfg))
|
|
64
|
+
return json.load(urllib.request.urlopen(req, timeout=8))
|
|
65
|
+
except Exception as e: # noqa: BLE001
|
|
66
|
+
return {"error": str(e)}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def chat(cfg, history, approved):
|
|
70
|
+
"""Stream the agent's steps; return commands awaiting approval."""
|
|
71
|
+
body = json.dumps({"messages": history, "approved": approved}).encode()
|
|
72
|
+
req = urllib.request.Request(cfg["url"].rstrip("/") + "/api/chat", body, _headers(cfg))
|
|
73
|
+
pending, buf = [], ""
|
|
74
|
+
with urllib.request.urlopen(req, timeout=300) as r:
|
|
75
|
+
for chunk in r:
|
|
76
|
+
buf += chunk.decode("utf-8", "ignore")
|
|
77
|
+
while "\n\n" in buf:
|
|
78
|
+
line, buf = buf.split("\n\n", 1)
|
|
79
|
+
line = line.strip()
|
|
80
|
+
if not line.startswith("data:"):
|
|
81
|
+
continue
|
|
82
|
+
e = json.loads(line[5:].strip()); t = e.get("type")
|
|
83
|
+
if t == "tool":
|
|
84
|
+
col = {"allow": C["g"], "soft_block": C["y"], "hard_block": C["r"]}.get(e["tier"], "")
|
|
85
|
+
print(f" {C['d']}{TOOL_ICON} {e['name']}({e.get('target','')}){C['x']} {col}[{e['tier']}]{C['x']}")
|
|
86
|
+
elif t == "approval":
|
|
87
|
+
pending.append(e["command"])
|
|
88
|
+
elif t == "message":
|
|
89
|
+
print(f"\n{e.get('markdown','')}\n")
|
|
90
|
+
history.append({"role": "assistant", "content": e.get("markdown", "")})
|
|
91
|
+
elif t == "error":
|
|
92
|
+
print(f" {C['r']}error: {e['text']}{C['x']}")
|
|
93
|
+
return pending
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def ask(cfg, history, approved, q):
|
|
97
|
+
history.append({"role": "user", "content": q})
|
|
98
|
+
pending = chat(cfg, history, approved)
|
|
99
|
+
while pending:
|
|
100
|
+
granted = False
|
|
101
|
+
for cmd in pending:
|
|
102
|
+
a = input(f" {C['y']}{WARN_ICON} approve state-changing command?{C['x']} {C['b']}{cmd}{C['x']} [y/N] ").strip().lower()
|
|
103
|
+
if a == "y":
|
|
104
|
+
granted = True
|
|
105
|
+
if cmd not in approved:
|
|
106
|
+
approved.append(cmd)
|
|
107
|
+
history.append({"role": "user", "content": "Approved — run the approved command(s) now."
|
|
108
|
+
if granted else "Denied — do not run that; stop or suggest a safer alternative."})
|
|
109
|
+
pending = chat(cfg, history, approved)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def status(cfg):
|
|
113
|
+
h = health(cfg)
|
|
114
|
+
ok = "error" not in h
|
|
115
|
+
print(f" endpoint : {cfg['url']}")
|
|
116
|
+
print(f" auth : {'token set' if cfg.get('token') else 'none'}")
|
|
117
|
+
if ok:
|
|
118
|
+
print(f" copilot : {C['g']}up{C['x']} model: {h.get('model','?')} llm: {'ready' if h.get('llm_ready') else 'loading'}")
|
|
119
|
+
else:
|
|
120
|
+
print(f" copilot : {C['r']}unreachable{C['x']} ({h['error']})")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def repl(cfg):
|
|
124
|
+
print(BANNER)
|
|
125
|
+
status(cfg)
|
|
126
|
+
print(f" {C['d']}type a prompt, or /help for commands{C['x']}\n")
|
|
127
|
+
history, approved = [], []
|
|
128
|
+
while True:
|
|
129
|
+
try:
|
|
130
|
+
q = input(f"{C['b']}{C['c']}ant>{C['x']} ").strip()
|
|
131
|
+
except (EOFError, KeyboardInterrupt):
|
|
132
|
+
print(); break
|
|
133
|
+
if not q:
|
|
134
|
+
continue
|
|
135
|
+
if q.startswith("/"):
|
|
136
|
+
parts = q[1:].split(maxsplit=1); cmd = parts[0]; arg = parts[1] if len(parts) > 1 else ""
|
|
137
|
+
if cmd in ("exit", "quit"):
|
|
138
|
+
break
|
|
139
|
+
elif cmd == "help":
|
|
140
|
+
print(" /connect <url> /login <token> /model /status /clear /exit")
|
|
141
|
+
elif cmd == "connect":
|
|
142
|
+
cfg["url"] = arg or cfg["url"]; save(cfg); status(cfg)
|
|
143
|
+
elif cmd == "login":
|
|
144
|
+
cfg["token"] = arg; save(cfg); print(" token saved")
|
|
145
|
+
elif cmd == "model":
|
|
146
|
+
print(f" model: {health(cfg).get('model','?')} (served by the copilot)")
|
|
147
|
+
elif cmd == "status":
|
|
148
|
+
status(cfg)
|
|
149
|
+
elif cmd == "clear":
|
|
150
|
+
history, approved = [], []; print(" conversation cleared")
|
|
151
|
+
else:
|
|
152
|
+
print(f" unknown command: /{cmd}")
|
|
153
|
+
continue
|
|
154
|
+
try:
|
|
155
|
+
ask(cfg, history, approved, q)
|
|
156
|
+
except urllib.error.HTTPError as e:
|
|
157
|
+
print(f" {C['r']}HTTP {e.code}: {e.read().decode('utf-8','ignore')[:160]}{C['x']}")
|
|
158
|
+
except Exception as e: # noqa: BLE001
|
|
159
|
+
print(f" {C['r']}request failed: {e}{C['x']}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
cfg = load()
|
|
164
|
+
a = sys.argv[1:]
|
|
165
|
+
if not a:
|
|
166
|
+
repl(cfg); return
|
|
167
|
+
if a[0] == "connect" and len(a) > 1:
|
|
168
|
+
cfg["url"] = a[1]; save(cfg); print(f"connected to {a[1]}"); status(cfg)
|
|
169
|
+
elif a[0] == "login" and len(a) > 1:
|
|
170
|
+
cfg["token"] = a[1]; save(cfg); print("token saved")
|
|
171
|
+
elif a[0] == "status":
|
|
172
|
+
status(cfg)
|
|
173
|
+
else: # one-shot prompt
|
|
174
|
+
print(BANNER)
|
|
175
|
+
ask(cfg, [], [], " ".join(a))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: antahkarana-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ant — the Antaḥkaraṇa governed-AI operator CLI: prompt your on-box copilot to investigate/operate a machine, read-only by default, with y/N approval for changes and catastrophic commands hard-blocked.
|
|
5
|
+
Author: Deepak Soni
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/antahkarana/
|
|
8
|
+
Keywords: llm,agent,cli,security,mcp,antahkarana,governed,ssh
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# ant — Antaḥkaraṇa governed-AI operator CLI
|
|
13
|
+
|
|
14
|
+
Author — Deepak Soni. A tiny, dependency-free CLI that prompts your **on-box copilot** (the Antaḥkaraṇa model +
|
|
15
|
+
control ring) from any terminal — Claude-Code style. It investigates a machine, reasons, runs **read-only**
|
|
16
|
+
commands immediately, and asks **y/N before any state-changing action**; catastrophic commands are hard-blocked
|
|
17
|
+
server-side.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
```bash
|
|
21
|
+
pip install antahkarana-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Use
|
|
25
|
+
```bash
|
|
26
|
+
ant connect http://YOUR-BOX:8090 # point it at your copilot (saved to ~/.ant/config.json)
|
|
27
|
+
ant # interactive: banner + prompt
|
|
28
|
+
ant "what is listening on this server?" # one-shot
|
|
29
|
+
ant status # connection + model health
|
|
30
|
+
```
|
|
31
|
+
In the REPL, slash commands: `/connect <url>` · `/login <token>` · `/model` · `/status` · `/clear` · `/exit`.
|
|
32
|
+
|
|
33
|
+
## What you can ask
|
|
34
|
+
- `what OS is this and what's listening on its ports?` → runs read-only, reports
|
|
35
|
+
- `create a file /tmp/demo.txt` → proposes it, asks **[y/N]**, runs only if you approve
|
|
36
|
+
- `rm -rf /tmp/x` → **hard-blocked** by the control ring
|
|
37
|
+
- `assess example.com` → passive web-recon → prioritized report
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
`ant` is a thin client over the copilot's `/api/chat` — the model (e.g. Qwen2.5-14B), the governed `run_shell`,
|
|
41
|
+
the web-recon tools, and the control ring all live on the box (or a remote SSH target the copilot is pointed at).
|
|
42
|
+
Nothing runs locally; every decision is governed and audited server-side.
|
|
43
|
+
|
|
44
|
+
Requires a running Antaḥkaraṇa copilot (see `antahkarana-net/box/copilot/`).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ant/__init__.py,sha256=2l5J6_CrEfngomOjmced3POM7OdTSnQQjs0Ehy3PTWE,168
|
|
2
|
+
ant/cli.py,sha256=OlFewO69SkpQlBrW60DwN4pptXKHvTFoSJfhM86YozE,7347
|
|
3
|
+
antahkarana_cli-0.1.0.dist-info/METADATA,sha256=GpjBWAhHljzqXfFd1DNcC7T3mO9bwP3n8zO4TBVKB0U,2100
|
|
4
|
+
antahkarana_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
antahkarana_cli-0.1.0.dist-info/entry_points.txt,sha256=Y6KfPuLvtohyfYX-g7h645Zbr140zodtjBoW2hJx2PU,37
|
|
6
|
+
antahkarana_cli-0.1.0.dist-info/top_level.txt,sha256=8g98uTVZGWavv2MVXE4KhTMrVT3XOxBl6Hh9Wh_wGAg,4
|
|
7
|
+
antahkarana_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ant
|