payd-labs-sentinel-cli 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.
- payd_labs_sentinel_cli-0.3.0/.gitignore +34 -0
- payd_labs_sentinel_cli-0.3.0/PKG-INFO +210 -0
- payd_labs_sentinel_cli-0.3.0/README.md +187 -0
- payd_labs_sentinel_cli-0.3.0/pyproject.toml +42 -0
- payd_labs_sentinel_cli-0.3.0/sentinel_cli/__init__.py +1 -0
- payd_labs_sentinel_cli-0.3.0/sentinel_cli/auth.py +168 -0
- payd_labs_sentinel_cli-0.3.0/sentinel_cli/cli.py +1328 -0
- payd_labs_sentinel_cli-0.3.0/sentinel_cli/client.py +321 -0
- payd_labs_sentinel_cli-0.3.0/sentinel_cli/config.py +16 -0
- payd_labs_sentinel_cli-0.3.0/sentinel_cli/mcp_server.py +781 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
*.egg
|
|
8
|
+
|
|
9
|
+
# Node
|
|
10
|
+
node_modules/
|
|
11
|
+
dist/
|
|
12
|
+
|
|
13
|
+
# Environment
|
|
14
|
+
.env
|
|
15
|
+
.env.local
|
|
16
|
+
.env.*.local
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
*.swo
|
|
23
|
+
*~
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
Thumbs.db
|
|
28
|
+
|
|
29
|
+
# Data
|
|
30
|
+
*.db
|
|
31
|
+
/data/
|
|
32
|
+
|
|
33
|
+
# Docker
|
|
34
|
+
docker-compose.override.yml
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: payd-labs-sentinel-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: CLI and MCP server for the Sentinel DevOps portal - manage deployments, services, and projects
|
|
5
|
+
Project-URL: Homepage, https://sentinel.paydlabs.com
|
|
6
|
+
Project-URL: Repository, https://github.com/getpayd-tech/payd-labs-sentinel-v1
|
|
7
|
+
Author-email: Payd Labs <dev@payd.money>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: cli,deployment,devops,docker,mcp
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
16
|
+
Classifier: Topic :: System :: Systems Administration
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: httpx>=0.28.0
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
20
|
+
Requires-Dist: rich>=13.0.0
|
|
21
|
+
Requires-Dist: typer>=0.15.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# sentinel-cli
|
|
25
|
+
|
|
26
|
+
CLI and MCP server for the [Sentinel](https://sentinel.paydlabs.com) DevOps portal. Manage deployments end-to-end from the terminal or via AI agents.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python3.12 -m pip install payd-labs-sentinel-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.12+.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
payd-sentinel login # one-time OTP via Payd Auth, caches token at ~/.sentinel/
|
|
40
|
+
|
|
41
|
+
# End-to-end bootstrap of a new service (one command):
|
|
42
|
+
payd-sentinel bootstrap \
|
|
43
|
+
--name my-app --type fastapi --domain my-app.paydlabs.com \
|
|
44
|
+
--repo https://github.com/getpayd-tech/my-app \
|
|
45
|
+
--create-db \
|
|
46
|
+
--env SECRET_KEY="$(openssl rand -hex 32)" \
|
|
47
|
+
--env APP_ENV=production \
|
|
48
|
+
--deploy
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Runs 7 steps: project create, env set, database create, Caddy route, server provision, write workflow to local git repo + set GitHub secret via `gh`, first deploy.
|
|
52
|
+
|
|
53
|
+
## Command reference
|
|
54
|
+
|
|
55
|
+
### Everyday ops
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
payd-sentinel status # All projects + their latest deploy
|
|
59
|
+
payd-sentinel projects # List projects
|
|
60
|
+
payd-sentinel services # List containers
|
|
61
|
+
payd-sentinel deploy <project> # Trigger deploy
|
|
62
|
+
payd-sentinel deploy <project> --tag v1.2.3
|
|
63
|
+
payd-sentinel rollback <project> <deploy-id>
|
|
64
|
+
payd-sentinel deployments [--project X]
|
|
65
|
+
payd-sentinel logs <container> [--tail 100] [--since 1h]
|
|
66
|
+
payd-sentinel restart|stop|start <container>
|
|
67
|
+
payd-sentinel audit [--action X] [--limit 30]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`payd-sentinel deploy <project> --tag <sha>` is authoritative for Sentinel-generated
|
|
71
|
+
single-container and blended projects, where the project `ghcr_image` maps to
|
|
72
|
+
one image or the generated `-api` and `-ui` images. For parameterized custom
|
|
73
|
+
multi-image compose stacks, put a shared `*IMAGE_TAG` variable in the compose
|
|
74
|
+
`image:` lines, for example `CONNECT_IMAGE_TAG`; Sentinel updates that variable
|
|
75
|
+
in the compose file directory's `.env` before `docker compose pull`. For custom
|
|
76
|
+
edge/router stacks, set `--deploy-config` with image prefixes and the edge
|
|
77
|
+
service so Sentinel can assert the live service/image map before reporting
|
|
78
|
+
success.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
payd-sentinel project update payd-connect-v2-sandbox \
|
|
84
|
+
--deploy-config '{"compose_source":"webhook_bundle","image_tag_variables":["CONNECT_IMAGE_TAG"],"project_image_prefixes":["ghcr.io/getpayd-tech/payd-connect-v2-sandbox-"],"edge_service":"payd-connect-v2-sandbox"}'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Projects
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
payd-sentinel project create <name> --type fastapi --domain X --repo URL
|
|
91
|
+
payd-sentinel project show <name>
|
|
92
|
+
payd-sentinel project update <name> --domain new --custom-domains
|
|
93
|
+
payd-sentinel project delete <name>
|
|
94
|
+
payd-sentinel project scan # Auto-discover /apps/
|
|
95
|
+
payd-sentinel project provision <name> # Write compose + .env + Caddy
|
|
96
|
+
payd-sentinel project service-key <name> # Generate API key for custom-domains API
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Environment variables
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
payd-sentinel env list <project> [--reveal]
|
|
103
|
+
payd-sentinel env set <project> KEY=VAL KEY2=VAL2 ...
|
|
104
|
+
payd-sentinel env unset <project> KEY1 KEY2
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Database (managed PostgreSQL)
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
payd-sentinel db list
|
|
111
|
+
payd-sentinel db create <name> [--password PW]
|
|
112
|
+
payd-sentinel db tables <db>
|
|
113
|
+
payd-sentinel db query <db> "SELECT * FROM ..."
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Domains + TLS
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
payd-sentinel domain list
|
|
120
|
+
payd-sentinel domain add <domain> --upstream container:port [--tls auto|cloudflare_dns|on_demand|off]
|
|
121
|
+
payd-sentinel domain remove <domain>
|
|
122
|
+
payd-sentinel domain reload
|
|
123
|
+
payd-sentinel domain tls status|enable|disable
|
|
124
|
+
payd-sentinel custom-domain list [--project X]
|
|
125
|
+
payd-sentinel custom-domain remove <domain>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Security (fail2ban + SSH auth log)
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
payd-sentinel security banned [--jail sshd]
|
|
132
|
+
payd-sentinel security ban <ip> [--jail sshd]
|
|
133
|
+
payd-sentinel security unban <ip> [--jail sshd]
|
|
134
|
+
payd-sentinel security activity [--tail 50]
|
|
135
|
+
payd-sentinel security auth [--tail 50] [--type success|failure|info]
|
|
136
|
+
payd-sentinel security ip <ip> # Full history (fail2ban + SSH)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Repo setup (close the loop on new services)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# End-to-end (recommended for new services):
|
|
143
|
+
payd-sentinel bootstrap --name X --type T --domain D --repo URL [...]
|
|
144
|
+
|
|
145
|
+
# For existing Sentinel projects that need the workflow added to their repo:
|
|
146
|
+
cd my-existing-repo
|
|
147
|
+
payd-sentinel repo setup <project>
|
|
148
|
+
# -> fetches generated workflow YAML from Sentinel
|
|
149
|
+
# -> writes .github/workflows/deploy.yml
|
|
150
|
+
# -> runs `gh secret set SENTINEL_WEBHOOK_SECRET ...`
|
|
151
|
+
# -> commits + pushes
|
|
152
|
+
# Flags: --no-secret, --no-commit, --message "msg"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Interactive wizard
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
payd-sentinel init # prompts for each field, runs the 9-step wizard
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Auth
|
|
162
|
+
|
|
163
|
+
Run `payd-sentinel login` once. Tokens are cached at `~/.sentinel/credentials.json` with auto-refresh.
|
|
164
|
+
|
|
165
|
+
Or set `SENTINEL_TOKEN` env var with a valid admin JWT to skip the login flow.
|
|
166
|
+
|
|
167
|
+
Override the API URL: `SENTINEL_URL=http://localhost:8000 payd-sentinel projects`
|
|
168
|
+
|
|
169
|
+
## MCP Server (for Claude Code / AI agents)
|
|
170
|
+
|
|
171
|
+
The package includes an MCP server that exposes 30 tools for AI agents.
|
|
172
|
+
|
|
173
|
+
Add to your Claude Code settings:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"mcpServers": {
|
|
178
|
+
"sentinel": {
|
|
179
|
+
"command": "sentinel-mcp"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Available tools
|
|
186
|
+
|
|
187
|
+
**Projects**: `sentinel_list_projects`, `sentinel_create_project`, `sentinel_update_project`, `sentinel_delete_project`, `sentinel_scan_projects`, `sentinel_provision_project`, `sentinel_project_status`, `sentinel_generate_service_key`, `sentinel_get_workflow`
|
|
188
|
+
|
|
189
|
+
**Deployments**: `sentinel_list_deployments`, `sentinel_deploy`, `sentinel_rollback`
|
|
190
|
+
|
|
191
|
+
**Services**: `sentinel_list_services`, `sentinel_restart_service`, `sentinel_stop_service`, `sentinel_start_service`, `sentinel_get_logs`
|
|
192
|
+
|
|
193
|
+
**Env**: `sentinel_list_env`, `sentinel_set_env`, `sentinel_unset_env`
|
|
194
|
+
|
|
195
|
+
**Database**: `sentinel_list_databases`, `sentinel_create_database`, `sentinel_list_tables`, `sentinel_db_query`
|
|
196
|
+
|
|
197
|
+
**Domains**: `sentinel_list_domains`, `sentinel_add_domain`, `sentinel_remove_domain`, `sentinel_reload_caddy`, `sentinel_list_custom_domains`
|
|
198
|
+
|
|
199
|
+
**Audit**: `sentinel_audit_log`
|
|
200
|
+
|
|
201
|
+
The MCP server reads auth from `~/.sentinel/credentials.json` (run `payd-sentinel login` first) or `SENTINEL_TOKEN` env var.
|
|
202
|
+
|
|
203
|
+
## What is Sentinel?
|
|
204
|
+
|
|
205
|
+
Sentinel is a self-hosted DevOps portal for managing Docker container deployments behind Caddy reverse proxy. It provides webhook-based deploys, automatic health checks with rollback, custom domain management with on-demand TLS, fail2ban monitoring, and a web UI.
|
|
206
|
+
|
|
207
|
+
[sentinel.paydlabs.com](https://sentinel.paydlabs.com) | [GitHub](https://github.com/getpayd-tech/payd-labs-sentinel-v1) | [Self-hosting guide](https://github.com/getpayd-tech/payd-labs-sentinel-v1/blob/main/SELFHOST.md)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
Legacy alias: `sentinel` remains available for backwards compatibility.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# sentinel-cli
|
|
2
|
+
|
|
3
|
+
CLI and MCP server for the [Sentinel](https://sentinel.paydlabs.com) DevOps portal. Manage deployments end-to-end from the terminal or via AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3.12 -m pip install payd-labs-sentinel-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.12+.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
payd-sentinel login # one-time OTP via Payd Auth, caches token at ~/.sentinel/
|
|
17
|
+
|
|
18
|
+
# End-to-end bootstrap of a new service (one command):
|
|
19
|
+
payd-sentinel bootstrap \
|
|
20
|
+
--name my-app --type fastapi --domain my-app.paydlabs.com \
|
|
21
|
+
--repo https://github.com/getpayd-tech/my-app \
|
|
22
|
+
--create-db \
|
|
23
|
+
--env SECRET_KEY="$(openssl rand -hex 32)" \
|
|
24
|
+
--env APP_ENV=production \
|
|
25
|
+
--deploy
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Runs 7 steps: project create, env set, database create, Caddy route, server provision, write workflow to local git repo + set GitHub secret via `gh`, first deploy.
|
|
29
|
+
|
|
30
|
+
## Command reference
|
|
31
|
+
|
|
32
|
+
### Everyday ops
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
payd-sentinel status # All projects + their latest deploy
|
|
36
|
+
payd-sentinel projects # List projects
|
|
37
|
+
payd-sentinel services # List containers
|
|
38
|
+
payd-sentinel deploy <project> # Trigger deploy
|
|
39
|
+
payd-sentinel deploy <project> --tag v1.2.3
|
|
40
|
+
payd-sentinel rollback <project> <deploy-id>
|
|
41
|
+
payd-sentinel deployments [--project X]
|
|
42
|
+
payd-sentinel logs <container> [--tail 100] [--since 1h]
|
|
43
|
+
payd-sentinel restart|stop|start <container>
|
|
44
|
+
payd-sentinel audit [--action X] [--limit 30]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`payd-sentinel deploy <project> --tag <sha>` is authoritative for Sentinel-generated
|
|
48
|
+
single-container and blended projects, where the project `ghcr_image` maps to
|
|
49
|
+
one image or the generated `-api` and `-ui` images. For parameterized custom
|
|
50
|
+
multi-image compose stacks, put a shared `*IMAGE_TAG` variable in the compose
|
|
51
|
+
`image:` lines, for example `CONNECT_IMAGE_TAG`; Sentinel updates that variable
|
|
52
|
+
in the compose file directory's `.env` before `docker compose pull`. For custom
|
|
53
|
+
edge/router stacks, set `--deploy-config` with image prefixes and the edge
|
|
54
|
+
service so Sentinel can assert the live service/image map before reporting
|
|
55
|
+
success.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
payd-sentinel project update payd-connect-v2-sandbox \
|
|
61
|
+
--deploy-config '{"compose_source":"webhook_bundle","image_tag_variables":["CONNECT_IMAGE_TAG"],"project_image_prefixes":["ghcr.io/getpayd-tech/payd-connect-v2-sandbox-"],"edge_service":"payd-connect-v2-sandbox"}'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Projects
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
payd-sentinel project create <name> --type fastapi --domain X --repo URL
|
|
68
|
+
payd-sentinel project show <name>
|
|
69
|
+
payd-sentinel project update <name> --domain new --custom-domains
|
|
70
|
+
payd-sentinel project delete <name>
|
|
71
|
+
payd-sentinel project scan # Auto-discover /apps/
|
|
72
|
+
payd-sentinel project provision <name> # Write compose + .env + Caddy
|
|
73
|
+
payd-sentinel project service-key <name> # Generate API key for custom-domains API
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Environment variables
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
payd-sentinel env list <project> [--reveal]
|
|
80
|
+
payd-sentinel env set <project> KEY=VAL KEY2=VAL2 ...
|
|
81
|
+
payd-sentinel env unset <project> KEY1 KEY2
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Database (managed PostgreSQL)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
payd-sentinel db list
|
|
88
|
+
payd-sentinel db create <name> [--password PW]
|
|
89
|
+
payd-sentinel db tables <db>
|
|
90
|
+
payd-sentinel db query <db> "SELECT * FROM ..."
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Domains + TLS
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
payd-sentinel domain list
|
|
97
|
+
payd-sentinel domain add <domain> --upstream container:port [--tls auto|cloudflare_dns|on_demand|off]
|
|
98
|
+
payd-sentinel domain remove <domain>
|
|
99
|
+
payd-sentinel domain reload
|
|
100
|
+
payd-sentinel domain tls status|enable|disable
|
|
101
|
+
payd-sentinel custom-domain list [--project X]
|
|
102
|
+
payd-sentinel custom-domain remove <domain>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Security (fail2ban + SSH auth log)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
payd-sentinel security banned [--jail sshd]
|
|
109
|
+
payd-sentinel security ban <ip> [--jail sshd]
|
|
110
|
+
payd-sentinel security unban <ip> [--jail sshd]
|
|
111
|
+
payd-sentinel security activity [--tail 50]
|
|
112
|
+
payd-sentinel security auth [--tail 50] [--type success|failure|info]
|
|
113
|
+
payd-sentinel security ip <ip> # Full history (fail2ban + SSH)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Repo setup (close the loop on new services)
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# End-to-end (recommended for new services):
|
|
120
|
+
payd-sentinel bootstrap --name X --type T --domain D --repo URL [...]
|
|
121
|
+
|
|
122
|
+
# For existing Sentinel projects that need the workflow added to their repo:
|
|
123
|
+
cd my-existing-repo
|
|
124
|
+
payd-sentinel repo setup <project>
|
|
125
|
+
# -> fetches generated workflow YAML from Sentinel
|
|
126
|
+
# -> writes .github/workflows/deploy.yml
|
|
127
|
+
# -> runs `gh secret set SENTINEL_WEBHOOK_SECRET ...`
|
|
128
|
+
# -> commits + pushes
|
|
129
|
+
# Flags: --no-secret, --no-commit, --message "msg"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Interactive wizard
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
payd-sentinel init # prompts for each field, runs the 9-step wizard
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Auth
|
|
139
|
+
|
|
140
|
+
Run `payd-sentinel login` once. Tokens are cached at `~/.sentinel/credentials.json` with auto-refresh.
|
|
141
|
+
|
|
142
|
+
Or set `SENTINEL_TOKEN` env var with a valid admin JWT to skip the login flow.
|
|
143
|
+
|
|
144
|
+
Override the API URL: `SENTINEL_URL=http://localhost:8000 payd-sentinel projects`
|
|
145
|
+
|
|
146
|
+
## MCP Server (for Claude Code / AI agents)
|
|
147
|
+
|
|
148
|
+
The package includes an MCP server that exposes 30 tools for AI agents.
|
|
149
|
+
|
|
150
|
+
Add to your Claude Code settings:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"mcpServers": {
|
|
155
|
+
"sentinel": {
|
|
156
|
+
"command": "sentinel-mcp"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Available tools
|
|
163
|
+
|
|
164
|
+
**Projects**: `sentinel_list_projects`, `sentinel_create_project`, `sentinel_update_project`, `sentinel_delete_project`, `sentinel_scan_projects`, `sentinel_provision_project`, `sentinel_project_status`, `sentinel_generate_service_key`, `sentinel_get_workflow`
|
|
165
|
+
|
|
166
|
+
**Deployments**: `sentinel_list_deployments`, `sentinel_deploy`, `sentinel_rollback`
|
|
167
|
+
|
|
168
|
+
**Services**: `sentinel_list_services`, `sentinel_restart_service`, `sentinel_stop_service`, `sentinel_start_service`, `sentinel_get_logs`
|
|
169
|
+
|
|
170
|
+
**Env**: `sentinel_list_env`, `sentinel_set_env`, `sentinel_unset_env`
|
|
171
|
+
|
|
172
|
+
**Database**: `sentinel_list_databases`, `sentinel_create_database`, `sentinel_list_tables`, `sentinel_db_query`
|
|
173
|
+
|
|
174
|
+
**Domains**: `sentinel_list_domains`, `sentinel_add_domain`, `sentinel_remove_domain`, `sentinel_reload_caddy`, `sentinel_list_custom_domains`
|
|
175
|
+
|
|
176
|
+
**Audit**: `sentinel_audit_log`
|
|
177
|
+
|
|
178
|
+
The MCP server reads auth from `~/.sentinel/credentials.json` (run `payd-sentinel login` first) or `SENTINEL_TOKEN` env var.
|
|
179
|
+
|
|
180
|
+
## What is Sentinel?
|
|
181
|
+
|
|
182
|
+
Sentinel is a self-hosted DevOps portal for managing Docker container deployments behind Caddy reverse proxy. It provides webhook-based deploys, automatic health checks with rollback, custom domain management with on-demand TLS, fail2ban monitoring, and a web UI.
|
|
183
|
+
|
|
184
|
+
[sentinel.paydlabs.com](https://sentinel.paydlabs.com) | [GitHub](https://github.com/getpayd-tech/payd-labs-sentinel-v1) | [Self-hosting guide](https://github.com/getpayd-tech/payd-labs-sentinel-v1/blob/main/SELFHOST.md)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
Legacy alias: `sentinel` remains available for backwards compatibility.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "payd-labs-sentinel-cli"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "CLI and MCP server for the Sentinel DevOps portal - manage deployments, services, and projects"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Payd Labs", email = "dev@payd.money" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["devops", "deployment", "docker", "mcp", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Software Development :: Build Tools",
|
|
21
|
+
"Topic :: System :: Systems Administration",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"typer>=0.15.0",
|
|
27
|
+
"httpx>=0.28.0",
|
|
28
|
+
"rich>=13.0.0",
|
|
29
|
+
"mcp>=1.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://sentinel.paydlabs.com"
|
|
34
|
+
Repository = "https://github.com/getpayd-tech/payd-labs-sentinel-v1"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
sentinel = "sentinel_cli.cli:app"
|
|
38
|
+
payd-sentinel = "sentinel_cli.cli:app"
|
|
39
|
+
sentinel-mcp = "sentinel_cli.mcp_server:main"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["sentinel_cli"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Sentinel CLI and MCP server."""
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Authentication - OTP login, token caching, and auto-refresh."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.prompt import Prompt
|
|
13
|
+
|
|
14
|
+
from sentinel_cli.config import CREDENTIALS_DIR, CREDENTIALS_FILE
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _decode_jwt_payload(token: str) -> dict:
|
|
21
|
+
"""Decode the payload segment of a JWT without signature verification."""
|
|
22
|
+
parts = token.split(".")
|
|
23
|
+
if len(parts) != 3:
|
|
24
|
+
raise ValueError("Invalid JWT format")
|
|
25
|
+
payload = parts[1]
|
|
26
|
+
payload += "=" * (4 - len(payload) % 4)
|
|
27
|
+
return json.loads(base64.urlsafe_b64decode(payload))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def save_credentials(auth_token: str, refresh_token: str) -> None:
|
|
31
|
+
"""Persist tokens to ~/.sentinel/credentials.json with restricted permissions."""
|
|
32
|
+
claims = _decode_jwt_payload(auth_token)
|
|
33
|
+
expires_at = claims.get("exp", int(time.time()) + 3600)
|
|
34
|
+
|
|
35
|
+
CREDENTIALS_DIR.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
CREDENTIALS_DIR.chmod(0o700)
|
|
37
|
+
|
|
38
|
+
data = {
|
|
39
|
+
"auth_token": auth_token,
|
|
40
|
+
"refresh_token": refresh_token,
|
|
41
|
+
"expires_at": expires_at,
|
|
42
|
+
"username": claims.get("username", ""),
|
|
43
|
+
}
|
|
44
|
+
CREDENTIALS_FILE.write_text(json.dumps(data, indent=2))
|
|
45
|
+
CREDENTIALS_FILE.chmod(0o600)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_credentials() -> dict | None:
|
|
49
|
+
"""Load cached credentials from disk. Returns None if missing or corrupt."""
|
|
50
|
+
if not CREDENTIALS_FILE.exists():
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
return json.loads(CREDENTIALS_FILE.read_text())
|
|
54
|
+
except (json.JSONDecodeError, OSError):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_valid_token(base_url: str) -> str | None:
|
|
59
|
+
"""Return a valid auth token, refreshing if needed.
|
|
60
|
+
|
|
61
|
+
Checks (in order):
|
|
62
|
+
1. SENTINEL_TOKEN env var
|
|
63
|
+
2. Cached credentials with auto-refresh
|
|
64
|
+
Returns None if no valid token is available.
|
|
65
|
+
"""
|
|
66
|
+
env_token = os.environ.get("SENTINEL_TOKEN")
|
|
67
|
+
if env_token:
|
|
68
|
+
return env_token
|
|
69
|
+
|
|
70
|
+
creds = load_credentials()
|
|
71
|
+
if not creds:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
now = time.time()
|
|
75
|
+
if creds["expires_at"] > now + 60:
|
|
76
|
+
return creds["auth_token"]
|
|
77
|
+
|
|
78
|
+
# Token expired or near expiry - try refresh
|
|
79
|
+
try:
|
|
80
|
+
new_auth, new_refresh = _refresh_token(base_url, creds["refresh_token"])
|
|
81
|
+
save_credentials(new_auth, new_refresh)
|
|
82
|
+
return new_auth
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
logger.debug("Token refresh failed: %s", exc)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _refresh_token(base_url: str, refresh_tok: str) -> tuple[str, str]:
|
|
89
|
+
"""Exchange a refresh token for new auth + refresh tokens."""
|
|
90
|
+
with httpx.Client(timeout=30.0) as client:
|
|
91
|
+
resp = client.post(
|
|
92
|
+
f"{base_url}/api/v1/auth/refresh",
|
|
93
|
+
json={"refresh_token": refresh_tok},
|
|
94
|
+
)
|
|
95
|
+
resp.raise_for_status()
|
|
96
|
+
data = resp.json()
|
|
97
|
+
auth_token = data.get("authToken") or data.get("access_token")
|
|
98
|
+
refresh_token = data.get("refreshToken") or data.get("refresh_token")
|
|
99
|
+
if not auth_token or not refresh_token:
|
|
100
|
+
raise ValueError("Missing tokens in refresh response")
|
|
101
|
+
return auth_token, refresh_token
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def interactive_login(base_url: str) -> None:
|
|
105
|
+
"""Run the full OTP login flow interactively."""
|
|
106
|
+
username = Prompt.ask("[bold]Username[/bold]")
|
|
107
|
+
password = Prompt.ask("[bold]Password[/bold]", password=True)
|
|
108
|
+
|
|
109
|
+
with httpx.Client(timeout=30.0) as client:
|
|
110
|
+
# Step 1: Login
|
|
111
|
+
resp = client.post(
|
|
112
|
+
f"{base_url}/api/v1/auth/login",
|
|
113
|
+
json={"username": username, "password": password},
|
|
114
|
+
)
|
|
115
|
+
if resp.status_code >= 400:
|
|
116
|
+
detail = resp.json().get("detail", "Login failed")
|
|
117
|
+
console.print(f"[red]Login failed:[/red] {detail}")
|
|
118
|
+
raise SystemExit(1)
|
|
119
|
+
|
|
120
|
+
data = resp.json()
|
|
121
|
+
session_token = data.get("sessionToken", "")
|
|
122
|
+
|
|
123
|
+
# Step 2: Request OTP
|
|
124
|
+
resp = client.post(
|
|
125
|
+
f"{base_url}/api/v1/auth/request-otp",
|
|
126
|
+
headers={"x-session-token": session_token},
|
|
127
|
+
content=b"",
|
|
128
|
+
)
|
|
129
|
+
if resp.status_code >= 400:
|
|
130
|
+
detail = resp.json().get("detail", "OTP request failed")
|
|
131
|
+
console.print(f"[red]OTP request failed:[/red] {detail}")
|
|
132
|
+
raise SystemExit(1)
|
|
133
|
+
|
|
134
|
+
otp_data = resp.json()
|
|
135
|
+
new_session = otp_data.get("sessionToken")
|
|
136
|
+
if new_session:
|
|
137
|
+
session_token = new_session
|
|
138
|
+
|
|
139
|
+
console.print("[dim]OTP sent. Check your phone/email.[/dim]")
|
|
140
|
+
|
|
141
|
+
# Step 3: Verify OTP
|
|
142
|
+
otp_code = Prompt.ask("[bold]OTP Code[/bold]")
|
|
143
|
+
resp = client.post(
|
|
144
|
+
f"{base_url}/api/v1/auth/verify-otp",
|
|
145
|
+
json={"otp": otp_code},
|
|
146
|
+
headers={"x-session-token": session_token},
|
|
147
|
+
)
|
|
148
|
+
if resp.status_code >= 400:
|
|
149
|
+
detail = resp.json().get("detail", "OTP verification failed")
|
|
150
|
+
console.print(f"[red]OTP verification failed:[/red] {detail}")
|
|
151
|
+
raise SystemExit(1)
|
|
152
|
+
|
|
153
|
+
data = resp.json()
|
|
154
|
+
auth_token = data.get("authToken") or data.get("access_token")
|
|
155
|
+
refresh_token = data.get("refreshToken") or data.get("refresh_token")
|
|
156
|
+
|
|
157
|
+
if not auth_token:
|
|
158
|
+
console.print("[red]No auth token in response[/red]")
|
|
159
|
+
raise SystemExit(1)
|
|
160
|
+
|
|
161
|
+
# Verify admin status
|
|
162
|
+
claims = _decode_jwt_payload(auth_token)
|
|
163
|
+
if not claims.get("is_admin"):
|
|
164
|
+
console.print("[red]Account is not an admin[/red]")
|
|
165
|
+
raise SystemExit(1)
|
|
166
|
+
|
|
167
|
+
save_credentials(auth_token, refresh_token or "")
|
|
168
|
+
console.print(f"[green]Logged in as {claims.get('username', username)}[/green]")
|