agentlock-sdk 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.
@@ -0,0 +1,65 @@
1
+ node_modules
2
+ .next
3
+ dist
4
+ .env
5
+ .env.local
6
+ .env.*.local
7
+ .turbo
8
+ .cache
9
+ .pnpm-store
10
+ *.tsbuildinfo
11
+ *.log
12
+ .DS_Store
13
+ .expo
14
+ android
15
+ ios
16
+
17
+ # Sensitive credential files — never commit
18
+ credentials.json
19
+ /credentials/
20
+ *.jks
21
+ *.keystore
22
+ *.pem
23
+ *.p8
24
+ *.p12
25
+ *.pfx
26
+ TESTDATEN.txt
27
+
28
+ # Claude Code local settings (may contain command history with secrets)
29
+ .claude/
30
+
31
+ # Playwright MCP screenshots (may contain sensitive UI state)
32
+ .playwright-mcp/
33
+
34
+ # Playwright test run artifacts
35
+ test-results/
36
+ playwright-report/
37
+
38
+ # Test scripts with hardcoded keys — never commit
39
+ test-flow.mjs
40
+ test-gateway.mjs
41
+ packages/shared/test-gateway.mjs
42
+ packages/shared/test-*.mjs
43
+ packages/shared/sign-*.mjs
44
+ packages/shared/e2e-ssh-test.mjs
45
+ scripts/e2e-ssh-test.mjs
46
+
47
+ # Throwaway working dir for E2E scratch files (signed requests, ed25519 keys)
48
+ tmp/
49
+ /tmp/
50
+
51
+ # superpowers brainstorm artifacts (server PIDs, HTML prototypes)
52
+ .superpowers/
53
+
54
+ # Python bytecode cache
55
+ __pycache__/
56
+ *.pyc
57
+
58
+ # Name-clash backup files (Windows duplicate-save format)
59
+ *# Name clash *#*
60
+
61
+ # Stray root-level test screenshots and placeholder configs
62
+ /test-*.png
63
+ /app.json
64
+ graphify-out/
65
+ .graphify_*
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentlock-sdk
3
+ Version: 0.2.0
4
+ Summary: AgentLock Python SDK for AI agent integration
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: httpx>=0.27.0
8
+ Provides-Extra: all
9
+ Requires-Dist: cryptography>=43.0.0; extra == 'all'
10
+ Requires-Dist: pynacl>=1.5.0; extra == 'all'
11
+ Provides-Extra: cryptography
12
+ Requires-Dist: cryptography>=43.0.0; extra == 'cryptography'
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
15
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
16
+ Provides-Extra: nacl
17
+ Requires-Dist: pynacl>=1.5.0; extra == 'nacl'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # agentlock-sdk (Python)
21
+
22
+ Python SDK for integrating AI agents with AgentLock.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install agentlock-sdk
28
+ # or with poetry:
29
+ poetry add agentlock-sdk
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ from agentlock import AgentLockClient, generate_keypair
36
+
37
+ # Generate once
38
+ keypair = generate_keypair()
39
+ print("Public key (register in dashboard):", keypair.public_key)
40
+
41
+ # Create client
42
+ client = AgentLockClient(
43
+ base_url="https://your-agentlock.vercel.app",
44
+ agent_id="your-agent-id",
45
+ private_key=keypair.private_key, # or load from env
46
+ )
47
+
48
+ # Request an action
49
+ result = client.request_action(
50
+ action_type="write",
51
+ tool="demo",
52
+ payload={"message": "Hello from Python!"},
53
+ )
54
+
55
+ print(result.decision) # ALLOW | REQUIRE_APPROVAL | BLOCK
56
+
57
+ if result.status == "PENDING":
58
+ final = client.await_result(result.request_id)
59
+ print(final)
60
+ ```
61
+
62
+ ## SDK Implementation
63
+
64
+ See `src/agentlock/client.py` for the full implementation.
65
+ Uses `pynacl` for Ed25519 signing.
66
+
67
+ ## SSH sessions
68
+
69
+ ```python
70
+ with client.ssh.open(credential_id="cred-1") as session:
71
+ print(session.run("uname -a").stdout)
72
+ print(session.run("uptime").stdout)
73
+ # auto-closes
74
+ ```
75
+
76
+ Re-attach to an existing session:
77
+
78
+ ```python
79
+ sessions = client.ssh.list()
80
+ ops = next((s for s in sessions if s.host == "ops-prod-01"), None)
81
+ if ops:
82
+ print(ops.run("tail -n 50 /var/log/nginx/access.log").stdout)
83
+ ```
84
+
85
+ ## Browser snapshot
86
+
87
+ ```python
88
+ snap = client.browser_snapshot(session_id)
89
+ # {"html": ..., "url": ..., "title": ..., "screenshot": ... (optional)}
90
+ ```
91
+
92
+ ## HTTP requests
93
+
94
+ ```python
95
+ res = client.http_request(
96
+ method="POST",
97
+ url="https://api.example.com/items",
98
+ body={"name": "x"},
99
+ credential_id="cred-api",
100
+ )
101
+ # {"status": 201, "headers": {...}, "body": {...}, "duration_ms": 42}
102
+ ```
@@ -0,0 +1,83 @@
1
+ # agentlock-sdk (Python)
2
+
3
+ Python SDK for integrating AI agents with AgentLock.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install agentlock-sdk
9
+ # or with poetry:
10
+ poetry add agentlock-sdk
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from agentlock import AgentLockClient, generate_keypair
17
+
18
+ # Generate once
19
+ keypair = generate_keypair()
20
+ print("Public key (register in dashboard):", keypair.public_key)
21
+
22
+ # Create client
23
+ client = AgentLockClient(
24
+ base_url="https://your-agentlock.vercel.app",
25
+ agent_id="your-agent-id",
26
+ private_key=keypair.private_key, # or load from env
27
+ )
28
+
29
+ # Request an action
30
+ result = client.request_action(
31
+ action_type="write",
32
+ tool="demo",
33
+ payload={"message": "Hello from Python!"},
34
+ )
35
+
36
+ print(result.decision) # ALLOW | REQUIRE_APPROVAL | BLOCK
37
+
38
+ if result.status == "PENDING":
39
+ final = client.await_result(result.request_id)
40
+ print(final)
41
+ ```
42
+
43
+ ## SDK Implementation
44
+
45
+ See `src/agentlock/client.py` for the full implementation.
46
+ Uses `pynacl` for Ed25519 signing.
47
+
48
+ ## SSH sessions
49
+
50
+ ```python
51
+ with client.ssh.open(credential_id="cred-1") as session:
52
+ print(session.run("uname -a").stdout)
53
+ print(session.run("uptime").stdout)
54
+ # auto-closes
55
+ ```
56
+
57
+ Re-attach to an existing session:
58
+
59
+ ```python
60
+ sessions = client.ssh.list()
61
+ ops = next((s for s in sessions if s.host == "ops-prod-01"), None)
62
+ if ops:
63
+ print(ops.run("tail -n 50 /var/log/nginx/access.log").stdout)
64
+ ```
65
+
66
+ ## Browser snapshot
67
+
68
+ ```python
69
+ snap = client.browser_snapshot(session_id)
70
+ # {"html": ..., "url": ..., "title": ..., "screenshot": ... (optional)}
71
+ ```
72
+
73
+ ## HTTP requests
74
+
75
+ ```python
76
+ res = client.http_request(
77
+ method="POST",
78
+ url="https://api.example.com/items",
79
+ body={"name": "x"},
80
+ credential_id="cred-api",
81
+ )
82
+ # {"status": 201, "headers": {...}, "body": {...}, "duration_ms": 42}
83
+ ```
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentlock-sdk"
7
+ version = "0.2.0"
8
+ description = "AgentLock Python SDK for AI agent integration"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ dependencies = [
13
+ "httpx>=0.27.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ nacl = ["pynacl>=1.5.0"]
18
+ cryptography = ["cryptography>=43.0.0"]
19
+ all = ["pynacl>=1.5.0", "cryptography>=43.0.0"]
20
+ dev = ["pytest>=8.0.0", "pytest-asyncio>=0.24.0"]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/agentlock"]
@@ -0,0 +1,36 @@
1
+ """AgentLock Python SDK"""
2
+
3
+ from .client import (
4
+ AgentLockClient,
5
+ create_client,
6
+ GatewayResult,
7
+ ExecutionResult,
8
+ BrowserActionResult,
9
+ McpToolInfo,
10
+ McpListToolsResult,
11
+ McpContentItem,
12
+ McpCallToolResult,
13
+ AgentMessage,
14
+ NewThreadResult,
15
+ )
16
+ from .errors import (
17
+ AgentLockError,
18
+ ApprovalDeniedError,
19
+ ApprovalTimeoutError,
20
+ SshSessionExpiredError,
21
+ SshSessionClosedError,
22
+ )
23
+ from .signing import generate_keypair, KeyPair
24
+ from .ssh import SshSession, SshRunResult
25
+
26
+ __all__ = [
27
+ "AgentLockClient", "create_client",
28
+ "AgentMessage", "BrowserActionResult", "ExecutionResult", "GatewayResult",
29
+ "McpCallToolResult", "McpContentItem", "McpListToolsResult", "McpToolInfo",
30
+ "NewThreadResult",
31
+ "SshSession", "SshRunResult",
32
+ "AgentLockError", "ApprovalDeniedError", "ApprovalTimeoutError",
33
+ "SshSessionExpiredError", "SshSessionClosedError",
34
+ "generate_keypair", "KeyPair",
35
+ ]
36
+ __version__ = "0.2.0"
@@ -0,0 +1,102 @@
1
+ """Base mixin: HTTP transport, signing, request/await primitives, shared dataclasses."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Optional
7
+
8
+ import httpx
9
+
10
+ from .signing import sign_request
11
+
12
+
13
+ @dataclass
14
+ class GatewayResult:
15
+ request_id: str
16
+ decision: str
17
+ status: str
18
+ message: Optional[str] = None
19
+ expires_at: Optional[str] = None
20
+
21
+
22
+ @dataclass
23
+ class ExecutionResult:
24
+ status: str
25
+ result: Optional[Dict[str, Any]] = None
26
+ error: Optional[str] = None
27
+
28
+
29
+ class _BaseClient:
30
+ """HTTP plumbing shared by all per-connector mixins.
31
+
32
+ Subclasses MUST set: ``base_url``, ``agent_id``, ``private_key``, ``timeout``,
33
+ ``_client`` (an ``httpx.Client``).
34
+ """
35
+
36
+ base_url: str
37
+ agent_id: str
38
+ private_key: str
39
+ timeout: float
40
+ _client: httpx.Client
41
+
42
+ def _request_with_retry(self, method: str, url: str, **kwargs) -> httpx.Response:
43
+ max_retries = 3
44
+ for attempt in range(max_retries + 1):
45
+ response = self._client.request(method, url, **kwargs)
46
+ if response.status_code not in (429, 502, 503, 504) or attempt == max_retries:
47
+ response.raise_for_status()
48
+ return response
49
+ retry_after = response.headers.get('retry-after')
50
+ delay = int(retry_after) if retry_after else min(2 ** attempt, 10)
51
+ time.sleep(delay)
52
+ raise RuntimeError('Unreachable')
53
+
54
+ def request_action(
55
+ self,
56
+ action_type: str,
57
+ tool: str,
58
+ payload: Dict[str, Any],
59
+ cost_estimate: Optional[float] = None,
60
+ idempotency_key: Optional[str] = None,
61
+ ) -> GatewayResult:
62
+ body: Dict[str, Any] = {"action_type": action_type, "tool": tool, "payload": payload}
63
+ if cost_estimate is not None:
64
+ body["cost_estimate"] = cost_estimate
65
+ if idempotency_key:
66
+ body["idempotency_key"] = idempotency_key
67
+
68
+ headers = sign_request(body, self.agent_id, self.private_key)
69
+ headers["Content-Type"] = "application/json"
70
+
71
+ response = self._request_with_retry(
72
+ "POST", f"{self.base_url}/api/gateway/request", json=body, headers=headers,
73
+ )
74
+ data = response.json()
75
+ return GatewayResult(
76
+ request_id=data["request_id"],
77
+ decision=data["decision"],
78
+ status=data["status"],
79
+ message=data.get("message"),
80
+ expires_at=data.get("expires_at"),
81
+ )
82
+
83
+ def await_result(
84
+ self,
85
+ request_id: str,
86
+ poll_interval: float = 2.0,
87
+ timeout: float = 300.0,
88
+ ) -> ExecutionResult:
89
+ deadline = time.time() + timeout
90
+ while time.time() < deadline:
91
+ poll_body = {"requestId": request_id}
92
+ headers = sign_request(poll_body, self.agent_id, self.private_key)
93
+ response = self._request_with_retry(
94
+ "GET", f"{self.base_url}/api/gateway/result/{request_id}", headers=headers,
95
+ )
96
+ data = response.json()
97
+ if data["status"] in {"SUCCEEDED", "FAILED", "DENIED", "EXPIRED", "BLOCKED"}:
98
+ return ExecutionResult(
99
+ status=data["status"], result=data.get("result"), error=data.get("error"),
100
+ )
101
+ time.sleep(poll_interval)
102
+ raise TimeoutError(f"Timeout waiting for result of request {request_id}")
@@ -0,0 +1,122 @@
1
+ """Browser session mixin."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from .signing import sign_request
8
+ from ._base import _BaseClient
9
+
10
+
11
+ @dataclass
12
+ class BrowserActionResult:
13
+ session_id: str
14
+ snapshot: str
15
+ page_url: str
16
+ page_title: str
17
+ action_performed: str = ""
18
+ screenshot: Optional[str] = None
19
+
20
+
21
+ class _BrowserMixin(_BaseClient):
22
+ def browser_open(
23
+ self,
24
+ url: str,
25
+ *,
26
+ allowed_domains: Optional[List[str]] = None,
27
+ ) -> BrowserActionResult:
28
+ payload: Dict[str, Any] = {"url": url}
29
+ if allowed_domains is not None:
30
+ payload["allowed_domains"] = allowed_domains
31
+
32
+ gateway_result = self.request_action(
33
+ action_type="write", tool="browser.open", payload=payload,
34
+ )
35
+ if gateway_result.status == "BLOCKED":
36
+ raise RuntimeError(f"Browser open blocked: {gateway_result.message}")
37
+
38
+ final_result = self.await_result(gateway_result.request_id)
39
+ if final_result.status == "DENIED":
40
+ raise RuntimeError("Browser session was denied")
41
+ if final_result.status == "EXPIRED":
42
+ raise RuntimeError("Browser session approval expired")
43
+ if final_result.status == "FAILED":
44
+ raise RuntimeError(f"Browser open failed: {final_result.error}")
45
+
46
+ result = final_result.result or {}
47
+ if not result.get("session_id"):
48
+ raise RuntimeError("No session_id in browser.open result")
49
+
50
+ return BrowserActionResult(
51
+ session_id=result["session_id"],
52
+ snapshot=result.get("snapshot", ""),
53
+ page_url=result.get("page_url", ""),
54
+ page_title=result.get("page_title", ""),
55
+ action_performed=result.get("action_performed", ""),
56
+ screenshot=result.get("screenshot"),
57
+ )
58
+
59
+ def browser_action(
60
+ self,
61
+ session_id: str,
62
+ tool: str,
63
+ params: Optional[Dict[str, Any]] = None,
64
+ ) -> BrowserActionResult:
65
+ if not tool.startswith("browser."):
66
+ tool = f"browser.{tool}"
67
+ payload: Dict[str, Any] = {"session_id": session_id}
68
+ if params:
69
+ payload.update(params)
70
+
71
+ body: Dict[str, Any] = {"action_type": "write", "tool": tool, "payload": payload}
72
+ headers = sign_request(body, self.agent_id, self.private_key)
73
+ headers["Content-Type"] = "application/json"
74
+
75
+ response = self._client.post(
76
+ f"{self.base_url}/api/gateway/request", json=body, headers=headers,
77
+ )
78
+ if not response.is_success:
79
+ err = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
80
+ raise RuntimeError(f"Browser action failed: {err.get('error', response.text)}")
81
+
82
+ data = response.json()
83
+ result = data.get("result")
84
+ if not result:
85
+ raise RuntimeError(data.get("error", "No result from browser action"))
86
+
87
+ return BrowserActionResult(
88
+ session_id=result.get("session_id", session_id),
89
+ snapshot=result.get("snapshot", ""),
90
+ page_url=result.get("page_url", ""),
91
+ page_title=result.get("page_title", ""),
92
+ action_performed=result.get("action_performed", ""),
93
+ screenshot=result.get("screenshot"),
94
+ )
95
+
96
+ def browser_fill_credentials(
97
+ self, session_id: str, credential_name: str, field_mapping: Dict[str, str],
98
+ ) -> BrowserActionResult:
99
+ return self.browser_action(
100
+ session_id, "browser.fill_credentials",
101
+ params={"credential_name": credential_name, "field_mapping": field_mapping},
102
+ )
103
+
104
+ def browser_close(self, session_id: str) -> BrowserActionResult:
105
+ return self.browser_action(session_id, "browser.close")
106
+
107
+ def browser_snapshot(self, session_id: str) -> Dict[str, Any]:
108
+ """Take an accessibility-tree snapshot of the current page.
109
+
110
+ Convenience wrapper over ``browser_action(session_id, 'snapshot')``.
111
+ Returns a dict with ``html``, ``url``, ``title``, and optionally
112
+ ``screenshot`` (base64) when the runner returns one.
113
+ """
114
+ r = self.browser_action(session_id, "browser.snapshot")
115
+ out: Dict[str, Any] = {
116
+ "html": r.snapshot,
117
+ "url": r.page_url,
118
+ "title": r.page_title,
119
+ }
120
+ if r.screenshot:
121
+ out["screenshot"] = r.screenshot
122
+ return out
@@ -0,0 +1,74 @@
1
+ """AgentLock Python SDK Client (composition root).
2
+
3
+ The actual connector logic lives in per-mixin modules: _base, browser,
4
+ mcp, messaging. This file wires them together so the public
5
+ ``AgentLockClient`` keeps its flat, instance-method-per-tool API.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import httpx
10
+
11
+ from ._base import _BaseClient, GatewayResult, ExecutionResult
12
+ from .browser import _BrowserMixin, BrowserActionResult
13
+ from .http import _HttpMixin
14
+ from .mcp import _McpMixin, McpToolInfo, McpListToolsResult, McpContentItem, McpCallToolResult
15
+ from .messaging import _MessagingMixin, AgentMessage, NewThreadResult
16
+ from .ssh import _SshMixin, SshSession, SshRunResult
17
+
18
+
19
+ class AgentLockClient(
20
+ _SshMixin,
21
+ _BrowserMixin,
22
+ _McpMixin,
23
+ _MessagingMixin,
24
+ _HttpMixin,
25
+ _BaseClient,
26
+ ):
27
+ def __init__(
28
+ self,
29
+ base_url: str,
30
+ agent_id: str,
31
+ private_key: str,
32
+ timeout: float = 30.0,
33
+ ):
34
+ if not base_url.startswith("https://") and not base_url.startswith("http://localhost"):
35
+ raise ValueError(
36
+ "AgentLockClient: base_url must use HTTPS (or http://localhost for development)"
37
+ )
38
+ self.base_url = base_url.rstrip("/")
39
+ self.agent_id = agent_id
40
+ self.private_key = private_key
41
+ self.timeout = timeout
42
+ self._client = httpx.Client(timeout=self.timeout)
43
+
44
+ def close(self):
45
+ self._client.close()
46
+ self.private_key = None # type: ignore[assignment]
47
+
48
+ def __enter__(self):
49
+ return self
50
+
51
+ def __exit__(self, *args):
52
+ self.close()
53
+
54
+ def __repr__(self) -> str:
55
+ return f"AgentLockClient(agent_id={self.agent_id!r})"
56
+
57
+
58
+ def create_client(
59
+ base_url: str, agent_id: str, private_key: str, timeout: float = 30.0,
60
+ ) -> AgentLockClient:
61
+ return AgentLockClient(
62
+ base_url=base_url, agent_id=agent_id, private_key=private_key, timeout=timeout,
63
+ )
64
+
65
+
66
+ # Re-export public dataclasses so existing `from agentlock.client import X` works.
67
+ __all__ = [
68
+ "AgentLockClient", "create_client",
69
+ "GatewayResult", "ExecutionResult",
70
+ "BrowserActionResult",
71
+ "McpToolInfo", "McpListToolsResult", "McpContentItem", "McpCallToolResult",
72
+ "AgentMessage", "NewThreadResult",
73
+ "SshSession", "SshRunResult",
74
+ ]
@@ -0,0 +1,38 @@
1
+ """Typed error classes for the AgentLock SDK."""
2
+ from __future__ import annotations
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+
7
+ class AgentLockError(Exception):
8
+ """Base class for all AgentLock SDK errors."""
9
+
10
+
11
+ class ApprovalDeniedError(AgentLockError):
12
+ def __init__(self, request_id: str, denial_reason: Optional[str] = None):
13
+ self.request_id = request_id
14
+ self.denial_reason = denial_reason
15
+ msg = f"Approval denied for request {request_id}"
16
+ if denial_reason:
17
+ msg += f": {denial_reason}"
18
+ super().__init__(msg)
19
+
20
+
21
+ class ApprovalTimeoutError(AgentLockError):
22
+ def __init__(self, request_id: str, waited_ms: int):
23
+ self.request_id = request_id
24
+ self.waited_ms = waited_ms
25
+ super().__init__(f"Approval timed out for request {request_id} after {waited_ms}ms")
26
+
27
+
28
+ class SshSessionExpiredError(AgentLockError):
29
+ def __init__(self, session_id: str, expires_at: datetime):
30
+ self.session_id = session_id
31
+ self.expires_at = expires_at
32
+ super().__init__(f"SSH session {session_id} expired at {expires_at.isoformat()}")
33
+
34
+
35
+ class SshSessionClosedError(AgentLockError):
36
+ def __init__(self, session_id: str):
37
+ self.session_id = session_id
38
+ super().__init__(f"SSH session {session_id} has been closed")