agoralia-cli 0.1.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,5 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .pytest_cache/
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: agoralia-cli
3
+ Version: 0.1.0
4
+ Summary: Agoralia CLI — the gated agent surface (MCP · REST · CLI) for compliant AI voice campaigns
5
+ Project-URL: Homepage, https://agoralia.app
6
+ Project-URL: Documentation, https://docs.agoralia.app
7
+ Project-URL: Source, https://github.com/giacomocavalcabo/agoralia-v2
8
+ Author: Agoralia
9
+ License-Expression: MIT
10
+ Keywords: agent,agoralia,ai,cli,mcp,voice
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Communications :: Telephony
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.24
18
+ Description-Content-Type: text/markdown
19
+
20
+ # agoralia-cli
21
+
22
+ The **Agoralia CLI** — the third agent surface (alongside MCP and the REST API) for running
23
+ compliant AI voice campaigns. It's a thin client over the *already-gated* REST API: same
24
+ policy, same human-approval flow, same audit. It adds nothing privileged — the gate lives
25
+ server-side; the CLI just speaks HTTP to it and renders the guided errors it returns.
26
+
27
+ Every call is tagged `X-Agent-Surface: cli`, so the action audit records that it came from
28
+ the CLI.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install agoralia-cli
34
+ ```
35
+
36
+ ## Authenticate
37
+
38
+ The CLI uses a Bearer token (the same kind the MCP surface uses).
39
+
40
+ ```bash
41
+ export AGORALIA_TOKEN=... # your Agoralia access token
42
+ export AGORALIA_API_URL=https://api.agoralia.app # optional (defaults to localhost:8000)
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```bash
48
+ # Generative dashboard pages (the agent composes a view; you see it at /pages/<slug>)
49
+ agoralia pages list
50
+ agoralia pages get q2-reactivation-calls
51
+ agoralia pages create ./spec.json
52
+
53
+ # Human "go" queue — approve/deny dangerous agent actions
54
+ agoralia approvals list
55
+ agoralia approvals decide <approval-id> approved
56
+
57
+ # Read-only audit of what your agents did
58
+ agoralia activity --limit 50
59
+
60
+ # Generic escape hatch to any gated route
61
+ agoralia call GET /campaigns
62
+ agoralia call POST /campaigns/<id>/start --data '{}'
63
+ ```
64
+
65
+ ## The approval flow
66
+
67
+ Dangerous actions (launching a campaign, spending, deleting) are gated. When you hit one
68
+ without an approval, the CLI tells you exactly what to do:
69
+
70
+ ```bash
71
+ $ agoralia call POST /campaigns/abc/start
72
+ ⚠ Approval required before this action can run.
73
+ approval_id: appr_123
74
+ → A human must approve it (POST /approvals/{id}/decide), then retry with header X-Approval-Id.
75
+ Once approved, retry with: --approval-id appr_123
76
+
77
+ # a human approves it…
78
+ $ agoralia approvals decide appr_123 approved
79
+
80
+ # …then retry
81
+ $ agoralia call POST /campaigns/abc/start --approval-id appr_123
82
+ ```
83
+
84
+ (Approval enforcement is controlled server-side by the `AGENT_APPROVAL_ENABLED` flag.)
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ pip install -e .
90
+ python -m pytest tests/
91
+ python -m agoralia_cli --help # same as the `agoralia` command
92
+ ```
@@ -0,0 +1,73 @@
1
+ # agoralia-cli
2
+
3
+ The **Agoralia CLI** — the third agent surface (alongside MCP and the REST API) for running
4
+ compliant AI voice campaigns. It's a thin client over the *already-gated* REST API: same
5
+ policy, same human-approval flow, same audit. It adds nothing privileged — the gate lives
6
+ server-side; the CLI just speaks HTTP to it and renders the guided errors it returns.
7
+
8
+ Every call is tagged `X-Agent-Surface: cli`, so the action audit records that it came from
9
+ the CLI.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install agoralia-cli
15
+ ```
16
+
17
+ ## Authenticate
18
+
19
+ The CLI uses a Bearer token (the same kind the MCP surface uses).
20
+
21
+ ```bash
22
+ export AGORALIA_TOKEN=... # your Agoralia access token
23
+ export AGORALIA_API_URL=https://api.agoralia.app # optional (defaults to localhost:8000)
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ # Generative dashboard pages (the agent composes a view; you see it at /pages/<slug>)
30
+ agoralia pages list
31
+ agoralia pages get q2-reactivation-calls
32
+ agoralia pages create ./spec.json
33
+
34
+ # Human "go" queue — approve/deny dangerous agent actions
35
+ agoralia approvals list
36
+ agoralia approvals decide <approval-id> approved
37
+
38
+ # Read-only audit of what your agents did
39
+ agoralia activity --limit 50
40
+
41
+ # Generic escape hatch to any gated route
42
+ agoralia call GET /campaigns
43
+ agoralia call POST /campaigns/<id>/start --data '{}'
44
+ ```
45
+
46
+ ## The approval flow
47
+
48
+ Dangerous actions (launching a campaign, spending, deleting) are gated. When you hit one
49
+ without an approval, the CLI tells you exactly what to do:
50
+
51
+ ```bash
52
+ $ agoralia call POST /campaigns/abc/start
53
+ ⚠ Approval required before this action can run.
54
+ approval_id: appr_123
55
+ → A human must approve it (POST /approvals/{id}/decide), then retry with header X-Approval-Id.
56
+ Once approved, retry with: --approval-id appr_123
57
+
58
+ # a human approves it…
59
+ $ agoralia approvals decide appr_123 approved
60
+
61
+ # …then retry
62
+ $ agoralia call POST /campaigns/abc/start --approval-id appr_123
63
+ ```
64
+
65
+ (Approval enforcement is controlled server-side by the `AGENT_APPROVAL_ENABLED` flag.)
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ pip install -e .
71
+ python -m pytest tests/
72
+ python -m agoralia_cli --help # same as the `agoralia` command
73
+ ```
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agoralia-cli"
7
+ version = "0.1.0"
8
+ description = "Agoralia CLI — the gated agent surface (MCP · REST · CLI) for compliant AI voice campaigns"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Agoralia" }]
13
+ keywords = ["agoralia", "voice", "ai", "agent", "mcp", "cli"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Communications :: Telephony",
20
+ ]
21
+ dependencies = ["httpx>=0.24"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://agoralia.app"
25
+ Documentation = "https://docs.agoralia.app"
26
+ Source = "https://github.com/giacomocavalcabo/agoralia-v2"
27
+
28
+ [project.scripts]
29
+ agoralia = "agoralia_cli.main:main"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/agoralia_cli"]
33
+
34
+ [tool.hatch.build.targets.sdist]
35
+ include = ["src/agoralia_cli", "README.md"]
@@ -0,0 +1,11 @@
1
+ """
2
+ Agoralia CLI — the third agent surface (after MCP and REST).
3
+
4
+ A thin client over the *already-gated* REST API: same policy core, same approval flow,
5
+ same audit. It adds nothing privileged — it just speaks HTTP to the gate and tags every
6
+ call `X-Agent-Surface: cli` so the audit knows the origin. The gating lives server-side;
7
+ the CLI only renders the guided errors the gate returns. See docs/AGENT_LAYER.md.
8
+ """
9
+ from agoralia_cli.client import ApprovalRequired, CliClient, CliError
10
+
11
+ __all__ = ["CliClient", "CliError", "ApprovalRequired"]
@@ -0,0 +1,4 @@
1
+ from agoralia_cli.main import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,55 @@
1
+ """HTTP client for the Agoralia CLI — a thin, gated client over the REST API."""
2
+ import httpx
3
+
4
+
5
+ class CliError(Exception):
6
+ """A non-approval error from the API (with the server's message/detail)."""
7
+
8
+
9
+ class ApprovalRequired(CliError):
10
+ """The gate blocked a dangerous action pending human approval.
11
+
12
+ Carries the approval_id + remediation so the CLI can tell the user exactly what to
13
+ do (approve it, then retry with --approval-id). This is the guided-error contract
14
+ surfacing on the CLI, identical to MCP/REST.
15
+ """
16
+
17
+ def __init__(self, approval_id: str | None, remediation: list[str]):
18
+ self.approval_id = approval_id
19
+ self.remediation = remediation
20
+ super().__init__("Approval required before this action can run.")
21
+
22
+
23
+ class CliClient:
24
+ """Speaks HTTP to the gated REST API. Auth = Bearer token; surface = cli."""
25
+
26
+ def __init__(self, base_url: str, token: str, *, approval_id: str | None = None, timeout: float = 30.0):
27
+ if not token:
28
+ raise CliError("Not authenticated. Set AGORALIA_TOKEN (or pass --token).")
29
+ self.base_url = base_url.rstrip("/")
30
+ self.timeout = timeout
31
+ self.headers = {"Authorization": f"Bearer {token}", "X-Agent-Surface": "cli"}
32
+ if approval_id:
33
+ self.headers["X-Approval-Id"] = approval_id
34
+
35
+ def request(self, method: str, path: str, *, json: dict | None = None, params: dict | None = None) -> dict:
36
+ with httpx.Client(timeout=self.timeout) as client:
37
+ resp = client.request(method, f"{self.base_url}{path}", headers=self.headers, json=json, params=params)
38
+ return self._handle(resp)
39
+
40
+ def _handle(self, resp: httpx.Response) -> dict:
41
+ if resp.status_code == 204:
42
+ return {"ok": True}
43
+ try:
44
+ body = resp.json()
45
+ except Exception:
46
+ body = {}
47
+ if resp.is_success:
48
+ return body
49
+
50
+ detail = body.get("detail", body) if isinstance(body, dict) else body
51
+ # Guided error: a dangerous action awaiting human "go".
52
+ if isinstance(detail, dict) and detail.get("code") == "approval_required":
53
+ raise ApprovalRequired(detail.get("approval_id"), detail.get("remediation", []))
54
+ msg = detail.get("error") if isinstance(detail, dict) else detail
55
+ raise CliError(f"HTTP {resp.status_code}: {msg or 'request failed'}")
@@ -0,0 +1,205 @@
1
+ """
2
+ Agoralia CLI entrypoint. Thin gated client over the REST API.
3
+
4
+ agoralia auth login --token <TOKEN> # mint a token in the dashboard, then store it
5
+ # or: export AGORALIA_TOKEN=... # Bearer token (same as the MCP surface)
6
+ # AGORALIA_API_URL defaults to https://api.agoralia.app
7
+
8
+ agoralia pages list
9
+ agoralia pages get <slug>
10
+ agoralia pages create spec.json
11
+ agoralia approvals list
12
+ agoralia approvals decide <id> approved
13
+ agoralia activity --limit 50
14
+ agoralia call GET /campaigns # generic escape hatch to any gated route
15
+ agoralia call POST /campaigns/<id>/start --approval-id <id>
16
+
17
+ Every call is gated server-side and tagged surface=cli in the audit. Dangerous actions
18
+ return a guided 'approval required' the CLI renders with the id + how to retry.
19
+ """
20
+ import argparse
21
+ import json
22
+ import os
23
+ import pathlib
24
+ import sys
25
+
26
+ from agoralia_cli.client import ApprovalRequired, CliClient, CliError
27
+
28
+ DEFAULT_URL = "https://api.agoralia.app"
29
+ CONFIG_FILE = pathlib.Path(os.path.expanduser("~/.agoralia/credentials.json"))
30
+
31
+
32
+ def _load_config() -> dict:
33
+ try:
34
+ return json.loads(CONFIG_FILE.read_text())
35
+ except Exception:
36
+ return {}
37
+
38
+
39
+ def _save_config(cfg: dict) -> None:
40
+ CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
41
+ CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
42
+ try:
43
+ CONFIG_FILE.chmod(0o600) # token is a secret
44
+ except OSError:
45
+ pass
46
+
47
+
48
+ def _resolve_token(args: argparse.Namespace) -> str:
49
+ return args.token or os.getenv("AGORALIA_TOKEN") or _load_config().get("token", "")
50
+
51
+
52
+ def _resolve_url(args: argparse.Namespace) -> str:
53
+ return args.api_url or os.getenv("AGORALIA_API_URL") or _load_config().get("api_url") or DEFAULT_URL
54
+
55
+
56
+ def _client(args: argparse.Namespace) -> CliClient:
57
+ return CliClient(_resolve_url(args), _resolve_token(args), approval_id=args.approval_id)
58
+
59
+
60
+ def _handle_auth(args: argparse.Namespace) -> int:
61
+ if args.action == "login":
62
+ token = args.token or os.getenv("AGORALIA_TOKEN", "")
63
+ if not token:
64
+ print(
65
+ "No token provided. Mint one in the dashboard (Settings → CLI tokens, or "
66
+ "POST /cli/token), then run:\n agoralia auth login --token <TOKEN>",
67
+ file=sys.stderr,
68
+ )
69
+ return 1
70
+ cfg = _load_config()
71
+ cfg["token"] = token
72
+ cfg["api_url"] = args.api_url or os.getenv("AGORALIA_API_URL") or DEFAULT_URL
73
+ _save_config(cfg)
74
+ print(f"Logged in. Token stored in {CONFIG_FILE} (api_url: {cfg['api_url']}).")
75
+ return 0
76
+ if args.action == "logout":
77
+ cfg = _load_config()
78
+ cfg.pop("token", None)
79
+ _save_config(cfg)
80
+ print("Logged out (token removed).")
81
+ return 0
82
+ if args.action == "status":
83
+ cfg = _load_config()
84
+ has = bool(_resolve_token(args))
85
+ print(json.dumps({
86
+ "authenticated": has,
87
+ "api_url": _resolve_url(args),
88
+ "token_source": "flag/env" if (args.token or os.getenv("AGORALIA_TOKEN")) else ("config" if cfg.get("token") else "none"),
89
+ }, indent=2))
90
+ return 0
91
+ return 1
92
+
93
+
94
+ def _print(data) -> None:
95
+ print(json.dumps(data, indent=2, default=str))
96
+
97
+
98
+ def build_parser() -> argparse.ArgumentParser:
99
+ p = argparse.ArgumentParser(prog="agoralia", description="Agoralia CLI — gated agent surface")
100
+ p.add_argument("--api-url", help="API base URL (or AGORALIA_API_URL)")
101
+ p.add_argument("--token", help="Bearer token (or AGORALIA_TOKEN)")
102
+ p.add_argument("--approval-id", help="Retry a dangerous action with a granted approval id")
103
+ sub = p.add_subparsers(dest="resource", required=True)
104
+
105
+ auth = sub.add_parser("auth", help="Authenticate the CLI").add_subparsers(dest="action", required=True)
106
+ al = auth.add_parser("login", help="Store a CLI token")
107
+ al.add_argument("--token", help="CLI token (or AGORALIA_TOKEN)")
108
+ al.add_argument("--api-url", help="API base URL (default https://api.agoralia.app)")
109
+ auth.add_parser("logout", help="Remove the stored token")
110
+ auth.add_parser("status", help="Show auth status")
111
+
112
+ pages = sub.add_parser("pages", help="Generative dashboard pages").add_subparsers(dest="action", required=True)
113
+ pages.add_parser("list")
114
+ g = pages.add_parser("get"); g.add_argument("slug")
115
+ c = pages.add_parser("create"); c.add_argument("spec_file", help="path to a view-spec JSON file")
116
+
117
+ appr = sub.add_parser("approvals", help="Human-go queue").add_subparsers(dest="action", required=True)
118
+ appr.add_parser("list")
119
+ d = appr.add_parser("decide"); d.add_argument("id"); d.add_argument("decision", choices=["approved", "denied"])
120
+
121
+ act = sub.add_parser("activity", help="Audit of agent actions")
122
+ act.add_argument("--limit", type=int, default=50)
123
+
124
+ conns = sub.add_parser("connections", help="Remote MCP servers the copilot can use").add_subparsers(dest="action", required=True)
125
+ conns.add_parser("list")
126
+ cc = conns.add_parser("connect", help="Connect a remote MCP server")
127
+ cc.add_argument("name")
128
+ cc.add_argument("url")
129
+ cc.add_argument("--auth-token", help="OAuth bearer token (stored encrypted server-side)")
130
+ cc.add_argument("--allowed-tool", action="append", default=[], dest="allowed_tools",
131
+ help="Whitelist a tool (repeatable). Omit to allow all tools.")
132
+ cd = conns.add_parser("disconnect"); cd.add_argument("id")
133
+
134
+ call = sub.add_parser("call", help="Generic gated request to any route")
135
+ call.add_argument("method", choices=["GET", "POST", "PATCH", "DELETE"])
136
+ call.add_argument("path")
137
+ call.add_argument("--data", help="JSON body (string or @file)")
138
+ return p
139
+
140
+
141
+ def dispatch(args: argparse.Namespace, client: CliClient) -> dict:
142
+ if args.resource == "pages":
143
+ if args.action == "list":
144
+ return client.request("GET", "/pages")
145
+ if args.action == "get":
146
+ return client.request("GET", f"/pages/{args.slug}")
147
+ if args.action == "create":
148
+ with open(args.spec_file) as f:
149
+ spec = json.load(f)
150
+ return client.request("POST", "/pages", json={"spec": spec})
151
+
152
+ if args.resource == "approvals":
153
+ if args.action == "list":
154
+ return client.request("GET", "/approvals")
155
+ if args.action == "decide":
156
+ return client.request("POST", f"/approvals/{args.id}/decide", json={"decision": args.decision})
157
+
158
+ if args.resource == "activity":
159
+ return client.request("GET", "/actions", params={"limit": args.limit})
160
+
161
+ if args.resource == "connections":
162
+ if args.action == "list":
163
+ return client.request("GET", "/mcp-connections")
164
+ if args.action == "connect":
165
+ body = {"name": args.name, "url": args.url, "allowed_tools": args.allowed_tools}
166
+ if args.auth_token:
167
+ body["auth_token"] = args.auth_token
168
+ return client.request("POST", "/mcp-connections", json=body)
169
+ if args.action == "disconnect":
170
+ return client.request("DELETE", f"/mcp-connections/{args.id}")
171
+
172
+ if args.resource == "call":
173
+ body = None
174
+ if args.data:
175
+ raw = args.data[1:] and open(args.data[1:]).read() if args.data.startswith("@") else args.data
176
+ body = json.loads(raw)
177
+ return client.request(args.method, args.path, json=body)
178
+
179
+ raise CliError(f"Unknown command: {args.resource}")
180
+
181
+
182
+ def main(argv: list[str] | None = None) -> int:
183
+ args = build_parser().parse_args(argv)
184
+ if args.resource == "auth":
185
+ return _handle_auth(args)
186
+ try:
187
+ result = dispatch(args, _client(args))
188
+ _print(result)
189
+ return 0
190
+ except ApprovalRequired as e:
191
+ print("⚠ Approval required before this action can run.", file=sys.stderr)
192
+ if e.approval_id:
193
+ print(f" approval_id: {e.approval_id}", file=sys.stderr)
194
+ for step in e.remediation:
195
+ print(f" → {step}", file=sys.stderr)
196
+ if e.approval_id:
197
+ print(f" Once approved, retry with: --approval-id {e.approval_id}", file=sys.stderr)
198
+ return 2
199
+ except CliError as e:
200
+ print(f"Error: {e}", file=sys.stderr)
201
+ return 1
202
+
203
+
204
+ if __name__ == "__main__":
205
+ raise SystemExit(main())