akashcli 4.7.9__tar.gz → 4.8.1__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.
- {akashcli-4.7.9 → akashcli-4.8.1}/PKG-INFO +1 -1
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/cli.py +305 -20
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.7.9 → akashcli-4.8.1}/pyproject.toml +1 -1
- {akashcli-4.7.9 → akashcli-4.8.1}/README.md +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/__init__.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/_net.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/auth.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/client.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/config.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/crypto.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/env_manager.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/exceptions.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/exec.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/integrity.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/logger.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/models.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/p2p.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/packages.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/relay.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/restore.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/storage.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/terminal.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/updater.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/uploader.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/utils.py +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.7.9 → akashcli-4.8.1}/setup.cfg +0 -0
|
@@ -2854,33 +2854,50 @@ def _resolve_agent_id(target_id: str) -> tuple[str, str, str]:
|
|
|
2854
2854
|
|
|
2855
2855
|
@agent_app.command("scopes")
|
|
2856
2856
|
def agent_scopes():
|
|
2857
|
-
"""View directory of all available roles, scopes, and
|
|
2857
|
+
"""View directory of all available roles, presets, scopes, and tools."""
|
|
2858
2858
|
with _Spin("loading scope directory…"):
|
|
2859
2859
|
pass
|
|
2860
2860
|
_p()
|
|
2861
|
-
print(f" {_C}◈{_RS} {_W}Akash Agent Scopes &
|
|
2861
|
+
print(f" {_C}◈{_RS} {_W}Akash Agent Scopes & Role Presets Directory{_RS}")
|
|
2862
|
+
print(f" {_GR}────────────────────────────────────────────────────────────{_RS}")
|
|
2862
2863
|
_p()
|
|
2863
|
-
print(f" {
|
|
2864
|
-
print(f" {_cy('@viewer')} {_DM}: Read-Only Inspector (Cannot edit code, delete files, or
|
|
2865
|
-
print(f" {_dim('Scopes: workspace:read, exec:read_only, fs:cwd_only')}")
|
|
2866
|
-
print(f" {_cy('@
|
|
2864
|
+
print(f" {_W}ROLE PRESETS:{_RS}")
|
|
2865
|
+
print(f" {_cy('@viewer')} {_DM}: Read-Only Inspector (Cannot edit code, delete files, or run mutating commands){_RS}")
|
|
2866
|
+
print(f" {_dim('Scopes: workspace:read, exec:read_only, fs:cwd_only, limits:max_commands=50')}")
|
|
2867
|
+
print(f" {_cy('@editor')} {_DM}: Safe Editor (Can create and edit files; cannot run shell or delete){_RS}")
|
|
2868
|
+
print(f" {_dim('Scopes: workspace:read, workspace:write, fs:cwd_only, vault:checkpoint')}")
|
|
2869
|
+
print(f" {_cy('@coder')} {_DM}: Safe Developer (Can edit code, build, and test; file deletions blocked){_RS}")
|
|
2867
2870
|
print(f" {_dim('Scopes: workspace:read, workspace:write, exec:build, fs:cwd_only, fs:no_delete, vault:checkpoint')}")
|
|
2868
2871
|
print(f" {_cy('@tester')} {_DM}: QA Runner (Can build and test; cannot edit source files){_RS}")
|
|
2869
2872
|
print(f" {_dim('Scopes: workspace:read, exec:build, fs:cwd_only, limits:timeout=30')}")
|
|
2873
|
+
print(f" {_cy('@admin')} {_DM}: Full DevOps & Package Manager (Can edit, install packages, network calls){_RS}")
|
|
2874
|
+
print(f" {_dim('Scopes: workspace:read, workspace:write, exec:build, exec:package_install, exec:network, vault:checkpoint')}")
|
|
2870
2875
|
print(f" {_cy('@full')} {_DM}: Unrestricted Master Access (*){_RS}")
|
|
2871
2876
|
_p()
|
|
2872
|
-
print(f" {
|
|
2873
|
-
print(f" {_ye('
|
|
2874
|
-
print(f" {_ye('
|
|
2875
|
-
print(f" {_ye('exec:
|
|
2877
|
+
print(f" {_W}GRANULAR PERMISSION SCOPES:{_RS}")
|
|
2878
|
+
print(f" {_ye('workspace:read')} {_DM}Enables inspecting files via akash_read_file and akash_list_dir{_RS}")
|
|
2879
|
+
print(f" {_ye('workspace:write')} {_DM}Enables writing files via akash_write_file{_RS}")
|
|
2880
|
+
print(f" {_ye('exec:read_only')} {_DM}Blocks mutating terminal commands (touch, mkdir, sed, git commit){_RS}")
|
|
2881
|
+
print(f" {_ye('fs:no_delete')} {_DM}Blocks destructive deletions (rm, unlink, git rm, rmdir){_RS}")
|
|
2882
|
+
print(f" {_ye('exec:package_install')} {_DM}Enables package managers (pip install, npm install, apt){_RS}")
|
|
2876
2883
|
print(f" {_ye('exec:network')} {_DM}Enables outbound network pipes (curl, wget){_RS}")
|
|
2877
|
-
print(f" {_ye('fs:cwd_only')} {_DM}Restricts operations strictly to current
|
|
2878
|
-
print(f" {_ye('vault:checkpoint')} {_DM}Enables
|
|
2884
|
+
print(f" {_ye('fs:cwd_only')} {_DM}Restricts operations strictly to current working directory{_RS}")
|
|
2885
|
+
print(f" {_ye('vault:checkpoint')} {_DM}Enables automatic undo rollback snapshots before AI edits{_RS}")
|
|
2879
2886
|
_p()
|
|
2880
|
-
print(f" {
|
|
2881
|
-
print(f"
|
|
2887
|
+
print(f" {_W}AVAILABLE MCP TOOLS:{_RS}")
|
|
2888
|
+
print(f" • {_C}akash_exec_command{_RS} {_DM}: Safe terminal runner with secret redaction and process timeouts{_RS}")
|
|
2889
|
+
print(f" • {_C}akash_read_file{_RS} {_DM}: Read files with line-range pagination directly into AI context{_RS}")
|
|
2890
|
+
print(f" • {_C}akash_write_file{_RS} {_DM}: Write or update files safely on device{_RS}")
|
|
2891
|
+
print(f" • {_C}akash_list_dir{_RS} {_DM}: Browse directory trees with sizes, types, and depth control{_RS}")
|
|
2892
|
+
print(f" • {_C}akash_checkpoint_create{_RS} {_DM}: Create an encrypted rollback snapshot before major changes{_RS}")
|
|
2893
|
+
print(f" • {_C}akash_undo{_RS} {_DM}: Revert workspace to pre-action state{_RS}")
|
|
2894
|
+
print(f" • {_C}akash_scope_details{_RS} {_DM}: Introspect active permissions and security rules{_RS}")
|
|
2895
|
+
_p()
|
|
2896
|
+
print(f" {_W}HOW TO ASSIGN OR UPDATE:{_RS}")
|
|
2897
|
+
print(f" Create with role : {_C}akash agent create -n 'Dev' -r @coder{_RS}")
|
|
2898
|
+
print(f" Switch role : {_C}akash agent update --id <ID> --role @admin{_RS}")
|
|
2882
2899
|
print(f" Add a scope : {_C}akash agent update --id <ID> --scopes +exec:package_install{_RS}")
|
|
2883
|
-
print(f"
|
|
2900
|
+
print(f" Inspect agent : {_C}akash agent info <ID>{_RS}")
|
|
2884
2901
|
_p()
|
|
2885
2902
|
|
|
2886
2903
|
@agent_app.command("info")
|
|
@@ -3220,18 +3237,39 @@ _LAST_UNDO_DIR = Path.home() / ".akash" / "undo_vault"
|
|
|
3220
3237
|
def _save_undo_checkpoint():
|
|
3221
3238
|
try:
|
|
3222
3239
|
import tarfile
|
|
3240
|
+
cwd = Path.cwd().resolve()
|
|
3241
|
+
home = Path.home().resolve()
|
|
3242
|
+
# Never archive the full home directory or system root!
|
|
3243
|
+
if cwd == home or cwd == Path("/") or str(cwd).startswith(("/proc", "/sys", "/dev")):
|
|
3244
|
+
return
|
|
3223
3245
|
_LAST_UNDO_DIR.mkdir(parents=True, exist_ok=True)
|
|
3224
3246
|
undo_tar = _LAST_UNDO_DIR / "workspace_last.tar.gz"
|
|
3247
|
+
excluded = {".git", "venv", "__pycache__", ".akash", "storage", ".termux", "node_modules", ".cache", "build", "dist"}
|
|
3225
3248
|
with tarfile.open(undo_tar, "w:gz") as tar:
|
|
3249
|
+
count = 0
|
|
3226
3250
|
for item in os.listdir("."):
|
|
3227
|
-
if item in (".
|
|
3251
|
+
if item in excluded or item.startswith(".") or os.path.islink(item):
|
|
3252
|
+
continue
|
|
3253
|
+
p = Path(item)
|
|
3254
|
+
if not p.exists() or p.is_symlink():
|
|
3255
|
+
continue
|
|
3256
|
+
try:
|
|
3257
|
+
if p.is_file() and p.stat().st_size > 5 * 1024 * 1024:
|
|
3258
|
+
continue
|
|
3259
|
+
except Exception:
|
|
3228
3260
|
continue
|
|
3229
|
-
if
|
|
3230
|
-
|
|
3261
|
+
tar.add(item, recursive=True, filter=lambda ti: None if (ti.name in excluded or ti.issym()) else ti)
|
|
3262
|
+
count += 1
|
|
3263
|
+
if count > 100:
|
|
3264
|
+
break
|
|
3231
3265
|
except Exception:
|
|
3232
3266
|
pass
|
|
3233
3267
|
|
|
3234
3268
|
def _restore_undo_checkpoint() -> tuple[bool, str]:
|
|
3269
|
+
cwd = Path.cwd().resolve()
|
|
3270
|
+
home = Path.home().resolve()
|
|
3271
|
+
if cwd == home or cwd == Path("/"):
|
|
3272
|
+
return False, "Undo snapshot is disabled when running in root or home directory for safety."
|
|
3235
3273
|
undo_tar = _LAST_UNDO_DIR / "workspace_last.tar.gz"
|
|
3236
3274
|
if not undo_tar.exists():
|
|
3237
3275
|
return False, "No undo checkpoint available for this session."
|
|
@@ -3342,6 +3380,185 @@ def _parse_temp_duration(val: str) -> int:
|
|
|
3342
3380
|
elif unit in ('d', 'day', 'days'): return num * 1440
|
|
3343
3381
|
return num
|
|
3344
3382
|
|
|
3383
|
+
def _handle_tool_read_file(args: dict, scopes: str) -> tuple[str, bool]:
|
|
3384
|
+
path_str = args.get("path", "").strip()
|
|
3385
|
+
if not path_str:
|
|
3386
|
+
return "Error: 'path' argument is required.", True
|
|
3387
|
+
|
|
3388
|
+
target_path = Path(path_str).expanduser()
|
|
3389
|
+
if not target_path.is_absolute():
|
|
3390
|
+
target_path = (Path.cwd() / target_path).resolve()
|
|
3391
|
+
else:
|
|
3392
|
+
target_path = target_path.resolve()
|
|
3393
|
+
|
|
3394
|
+
scopes_list = [s.strip().lower() for s in scopes.split(",") if s.strip()]
|
|
3395
|
+
if "*" not in scopes_list and "@full" not in scopes_list:
|
|
3396
|
+
if "fs:cwd_only" in scopes_list:
|
|
3397
|
+
cwd = Path.cwd().resolve()
|
|
3398
|
+
try:
|
|
3399
|
+
target_path.relative_to(cwd)
|
|
3400
|
+
except ValueError:
|
|
3401
|
+
return f"⛔ Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
|
|
3402
|
+
|
|
3403
|
+
if not target_path.exists():
|
|
3404
|
+
return f"Error: File '{path_str}' not found.", True
|
|
3405
|
+
if target_path.is_dir():
|
|
3406
|
+
return f"Error: '{path_str}' is a directory, not a file. Use akash_list_dir to view directory contents.", True
|
|
3407
|
+
|
|
3408
|
+
start_line = int(args.get("start_line", 1) or 1)
|
|
3409
|
+
end_line = args.get("end_line")
|
|
3410
|
+
end_line = int(end_line) if end_line is not None else None
|
|
3411
|
+
max_bytes = int(args.get("max_bytes", 65536) or 65536)
|
|
3412
|
+
|
|
3413
|
+
try:
|
|
3414
|
+
with open(target_path, "rb") as f:
|
|
3415
|
+
chunk = f.read(1024)
|
|
3416
|
+
if b"\x00" in chunk:
|
|
3417
|
+
return f"Binary file detected ({target_path.stat().st_size} bytes). Raw binary cannot be rendered in text context.", True
|
|
3418
|
+
|
|
3419
|
+
with open(target_path, "r", encoding="utf-8", errors="replace") as f:
|
|
3420
|
+
lines = f.readlines()
|
|
3421
|
+
|
|
3422
|
+
total_lines = len(lines)
|
|
3423
|
+
s_idx = max(0, start_line - 1)
|
|
3424
|
+
e_idx = end_line if end_line is not None else total_lines
|
|
3425
|
+
selected_lines = lines[s_idx:e_idx]
|
|
3426
|
+
|
|
3427
|
+
content = "".join(selected_lines)
|
|
3428
|
+
if len(content.encode("utf-8")) > max_bytes:
|
|
3429
|
+
content = content[:max_bytes] + f"\n... [Output truncated at {max_bytes} bytes]"
|
|
3430
|
+
|
|
3431
|
+
rel_name = target_path.name
|
|
3432
|
+
header = f"=== File: {rel_name} (Lines {s_idx + 1}-{min(e_idx, total_lines)} of {total_lines}) ===\n"
|
|
3433
|
+
return header + content, False
|
|
3434
|
+
except Exception as e:
|
|
3435
|
+
return f"Error reading file '{path_str}': {e}", True
|
|
3436
|
+
|
|
3437
|
+
def _handle_tool_write_file(args: dict, scopes: str) -> tuple[str, bool]:
|
|
3438
|
+
path_str = args.get("path", "").strip()
|
|
3439
|
+
content = args.get("content", "")
|
|
3440
|
+
overwrite = args.get("overwrite", True)
|
|
3441
|
+
|
|
3442
|
+
if not path_str:
|
|
3443
|
+
return "Error: 'path' argument is required.", True
|
|
3444
|
+
|
|
3445
|
+
target_path = Path(path_str).expanduser()
|
|
3446
|
+
if not target_path.is_absolute():
|
|
3447
|
+
target_path = (Path.cwd() / target_path).resolve()
|
|
3448
|
+
else:
|
|
3449
|
+
target_path = target_path.resolve()
|
|
3450
|
+
|
|
3451
|
+
scopes_list = [s.strip().lower() for s in scopes.split(",") if s.strip()]
|
|
3452
|
+
if "*" not in scopes_list and "@full" not in scopes_list:
|
|
3453
|
+
if "exec:read_only" in scopes_list or "@viewer" in scopes_list:
|
|
3454
|
+
return "⛔ Permission denied: Agent is in read-only mode ('exec:read_only'). File writing is blocked.", True
|
|
3455
|
+
if "fs:cwd_only" in scopes_list:
|
|
3456
|
+
cwd = Path.cwd().resolve()
|
|
3457
|
+
try:
|
|
3458
|
+
target_path.relative_to(cwd)
|
|
3459
|
+
except ValueError:
|
|
3460
|
+
return f"⛔ Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
|
|
3461
|
+
|
|
3462
|
+
if target_path.exists() and not overwrite:
|
|
3463
|
+
return f"Error: File '{path_str}' already exists and overwrite is set to False.", True
|
|
3464
|
+
|
|
3465
|
+
try:
|
|
3466
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
3467
|
+
target_path.write_text(content, encoding="utf-8")
|
|
3468
|
+
byte_count = len(content.encode("utf-8"))
|
|
3469
|
+
line_count = len(content.splitlines())
|
|
3470
|
+
return f"✓ File '{path_str}' successfully written ({byte_count} bytes, {line_count} lines).", False
|
|
3471
|
+
except Exception as e:
|
|
3472
|
+
return f"Error writing file '{path_str}': {e}", True
|
|
3473
|
+
|
|
3474
|
+
def _handle_tool_list_dir(args: dict, scopes: str) -> tuple[str, bool]:
|
|
3475
|
+
path_str = args.get("path", ".").strip() or "."
|
|
3476
|
+
depth = min(max(int(args.get("depth", 1) or 1), 1), 3)
|
|
3477
|
+
|
|
3478
|
+
target_path = Path(path_str).expanduser()
|
|
3479
|
+
if not target_path.is_absolute():
|
|
3480
|
+
target_path = (Path.cwd() / target_path).resolve()
|
|
3481
|
+
else:
|
|
3482
|
+
target_path = target_path.resolve()
|
|
3483
|
+
|
|
3484
|
+
scopes_list = [s.strip().lower() for s in scopes.split(",") if s.strip()]
|
|
3485
|
+
if "*" not in scopes_list and "@full" not in scopes_list:
|
|
3486
|
+
if "fs:cwd_only" in scopes_list:
|
|
3487
|
+
cwd = Path.cwd().resolve()
|
|
3488
|
+
try:
|
|
3489
|
+
target_path.relative_to(cwd)
|
|
3490
|
+
except ValueError:
|
|
3491
|
+
return f"⛔ Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
|
|
3492
|
+
|
|
3493
|
+
if not target_path.exists():
|
|
3494
|
+
return f"Error: Path '{path_str}' not found.", True
|
|
3495
|
+
if not target_path.is_dir():
|
|
3496
|
+
return f"Error: '{path_str}' is not a directory. Use akash_read_file for files.", True
|
|
3497
|
+
|
|
3498
|
+
try:
|
|
3499
|
+
output_lines = [f"Directory listing of: {target_path}"]
|
|
3500
|
+
|
|
3501
|
+
def _walk(curr_p: Path, cur_depth: int, prefix: str = ""):
|
|
3502
|
+
if cur_depth > depth:
|
|
3503
|
+
return
|
|
3504
|
+
try:
|
|
3505
|
+
entries = sorted(list(curr_p.iterdir()), key=lambda p: (not p.is_dir(), p.name.lower()))
|
|
3506
|
+
except PermissionError:
|
|
3507
|
+
output_lines.append(f"{prefix}└── [Permission Denied]")
|
|
3508
|
+
return
|
|
3509
|
+
|
|
3510
|
+
excluded = {".git", "__pycache__", "venv", ".akash", "node_modules", "storage", ".cache"}
|
|
3511
|
+
filtered = [e for e in entries if e.name not in excluded and not (e.is_symlink() and e.name == "storage")]
|
|
3512
|
+
|
|
3513
|
+
for idx, entry in enumerate(filtered[:100]):
|
|
3514
|
+
is_last = (idx == len(filtered) - 1)
|
|
3515
|
+
ptr = "└── " if is_last else "├── "
|
|
3516
|
+
sub_pfx = " " if is_last else "│ "
|
|
3517
|
+
if entry.is_dir():
|
|
3518
|
+
output_lines.append(f"{prefix}{ptr}📁 {entry.name}/")
|
|
3519
|
+
if cur_depth < depth and not entry.is_symlink():
|
|
3520
|
+
_walk(entry, cur_depth + 1, prefix + sub_pfx)
|
|
3521
|
+
elif entry.is_symlink():
|
|
3522
|
+
output_lines.append(f"{prefix}{ptr}🔗 {entry.name} -> {os.readlink(entry)}")
|
|
3523
|
+
else:
|
|
3524
|
+
try:
|
|
3525
|
+
sz = entry.stat().st_size
|
|
3526
|
+
sz_str = f"{sz / 1024:.1f}KB" if sz >= 1024 else f"{sz}B"
|
|
3527
|
+
except Exception:
|
|
3528
|
+
sz_str = "?"
|
|
3529
|
+
output_lines.append(f"{prefix}{ptr}📄 {entry.name} ({sz_str})")
|
|
3530
|
+
if len(filtered) > 100:
|
|
3531
|
+
output_lines.append(f"{prefix}... [{len(filtered) - 100} more items omitted]")
|
|
3532
|
+
|
|
3533
|
+
_walk(target_path, 1, "")
|
|
3534
|
+
return "\n".join(output_lines), False
|
|
3535
|
+
except Exception as e:
|
|
3536
|
+
return f"Error listing directory '{path_str}': {e}", True
|
|
3537
|
+
|
|
3538
|
+
def _handle_tool_scope_details(scopes: str, profile_name: str, profile_id: str) -> tuple[str, bool]:
|
|
3539
|
+
scopes_list = [s.strip() for s in scopes.split(",") if s.strip()]
|
|
3540
|
+
lines = [
|
|
3541
|
+
f"=== Akash Agent Security Scope Profile ===",
|
|
3542
|
+
f"Profile: {profile_name} (ID: {profile_id})",
|
|
3543
|
+
f"Active Scopes ({len(scopes_list)}): {', '.join(scopes_list)}",
|
|
3544
|
+
"",
|
|
3545
|
+
"Enforced Policies:"
|
|
3546
|
+
]
|
|
3547
|
+
if "*" in scopes_list or "@full" in scopes_list:
|
|
3548
|
+
lines.append(" • Full Access: No restrictions applied.")
|
|
3549
|
+
else:
|
|
3550
|
+
if "fs:cwd_only" in scopes_list:
|
|
3551
|
+
lines.append(" • Workspace Jailing (fs:cwd_only): Access restricted strictly to current project root; path traversal ('..') is blocked.")
|
|
3552
|
+
if "fs:no_delete" in scopes_list:
|
|
3553
|
+
lines.append(" • Delete Prevention (fs:no_delete): Destructive deletions (rm, rmdir, unlink) are blocked.")
|
|
3554
|
+
if "exec:read_only" in scopes_list or "@viewer" in scopes_list:
|
|
3555
|
+
lines.append(" • Read-Only Mode (exec:read_only): File mutations, file writes, and state changes are blocked.")
|
|
3556
|
+
if "exec:package_install" not in scopes_list:
|
|
3557
|
+
lines.append(" • Package Lock: pip/npm/apt installations are blocked (requires 'exec:package_install').")
|
|
3558
|
+
if "vault:checkpoint" in scopes_list:
|
|
3559
|
+
lines.append(" • Safety Checkpoints (vault:checkpoint): AI modifications create rollback snapshots.")
|
|
3560
|
+
return "\n".join(lines), False
|
|
3561
|
+
|
|
3345
3562
|
@agent_app.command("serve")
|
|
3346
3563
|
def mcp_serve(
|
|
3347
3564
|
url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
|
|
@@ -3390,8 +3607,12 @@ def mcp_serve(
|
|
|
3390
3607
|
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
3391
3608
|
print(" " + _DM + "Active Tools:" + _RS)
|
|
3392
3609
|
print(" • akash_exec_command " + _dim("(Safe terminal runner)"))
|
|
3610
|
+
print(" • akash_read_file " + _dim("(Inspect & import files into AI)"))
|
|
3611
|
+
print(" • akash_write_file " + _dim("(Create or edit workspace files)"))
|
|
3612
|
+
print(" • akash_list_dir " + _dim("(Explore directory trees)"))
|
|
3393
3613
|
print(" • akash_checkpoint_create " + _dim("(Rollback snapshot)"))
|
|
3394
3614
|
print(" • akash_undo " + _dim("(Revert last AI edit)"))
|
|
3615
|
+
print(" • akash_scope_details " + _dim("(Inspect active session permissions)"))
|
|
3395
3616
|
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
3396
3617
|
print(" " + _DM + "[LIVE ACTIVITY]" + _RS)
|
|
3397
3618
|
|
|
@@ -3466,7 +3687,10 @@ def mcp_serve(
|
|
|
3466
3687
|
ws.send(json.dumps({
|
|
3467
3688
|
"type": "tool_response",
|
|
3468
3689
|
"id": call_id,
|
|
3469
|
-
"result": {
|
|
3690
|
+
"result": {
|
|
3691
|
+
"content": [{"type": "text", "text": f"⛔ Security Policy Block: {sec_reason}\nCommand: {cmd}"}],
|
|
3692
|
+
"isError": True
|
|
3693
|
+
}
|
|
3470
3694
|
}))
|
|
3471
3695
|
else:
|
|
3472
3696
|
if any(k in cmd for k in ["touch", "mkdir", "mv", "cp", "sed", ">", ">>", "python", "npm", "pip"]):
|
|
@@ -3526,10 +3750,17 @@ def mcp_serve(
|
|
|
3526
3750
|
ec_icon = f"{_G}✓{_RS}" if returncode == 0 else f"{_R}✗{_RS}"
|
|
3527
3751
|
print(f"{ts_pfx} {ec_icon} {ec_color}exec:{_RS} {disp_cmd} {ec_color}[{returncode}]{_RS} {_dim(f'{elapsed_ms:.1f}ms')}")
|
|
3528
3752
|
|
|
3753
|
+
if returncode != 0:
|
|
3754
|
+
res_content = bounded_out or f"Command exited with status code {returncode} (no output)"
|
|
3755
|
+
if not res_content.startswith("⛔"):
|
|
3756
|
+
res_content = f"⛔ Command execution failed (exit code {returncode}):\n{res_content}"
|
|
3757
|
+
else:
|
|
3758
|
+
res_content = bounded_out or "(command completed successfully with no output)"
|
|
3759
|
+
|
|
3529
3760
|
ws.send(json.dumps({
|
|
3530
3761
|
"type": "tool_response",
|
|
3531
3762
|
"id": call_id,
|
|
3532
|
-
"result": {"content": [{"type": "text", "text":
|
|
3763
|
+
"result": {"content": [{"type": "text", "text": res_content}], "isError": returncode != 0}
|
|
3533
3764
|
}))
|
|
3534
3765
|
elif t_name == "akash_undo":
|
|
3535
3766
|
with _Spin("undo: reverting workspace to checkpoint", min_seconds=0.25, prefix=ts_pfx):
|
|
@@ -3554,6 +3785,60 @@ def mcp_serve(
|
|
|
3554
3785
|
"id": call_id,
|
|
3555
3786
|
"result": {"content": [{"type": "text", "text": f"Checkpoint '{label}' created."}]}
|
|
3556
3787
|
}))
|
|
3788
|
+
|
|
3789
|
+
elif t_name == "akash_read_file":
|
|
3790
|
+
f_path = args.get("path", "")
|
|
3791
|
+
disp_f = f_path if len(f_path) <= 32 else f_path[:29] + "…"
|
|
3792
|
+
with _Spin(f"read: {disp_f}", min_seconds=0.15, prefix=ts_pfx):
|
|
3793
|
+
res_text, is_err = _handle_tool_read_file(args, cached.get("scopes", "*"))
|
|
3794
|
+
elapsed_ms = (time.monotonic() - t0) * 1000
|
|
3795
|
+
icon = f"{_G}✓{_RS}" if not is_err else f"{_R}✗{_RS}"
|
|
3796
|
+
print(f"{ts_pfx} {icon} {(_G if not is_err else _R)}read:{_RS} {disp_f} {_dim(f'{elapsed_ms:.1f}ms')}")
|
|
3797
|
+
ws.send(json.dumps({
|
|
3798
|
+
"type": "tool_response",
|
|
3799
|
+
"id": call_id,
|
|
3800
|
+
"result": {"content": [{"type": "text", "text": res_text}], "isError": is_err}
|
|
3801
|
+
}))
|
|
3802
|
+
|
|
3803
|
+
elif t_name == "akash_write_file":
|
|
3804
|
+
f_path = args.get("path", "")
|
|
3805
|
+
disp_f = f_path if len(f_path) <= 32 else f_path[:29] + "…"
|
|
3806
|
+
_save_undo_checkpoint()
|
|
3807
|
+
with _Spin(f"write: {disp_f}", min_seconds=0.15, prefix=ts_pfx):
|
|
3808
|
+
res_text, is_err = _handle_tool_write_file(args, cached.get("scopes", "*"))
|
|
3809
|
+
elapsed_ms = (time.monotonic() - t0) * 1000
|
|
3810
|
+
icon = f"{_G}✓{_RS}" if not is_err else f"{_R}✗{_RS}"
|
|
3811
|
+
print(f"{ts_pfx} {icon} {(_G if not is_err else _R)}write:{_RS} {disp_f} {_dim(f'{elapsed_ms:.1f}ms')}")
|
|
3812
|
+
ws.send(json.dumps({
|
|
3813
|
+
"type": "tool_response",
|
|
3814
|
+
"id": call_id,
|
|
3815
|
+
"result": {"content": [{"type": "text", "text": res_text}], "isError": is_err}
|
|
3816
|
+
}))
|
|
3817
|
+
|
|
3818
|
+
elif t_name == "akash_list_dir":
|
|
3819
|
+
d_path = args.get("path", ".")
|
|
3820
|
+
disp_d = d_path if len(d_path) <= 32 else d_path[:29] + "…"
|
|
3821
|
+
with _Spin(f"list: {disp_d}", min_seconds=0.15, prefix=ts_pfx):
|
|
3822
|
+
res_text, is_err = _handle_tool_list_dir(args, cached.get("scopes", "*"))
|
|
3823
|
+
elapsed_ms = (time.monotonic() - t0) * 1000
|
|
3824
|
+
icon = f"{_G}✓{_RS}" if not is_err else f"{_R}✗{_RS}"
|
|
3825
|
+
print(f"{ts_pfx} {icon} {(_G if not is_err else _R)}list:{_RS} {disp_d} {_dim(f'{elapsed_ms:.1f}ms')}")
|
|
3826
|
+
ws.send(json.dumps({
|
|
3827
|
+
"type": "tool_response",
|
|
3828
|
+
"id": call_id,
|
|
3829
|
+
"result": {"content": [{"type": "text", "text": res_text}], "isError": is_err}
|
|
3830
|
+
}))
|
|
3831
|
+
|
|
3832
|
+
elif t_name == "akash_scope_details":
|
|
3833
|
+
with _Spin("inspecting scopes…", min_seconds=0.15, prefix=ts_pfx):
|
|
3834
|
+
res_text, is_err = _handle_tool_scope_details(cached.get("scopes", "*"), pname, profile_id)
|
|
3835
|
+
elapsed_ms = (time.monotonic() - t0) * 1000
|
|
3836
|
+
print(f"{ts_pfx} {_C}🔒 scopes:{_RS} {_dim(pname)} {_dim(f'{elapsed_ms:.1f}ms')}")
|
|
3837
|
+
ws.send(json.dumps({
|
|
3838
|
+
"type": "tool_response",
|
|
3839
|
+
"id": call_id,
|
|
3840
|
+
"result": {"content": [{"type": "text", "text": res_text}], "isError": is_err}
|
|
3841
|
+
}))
|
|
3557
3842
|
except KeyboardInterrupt:
|
|
3558
3843
|
print("\n " + _DM + "Worker stopped cleanly." + _RS + "\n")
|
|
3559
3844
|
break
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|