xybern 2.0.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.
xybern-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: xybern
3
+ Version: 2.0.0
4
+ Summary: Identity & authorisation infrastructure for AI agents — install once, discover every agent, authorise every action.
5
+ Author-email: Xybern <info@xybern.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://www.xybern.com
8
+ Project-URL: Documentation, https://docs.xybern.com/authorization/sdk
9
+ Keywords: ai,agents,authorization,identity,governance,langchain,crewai,mcp,provenance
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: requests>=2.25.0
13
+ Provides-Extra: crypto
14
+ Requires-Dist: cryptography>=41.0.0; extra == "crypto"
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == "dev"
17
+
18
+ # Xybern
19
+
20
+ **Identity & authorisation infrastructure for AI agents.** Install once — Xybern
21
+ discovers every AI agent in your system, gives each a cryptographic identity, and
22
+ (when you turn enforcement on) authorises every action *before* it executes.
23
+
24
+ ```bash
25
+ pip install xybern
26
+ xybern login # browser device-code flow (auto-links your workspace)
27
+ ```
28
+
29
+ ```python
30
+ from xybern import auto
31
+ auto.connect() # discovers frameworks + agents + tools, registers them, instruments them
32
+ ```
33
+
34
+ ```
35
+ ✓ Detected frameworks: CrewAI, LangGraph, 2 MCP servers
36
+ ✓ Found 12 agents · 48 tools
37
+ ✓ Registered to workspace "Acme Corp" (each issued a cryptographic identity)
38
+ ✓ Mode: OBSERVE — actions logged, nothing blocked yet
39
+ ```
40
+
41
+ ## What it does
42
+
43
+ - **Auto-discovery** — detects and instruments LangChain, CrewAI, OpenAI Agents SDK,
44
+ MCP servers, LangGraph, AutoGen, Semantic Kernel, LlamaIndex (and FastAPI/Celery).
45
+ No manual wiring; agents appear in your Xybern dashboard as your app creates them.
46
+ - **Cryptographic identity** — every discovered agent is registered and issued an
47
+ identity, so its actions are attributable and signable.
48
+ - **Authorisation before execution** — each tool/agent action passes through Xybern's
49
+ policy engine; `allow` / `block` / `escalate`.
50
+ - **Observe-first & fail-open** — default mode only *logs* (never blocks). When you
51
+ switch to enforce, the SDK fails open if Xybern is unreachable, so it can't take
52
+ your agents down.
53
+ - **Privacy** — sends content **hashes** by default, not raw payloads.
54
+
55
+ ## Modes
56
+
57
+ ```python
58
+ auto.connect() # OBSERVE (default): log + inventory, never blocks
59
+ auto.connect(mode="enforce") # authorise actions (allow/block/escalate)
60
+ ```
61
+ or persist it: `xybern enforce on` / `xybern enforce off`.
62
+
63
+ ## CLI
64
+
65
+ ```
66
+ xybern login [--api-key xb_...] # device-code flow, or paste a key / set XYBERN_API_KEY
67
+ xybern agents # dry-run: what would be discovered
68
+ xybern status
69
+ xybern enforce on|off
70
+ xybern logout
71
+ ```
72
+
73
+ ## Auth options
74
+
75
+ 1. **Device code** — `xybern login` opens a browser; approve + pick a workspace; a
76
+ scoped key is minted and stored in `~/.xybern/credentials.json`.
77
+ 2. **API key** — `xybern login --api-key xb_...`, or `export XYBERN_API_KEY=xb_...`,
78
+ or `auto.connect(api_key="xb_...")`.
79
+
80
+ ## Configuration
81
+
82
+ | Option | Default | Meaning |
83
+ | --- | --- | --- |
84
+ | `mode` | `observe` | `observe` (log only) or `enforce` (act on decisions) |
85
+ | `fail_open` | `True` | allow actions through if Xybern is unreachable (enforce mode) |
86
+ | `redact` | `True` | send content hashes instead of raw content |
87
+ | `frameworks` | all | restrict to specific frameworks |
88
+
89
+ Docs: https://docs.xybern.com/authorization/sdk
xybern-2.0.0/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Xybern
2
+
3
+ **Identity & authorisation infrastructure for AI agents.** Install once — Xybern
4
+ discovers every AI agent in your system, gives each a cryptographic identity, and
5
+ (when you turn enforcement on) authorises every action *before* it executes.
6
+
7
+ ```bash
8
+ pip install xybern
9
+ xybern login # browser device-code flow (auto-links your workspace)
10
+ ```
11
+
12
+ ```python
13
+ from xybern import auto
14
+ auto.connect() # discovers frameworks + agents + tools, registers them, instruments them
15
+ ```
16
+
17
+ ```
18
+ ✓ Detected frameworks: CrewAI, LangGraph, 2 MCP servers
19
+ ✓ Found 12 agents · 48 tools
20
+ ✓ Registered to workspace "Acme Corp" (each issued a cryptographic identity)
21
+ ✓ Mode: OBSERVE — actions logged, nothing blocked yet
22
+ ```
23
+
24
+ ## What it does
25
+
26
+ - **Auto-discovery** — detects and instruments LangChain, CrewAI, OpenAI Agents SDK,
27
+ MCP servers, LangGraph, AutoGen, Semantic Kernel, LlamaIndex (and FastAPI/Celery).
28
+ No manual wiring; agents appear in your Xybern dashboard as your app creates them.
29
+ - **Cryptographic identity** — every discovered agent is registered and issued an
30
+ identity, so its actions are attributable and signable.
31
+ - **Authorisation before execution** — each tool/agent action passes through Xybern's
32
+ policy engine; `allow` / `block` / `escalate`.
33
+ - **Observe-first & fail-open** — default mode only *logs* (never blocks). When you
34
+ switch to enforce, the SDK fails open if Xybern is unreachable, so it can't take
35
+ your agents down.
36
+ - **Privacy** — sends content **hashes** by default, not raw payloads.
37
+
38
+ ## Modes
39
+
40
+ ```python
41
+ auto.connect() # OBSERVE (default): log + inventory, never blocks
42
+ auto.connect(mode="enforce") # authorise actions (allow/block/escalate)
43
+ ```
44
+ or persist it: `xybern enforce on` / `xybern enforce off`.
45
+
46
+ ## CLI
47
+
48
+ ```
49
+ xybern login [--api-key xb_...] # device-code flow, or paste a key / set XYBERN_API_KEY
50
+ xybern agents # dry-run: what would be discovered
51
+ xybern status
52
+ xybern enforce on|off
53
+ xybern logout
54
+ ```
55
+
56
+ ## Auth options
57
+
58
+ 1. **Device code** — `xybern login` opens a browser; approve + pick a workspace; a
59
+ scoped key is minted and stored in `~/.xybern/credentials.json`.
60
+ 2. **API key** — `xybern login --api-key xb_...`, or `export XYBERN_API_KEY=xb_...`,
61
+ or `auto.connect(api_key="xb_...")`.
62
+
63
+ ## Configuration
64
+
65
+ | Option | Default | Meaning |
66
+ | --- | --- | --- |
67
+ | `mode` | `observe` | `observe` (log only) or `enforce` (act on decisions) |
68
+ | `fail_open` | `True` | allow actions through if Xybern is unreachable (enforce mode) |
69
+ | `redact` | `True` | send content hashes instead of raw content |
70
+ | `frameworks` | all | restrict to specific frameworks |
71
+
72
+ Docs: https://docs.xybern.com/authorization/sdk
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xybern"
7
+ version = "2.0.0"
8
+ description = "Identity & authorisation infrastructure for AI agents — install once, discover every agent, authorise every action."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Xybern", email = "info@xybern.com" }]
13
+ keywords = ["ai", "agents", "authorization", "identity", "governance", "langchain", "crewai", "mcp", "provenance"]
14
+ dependencies = [
15
+ "requests>=2.25.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ # Per-agent cryptographic identity signing (Ed25519). Optional — falls back to
20
+ # unsigned registration if absent.
21
+ crypto = ["cryptography>=41.0.0"]
22
+ dev = ["pytest"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://www.xybern.com"
26
+ Documentation = "https://docs.xybern.com/authorization/sdk"
27
+
28
+ [project.scripts]
29
+ xybern = "xybern.cli.__main__:main"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["."]
33
+ include = ["xybern*"]
xybern-2.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,29 @@
1
+ """
2
+ Xybern — identity & authorisation infrastructure for AI agents.
3
+
4
+ Install once; Xybern discovers every AI agent in your system, gives each a
5
+ cryptographic identity, and (when you turn enforcement on) authorises every
6
+ action before it executes.
7
+
8
+ Quick start::
9
+
10
+ from xybern import auto
11
+ auto.connect() # uses the key from `xybern login`, env, or config
12
+
13
+ # discovers frameworks + agents + tools, registers them to your workspace
14
+ # (each gets a DID), and instruments them in OBSERVE mode by default.
15
+
16
+ CLI::
17
+
18
+ xybern login # device-code browser flow (auto-picks your workspace key)
19
+ xybern agents # dry-run: show what would be discovered
20
+ xybern status
21
+ xybern enforce on # switch from observe to active authorisation
22
+ """
23
+
24
+ from . import auto
25
+ from .config import Config
26
+
27
+ __version__ = "2.0.0"
28
+
29
+ __all__ = ["auto", "Config", "__version__"]
@@ -0,0 +1,167 @@
1
+ """The one-line entrypoint: discover, register (with identity), instrument.
2
+
3
+ from xybern import auto
4
+ auto.connect()
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import logging
11
+ from dataclasses import dataclass, field
12
+ from typing import Dict, List, Optional
13
+
14
+ from . import discovery
15
+ from .client import XybernClient
16
+ from .config import Config
17
+
18
+ log = logging.getLogger("xybern")
19
+
20
+
21
+ @dataclass
22
+ class ConnectResult:
23
+ authenticated: bool
24
+ mode: str
25
+ frameworks: List[str] = field(default_factory=list)
26
+ agents: List[str] = field(default_factory=list)
27
+ instrumented: List[str] = field(default_factory=list)
28
+ workspace: Optional[str] = None
29
+
30
+ def summary(self) -> str:
31
+ lines = [
32
+ f"✓ Detected frameworks: {', '.join(self.frameworks) or 'none'}",
33
+ f"✓ Found {len(self.agents)} agent(s)/tool(s)",
34
+ ]
35
+ if self.authenticated:
36
+ lines.append(f"✓ Registered to workspace (each issued an identity)")
37
+ lines.append(f"✓ Mode: {self.mode.upper()}" +
38
+ (" — actions logged, nothing blocked yet" if self.mode == "observe" else " — authorising actions"))
39
+ else:
40
+ lines.append("⚠ Not connected — run `xybern login` (or set XYBERN_API_KEY) to register + govern")
41
+ return "\n".join(lines)
42
+
43
+
44
+ class _Registrar:
45
+ """Idempotently maps discovered agents → registered agent_ids (with identity)."""
46
+ def __init__(self, client: XybernClient, cfg: Config):
47
+ self.client = client
48
+ self.cfg = cfg
49
+ self._by_key: Dict[str, str] = {} # name|framework -> agent_id
50
+ self._loaded = False
51
+
52
+ def _key(self, name: str, framework: str) -> str:
53
+ return f"{framework}|{name}".lower()
54
+
55
+ def _load_existing(self):
56
+ if self._loaded or not self.cfg.authenticated:
57
+ return
58
+ self._loaded = True
59
+ try:
60
+ data = self.client.list_agents()
61
+ for a in (data.get("agents") or []):
62
+ self._by_key[self._key(a.get("name", ""), a.get("framework", ""))] = a.get("agent_id")
63
+ except Exception:
64
+ pass
65
+
66
+ def ensure(self, info: "discovery.AgentInfo") -> Optional[str]:
67
+ if not self.cfg.authenticated:
68
+ return None
69
+ self._load_existing()
70
+ k = self._key(info.name, info.framework)
71
+ if k in self._by_key:
72
+ return self._by_key[k]
73
+ try:
74
+ resp = self.client.register_agent(
75
+ name=info.name, framework=info.framework,
76
+ capabilities=info.capabilities or [],
77
+ description=f"Auto-discovered {info.kind} via Xybern SDK",
78
+ )
79
+ agent_id = (resp.get("agent") or {}).get("agent_id")
80
+ if agent_id:
81
+ self._by_key[k] = agent_id
82
+ return agent_id
83
+ except Exception as e:
84
+ log.debug("xybern: register failed for %s: %s", info.name, e)
85
+ return None
86
+
87
+
88
+ _STATE: Dict = {"connected": False}
89
+
90
+
91
+ def connect(api_key: Optional[str] = None, mode: Optional[str] = None,
92
+ frameworks: Optional[List[str]] = None, instrument: bool = True,
93
+ **overrides) -> ConnectResult:
94
+ """Discover agents, register them (with cryptographic identity), and instrument
95
+ the detected frameworks. OBSERVE mode by default — nothing is ever blocked until
96
+ you switch to enforce (`xybern enforce on` or mode="enforce")."""
97
+ cfg = Config.resolve(api_key=api_key, mode=mode, frameworks=frameworks, **overrides)
98
+ client = XybernClient(cfg)
99
+
100
+ from .telemetry import Telemetry
101
+ tel = Telemetry(client, cfg)
102
+ tel.start()
103
+ registrar = _Registrar(client, cfg)
104
+
105
+ # 1) inventory whatever already exists, register each
106
+ found = discovery.discover_all(cfg.frameworks)
107
+ for a in found:
108
+ registrar.ensure(a)
109
+
110
+ # 2) the action hook used by all framework interceptors
111
+ def hook(framework, action_type, agent_name, content, metadata) -> bool:
112
+ return _on_action(cfg, client, tel, registrar, framework, action_type,
113
+ agent_name, content, metadata)
114
+
115
+ def on_discover(info):
116
+ registrar.ensure(info)
117
+
118
+ instrumented = []
119
+ if instrument:
120
+ instrumented = discovery.instrument_all(hook, on_discover, cfg.frameworks)
121
+
122
+ _STATE.update({"connected": True, "cfg": cfg, "client": client,
123
+ "telemetry": tel, "registrar": registrar})
124
+
125
+ return ConnectResult(
126
+ authenticated=cfg.authenticated, mode=cfg.mode,
127
+ frameworks=discovery.detected_frameworks(cfg.frameworks),
128
+ agents=[a.name for a in found], instrumented=instrumented,
129
+ workspace=cfg.workspace,
130
+ )
131
+
132
+
133
+ def _on_action(cfg, client, tel, registrar, framework, action_type, agent_name,
134
+ content, metadata) -> bool:
135
+ """Return True to allow, False to block. OBSERVE always allows (logs only)."""
136
+ agent_id = None
137
+ if agent_name:
138
+ from .discovery import AgentInfo
139
+ agent_id = registrar.ensure(AgentInfo(name=agent_name, framework=framework))
140
+
141
+ if cfg.mode != "enforce":
142
+ # observe: enqueue, never block
143
+ if cfg.authenticated:
144
+ ev = {"action_type": action_type, "agent_id": agent_id,
145
+ "metadata": {"framework": framework, **(metadata or {})}}
146
+ if content is not None:
147
+ ev["metadata"]["content_sha256"] = hashlib.sha256(
148
+ content.encode("utf-8", "replace")).hexdigest()
149
+ tel.record(ev)
150
+ return True
151
+
152
+ # enforce: synchronous decision, fail-open per config
153
+ if not cfg.authenticated:
154
+ return True
155
+ try:
156
+ result = client.intercept(action_type=action_type, action_content=content,
157
+ agent_id=agent_id,
158
+ metadata={"framework": framework, **(metadata or {})})
159
+ decision = result.get("decision", "allow")
160
+ return decision == "allow"
161
+ except Exception:
162
+ return cfg.fail_open
163
+
164
+
165
+ def status() -> Dict:
166
+ return {"connected": _STATE.get("connected", False),
167
+ "mode": getattr(_STATE.get("cfg"), "mode", None)}
File without changes
@@ -0,0 +1,158 @@
1
+ """`xybern` command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import socket
8
+ import sys
9
+ import time
10
+
11
+ from ..config import (Config, DEFAULT_BASE_URL, save_credentials, clear_credentials,
12
+ set_mode)
13
+ from ..client import XybernClient
14
+
15
+
16
+ def _client(api_key=None) -> XybernClient:
17
+ base = os.environ.get("XYBERN_BASE_URL", DEFAULT_BASE_URL)
18
+ return XybernClient(Config(api_key=api_key, base_url=base))
19
+
20
+
21
+ # ── commands ──────────────────────────────────────────────────────────────────
22
+
23
+ def cmd_login(args) -> int:
24
+ base = os.environ.get("XYBERN_BASE_URL", DEFAULT_BASE_URL)
25
+
26
+ # Path 1 — explicit/paste key (or env). Validate then save.
27
+ api_key = args.api_key or os.environ.get("XYBERN_API_KEY")
28
+ if api_key:
29
+ cli = _client(api_key)
30
+ try:
31
+ cli.whoami()
32
+ except Exception as e:
33
+ print(f"✗ Could not validate that API key: {e}", file=sys.stderr)
34
+ return 1
35
+ save_credentials(api_key, base_url=base)
36
+ print("✓ Logged in with API key. Credentials saved to ~/.xybern/credentials.json")
37
+ return 0
38
+
39
+ # Path 2 — device-code browser flow (auto-picks your workspace key)
40
+ cli = _client()
41
+ try:
42
+ start = cli.device_auth_start(hostname=socket.gethostname())
43
+ except Exception as e:
44
+ print(f"✗ Device login unavailable ({e}).\n"
45
+ f" Use: xybern login --api-key xb_... (create a key in your Xybern dashboard)",
46
+ file=sys.stderr)
47
+ return 1
48
+
49
+ url = start.get("verification_url") or start.get("verification_uri")
50
+ user_code = start.get("user_code")
51
+ device_code = start.get("device_code")
52
+ interval = int(start.get("interval", 5))
53
+ expires = int(start.get("expires_in", 600))
54
+
55
+ print("\nTo authorise this machine, open:\n")
56
+ print(f" {url}")
57
+ print(f"\nand enter the code: {user_code}\n")
58
+ try:
59
+ import webbrowser
60
+ webbrowser.open(url)
61
+ except Exception:
62
+ pass
63
+
64
+ print("Waiting for approval…", end="", flush=True)
65
+ deadline = time.time() + expires
66
+ while time.time() < deadline:
67
+ time.sleep(interval)
68
+ print(".", end="", flush=True)
69
+ try:
70
+ poll = cli.device_auth_poll(device_code)
71
+ except Exception:
72
+ continue
73
+ if poll.get("api_key"):
74
+ save_credentials(poll["api_key"], base_url=base, workspace=poll.get("workspace"))
75
+ print(f"\n✓ Logged in to workspace {poll.get('workspace') or ''}. "
76
+ f"Credentials saved.")
77
+ return 0
78
+ if poll.get("status") in ("denied", "expired"):
79
+ print(f"\n✗ Login {poll['status']}.", file=sys.stderr)
80
+ return 1
81
+ print("\n✗ Timed out waiting for approval.", file=sys.stderr)
82
+ return 1
83
+
84
+
85
+ def cmd_logout(args) -> int:
86
+ clear_credentials()
87
+ print("✓ Logged out.")
88
+ return 0
89
+
90
+
91
+ def cmd_agents(args) -> int:
92
+ from .. import discovery
93
+ fw = discovery.detected_frameworks()
94
+ found = discovery.discover_all()
95
+ print(f"✓ Detected frameworks: {', '.join(fw) or 'none'}")
96
+ print(f"✓ Found {len(found)} agent(s)/tool(s) already instantiated"
97
+ + ("" if found else " (more appear as your app creates them)"))
98
+ by_fw = {}
99
+ for a in found:
100
+ by_fw.setdefault(a.framework, []).append(a)
101
+ for f, items in by_fw.items():
102
+ print(f" • {f}: " + ", ".join(sorted({i.name for i in items}))[:200])
103
+ cfg = Config.resolve()
104
+ if not cfg.authenticated:
105
+ print("\n⚠ Not connected. Run `xybern login`, then `auto.connect()` in your app "
106
+ "to register these agents (each gets an identity).")
107
+ return 0
108
+
109
+
110
+ def cmd_status(args) -> int:
111
+ cfg = Config.resolve()
112
+ print(f"Base URL: {cfg.base_url}")
113
+ print(f"Mode: {cfg.mode}")
114
+ print(f"Auth: {'✓ logged in' if cfg.authenticated else '✗ not logged in'}")
115
+ if cfg.authenticated:
116
+ try:
117
+ n = len((_client(cfg.api_key).list_agents().get('agents') or []))
118
+ print(f"Workspace: {cfg.workspace or '(linked)'} · {n} registered agent(s)")
119
+ except Exception as e:
120
+ print(f"Workspace: (could not reach control plane: {e})")
121
+ return 0
122
+
123
+
124
+ def cmd_enforce(args) -> int:
125
+ if args.state == "on":
126
+ set_mode("enforce")
127
+ print("✓ Enforcement ON — actions will be authorised (allow/block/escalate) on next connect.")
128
+ else:
129
+ set_mode("observe")
130
+ print("✓ Enforcement OFF — observe only (actions logged, nothing blocked).")
131
+ return 0
132
+
133
+
134
+ def main(argv=None) -> int:
135
+ p = argparse.ArgumentParser(prog="xybern", description="Xybern — identity & authorisation for AI agents")
136
+ sub = p.add_subparsers(dest="cmd")
137
+
138
+ lp = sub.add_parser("login", help="Authenticate this machine (device code, or --api-key)")
139
+ lp.add_argument("--api-key", help="Use an API key directly instead of the browser flow")
140
+ lp.set_defaults(func=cmd_login)
141
+
142
+ sub.add_parser("logout", help="Remove stored credentials").set_defaults(func=cmd_logout)
143
+ sub.add_parser("agents", help="Discover agents/tools in this project (dry run)").set_defaults(func=cmd_agents)
144
+ sub.add_parser("status", help="Show login + mode").set_defaults(func=cmd_status)
145
+
146
+ ep = sub.add_parser("enforce", help="Turn enforcement on/off")
147
+ ep.add_argument("state", choices=["on", "off"])
148
+ ep.set_defaults(func=cmd_enforce)
149
+
150
+ args = p.parse_args(argv)
151
+ if not getattr(args, "func", None):
152
+ p.print_help()
153
+ return 0
154
+ return args.func(args)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ sys.exit(main())
@@ -0,0 +1,83 @@
1
+ """Thin REST client for the Xybern control plane."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import requests
9
+
10
+ from .config import Config
11
+
12
+
13
+ class XybernClient:
14
+ def __init__(self, config: Config):
15
+ self.cfg = config
16
+ self._session = requests.Session()
17
+ if config.api_key:
18
+ self._session.headers.update({"X-API-Key": config.api_key})
19
+ self._session.headers.update({"User-Agent": "xybern-sdk/2.0"})
20
+
21
+ # ── low level ────────────────────────────────────────────────────────────
22
+ def _url(self, path: str) -> str:
23
+ return f"{self.cfg.base_url.rstrip('/')}/{path.lstrip('/')}"
24
+
25
+ def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
26
+ r = self._session.post(self._url(path), json=body, timeout=self.cfg.timeout)
27
+ r.raise_for_status()
28
+ return r.json()
29
+
30
+ def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
31
+ r = self._session.get(self._url(path), params=params or {}, timeout=self.cfg.timeout)
32
+ r.raise_for_status()
33
+ return r.json()
34
+
35
+ # ── agents / registration ────────────────────────────────────────────────
36
+ def register_agent(self, name: str, framework: str = "custom",
37
+ description: Optional[str] = None,
38
+ capabilities: Optional[List[str]] = None,
39
+ permissions: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
40
+ """Register (or idempotently re-register) an agent; returns agent + credential."""
41
+ return self._post("/enforce/agents", {
42
+ "name": name,
43
+ "framework": framework,
44
+ "description": description,
45
+ "capabilities": capabilities or [],
46
+ "permissions": permissions or {},
47
+ })
48
+
49
+ def list_agents(self) -> Dict[str, Any]:
50
+ return self._get("/enforce/agents")
51
+
52
+ # ── enforcement ────────────────────────────────────────────────────────────
53
+ def intercept(self, action_type: str, action_content: Optional[str] = None,
54
+ agent_id: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None,
55
+ ) -> Dict[str, Any]:
56
+ """Authorise one action. Respects redact (sends a content hash by default)."""
57
+ body: Dict[str, Any] = {"action_type": action_type, "agent_id": agent_id,
58
+ "metadata": metadata or {}}
59
+ if action_content is not None:
60
+ if self.cfg.redact:
61
+ body["metadata"]["content_sha256"] = hashlib.sha256(
62
+ action_content.encode("utf-8", "replace")).hexdigest()
63
+ body["action_content"] = None
64
+ else:
65
+ body["action_content"] = action_content
66
+ return self._post("/enforce/intercept", body)
67
+
68
+ def batch_intercept(self, actions: List[Dict[str, Any]]) -> Dict[str, Any]:
69
+ return self._post("/enforce/batch", {"actions": actions})
70
+
71
+ def list_policies(self) -> Dict[str, Any]:
72
+ return self._get("/enforce/policies")
73
+
74
+ def whoami(self) -> Dict[str, Any]:
75
+ """Lightweight auth/workspace check (lists agents; 200 => key valid)."""
76
+ return self._get("/enforce/agents")
77
+
78
+ # ── device-code login ──────────────────────────────────────────────────────
79
+ def device_auth_start(self, hostname: str) -> Dict[str, Any]:
80
+ return self._post("/auth/device/start", {"hostname": hostname, "client": "xybern-sdk"})
81
+
82
+ def device_auth_poll(self, device_code: str) -> Dict[str, Any]:
83
+ return self._post("/auth/device/poll", {"device_code": device_code})