sernixa-cli 0.2.0__tar.gz → 0.3.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.
Files changed (26) hide show
  1. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/PKG-INFO +10 -4
  2. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/README.md +7 -2
  3. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/pyproject.toml +3 -2
  4. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/__init__.py +1 -1
  5. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/discovery.py +3 -3
  6. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/formatting.py +21 -38
  7. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/local_agent_targets.py +41 -6
  8. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/main.py +192 -112
  9. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli.egg-info/PKG-INFO +10 -4
  10. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli.egg-info/requires.txt +1 -1
  11. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/tests/test_cli.py +311 -6
  12. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/tests/test_discovery.py +6 -5
  13. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/tests/test_local_agent_targets.py +34 -8
  14. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/__main__.py +0 -0
  15. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/adapters.py +0 -0
  16. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/config.py +0 -0
  17. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/hooks.py +0 -0
  18. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/mcp_server.py +0 -0
  19. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli/native_launcher.py +0 -0
  20. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli.egg-info/SOURCES.txt +0 -0
  21. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli.egg-info/dependency_links.txt +0 -0
  22. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli.egg-info/entry_points.txt +0 -0
  23. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/sernixa_cli.egg-info/top_level.txt +0 -0
  24. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/setup.cfg +0 -0
  25. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/tests/test_mcp_server.py +0 -0
  26. {sernixa_cli-0.2.0 → sernixa_cli-0.3.0}/tests/test_native_launcher.py +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sernixa-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Official Sernixa CLI for policy-governed local AI agent actions
5
5
  Author: Sernixa Team
6
6
  License-Expression: MIT
7
+ Project-URL: Changelog, https://github.com/abhishekdhull63/Sernixa.ai-Web/tree/main/apps/cli/CHANGELOG.md
7
8
  Project-URL: Homepage, https://sernixa.com
8
9
  Project-URL: Repository, https://github.com/abhishekdhull63/Sernixa.ai-Web
9
10
  Project-URL: Documentation, https://github.com/abhishekdhull63/Sernixa.ai-Web/tree/main/docs/cli.md
@@ -20,7 +21,7 @@ Requires-Python: >=3.10
20
21
  Description-Content-Type: text/markdown
21
22
  Requires-Dist: platformdirs>=4.0.0
22
23
  Requires-Dist: sernixa>=0.4.0
23
- Requires-Dist: sernixa-agent>=1.6.0
24
+ Requires-Dist: sernixa-agent>=1.6.1
24
25
  Requires-Dist: typer>=0.12.0
25
26
 
26
27
  # Sernixa CLI
@@ -33,7 +34,7 @@ The official terminal and local-agent integration for Sernixa governance. The CL
33
34
  pip install --upgrade sernixa-cli
34
35
  sernixa auth login
35
36
  sernixa setup
36
- sernixa doctor
37
+ sernixa doctor --strict
37
38
  ```
38
39
 
39
40
  `sernixa setup` configures Claude Code, Claude Desktop, Codex, and Cursor, preserves unrelated entries, backs up changed files, registers the bounded `sernixa://` dashboard launcher, and reports host restart or hook-trust steps. Preview without changing files:
@@ -85,11 +86,13 @@ sernixa hook install codex
85
86
  sernixa hook install cursor
86
87
 
87
88
  sernixa hook repair codex
88
- sernixa hook status --json
89
+ sernixa hook status
89
90
  ```
90
91
 
91
92
  Claude Code and Codex hooks call `sernixa hook run <provider>` before tool execution. `allow` continues through normal host permissions and `deny` blocks. Claude Code maps `review` to its supported human prompt. Current Codex `PreToolUse` does not support an `ask` result, so Sernixa maps `review` to a fail-closed deny with an explicit human-review reason instead of returning a hook shape that Codex would ignore. An API/configuration failure exits with code `2`, which both hosts treat as blocking; there is no local allow fallback.
92
93
 
94
+ Hook handlers allow 35 seconds for the live policy request; the CLI request budget is 30 seconds. This prevents slow semantic evaluations from being killed by a shorter host-hook timeout.
95
+
93
96
  Claude Desktop and Cursor run `sernixa mcp serve`. The stdio MCP server exposes three non-mutating tools:
94
97
 
95
98
  - `sernixa_policy_check`: evaluate one proposed action.
@@ -117,6 +120,8 @@ Exit codes are stable:
117
120
  - `1`: deny, review required, invalid evidence, or non-success terminal result
118
121
  - `2`: configuration, parsing, auth, network, or API failure
119
122
 
123
+ Human output is intentionally concise. Use `--json` only when a script needs structured details; JSON decisions omit the duplicated raw backend payload.
124
+
120
125
  `validate-plan` calls the canonical plan endpoint. `exec` and `codex-exec` run the child command only after every planned step is allowed and bound to its proof.
121
126
 
122
127
  ## Audit, approvals, and Flight Recorder
@@ -139,6 +144,7 @@ These commands call the authenticated organization endpoints and inherit backend
139
144
  ## Stable configuration overrides
140
145
 
141
146
  - `SERNIXA_CONFIG_FILE`
147
+ - `SERNIXA_CLI_TIMEOUT_SECONDS`
142
148
  - `SERNIXA_PLAN_SESSION_FILE`
143
149
  - `SERNIXA_CLAUDE_CONFIG_FILE`
144
150
  - `SERNIXA_CLAUDE_DESKTOP_CONFIG_FILE`
@@ -8,7 +8,7 @@ The official terminal and local-agent integration for Sernixa governance. The CL
8
8
  pip install --upgrade sernixa-cli
9
9
  sernixa auth login
10
10
  sernixa setup
11
- sernixa doctor
11
+ sernixa doctor --strict
12
12
  ```
13
13
 
14
14
  `sernixa setup` configures Claude Code, Claude Desktop, Codex, and Cursor, preserves unrelated entries, backs up changed files, registers the bounded `sernixa://` dashboard launcher, and reports host restart or hook-trust steps. Preview without changing files:
@@ -60,11 +60,13 @@ sernixa hook install codex
60
60
  sernixa hook install cursor
61
61
 
62
62
  sernixa hook repair codex
63
- sernixa hook status --json
63
+ sernixa hook status
64
64
  ```
65
65
 
66
66
  Claude Code and Codex hooks call `sernixa hook run <provider>` before tool execution. `allow` continues through normal host permissions and `deny` blocks. Claude Code maps `review` to its supported human prompt. Current Codex `PreToolUse` does not support an `ask` result, so Sernixa maps `review` to a fail-closed deny with an explicit human-review reason instead of returning a hook shape that Codex would ignore. An API/configuration failure exits with code `2`, which both hosts treat as blocking; there is no local allow fallback.
67
67
 
68
+ Hook handlers allow 35 seconds for the live policy request; the CLI request budget is 30 seconds. This prevents slow semantic evaluations from being killed by a shorter host-hook timeout.
69
+
68
70
  Claude Desktop and Cursor run `sernixa mcp serve`. The stdio MCP server exposes three non-mutating tools:
69
71
 
70
72
  - `sernixa_policy_check`: evaluate one proposed action.
@@ -92,6 +94,8 @@ Exit codes are stable:
92
94
  - `1`: deny, review required, invalid evidence, or non-success terminal result
93
95
  - `2`: configuration, parsing, auth, network, or API failure
94
96
 
97
+ Human output is intentionally concise. Use `--json` only when a script needs structured details; JSON decisions omit the duplicated raw backend payload.
98
+
95
99
  `validate-plan` calls the canonical plan endpoint. `exec` and `codex-exec` run the child command only after every planned step is allowed and bound to its proof.
96
100
 
97
101
  ## Audit, approvals, and Flight Recorder
@@ -114,6 +118,7 @@ These commands call the authenticated organization endpoints and inherit backend
114
118
  ## Stable configuration overrides
115
119
 
116
120
  - `SERNIXA_CONFIG_FILE`
121
+ - `SERNIXA_CLI_TIMEOUT_SECONDS`
117
122
  - `SERNIXA_PLAN_SESSION_FILE`
118
123
  - `SERNIXA_CLAUDE_CONFIG_FILE`
119
124
  - `SERNIXA_CLAUDE_DESKTOP_CONFIG_FILE`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sernixa-cli"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Official Sernixa CLI for policy-governed local AI agent actions"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -20,11 +20,12 @@ classifiers = [
20
20
  dependencies = [
21
21
  "platformdirs>=4.0.0",
22
22
  "sernixa>=0.4.0",
23
- "sernixa-agent>=1.6.0",
23
+ "sernixa-agent>=1.6.1",
24
24
  "typer>=0.12.0",
25
25
  ]
26
26
 
27
27
  [project.urls]
28
+ Changelog = "https://github.com/abhishekdhull63/Sernixa.ai-Web/tree/main/apps/cli/CHANGELOG.md"
28
29
  Homepage = "https://sernixa.com"
29
30
  Repository = "https://github.com/abhishekdhull63/Sernixa.ai-Web"
30
31
  Documentation = "https://github.com/abhishekdhull63/Sernixa.ai-Web/tree/main/docs/cli.md"
@@ -1,3 +1,3 @@
1
1
  """Official Sernixa command-line client."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -44,7 +44,7 @@ def _agent_executable() -> str:
44
44
  executable = shutil.which("sernixa-agent")
45
45
  if not executable:
46
46
  raise DiscoveryError(
47
- "sernixa-agent is not installed. Upgrade sernixa-cli or install sernixa-agent>=1.6.0."
47
+ "sernixa-agent is not installed. Upgrade sernixa-cli or install sernixa-agent>=1.6.1."
48
48
  )
49
49
  return executable
50
50
 
@@ -155,14 +155,14 @@ def run_discovery(
155
155
  agent_run = _run_agent(
156
156
  [
157
157
  executable,
158
- "daemon",
158
+ "tasks-once",
159
159
  "--config",
160
160
  str(config_path),
161
161
  "--api-base-url",
162
162
  config.base_url,
163
163
  "--token-env",
164
164
  "SERNIXA_DISCOVERY_TOKEN",
165
- "--once",
165
+ "--json",
166
166
  ],
167
167
  extra_env=agent_env,
168
168
  )
@@ -20,7 +20,7 @@ def exit_code_for_decision(decision: dict[str, Any]) -> int:
20
20
  def decision_summary(
21
21
  backend_decision: dict[str, Any],
22
22
  *,
23
- include_raw: bool = True,
23
+ include_raw: bool = False,
24
24
  request: dict[str, Any] | None = None,
25
25
  ) -> dict[str, Any]:
26
26
  allowed = is_allowed(backend_decision)
@@ -60,51 +60,34 @@ def render_decision(
60
60
  )
61
61
  return
62
62
  if allowed:
63
- typer.secho("Allowed by Sernixa policy.", fg=typer.colors.GREEN)
64
- matched = backend_decision.get("matched_policies") or []
65
- if matched:
66
- typer.echo(f"Matched policies: {', '.join(str(item) for item in matched)}")
63
+ typer.secho("ALLOW", fg=typer.colors.GREEN)
67
64
  return
68
65
  review_required = (
69
66
  str(backend_decision.get("final_decision") or "").casefold() == "review"
70
67
  )
68
+ violations = _violations(backend_decision, request=request)
69
+ primary = violations[0] if violations else {}
70
+ category = primary.get("risk_category") or primary.get("category")
71
+ severity = primary.get("severity")
72
+ heading = "REVIEW" if review_required else "DENY"
73
+ details = [str(item) for item in (category, severity) if item]
74
+ if details:
75
+ heading = f"{heading} · {' · '.join(details)}"
71
76
  typer.secho(
72
- "Human review required by Sernixa policy."
73
- if review_required
74
- else "Denied by Sernixa policy.",
77
+ heading,
75
78
  fg=typer.colors.YELLOW if review_required else typer.colors.RED,
76
79
  err=True,
77
80
  )
78
- for violation in _violations(backend_decision, request=request):
79
- label = (
80
- violation.get("policy_id")
81
- or violation.get("rule")
82
- or violation.get("principle")
83
- or "policy"
84
- )
85
- category = violation.get("risk_category") or violation.get("category")
86
- severity = violation.get("severity")
87
- message = (
88
- violation.get("message") or violation.get("reason") or "Denied by policy."
89
- )
90
- prefix = f"{label}"
91
- if category:
92
- prefix = f"{prefix} [{category}]"
93
- if severity:
94
- prefix = f"{prefix} severity={severity}"
95
- typer.echo(f"- {prefix}: {message}", err=True)
96
- if violation.get("principle"):
97
- typer.echo(f" principle: {violation['principle']}", err=True)
98
- if category:
99
- typer.echo(f" risk category: {category}", err=True)
100
- if violation.get("severity"):
101
- typer.echo(f" severity: {violation['severity']}", err=True)
102
- if violation.get("resource"):
103
- typer.echo(f" resource: {violation['resource']}", err=True)
104
- if violation.get("action"):
105
- typer.echo(f" action: {violation['action']}", err=True)
106
- if violation.get("remediation"):
107
- typer.echo(f" remediation: {violation['remediation']}", err=True)
81
+ message = primary.get("message") or primary.get("reason")
82
+ if not message:
83
+ reasons = backend_decision.get("reasons") or []
84
+ message = reasons[0] if reasons else "Blocked by policy."
85
+ typer.echo(f"Reason: {message}", err=True)
86
+ remediation = primary.get("remediation") or _first_remediation(
87
+ backend_decision.get("remediation")
88
+ )
89
+ if remediation:
90
+ typer.echo(f"Next: {remediation}", err=True)
108
91
 
109
92
 
110
93
  def hook_response(
@@ -17,6 +17,7 @@ ConfigFormat = Literal["json", "toml"]
17
17
 
18
18
  SERNIXA_TOML_BEGIN = "# BEGIN SERNIXA MANAGED CODEX HOOK"
19
19
  SERNIXA_TOML_END = "# END SERNIXA MANAGED CODEX HOOK"
20
+ HOOK_TIMEOUT_SECONDS = 35
20
21
 
21
22
 
22
23
  class TargetConfigError(RuntimeError):
@@ -132,6 +133,14 @@ class TargetStatus:
132
133
  _status_path(candidate.path) for candidate in self.candidates
133
134
  ]
134
135
  primary_write_target = _status_path(self.primary_write_target)
136
+ enforcement_mode = (
137
+ "mcp_server"
138
+ if self.target_id in {"claude-desktop", "cursor"}
139
+ else "pre_tool_hook"
140
+ )
141
+ integration_effective = self.wrapper_valid and (
142
+ enforcement_mode == "mcp_server" or self.hooks_effective
143
+ )
135
144
  return {
136
145
  "target_id": self.target_id,
137
146
  "target": self.target_id,
@@ -157,6 +166,8 @@ class TargetStatus:
157
166
  "installed": self.sernixa_installed,
158
167
  "detected": self.sernixa_installed,
159
168
  "wrapper_valid": self.wrapper_valid,
169
+ "enforcement_mode": enforcement_mode,
170
+ "integration_effective": integration_effective,
160
171
  "malformed": self.malformed,
161
172
  "parse_errors": list(self.parse_errors),
162
173
  "parse_error": self.parse_errors[0] if self.parse_errors else None,
@@ -353,14 +364,14 @@ def hook_snippet(target_name: str) -> dict[str, Any]:
353
364
  "type": "command",
354
365
  "command": command[0],
355
366
  "args": command[1:],
356
- "timeout": 10,
367
+ "timeout": HOOK_TIMEOUT_SECONDS,
357
368
  "statusMessage": "Checking Sernixa policy",
358
369
  }
359
370
  else:
360
371
  handler = {
361
372
  "type": "command",
362
373
  "command": " ".join(command),
363
- "timeout": 10,
374
+ "timeout": HOOK_TIMEOUT_SECONDS,
364
375
  "statusMessage": "Checking Sernixa policy",
365
376
  }
366
377
  return {"hooks": {"PreToolUse": [{"matcher": "*", "hooks": [handler]}]}}
@@ -385,7 +396,7 @@ def install_target(target_name: str) -> tuple[TargetResolution, str, Path | None
385
396
  merged, changed = _merge_hooks(existing, snippet, resolution.provider)
386
397
  backup_path = _backup(path) if path.exists() and changed else None
387
398
  _write_private_text(path, json.dumps(merged, indent=2, sort_keys=True) + "\n")
388
- return resolution, "installed" if changed else "updated", backup_path
399
+ return resolution, "installed" if changed else "already_valid", backup_path
389
400
 
390
401
 
391
402
  def target_status(target_name: str) -> TargetStatus:
@@ -440,6 +451,9 @@ def target_status(target_name: str) -> TargetStatus:
440
451
  selected_reason=resolution.selected_reason,
441
452
  uncertainty=resolution.uncertainty,
442
453
  detected_hook_sources=tuple(detected_sources),
454
+ hooks_declared=wrapper_valid if resolution.target_id == "claude" else False,
455
+ hooks_enabled=wrapper_valid if resolution.target_id == "claude" else False,
456
+ hooks_effective=wrapper_valid if resolution.target_id == "claude" else False,
443
457
  )
444
458
 
445
459
 
@@ -971,7 +985,7 @@ def _codex_toml_block() -> str:
971
985
  "[[hooks.PreToolUse.hooks]]\n"
972
986
  'type = "command"\n'
973
987
  'command = "sernixa hook run codex"\n'
974
- "timeout = 10\n"
988
+ f"timeout = {HOOK_TIMEOUT_SECONDS}\n"
975
989
  'statusMessage = "Checking Sernixa policy"\n'
976
990
  f"{SERNIXA_TOML_END}\n"
977
991
  )
@@ -1074,7 +1088,24 @@ def _mcp_server_state(
1074
1088
 
1075
1089
 
1076
1090
  def _group_has_expected_wrapper(group: Any, provider: Provider) -> bool:
1077
- return _expected_command(provider) in _commands(group)
1091
+ if not isinstance(group, dict):
1092
+ return False
1093
+ for hook in group.get("hooks") or []:
1094
+ if not isinstance(hook, dict):
1095
+ continue
1096
+ command = hook.get("command")
1097
+ args = hook.get("args")
1098
+ full = (
1099
+ " ".join([command, *[str(item) for item in args]])
1100
+ if isinstance(command, str) and isinstance(args, list)
1101
+ else command
1102
+ )
1103
+ if (
1104
+ full == _expected_command(provider)
1105
+ and hook.get("timeout") == HOOK_TIMEOUT_SECONDS
1106
+ ):
1107
+ return True
1108
+ return False
1078
1109
 
1079
1110
 
1080
1111
  def _is_sernixa_group_for_provider(group: Any, provider: Provider) -> bool:
@@ -1122,7 +1153,11 @@ def _toml_find_sernixa_wrapper(value: Any, *, expect_valid: bool) -> bool:
1122
1153
  if isinstance(args, list)
1123
1154
  else command
1124
1155
  )
1125
- if expect_valid and full == "sernixa hook run codex":
1156
+ if (
1157
+ expect_valid
1158
+ and full == "sernixa hook run codex"
1159
+ and value.get("timeout") == HOOK_TIMEOUT_SECONDS
1160
+ ):
1126
1161
  return True
1127
1162
  if not expect_valid and "sernixa" in full and "codex" in full:
1128
1163
  return True