dsh-sslmanager
by jenawant-rgb
README.md
# dsh-sslmanager
**SSL certificates for your coding agent.** See what is about to expire, issue a
replacement, push it to every server, and revoke a leaked one — from the chat,
with every state-changing call gated by an approval prompt.
Backed by [SetHttps](https://sethttps.com): Let's Encrypt wildcard certificates
over DNS-01, deployed to your own servers.
[](https://sethttps.com)
[](https://sethttps.com/guide.html)
[](LICENSE)
```
index.js native DSH plugin — registers 11 tools on ctx.tools
client.js Web Client dock panel: certificates expiring soon
cordis.patch.yml DSH bundle patch (the plugin row + config)
dev/ mock SSLManager API + harnesses, no network needed
```
Using Codex? The plugin for it lives in
[codex-sslmanager](https://github.com/jenawant-rgb/codex-sslmanager); this
repository is the DSH plugin only.
## Why this exists
Certificate expiry is the one failure in a normal web stack that is **silent
until it is total**: nothing degrades, nothing warns, and then every browser
refuses to connect at once.
An agent that already reads your repo and your logs is a good place to notice
that. But noticing is not fixing — so this plugin also lets it write, to a CA and
to live servers, and nothing else:
- reads are free and safe
- writes always ask a human first
- no tool ever returns private key material or server credentials
## What it looks like
```
you ▸ what's expiring this month?
ssl_cert_expiring(days: 30)
▸ api.example.com 9 days left deployed
shop.example.com 21 days left deployed
legacy.example.com 34 days not deployed
you ▸ renew api.example.com and ship it to the sg node
ssl_cert_apply(domainName: "api.example.com")
⏸ issue a certificate for api.example.com — current one expires in 9 days
[approve] [reject]
ssl_cert_deploy(domainName: "api.example.com", remoteId: 3)
⏸ deploy api.example.com to edge-sg-01 (203.0.113.10) and reload
[approve] [reject]
▸ uploaded, reloaded, live certificate matches the new serial
```
## Install
### DeepSeek Harness (native plugin)
```bash
# from the repository
dsh plugin --profile <name> add github:jenawant-rgb/dsh-sslmanager
# or from a local checkout
dsh plugin --profile <name> add /path/to/dsh-sslmanager
# then select the bundle in the profile manifest:
# dsh.profile.bundles += "@sslmanager/dsh-sslmanager"
```
The plugin panel accepts the same `github:` spec.
The row config lives in [`cordis.patch.yml`](cordis.patch.yml):
```yaml
- insert:
- id: sslmanager
name: '@sslmanager/dsh-sslmanager'
config:
baseUrl: https://sethttps.com # or http://127.0.0.1:8030 for a local server
tokenRef: SSLMGR_TOKEN
```
### Codex and other MCP clients
The MCP server lives in the Codex plugin repository so the two packages share
nothing: <https://github.com/jenawant-rgb/codex-sslmanager>.
```bash
codex plugin marketplace add jenawant-rgb/codex-sslmanager
codex plugin add sslmanager@sslmanager
```
## Configuration
| Key | Default | Meaning |
| --- | --- | --- |
| `baseUrl` | `http://127.0.0.1:8030` | SSLManager API root. Use `https://sethttps.com` for the hosted service |
| `tokenRef` | `SSLMGR_TOKEN` | Credential-store entry (or env var) holding the token |
| `usernameEnv` / `passwordEnv` | `SSLMGR_USERNAME` / `SSLMGR_PASSWORD` | Fallback login, only used when no token is present |
### Getting a token
Create a scoped personal access token on the console's **访问令牌** (`/token`)
page, or mint one from the API. The login endpoint is OAuth2 password form
encoded, not JSON:
```bash
# 1. log in once to get a short-lived token
LOGIN=$(curl -s -X POST https://sethttps.com/api/user/token \
-d 'username=you@example.com' -d 'password=…' | jq -r .access_token)
# 2. create a scoped token for the agent
curl -s -X POST https://sethttps.com/api/token/ \
-H "Authorization: Bearer $LOGIN" \
-H 'Content-Type: application/json' \
-d '{"name":"dsh","scopes":["cert:read","cert:write","domain:read","deploy:read","deploy:write"],"expires_in_days":90}'
```
PATs are pinned to the routes and scopes the server allows: they cannot reach
`/api/user`, `/api/admin` or `/api/token`, and a write tool without its scope
fails with 403.
Resolution order, first hit wins:
1. `config.token` — development only, never commit it
2. the credential-store entry named by `config.tokenRef` (default `SSLMGR_TOKEN`)
3. that same name as an environment variable
4. username / password login against `/api/user/token` (read from `SSLMGR_USERNAME` / `SSLMGR_PASSWORD`)
A 401 invalidates the cached token and retries once.
### Entering the token
The dock panel above the composer is also the setup form: click **设置 → 填入令牌**
(or it opens by itself when no credential is stored), paste the `ssm_…` value and
save. The browser posts it to the Harness host, which validates it against
SSLManager and writes it to the credential store under `SSLMGR_TOKEN`; the value
is never echoed back or added to the conversation.
Editing `~/.dsh/.credentials.yaml` by hand still works:
```yaml
refs:
SSLMGR_TOKEN: ssm_…
```
The panel only appears after a restart, because Web Client modules are composed
when the Harness boots. The tools are available immediately.
## Tools
**Read-only** — safe to call at any time:
| Tool | Purpose |
| --- | --- |
| `ssl_cert_list` | Certificates with domain, status, expiry, deployment state |
| `ssl_cert_expiring` | What expires within N days, soonest first |
| `ssl_cert_get` | One certificate's metadata and server-side file paths |
| `ssl_cert_status` | Current certificate for a domain (poll after apply) |
| `ssl_domain_list` | Tracked domains and their verify / cert / deploy status |
| `ssl_remote_list` | Deployment targets with host and certificate paths |
| `ssl_probe` | Live TLS probe of any host (issuer, expiry, SANs, self-signed) |
**State-changing** — always behind an approval:
| Tool | Effect | Endpoint |
| --- | --- | --- |
| `ssl_cert_apply` | Issue a certificate (renewal is a fresh issuance) | `POST /api/certificate/apply` |
| `ssl_cert_deploy` | Push the current certificate over SFTP + trigger reload | `POST /api/certificate/deploy` |
| `ssl_cert_revoke` | Revoke at the CA — irreversible | `POST /api/certificate/revoke` |
| `ssl_domain_check` | Re-run DNS (`_acme-challenge`) verification | `POST /api/domain/check` |
No tool returns `key_content`, `cert_content` or remote credentials; the
projections in `toCertificate()` and `ssl_remote_list` drop them defensively.
## Safety model
Every state-changing tool goes through `authorize()`:
1. **Harness (approval UI available)** — `ctx.approval.request()` with a one-line
summary (`Issue an SSL certificate for example.com — expires in 9 days`).
Only `allowed-once` proceeds; `rejected`, `cancelled` and `unavailable` abort
before touching the API. Request and outcome land in the session audit log.
2. **MCP / headless (no approval UI)** — the call is refused unless it carries
`confirm: true`. A model can still set that flag, but the refusal message makes
the deliberate step visible in the transcript.
`ssl_cert_apply` pre-checks that the domain exists and is `verified`, and fails
with the list of known domains otherwise. `ssl_cert_deploy` checks the remote id
against `ssl_remote_list` before asking for approval.
## Web Client panel
`client.js` registers into `conversation.input.dock`: a compact strip above the
composer showing how many certificates expire within 45 days, expanding into a
list with per-certificate days left and deployment state. It refreshes every
10 minutes.
Data comes from an exact `GET /api/sslmanager/expiring` route registered by the
host plugin. The handler calls `connection.admit(request)` first, so the endpoint
uses the same browser-cookie authentication as the rest of the API bridge and is
never publicly readable.
## Local development
```bash
python3 dev/mock_sslmanager.py 8030 & # fake API: certs, domains, remotes, writes
node dev/harness.mjs # all 11 tools, approval allow/reject/absent, leak checks
node dev/mcp-test.mjs # JSON-RPC handshake, read + confirm-gated write
```
The module intentionally imports nothing at runtime: a `link:`ed install resolves
from the checkout directory, where Harness packages such as `@deepseek-ai/dsh-tools`
are not visible. Tool definitions are plain registry definitions with standard
JSON Schema.
## Roadmap
- Audit trail of who called what, independent of the Harness session log
- Per-certificate severity in the client panel, not just a count
- `ssl_cert_deploy` is synchronous and can outlast a default tool timeout on a
slow SSH target — move it to a job with progress
- The console page for tokens lives in the SSLManager front end (`/token`), not
in this repository
## Links
| | |
| --- | --- |
| Hosted service | https://sethttps.com |
| Docs — quickstart, DNS, Nginx, panels | https://sethttps.com/guide.html |
| The script that runs on your server (open source) | https://github.com/jenawant-rgb/sethttps-agent |
| Free tools, no account | [SSL expiry check](https://sethttps.com/ssl-check.html) · [CNAME check](https://sethttps.com/cname-check.html) |
## License
MIT — see [LICENSE](LICENSE).
---
## 中文说明
**给编码助手用的 SSL 证书工具。** 在对话里查看哪些证书快到期、签发、推送到服务器、吊销泄露的证书,
所有写操作都会先弹审批。
数据来自 [SetHttps](https://sethttps.com):Let's Encrypt 泛域名证书,一条 CNAME 过 DNS-01,
签发后自动分发到你自己的服务器,到期前自动续期。
- 读操作(7 个工具)随时可用:证书列表、到期筛选、单张详情、DNS 验证状态、部署节点、实时 TLS 探测
- 写操作(4 个工具)必须审批:签发、部署、吊销、重新验证
- 不返回私钥、不返回服务器密码;Token 只能访问证书相关路由
安装与配置见上方英文部分。本地调试不需要联网:
```bash
python3 dev/mock_sslmanager.py 8030 &
node dev/harness.mjs
```
文档:https://sethttps.com/guide.html | 服务器端脚本(已开源):https://github.com/jenawant-rgb/sethttps-agent
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues