sernixa-cli 0.1.1__tar.gz → 0.2.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 (30) hide show
  1. sernixa_cli-0.2.0/PKG-INFO +149 -0
  2. sernixa_cli-0.2.0/README.md +124 -0
  3. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/pyproject.toml +3 -2
  4. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli/__init__.py +1 -1
  5. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli/adapters.py +19 -7
  6. sernixa_cli-0.2.0/sernixa_cli/discovery.py +198 -0
  7. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli/formatting.py +53 -18
  8. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli/local_agent_targets.py +217 -60
  9. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli/main.py +460 -26
  10. sernixa_cli-0.2.0/sernixa_cli/mcp_server.py +233 -0
  11. sernixa_cli-0.2.0/sernixa_cli/native_launcher.py +275 -0
  12. sernixa_cli-0.2.0/sernixa_cli.egg-info/PKG-INFO +149 -0
  13. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli.egg-info/SOURCES.txt +7 -1
  14. sernixa_cli-0.2.0/sernixa_cli.egg-info/requires.txt +4 -0
  15. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/tests/test_cli.py +180 -1
  16. sernixa_cli-0.2.0/tests/test_discovery.py +88 -0
  17. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/tests/test_local_agent_targets.py +124 -23
  18. sernixa_cli-0.2.0/tests/test_mcp_server.py +110 -0
  19. sernixa_cli-0.2.0/tests/test_native_launcher.py +83 -0
  20. sernixa_cli-0.1.1/PKG-INFO +0 -179
  21. sernixa_cli-0.1.1/README.md +0 -155
  22. sernixa_cli-0.1.1/sernixa_cli.egg-info/PKG-INFO +0 -179
  23. sernixa_cli-0.1.1/sernixa_cli.egg-info/requires.txt +0 -3
  24. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli/__main__.py +0 -0
  25. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli/config.py +0 -0
  26. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli/hooks.py +0 -0
  27. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli.egg-info/dependency_links.txt +0 -0
  28. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli.egg-info/entry_points.txt +0 -0
  29. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/sernixa_cli.egg-info/top_level.txt +0 -0
  30. {sernixa_cli-0.1.1 → sernixa_cli-0.2.0}/setup.cfg +0 -0
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: sernixa-cli
3
+ Version: 0.2.0
4
+ Summary: Official Sernixa CLI for policy-governed local AI agent actions
5
+ Author: Sernixa Team
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://sernixa.com
8
+ Project-URL: Repository, https://github.com/abhishekdhull63/Sernixa.ai-Web
9
+ Project-URL: Documentation, https://github.com/abhishekdhull63/Sernixa.ai-Web/tree/main/docs/cli.md
10
+ Keywords: sernixa,cli,governance,agents,policy
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: platformdirs>=4.0.0
22
+ Requires-Dist: sernixa>=0.4.0
23
+ Requires-Dist: sernixa-agent>=1.6.0
24
+ Requires-Dist: typer>=0.12.0
25
+
26
+ # Sernixa CLI
27
+
28
+ The official terminal and local-agent integration for Sernixa governance. The CLI evaluates actions against the real Sernixa control plane, installs supported agent hooks and MCP configuration, launches safe local discovery, and exposes operational evidence without a dashboard-only workflow.
29
+
30
+ ## Install and set up
31
+
32
+ ```bash
33
+ pip install --upgrade sernixa-cli
34
+ sernixa auth login
35
+ sernixa setup
36
+ sernixa doctor
37
+ ```
38
+
39
+ `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:
40
+
41
+ ```bash
42
+ sernixa setup --dry-run
43
+ sernixa setup --target codex --target cursor --json
44
+ ```
45
+
46
+ For local repository development:
47
+
48
+ ```bash
49
+ python3 -m venv .venv-cli
50
+ source .venv-cli/bin/activate
51
+ pip install -e packages/sernixa -e packages/sernixa-agent -e apps/cli
52
+ ```
53
+
54
+ The CLI defaults to `https://api.sernixa.com`. Stored credentials use the platform user-config directory and mode `0600` on Unix. `SERNIXA_API_KEY` and `SERNIXA_BASE_URL` override the stored configuration for non-interactive use.
55
+
56
+ ## One-command Auto Discover
57
+
58
+ ```bash
59
+ sernixa discover
60
+ sernixa discover --json
61
+ sernixa discover --job discjob_0123456789abcdef0123456789abcdef
62
+ ```
63
+
64
+ The command validates auth, initializes the supported local agent when needed, passes the stored credential only through the one-shot child-process environment, enrolls the stable machine ID, creates or accepts a policy-scoped job for that machine, runs one sync/task cycle, and waits for the terminal job result. The credential is not printed or duplicated into a second token file.
65
+
66
+ The Command Center opens the same flow through `sernixa://discover` after a direct user click and an OS/browser confirmation. The native URL accepts only the fixed discovery action and an optional validated job ID; it cannot carry commands, credentials, paths, or environment values. If the launcher is unavailable, the dashboard shows the exact fallback command.
67
+
68
+ Discovery candidates are observations for review. Enrollment is inventory. An accepted agent sync is proof. No discovery result silently becomes a managed MCP server.
69
+
70
+ ## Codex, Claude, and Cursor
71
+
72
+ Supported global targets:
73
+
74
+ - `claude`: Claude Code `PreToolUse` exec-form hook in `~/.claude/settings.json`.
75
+ - `claude-desktop`: real stdio MCP server in the OS-specific Claude Desktop configuration.
76
+ - `codex`: current `PreToolUse` JSON or array-of-tables TOML hook in `~/.codex`. Hooks are effective by default unless `[features].hooks = false`; users still review/trust new non-managed hooks in Codex.
77
+ - `cursor`: global MCP entry in `~/.cursor/mcp.json`, shared by Cursor IDE and CLI.
78
+
79
+ Install or repair an individual integration:
80
+
81
+ ```bash
82
+ sernixa hook install claude
83
+ sernixa hook install claude-desktop
84
+ sernixa hook install codex
85
+ sernixa hook install cursor
86
+
87
+ sernixa hook repair codex
88
+ sernixa hook status --json
89
+ ```
90
+
91
+ 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
+ Claude Desktop and Cursor run `sernixa mcp serve`. The stdio MCP server exposes three non-mutating tools:
94
+
95
+ - `sernixa_policy_check`: evaluate one proposed action.
96
+ - `sernixa_plan_check`: evaluate a declared multi-step plan.
97
+ - `sernixa_identity`: validate auth and return non-secret principal metadata.
98
+
99
+ MCP tools provide explicit model capabilities; the installed hooks are the local tool-call enforcement points.
100
+
101
+ ## Governance and execution
102
+
103
+ ```bash
104
+ sernixa validate payload.json
105
+ cat payload.json | sernixa validate --json
106
+ sernixa validate --from-claude < claude-hook.json
107
+ sernixa validate --from-codex < codex-hook.jsonl
108
+ sernixa validate-plan --agent-type codex plan.json
109
+ sernixa validate-many --output json policies/*.json
110
+ sernixa exec --plan plan.json -- npm test
111
+ sernixa codex-exec --plan codex-plan.json -- codex exec "fix lint"
112
+ ```
113
+
114
+ Exit codes are stable:
115
+
116
+ - `0`: success or policy allow
117
+ - `1`: deny, review required, invalid evidence, or non-success terminal result
118
+ - `2`: configuration, parsing, auth, network, or API failure
119
+
120
+ `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
+
122
+ ## Audit, approvals, and Flight Recorder
123
+
124
+ ```bash
125
+ sernixa events --limit 25
126
+ sernixa events --type governance_agent_plan_evaluated --json
127
+
128
+ sernixa approvals list
129
+ sernixa approvals decide approval_123 approved --reason "Reviewed bounded deployment."
130
+ sernixa approvals decide approval_456 rejected --reason "Production scope is too broad."
131
+
132
+ sernixa flight verify
133
+ sernixa flight verify --stream stream_123 --json
134
+ sernixa flight collectors
135
+ ```
136
+
137
+ These commands call the authenticated organization endpoints and inherit backend permissions and feature entitlements. `sernixa decisions` remains a backward-compatible alias for the pending approval queue. Replay is not claimed because the backend does not expose an authorized decision-replay endpoint.
138
+
139
+ ## Stable configuration overrides
140
+
141
+ - `SERNIXA_CONFIG_FILE`
142
+ - `SERNIXA_PLAN_SESSION_FILE`
143
+ - `SERNIXA_CLAUDE_CONFIG_FILE`
144
+ - `SERNIXA_CLAUDE_DESKTOP_CONFIG_FILE`
145
+ - `SERNIXA_CODEX_HOOKS_FILE`
146
+ - `SERNIXA_CODEX_CONFIG_FILE`
147
+ - `SERNIXA_CURSOR_CONFIG_FILE`
148
+
149
+ Project-scoped Codex files are intentionally not modified by global setup. Invalid JSON or TOML fails closed with the exact path and a repair command. Existing files are backed up before material changes.
@@ -0,0 +1,124 @@
1
+ # Sernixa CLI
2
+
3
+ The official terminal and local-agent integration for Sernixa governance. The CLI evaluates actions against the real Sernixa control plane, installs supported agent hooks and MCP configuration, launches safe local discovery, and exposes operational evidence without a dashboard-only workflow.
4
+
5
+ ## Install and set up
6
+
7
+ ```bash
8
+ pip install --upgrade sernixa-cli
9
+ sernixa auth login
10
+ sernixa setup
11
+ sernixa doctor
12
+ ```
13
+
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:
15
+
16
+ ```bash
17
+ sernixa setup --dry-run
18
+ sernixa setup --target codex --target cursor --json
19
+ ```
20
+
21
+ For local repository development:
22
+
23
+ ```bash
24
+ python3 -m venv .venv-cli
25
+ source .venv-cli/bin/activate
26
+ pip install -e packages/sernixa -e packages/sernixa-agent -e apps/cli
27
+ ```
28
+
29
+ The CLI defaults to `https://api.sernixa.com`. Stored credentials use the platform user-config directory and mode `0600` on Unix. `SERNIXA_API_KEY` and `SERNIXA_BASE_URL` override the stored configuration for non-interactive use.
30
+
31
+ ## One-command Auto Discover
32
+
33
+ ```bash
34
+ sernixa discover
35
+ sernixa discover --json
36
+ sernixa discover --job discjob_0123456789abcdef0123456789abcdef
37
+ ```
38
+
39
+ The command validates auth, initializes the supported local agent when needed, passes the stored credential only through the one-shot child-process environment, enrolls the stable machine ID, creates or accepts a policy-scoped job for that machine, runs one sync/task cycle, and waits for the terminal job result. The credential is not printed or duplicated into a second token file.
40
+
41
+ The Command Center opens the same flow through `sernixa://discover` after a direct user click and an OS/browser confirmation. The native URL accepts only the fixed discovery action and an optional validated job ID; it cannot carry commands, credentials, paths, or environment values. If the launcher is unavailable, the dashboard shows the exact fallback command.
42
+
43
+ Discovery candidates are observations for review. Enrollment is inventory. An accepted agent sync is proof. No discovery result silently becomes a managed MCP server.
44
+
45
+ ## Codex, Claude, and Cursor
46
+
47
+ Supported global targets:
48
+
49
+ - `claude`: Claude Code `PreToolUse` exec-form hook in `~/.claude/settings.json`.
50
+ - `claude-desktop`: real stdio MCP server in the OS-specific Claude Desktop configuration.
51
+ - `codex`: current `PreToolUse` JSON or array-of-tables TOML hook in `~/.codex`. Hooks are effective by default unless `[features].hooks = false`; users still review/trust new non-managed hooks in Codex.
52
+ - `cursor`: global MCP entry in `~/.cursor/mcp.json`, shared by Cursor IDE and CLI.
53
+
54
+ Install or repair an individual integration:
55
+
56
+ ```bash
57
+ sernixa hook install claude
58
+ sernixa hook install claude-desktop
59
+ sernixa hook install codex
60
+ sernixa hook install cursor
61
+
62
+ sernixa hook repair codex
63
+ sernixa hook status --json
64
+ ```
65
+
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
+
68
+ Claude Desktop and Cursor run `sernixa mcp serve`. The stdio MCP server exposes three non-mutating tools:
69
+
70
+ - `sernixa_policy_check`: evaluate one proposed action.
71
+ - `sernixa_plan_check`: evaluate a declared multi-step plan.
72
+ - `sernixa_identity`: validate auth and return non-secret principal metadata.
73
+
74
+ MCP tools provide explicit model capabilities; the installed hooks are the local tool-call enforcement points.
75
+
76
+ ## Governance and execution
77
+
78
+ ```bash
79
+ sernixa validate payload.json
80
+ cat payload.json | sernixa validate --json
81
+ sernixa validate --from-claude < claude-hook.json
82
+ sernixa validate --from-codex < codex-hook.jsonl
83
+ sernixa validate-plan --agent-type codex plan.json
84
+ sernixa validate-many --output json policies/*.json
85
+ sernixa exec --plan plan.json -- npm test
86
+ sernixa codex-exec --plan codex-plan.json -- codex exec "fix lint"
87
+ ```
88
+
89
+ Exit codes are stable:
90
+
91
+ - `0`: success or policy allow
92
+ - `1`: deny, review required, invalid evidence, or non-success terminal result
93
+ - `2`: configuration, parsing, auth, network, or API failure
94
+
95
+ `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
+
97
+ ## Audit, approvals, and Flight Recorder
98
+
99
+ ```bash
100
+ sernixa events --limit 25
101
+ sernixa events --type governance_agent_plan_evaluated --json
102
+
103
+ sernixa approvals list
104
+ sernixa approvals decide approval_123 approved --reason "Reviewed bounded deployment."
105
+ sernixa approvals decide approval_456 rejected --reason "Production scope is too broad."
106
+
107
+ sernixa flight verify
108
+ sernixa flight verify --stream stream_123 --json
109
+ sernixa flight collectors
110
+ ```
111
+
112
+ These commands call the authenticated organization endpoints and inherit backend permissions and feature entitlements. `sernixa decisions` remains a backward-compatible alias for the pending approval queue. Replay is not claimed because the backend does not expose an authorized decision-replay endpoint.
113
+
114
+ ## Stable configuration overrides
115
+
116
+ - `SERNIXA_CONFIG_FILE`
117
+ - `SERNIXA_PLAN_SESSION_FILE`
118
+ - `SERNIXA_CLAUDE_CONFIG_FILE`
119
+ - `SERNIXA_CLAUDE_DESKTOP_CONFIG_FILE`
120
+ - `SERNIXA_CODEX_HOOKS_FILE`
121
+ - `SERNIXA_CODEX_CONFIG_FILE`
122
+ - `SERNIXA_CURSOR_CONFIG_FILE`
123
+
124
+ Project-scoped Codex files are intentionally not modified by global setup. Invalid JSON or TOML fails closed with the exact path and a repair command. Existing files are backed up before material changes.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sernixa-cli"
3
- version = "0.1.1"
3
+ version = "0.2.0"
4
4
  description = "Official Sernixa CLI for policy-governed local AI agent actions"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -19,7 +19,8 @@ classifiers = [
19
19
  ]
20
20
  dependencies = [
21
21
  "platformdirs>=4.0.0",
22
- "sernixa>=0.3.1",
22
+ "sernixa>=0.4.0",
23
+ "sernixa-agent>=1.6.0",
23
24
  "typer>=0.12.0",
24
25
  ]
25
26
 
@@ -1,3 +1,3 @@
1
1
  """Official Sernixa command-line client."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.2.0"
@@ -85,13 +85,19 @@ def canonicalize_payload(raw: Any, *, mode: ProviderMode) -> CanonicalGovernance
85
85
  raise AdapterError("Governance payload must be a JSON object.")
86
86
  return CanonicalGovernanceRequest(input=raw, source="plain")
87
87
  if mode == "claude":
88
- return CanonicalGovernanceRequest(input=_claude_to_governance(raw), source="claude")
88
+ return CanonicalGovernanceRequest(
89
+ input=_claude_to_governance(raw), source="claude"
90
+ )
89
91
  if mode == "codex":
90
- return CanonicalGovernanceRequest(input=_codex_to_governance(raw), source="codex")
92
+ return CanonicalGovernanceRequest(
93
+ input=_codex_to_governance(raw), source="codex"
94
+ )
91
95
  raise AdapterError(f"Unsupported payload adapter: {mode}")
92
96
 
93
97
 
94
- def payload_from_content(content: str, *, mode: ProviderMode) -> CanonicalGovernanceRequest:
98
+ def payload_from_content(
99
+ content: str, *, mode: ProviderMode
100
+ ) -> CanonicalGovernanceRequest:
95
101
  raw = load_json_payload(content)
96
102
  return canonicalize_payload(raw, mode=mode)
97
103
 
@@ -128,7 +134,9 @@ def _codex_to_governance(raw: Any) -> dict[str, Any]:
128
134
  event = _select_codex_event(raw)
129
135
  if not isinstance(event, dict):
130
136
  raise AdapterError("Codex payload must be a JSON object or JSON-lines stream.")
131
- tool_name = _string(event.get("tool_name") or event.get("name") or event.get("tool"))
137
+ tool_name = _string(
138
+ event.get("tool_name") or event.get("name") or event.get("tool")
139
+ )
132
140
  tool_input = event.get("tool_input")
133
141
  if tool_input is None:
134
142
  if isinstance(event.get("input"), dict):
@@ -178,7 +186,9 @@ def _select_codex_event(raw: Any) -> Any:
178
186
  )
179
187
  ]
180
188
  if not candidates:
181
- raise AdapterError("No recognizable Codex tool event found in JSON-lines stream.")
189
+ raise AdapterError(
190
+ "No recognizable Codex tool event found in JSON-lines stream."
191
+ )
182
192
  return candidates[-1]
183
193
 
184
194
 
@@ -243,7 +253,8 @@ def _action_for_tool(tool_name: str, tool_input: dict[str, Any]) -> dict[str, An
243
253
  return {
244
254
  "action": "network.request",
245
255
  "operation_class": "network",
246
- "resource": _string(tool_input.get("url")) or _string(tool_input.get("query")),
256
+ "resource": _string(tool_input.get("url"))
257
+ or _string(tool_input.get("query")),
247
258
  "intent": _string(tool_input.get("prompt")),
248
259
  }
249
260
  if normalized.startswith("mcp__"):
@@ -258,7 +269,8 @@ def _action_for_tool(tool_name: str, tool_input: dict[str, Any]) -> dict[str, An
258
269
  "action": "agent.delegate",
259
270
  "operation_class": "delegate",
260
271
  "resource": _string(tool_input.get("subagent_type")),
261
- "intent": _string(tool_input.get("prompt")) or _string(tool_input.get("description")),
272
+ "intent": _string(tool_input.get("prompt"))
273
+ or _string(tool_input.get("description")),
262
274
  }
263
275
  return {
264
276
  "action": f"tool.{normalized}",
@@ -0,0 +1,198 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from sernixa import Client
12
+
13
+ from .config import CliConfig
14
+
15
+
16
+ class DiscoveryError(RuntimeError):
17
+ pass
18
+
19
+
20
+ TERMINAL_JOB_STATES = {"completed", "failed", "expired", "cancelled"}
21
+
22
+
23
+ def _run_agent(
24
+ command: list[str],
25
+ *,
26
+ input_text: str | None = None,
27
+ extra_env: dict[str, str] | None = None,
28
+ ) -> subprocess.CompletedProcess[str]:
29
+ completed = subprocess.run(
30
+ command,
31
+ input=input_text,
32
+ text=True,
33
+ capture_output=True,
34
+ check=False,
35
+ env={**os.environ, **(extra_env or {})},
36
+ )
37
+ if completed.returncode != 0:
38
+ detail = completed.stderr.strip() or completed.stdout.strip()
39
+ raise DiscoveryError(detail or f"Command failed: {' '.join(command)}")
40
+ return completed
41
+
42
+
43
+ def _agent_executable() -> str:
44
+ executable = shutil.which("sernixa-agent")
45
+ if not executable:
46
+ raise DiscoveryError(
47
+ "sernixa-agent is not installed. Upgrade sernixa-cli or install sernixa-agent>=1.6.0."
48
+ )
49
+ return executable
50
+
51
+
52
+ def _ensure_agent_ready(
53
+ config: CliConfig,
54
+ *,
55
+ agent_config_path: Path,
56
+ ) -> tuple[str, dict[str, Any], bool, dict[str, str]]:
57
+ executable = _agent_executable()
58
+ agent_env = {"SERNIXA_DISCOVERY_TOKEN": config.api_key}
59
+ initialized = False
60
+ if not agent_config_path.exists():
61
+ _run_agent(
62
+ [
63
+ executable,
64
+ "init",
65
+ "--config",
66
+ str(agent_config_path),
67
+ "--api-base-url",
68
+ config.base_url,
69
+ ]
70
+ )
71
+ initialized = True
72
+ status = _run_agent(
73
+ [
74
+ executable,
75
+ "status",
76
+ "--config",
77
+ str(agent_config_path),
78
+ "--token-env",
79
+ "SERNIXA_DISCOVERY_TOKEN",
80
+ "--json",
81
+ ],
82
+ extra_env=agent_env,
83
+ )
84
+ try:
85
+ status_payload = json.loads(status.stdout)
86
+ except json.JSONDecodeError as exc:
87
+ raise DiscoveryError("sernixa-agent returned invalid status JSON.") from exc
88
+ auth = status_payload.get("auth") if isinstance(status_payload, dict) else None
89
+ if not isinstance(auth, dict) or not auth.get("ok"):
90
+ raise DiscoveryError(
91
+ "Unable to pass the stored CLI credential to the local discovery agent."
92
+ )
93
+ return executable, status_payload, initialized, agent_env
94
+
95
+
96
+ def _discovery_job_payload(
97
+ policy: dict[str, Any], *, machine_id: str
98
+ ) -> dict[str, Any]:
99
+ process_enabled = bool(policy.get("process_inspection_enabled", False))
100
+ config_enabled = bool(policy.get("mcp_config_inspection_enabled", False))
101
+ return {
102
+ "scope": {"mode": "local_agent", "machine_ids": [machine_id]},
103
+ "requested_scan": {
104
+ "network_ports": "policy_default",
105
+ "stdio_detection": process_enabled or config_enabled,
106
+ "process_inspection": process_enabled,
107
+ "mcp_config_inspection": config_enabled,
108
+ "controlled_stdio_validation": False,
109
+ "max_candidates": min(int(policy.get("max_candidates") or 50), 100),
110
+ },
111
+ }
112
+
113
+
114
+ def run_discovery(
115
+ client: Client,
116
+ config: CliConfig,
117
+ *,
118
+ job_id: str | None = None,
119
+ agent_config_path: Path | None = None,
120
+ timeout_seconds: float = 120.0,
121
+ poll_interval: float = 1.0,
122
+ ) -> dict[str, Any]:
123
+ """Enroll, dispatch, execute, and verify one bounded Auto Discover run."""
124
+ identity = client.whoami()
125
+ config_path = agent_config_path or Path.home() / ".sernixa" / "agent.json"
126
+ executable, agent_status, initialized, agent_env = _ensure_agent_ready(
127
+ config, agent_config_path=config_path
128
+ )
129
+ machine_id = str(agent_status.get("machine_id") or "")
130
+ if not machine_id:
131
+ raise DiscoveryError("sernixa-agent did not return a stable machine ID.")
132
+ _run_agent(
133
+ [
134
+ executable,
135
+ "enroll",
136
+ "--config",
137
+ str(config_path),
138
+ "--api-base-url",
139
+ config.base_url,
140
+ "--token-env",
141
+ "SERNIXA_DISCOVERY_TOKEN",
142
+ ],
143
+ extra_env=agent_env,
144
+ )
145
+ if job_id:
146
+ job = client.auto_discovery_job(job_id)
147
+ else:
148
+ policy = client.auto_discovery_policy()
149
+ job = client.create_auto_discovery_job(
150
+ _discovery_job_payload(policy, machine_id=machine_id)
151
+ )
152
+ job_id = str(job.get("job_id") or "")
153
+ if not job_id:
154
+ raise DiscoveryError("Sernixa did not return an Auto Discover job ID.")
155
+ agent_run = _run_agent(
156
+ [
157
+ executable,
158
+ "daemon",
159
+ "--config",
160
+ str(config_path),
161
+ "--api-base-url",
162
+ config.base_url,
163
+ "--token-env",
164
+ "SERNIXA_DISCOVERY_TOKEN",
165
+ "--once",
166
+ ],
167
+ extra_env=agent_env,
168
+ )
169
+ try:
170
+ agent_summary = json.loads(agent_run.stdout) if agent_run.stdout.strip() else {}
171
+ except json.JSONDecodeError:
172
+ agent_summary = {
173
+ "status": "completed",
174
+ "detail": agent_run.stdout.strip()[:500],
175
+ }
176
+ deadline = time.monotonic() + timeout_seconds
177
+ while str(job.get("status")) not in TERMINAL_JOB_STATES:
178
+ if time.monotonic() >= deadline:
179
+ raise DiscoveryError(
180
+ f"Timed out waiting for Auto Discover job {job_id}. The job may still complete in Sernixa."
181
+ )
182
+ time.sleep(max(0.05, poll_interval))
183
+ job = client.auto_discovery_job(job_id)
184
+ return {
185
+ "organization_id": identity.get("organization_id"),
186
+ "job": job,
187
+ "agent": {
188
+ "config_path": str(config_path.resolve(strict=False)),
189
+ "initialized": initialized,
190
+ "machine_id": agent_status.get("machine_id"),
191
+ "version": agent_status.get("agent_version"),
192
+ "cycle": agent_summary,
193
+ },
194
+ "trust_boundary": (
195
+ "Discovery candidates are observations for review. Enrollment is inventory; "
196
+ "the accepted agent sync is the proof recorded by Sernixa."
197
+ ),
198
+ }