@lllanyard/cli 0.0.0-stage → 0.1.0
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.
- package/LICENSE.md +91 -0
- package/README.md +377 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +829 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved.
|
|
2
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
3
|
+
|
|
4
|
+
Parameters
|
|
5
|
+
|
|
6
|
+
Licensor: Ethan Askew T/A LLLanyard
|
|
7
|
+
Licensed Work: LLLanyard (all versions). The Licensed Work is (c) 2026
|
|
8
|
+
Ethan Askew T/A LLLanyard.
|
|
9
|
+
Additional Use Grant: You may make production use of the Licensed Work only if
|
|
10
|
+
BOTH of the following are true:
|
|
11
|
+
|
|
12
|
+
(1) Integrity. You use the Licensed Work's license-key
|
|
13
|
+
verification and usage limits as published. You do not
|
|
14
|
+
remove, disable, bypass, weaken, forge or tamper with any
|
|
15
|
+
license-key check, signature check, or limit on keys,
|
|
16
|
+
agents, users, seats, tiers or features, and you do not
|
|
17
|
+
help anyone else do so, whether by modifying the Licensed
|
|
18
|
+
Work, by providing modified copies, or by distributing
|
|
19
|
+
license keys or tools that produce them.
|
|
20
|
+
|
|
21
|
+
(2) Entitlement. Your use is either
|
|
22
|
+
(a) within the free-tier limits of the version you are
|
|
23
|
+
using (the limits defined in packages/core/src/limits.ts
|
|
24
|
+
of that version, and without the shared or remote
|
|
25
|
+
serving, dashboard or other features that version
|
|
26
|
+
reserves for paid tiers); or
|
|
27
|
+
(b) covered by a valid, unexpired paid license key
|
|
28
|
+
issued to you by the Licensor, used within its terms
|
|
29
|
+
(including its tier and seat count).
|
|
30
|
+
|
|
31
|
+
In addition, you may not offer the Licensed Work, or a
|
|
32
|
+
product derived from it, to third parties on a hosted,
|
|
33
|
+
managed or embedded basis where it competes with paid
|
|
34
|
+
versions of the Licensed Work offered by the Licensor.
|
|
35
|
+
Using the Licensed Work for the internal purposes of your
|
|
36
|
+
own organization is not such an offering.
|
|
37
|
+
|
|
38
|
+
Non-production use (evaluation, testing, development,
|
|
39
|
+
research) is permitted by the Terms below without
|
|
40
|
+
restriction as to limits.
|
|
41
|
+
Change Date: Four years from the date the Licensed Work is published.
|
|
42
|
+
Change License: Apache License, Version 2.0
|
|
43
|
+
|
|
44
|
+
For information about alternative licensing arrangements for the Licensed Work,
|
|
45
|
+
or to buy a paid license key, please contact Ethan Askew T/A LLLanyard
|
|
46
|
+
(licensing contact: sales@lllanyard.co.uk).
|
|
47
|
+
|
|
48
|
+
Notice
|
|
49
|
+
|
|
50
|
+
Business Source License 1.1
|
|
51
|
+
|
|
52
|
+
Terms
|
|
53
|
+
|
|
54
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
55
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
56
|
+
Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
57
|
+
|
|
58
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
59
|
+
available distribution of a specific version of the Licensed Work under this
|
|
60
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
61
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
62
|
+
above terminate.
|
|
63
|
+
|
|
64
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
65
|
+
currently in effect as described in this License, you must purchase a
|
|
66
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
67
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
68
|
+
|
|
69
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
70
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
71
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
72
|
+
for each version of the Licensed Work released by Licensor.
|
|
73
|
+
|
|
74
|
+
You must conspicuously display this License on each original or modified copy
|
|
75
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
76
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
77
|
+
License apply to your use of that work.
|
|
78
|
+
|
|
79
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
80
|
+
terminate your rights under this License for the current and all other
|
|
81
|
+
versions of the Licensed Work.
|
|
82
|
+
|
|
83
|
+
This License does not grant you any right in any trademark or logo of
|
|
84
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
85
|
+
Licensor as expressly required by this License).
|
|
86
|
+
|
|
87
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
88
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
89
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
90
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
91
|
+
TITLE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,378 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LLLanyard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
By **Ethan Askew T/A LLLanyard**. Source-available under the Business Source License 1.1 (see [License](#license)).
|
|
4
|
+
|
|
5
|
+
Keep API keys out of AI agents. Your keys live in the **OS keychain**; tools like Claude Code, Cursor
|
|
6
|
+
or AutoGPT only ever hold a worthless placeholder token and talk to a local proxy that injects the real key.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
agent (token lly_…) ──► 127.0.0.1:3000 ──► api.openai.com / api.anthropic.com
|
|
10
|
+
│ verifies token, rate-limits, adds the real key from the OS keychain
|
|
11
|
+
└─ macOS Keychain · Windows Credential Manager (DPAPI) · Linux Secret Service
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @lllanyard/cli # needs Node 22 or later; from source: npm install && npm run build && npm link -w packages/cli
|
|
18
|
+
|
|
19
|
+
lllanyard doctor # confirms the OS keychain works
|
|
20
|
+
lllanyard ui # or use the visual app for everything below
|
|
21
|
+
lllanyard add anthropic # hidden prompt – the key never touches argv/history
|
|
22
|
+
lllanyard add openai
|
|
23
|
+
lllanyard setup claude-desktop --scripts-dir ~/Scripts # Claude Desktop on a subscription: see below
|
|
24
|
+
lllanyard agent create claude-code # API-key mode: prints a placeholder token ONCE + the env vars to set
|
|
25
|
+
lllanyard start # proxy on 127.0.0.1:3000
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then, in the shell that launches your tool, set the values `agent create` printed, e.g.:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:3000/anthropic
|
|
32
|
+
export ANTHROPIC_API_KEY=lly_... # placeholder, useless outside the proxy
|
|
33
|
+
export OPENAI_BASE_URL=http://127.0.0.1:3000/openai/v1
|
|
34
|
+
export OPENAI_API_KEY=lly_...
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Compatibility notes (verify against each tool's own docs):
|
|
38
|
+
- Claude Code reads its base URL and key from these environment variables; if `ANTHROPIC_AUTH_TOKEN` is also set it may take precedence, so unset it. Only API-key mode is proxied, not subscription logins. The Claude Desktop app reportedly ignores these variables; for Desktop (and subscription use generally) see [Claude Desktop](#3-claude-desktop-and-subscription-chat-apps) below.
|
|
39
|
+
- Cursor makes some model calls from its own servers, so a localhost base URL may not work there. Test before promising support.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
| Command | What it does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `add <provider> [--label N] [--rotate-days N] [--env-name VAR]` | Store a key (hidden prompt or `--stdin`). `--label` doubles as a *client* name for agency/freelancer use. |
|
|
46
|
+
| `env <provider>[:label] VAR` | The environment variable a key is given to scripts as, e.g. `lllanyard env trmm TRMM_API_KEY`. |
|
|
47
|
+
| `exec <command> [args…]` | Run a script with the keys it uses: `lllanyard exec pwsh -File ./Onboard-Client.ps1 -DryRun`. LLLanyard reads the script, and the helper files it loads (quoted paths, `source`/`.` lines, Python imports), for your keys' variable names and passes only those, for that run only. Keys in the output are redacted; each run is logged and shown by `usage`. `exec --keys trmm,itflow -- <command>` names the keys yourself (needed for programs that aren't scripts). Run from a terminal, the script gets a real terminal of its own (prompts like `Read-Host`, colours and window size work) on macOS, Linux and Windows, and its output is still redacted. |
|
|
48
|
+
| `list`, `remove`, `rotate`, `status` | Manage keys; `status` shows rotation due dates and exits 1 if anything is overdue (cron/CI friendly). |
|
|
49
|
+
| `agent create <name> [--client N] [--providers a,b]` / `list` / `revoke` | Placeholder tokens. `--client acme` pins an agent to the `acme` key; `--providers` limits which APIs it may reach (least privilege). |
|
|
50
|
+
| `provider add <id> --upstream https://… [--auth bearer\|header --header NAME --prefix TXT]` / `list` / `remove` | Teach the proxy a new API host (GitHub, Slack, your own service). Built-ins: openai, anthropic, gemini, openrouter, github, slack, stripe, notion (confirm each against the vendor's docs). |
|
|
51
|
+
| `tool add <name> --env VAR=provider[:label] [--allow-host H] [--kill-on-leak] [--pass-env V] [--inherit-env] [--no-scrub] -- <command> [args…]` / `list` / `remove` | Approve a command (e.g. an MCP server) to receive keys as environment variables, with the safeguards below. |
|
|
52
|
+
| `run <name> [-- args…]` | Run an approved tool with its keys injected and scrubbed from its output. Extra args are appended only if the tool was approved with `--allow-args`. For script tools (python, node, sh, pwsh…) the script file is pinned by hash at approval and `run` refuses if it has changed since; `--allow-args` is refused for bare interpreters (`python3 -c`, `-m`) and launchers (`env`, `xargs`, `sudo`, `npx`). `run --env … -- <cmd>` is a one-off that needs a human to confirm at a terminal. |
|
|
53
|
+
| `scan [paths] [--import --apply [--redact]]` | Find plaintext keys in `.env` files, MCP/IDE configs and shell profiles, then move them into the keychain. Dry run unless `--apply`. |
|
|
54
|
+
| `backup [--out FILE] [--passphrase-file F]` | Write one encrypted file (AES-256-GCM, scrypt passphrase) holding your keys and their metadata, custom providers, approved tools, agents and license. Agents are saved as token hashes, so existing agent tokens keep working after a restore. |
|
|
55
|
+
| `restore <file> [--overwrite] [--dry-run] [--passphrase-file F]` | Merge a backup into this machine's keychain or vault. Every item is validated and the key limit checked before anything is written; existing items are kept unless `--overwrite`, and nothing is ever deleted. |
|
|
56
|
+
| `start [--port N]` | Run the proxy (loopback only). Logs JSON lines to stdout. |
|
|
57
|
+
|
|
58
|
+
## Using it with tools (APIs and MCP servers)
|
|
59
|
+
|
|
60
|
+
Two routes, depending on how the tool talks:
|
|
61
|
+
|
|
62
|
+
**1. HTTP APIs go through the proxy** (the tool only holds the placeholder token):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
lllanyard add github # hidden prompt
|
|
66
|
+
lllanyard agent create reviewer --providers github,anthropic
|
|
67
|
+
# point the tool's API base URL at http://127.0.0.1:3000/github and use the lly_ token as its key
|
|
68
|
+
lllanyard provider add acme --upstream https://api.acme.dev --auth header --header X-Api-Key # your own API
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**2. MCP servers and CLI tools read keys from environment variables**, so the proxy can't help. Approve the command once, then
|
|
72
|
+
reference it by name so no secret ever appears in the MCP config file:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
lllanyard tool add github-mcp --env GITHUB_PERSONAL_ACCESS_TOKEN=github \\
|
|
76
|
+
--allow-host api.github.com --allow-host registry.npmjs.org --kill-on-leak \\
|
|
77
|
+
-- npx -y @modelcontextprotocol/server-github
|
|
78
|
+
# MCP client config (Claude Desktop / Claude Code / Cursor style):
|
|
79
|
+
# { "command": "lllanyard", "args": ["run", "github-mcp"] }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**3. Claude Desktop and Claude Code on a subscription.** These sign in to an account, not an API key, so there is no API
|
|
83
|
+
call to proxy and no extra API billing. Connect them once, from the command line or from the app's Scripts tab
|
|
84
|
+
("Connect AI apps"):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
lllanyard setup claude-desktop --scripts-dir ~/Scripts # --restart quits and reopens Claude Desktop if it's open (macOS)
|
|
88
|
+
lllanyard setup claude-code # adds a short section to ~/.claude/CLAUDE.md
|
|
89
|
+
lllanyard setup # shows what's connected; --remove undoes either
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Claude Desktop rewrites its config file when it quits, so a change made while it's open is lost. `setup` therefore only
|
|
93
|
+
changes it while the app is closed: on macOS it offers to quit Claude Desktop, make the change and reopen it (`--restart`
|
|
94
|
+
does this without asking); on Windows and Linux, quit it first (including from the system tray).
|
|
95
|
+
|
|
96
|
+
`setup claude-desktop` adds LLLanyard's MCP server to `claude_desktop_config.json` (macOS `~/Library/Application Support/Claude/`,
|
|
97
|
+
Windows `%APPDATA%\Claude\`, Linux `~/.config/Claude/`). It keeps your other settings, saves the old file as
|
|
98
|
+
`claude_desktop_config.json.lllanyard-backup`, and uses absolute paths plus your current `PATH`, since desktop apps start
|
|
99
|
+
MCP servers with a minimal one. If you move or reinstall Node or LLLanyard, run it again (`setup` warns when the paths are
|
|
100
|
+
stale). `setup claude-code` adds a marked section telling Claude Code to run scripts with `lllanyard exec` and never read
|
|
101
|
+
keys from `.env` files. It leaves the rest of the file alone.
|
|
102
|
+
|
|
103
|
+
In Claude Desktop, Claude then sees `list_scripts` and `run_script` for tools you approved with `lllanyard tool add`:
|
|
104
|
+
the script runs with its keys, the output (keys redacted) and exit code go back to Claude, and the run is logged
|
|
105
|
+
(`lllanyard usage`). It can't run anything you haven't approved, can't change the command, and only adds arguments
|
|
106
|
+
where you said `--allow-args`. Scripts get no input, so ones that prompt (`Read-Host`) need a non-interactive switch.
|
|
107
|
+
Runs stop after 10 minutes.
|
|
108
|
+
|
|
109
|
+
With `--scripts-dir DIR` (repeatable) you don't have to approve scripts one by one. Claude also gets
|
|
110
|
+
`run_file`, which can run any `.ps1`, `.py`, `.sh`, `.zsh` or `.js` file in those folders, but only after **you** click
|
|
111
|
+
Run in a dialog LLLanyard shows (Deny is the default; it gives up after 2 minutes). The dialog shows the script, its
|
|
112
|
+
arguments, the keys it will get (only the ones it uses, as with `exec`), and a warning if the script or a helper changed
|
|
113
|
+
in the last day, since an AI may have written it. The files are hashed before the dialog and checked again at launch,
|
|
114
|
+
so a script edited while you decide isn't run. Symlinks out of the folder, your home directory and `/` are refused.
|
|
115
|
+
Dialogs: macOS built in, Windows built in, Linux needs `zenity`; with no dialog, nothing runs.
|
|
116
|
+
|
|
117
|
+
Why "approve once": if `run` could inject any key into any command, an agent that can edit your MCP config could point it at
|
|
118
|
+
`curl https://evil` and walk off with the key. Approved tools live in the keychain, with the command pinned to its absolute
|
|
119
|
+
path, so editing a file can't change what receives a key. Custom providers are stored in the keychain for the same reason.
|
|
120
|
+
|
|
121
|
+
### Leak safeguards for tool processes
|
|
122
|
+
|
|
123
|
+
A tool started with `run` holds its key, so LLLanyard puts guard rails around it:
|
|
124
|
+
|
|
125
|
+
| Safeguard | What it does | Default |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| Minimal environment | The tool inherits only basics (PATH, HOME, temp dirs, locale, CA settings), so it can't see your *other* API keys or cloud credentials. `--pass-env VAR` adds specific variables; `--inherit-env` restores everything. | on |
|
|
128
|
+
| Output scrubbing | Removes the key from the tool's stdout/stderr, including base64, base64url, hex and URL-encoded copies. Streams, so MCP's newline-delimited stdio never stalls. | on |
|
|
129
|
+
| Leak alert / kill | The first time the tool prints its key you get a warning on stderr; with `--kill-on-leak` the tool is stopped at once (exit code 70). A tool echoing its own token is a strong sign it is buggy or compromised. | warn |
|
|
130
|
+
| Egress allowlist | `--allow-host api.github.com` (repeatable, `*.example.com` wildcards) starts a loopback proxy protected by a per-run password; the tool's proxy variables point at it, so it can only open HTTPS tunnels to listed hosts on public IPs. Denied attempts are logged. | off |
|
|
131
|
+
| Clean shutdown | Signals are forwarded and the tool is killed if LLLANYARD exits. Keys go only into the child's environment, never the parent's. | on |
|
|
132
|
+
| Pinned command | The command is stored as an absolute path in the keychain, so a PATH swap or config edit can't change what receives the key. | on |
|
|
133
|
+
|
|
134
|
+
### Enforced isolation: `--sandbox`
|
|
135
|
+
|
|
136
|
+
Without a sandbox the safeguards above are advisory: a malicious tool can ignore proxy settings. `--sandbox` makes them
|
|
137
|
+
OS-enforced:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
lllanyard tool add github-mcp --sandbox --allow-host api.github.com --allow-host registry.npmjs.org \
|
|
141
|
+
--env GITHUB_PERSONAL_ACCESS_TOKEN=github -- npx -y @modelcontextprotocol/server-github
|
|
142
|
+
lllanyard doctor # reports whether a sandbox works on this machine
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
| | Linux (bubblewrap) | macOS (Seatbelt `sandbox-exec`) |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| Network | No network at all. The only way out is a unix socket to the host egress proxy (via a tiny in-sandbox bridge), which allows only `--allow-host` hosts. DNS is resolved on the host. | Denied, except loopback to the egress proxy port |
|
|
148
|
+
| Home directory | Replaced by an empty tmpfs: no `~/.ssh`, `~/.aws`, browser profiles, shell history | Reads under `$HOME` denied except the project and shares |
|
|
149
|
+
| Writes | Only the current folder (and `--share-rw`); `/tmp` is private | Only the current folder, `--share-rw` and temp |
|
|
150
|
+
| Your keychain | No D-Bus/Secret Service socket, no `/run`; fresh `/proc` shows no other processes, so the parent's environment can't be read | Keychain services blocked (`SecurityServer`, `securityd`) |
|
|
151
|
+
| Other | `--unshare-all`, all capabilities dropped, dies with LLLANYARD, `--new-session` when non-interactive | |
|
|
152
|
+
|
|
153
|
+
- **Fails closed.** Before each run a throwaway sandbox is started; if it can't start, the tool is **not** run (no silent
|
|
154
|
+
fallback). A tool with `--sandbox` and no `--allow-host` gets no network.
|
|
155
|
+
- Run it from the project folder: only the current directory is writable, and running from your home directory is refused.
|
|
156
|
+
Credential folders (`~/.ssh`, `~/.aws`, `~/.gnupg`, ...) cannot be shared even with `--share-ro/--share-rw`.
|
|
157
|
+
- Linux needs `sudo apt install bubblewrap`. Ubuntu 24.04+ restricts unprivileged user namespaces; `doctor` explains the fix.
|
|
158
|
+
- **Windows:** no sandbox yet. Use WSL2 (Debian/Ubuntu) to get the Linux one. Docker mode is on the roadmap.
|
|
159
|
+
- **Verification status:** the argument/profile generation, mount safety, fail-closed behaviour and the egress bridge are
|
|
160
|
+
unit-tested, and a test proves in a real Linux network namespace that a process has no route to the host or internet and
|
|
161
|
+
can reach only allowlisted hosts through the bridge. **Not yet run against a real `bwrap` or on macOS** (neither was
|
|
162
|
+
available where this was built). Treat the first release as beta on those paths and run `lllanyard doctor` first.
|
|
163
|
+
|
|
164
|
+
Honest limits:
|
|
165
|
+
|
|
166
|
+
- **Without `--sandbox`, the egress allowlist is best-effort.** It constrains tools that honour proxy settings (most HTTP
|
|
167
|
+
clients; recent Node via `NODE_USE_ENV_PROXY`, which may print an "experimental" notice). Tools launched with `npx` also
|
|
168
|
+
need `registry.npmjs.org` allowed; pre-installing the package is better.
|
|
169
|
+
- A sandboxed tool still holds its own key and can send it to any allowlisted host, so allow only hosts you trust.
|
|
170
|
+
- **The key is still in the tool's memory and environment**; unsandboxed, it is readable by your OS user (`/proc/<pid>/environ`, Task Manager).
|
|
171
|
+
- **Scrubbing covers UTF-8 text.** Binary output, or a tool that splits or otherwise encodes the key (encryption, custom
|
|
172
|
+
alphabets), gets through. `--no-scrub` exists for binary tools.
|
|
173
|
+
- If LLLANYARD itself is force-killed (SIGKILL), the tool can be orphaned; a normal exit or Ctrl+C stops it.
|
|
174
|
+
|
|
175
|
+
Use `run` for tools you trust, and give each tool the narrowest key you can (a read-only GitHub token, not your admin one).
|
|
176
|
+
|
|
177
|
+
Env: `LLLANYARD_PORT`, `LLLANYARD_RPM` (requests/min per agent, default 120), `LLLANYARD_ALLOW_ORIGIN=1` (allow Electron-style requests that carry an Origin header).
|
|
178
|
+
|
|
179
|
+
## Security model
|
|
180
|
+
|
|
181
|
+
**Protects against:** keys sitting in `.env`/config files an agent can read; keys leaking into prompts, logs, screenshots or
|
|
182
|
+
git; prompt-injected agents echoing their "key" (it is only a placeholder); runaway request loops (rate limit + kill switch).
|
|
183
|
+
|
|
184
|
+
**Does NOT protect against:** malicious code running *as your OS user*. A fully compromised agent that can run shell
|
|
185
|
+
commands can ask the OS keychain for the key directly (macOS prompts; Windows and Linux generally do not). For
|
|
186
|
+
untrusted agents, run the proxy under a separate OS user or on another machine (the planned self-hosted shared proxy).
|
|
187
|
+
|
|
188
|
+
Built-in hardening:
|
|
189
|
+
- Zero runtime dependencies in the proxy (Node built-ins only); the only native dependency is `@napi-rs/keyring`.
|
|
190
|
+
- Binds to `127.0.0.1` only; rejects non-loopback `Host` headers (DNS rebinding) and browser `Origin` requests.
|
|
191
|
+
- Agent tokens are 256-bit, stored only as SHA-256 hashes, compared in constant time.
|
|
192
|
+
- Upstream host is fixed per provider; request paths can't change it; redirects are never followed.
|
|
193
|
+
- Cookies are stripped both ways; logs contain agent/provider/status only (no keys, bodies or query strings).
|
|
194
|
+
- No command ever prints a stored key. `scan` masks keys, and findings hold plaintext in a private field that is
|
|
195
|
+
excluded from JSON/inspect output. `--redact` edits atomically and writes **no backup** (a backup is another plaintext copy).
|
|
196
|
+
- Keys given on the command line are refused by default (`--allow-argv` overrides).
|
|
197
|
+
- Custom providers must use https and a public hostname (no IPs, no credentials in the URL). The proxy also refuses to connect
|
|
198
|
+
to private, loopback, link-local (cloud metadata) or CGNAT addresses, including hostnames that resolve to them, unless a
|
|
199
|
+
provider was explicitly created with `--allow-local` for a loopback server.
|
|
200
|
+
- Built-in provider names can't be redefined; names like `constructor` are never treated as providers.
|
|
201
|
+
- Agents created with `--providers` get a 403 for anything else.
|
|
202
|
+
- `run`: keys go only into the child's environment (never the parent's), blocked variables (`LD_*`, `DYLD_*`, `PATH`,
|
|
203
|
+
`NODE_OPTIONS`...) are refused, and on Windows `.cmd` shims (npx) run through a shell only if every argument is plain.
|
|
204
|
+
- Limits: JavaScript cannot wipe strings from memory; the proxy caches a key for 30 s. Revoking an agent takes ≤10 s to apply.
|
|
205
|
+
|
|
206
|
+
Keys that sat in plaintext should be treated as exposed: after `scan --import`, create new keys at the provider and run `rotate`.
|
|
207
|
+
|
|
208
|
+
## Platform notes
|
|
209
|
+
|
|
210
|
+
- **macOS / Windows:** work out of the box (Keychain / Credential Manager).
|
|
211
|
+
- **Debian/Ubuntu:** needs a Secret Service: `sudo apt install gnome-keyring libsecret-1-0`, inside a desktop/D-Bus session.
|
|
212
|
+
Headless servers are not supported yet.
|
|
213
|
+
|
|
214
|
+
## Tiers (planned)
|
|
215
|
+
|
|
216
|
+
Free: up to 10 keys, local only. Pro: shared directories via a self-hosted shared proxy (keys stay on the host's keychain)
|
|
217
|
+
and later an end-to-end-encrypted cloud option, plus dashboard, rotation assistant and local-LLM helper.
|
|
218
|
+
|
|
219
|
+
### Desktop app and rotation assistant
|
|
220
|
+
|
|
221
|
+
`lllanyard ui` opens a private local page in your browser (`apps/desktop` wraps the same page in a native Tauri window):
|
|
222
|
+
keys with rotation badges, add/replace/remove (keys are write-only: only the last 4 characters are ever shown), agent
|
|
223
|
+
tokens, usage, proxy start/stop, license, and the **rotation assistant**.
|
|
224
|
+
|
|
225
|
+
- The assistant gets **metadata only** (provider, label, age, due date, request counts), never a key, and only talks to a model
|
|
226
|
+
**on this computer** (Ollama at `127.0.0.1:11434` by default, or an OpenAI-compatible local server such as LM Studio or
|
|
227
|
+
llama.cpp). A non-localhost endpoint is refused. With no model running, "Quick review" still gives a rule-based plan.
|
|
228
|
+
- The UI server listens on loopback only and needs a random per-run session secret (in the link's `#` fragment, removed from the
|
|
229
|
+
address bar on load) on every API call. It also checks the Host header (DNS rebinding), refuses foreign Origins and
|
|
230
|
+
cross-site fetches, requires JSON for writes, caps request size, and serves a strict Content-Security-Policy with a per-run
|
|
231
|
+
nonce. All text is inserted with `textContent`, never as HTML.
|
|
232
|
+
- Honest limits: any program running as your user could still read the session secret from the process list or the printed
|
|
233
|
+
link, as it could read your keychain; this protects against web pages and other websites, not against local malware.
|
|
234
|
+
The Tauri shell has not been compiled yet (see `apps/desktop/README.md`).
|
|
235
|
+
|
|
236
|
+
### MCP server for agents
|
|
237
|
+
|
|
238
|
+
`lllanyard mcp` is a read-only [MCP](https://modelcontextprotocol.io) server over stdio. Add it to Claude Code, Cursor, etc.:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{ "mcpServers": { "lllanyard": { "command": "lllanyard", "args": ["mcp"] } } }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Tools: `list_providers`, `key_status` (age and rotation due dates), `usage_summary`, `how_to_connect`. It can answer "is my
|
|
245
|
+
OpenAI key due for rotation?" or "what is the base URL for GitHub?" and cannot return a key, a token or any hint of one
|
|
246
|
+
(not even the last 4 characters), nor change anything. `--client acme` limits it to one client's keys. With the file vault the
|
|
247
|
+
passphrase must come from `LLLANYARD_VAULT_PASSPHRASE_FILE` (no prompt is possible on stdio). Note that anything an agent can
|
|
248
|
+
run as your user can also run `lllanyard` directly; the MCP server is a safe *information* channel, not an access boundary.
|
|
249
|
+
|
|
250
|
+
### Sharing with a team (Pro)
|
|
251
|
+
|
|
252
|
+
One computer (a small server, or an always-on workstation) holds the real keys; teammates get their own agent tokens.
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
lllanyard serve --host 0.0.0.0 --port 3443 --tls-cert cert.pem --tls-key key.pem --allow-host keys.example.com
|
|
256
|
+
lllanyard agent create alice-laptop --client acme --providers openai --expires 90d
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Teammates point their tools at `https://keys.example.com:3443/openai/v1` and use their token as the API key. They can
|
|
260
|
+
*use* a key but never read it. Revoking a token cuts that person off within ~10 seconds.
|
|
261
|
+
|
|
262
|
+
| Safeguard | |
|
|
263
|
+
|---|---|
|
|
264
|
+
| TLS required | Non-loopback binds refuse to start without `--tls-cert/--tls-key` (TLS 1.2+); the key file must be `chmod 600` |
|
|
265
|
+
| Host allowlist | Only the names given with `--allow-host` (plus loopback) are accepted: DNS-rebinding protection stays on |
|
|
266
|
+
| Brute-force lockout | 10 bad tokens in 10 minutes locks that address out for 15 minutes, even for a valid token |
|
|
267
|
+
| Per-person scope | Each token is limited to a client (`--client`), optionally to providers, and can expire (`--expires`) |
|
|
268
|
+
| Audit trail | Every request is logged with agent name and caller address; `lllanyard usage` breaks it down by agent and client |
|
|
269
|
+
| Rate limits + kill switch | Per token, as before |
|
|
270
|
+
| Seats | Free: 5 agent tokens. Licensed: 3 tokens per seat (`AGENTS_PER_SEAT`, a pricing hypothesis) |
|
|
271
|
+
|
|
272
|
+
Put it behind a VPN (Tailscale, WireGuard) or a firewall if you can; the lockout and TLS are a second layer, not a reason to
|
|
273
|
+
expose it to the whole internet. `X-Forwarded-For` is deliberately ignored, so a reverse proxy in front shows as one address.
|
|
274
|
+
|
|
275
|
+
**Headless servers and containers: the encrypted file vault.** The OS keychain needs a desktop login, so on a server use
|
|
276
|
+
`LLLANYARD_BACKEND=file`. Everything (keys, agents, tools, license) then lives in one encrypted file,
|
|
277
|
+
`~/.lllanyard/vault.enc` (override with `LLLANYARD_VAULT_PATH`): AES-256-GCM, key from your passphrase via scrypt, atomic
|
|
278
|
+
writes, safe with several processes. Wrong passphrases and edited files are detected.
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
echo 'a long random passphrase' > /etc/lllanyard/pass && chmod 600 /etc/lllanyard/pass
|
|
282
|
+
export LLLANYARD_BACKEND=file LLLANYARD_VAULT_PASSPHRASE_FILE=/etc/lllanyard/pass
|
|
283
|
+
lllanyard add openai --stdin < key.txt && lllanyard serve --host 0.0.0.0 ...
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
On a terminal with no passphrase file you are prompted instead (hidden). Under systemd use `LoadCredential=` and point
|
|
287
|
+
`LLLANYARD_VAULT_PASSPHRASE_FILE` at `$CREDENTIALS_DIRECTORY/pass`. Honest limit: it protects the vault at rest (backups,
|
|
288
|
+
disk images, other users); someone who can read both the vault and the passphrase file as your user can open it. For
|
|
289
|
+
stronger custody use a cloud KMS or a hardware token, a later step. Changing the passphrase is not implemented yet.
|
|
290
|
+
|
|
291
|
+
### Usage and dashboard
|
|
292
|
+
|
|
293
|
+
The proxy keeps a metadata-only request log (`~/.lllanyard/usage.jsonl`, owner-only, rotates at 10 MB; no keys, bodies or
|
|
294
|
+
query strings). Blocked egress attempts from sandboxed tools are added too.
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
lllanyard usage --since 7d # free: requests, errors, 429s, kill-switch trips, p50/p95 by provider/agent/client
|
|
298
|
+
lllanyard dashboard # Pro: self-contained HTML page (no scripts, no network requests)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Licenses
|
|
302
|
+
|
|
303
|
+
Buy Pro or Team at [lllanyard.app](https://lllanyard.app/pricing). Paid tiers are unlocked by a signed license key that
|
|
304
|
+
is verified **offline** (Ed25519):
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
lllanyard license activate lly1.… # or --key lly1.…, or --stdin
|
|
308
|
+
lllanyard license status
|
|
309
|
+
lllanyard license refresh # fetch the renewed key after your subscription renews
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The only time LLLanyard contacts anything of ours: in the last 7 days before a paid license runs out, `start` and `ui`
|
|
313
|
+
send that license (nothing else) to lllanyard.app to fetch its renewed key. Set `LLLANYARD_NO_LICENSE_REFRESH=1` to turn
|
|
314
|
+
this off; each renewed key is also emailed.
|
|
315
|
+
|
|
316
|
+
Pro lifts the key cap to 250, Team to 500 (`TIER_LIMITS` in `packages/core/src/license.ts`). An expired license drops
|
|
317
|
+
back to free: existing keys keep working, you just can't add past the free cap.
|
|
318
|
+
|
|
319
|
+
**Vendor setup (once):** `node tools/issue-license.mjs gen-keys ~/lly-keys` creates a keypair; paste the public key into
|
|
320
|
+
`LICENSE_PUBLIC_KEY`, keep the private key offline. Issue a customer license with
|
|
321
|
+
`node tools/issue-license.mjs issue --key ~/lly-keys/license-private.pem --tier pro --days 365 --sub order-123`.
|
|
322
|
+
Until `LICENSE_PUBLIC_KEY` is set, no license validates.
|
|
323
|
+
|
|
324
|
+
**Honest limit:** a local check can be patched in private. The BSL license (see License) forbids it and makes such use
|
|
325
|
+
unlicensed, but the features worth paying for (shared proxy, cloud sync) should also live where a local patch can't reach.
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
LLLANYARD is source-available under the **Business Source License 1.1** (`LICENSE.md`), licensor **Ethan Askew T/A LLLanyard**.
|
|
330
|
+
In plain words (the license text is what counts):
|
|
331
|
+
|
|
332
|
+
- You can read, copy, modify and **evaluate or develop** with it freely.
|
|
333
|
+
- **Production use** is free within the free tier (10 keys, 5 agent tokens, local only). Beyond that you need a paid license key
|
|
334
|
+
from Ethan Askew T/A LLLanyard.
|
|
335
|
+
- You **may not remove, bypass or tamper with the license-key checks or usage limits**, or help others do so. Doing it makes
|
|
336
|
+
your use unlicensed, and the license terminates.
|
|
337
|
+
- You may not resell it as a hosted/embedded competing product.
|
|
338
|
+
- Each version converts to Apache 2.0 four years after its release.
|
|
339
|
+
|
|
340
|
+
The Business Source License is a *licence*, not technical protection: it makes patching out the checks a breach (a legal and
|
|
341
|
+
commercial remedy), but cannot physically stop someone doing it in private. Keep the genuinely valuable features (shared
|
|
342
|
+
proxy operations, cloud sync, support, updates) where only you can provide them.
|
|
343
|
+
|
|
344
|
+
Before publishing: fill in the licensing contact email in `LICENSE.md`, and have a lawyer review the **Additional Use Grant**
|
|
345
|
+
(it is my drafting, not boilerplate) and the Change License choice. The BSL body text was taken from a published BSL 1.1 copy
|
|
346
|
+
(HashiCorp's) because the official page was unreachable; compare it with mariadb.com/bsl11, which also includes a
|
|
347
|
+
"Covenants of Licensor" section you should keep if you use the name "Business Source License". PolyForm Shield and
|
|
348
|
+
Noncommercial texts remain in `docs/license-options/` as alternatives; note Shield does **not** forbid patching out checks.
|
|
349
|
+
|
|
350
|
+
## Verifying on your machine
|
|
351
|
+
|
|
352
|
+
`node tools/smoke.mjs` (add `--keychain` to also test your real OS keychain once) runs the real CLI, proxy, UI and MCP server
|
|
353
|
+
against a throwaway vault and prints PASS/FAIL plus a one-line report for bug reports. See `RELEASING.md` for the full
|
|
354
|
+
pre-launch checklist.
|
|
355
|
+
|
|
356
|
+
## Tests
|
|
357
|
+
|
|
358
|
+
`npm test` (tsx + node:test, 109 tests): storage, tokens, scanner/redaction, rotation, provider and tool validation, the output
|
|
359
|
+
scrubber (split chunks, base64/hex/URL forms), the `run` engine with real child processes (minimal environment, leak warning
|
|
360
|
+
and kill, egress wiring), the egress proxy (allow/deny, auth, private-address blocking), and 15 proxy integration tests
|
|
361
|
+
against a fake upstream (key injection, SSE streaming, redirects, Host/Origin checks, path tricks, rate limit, SSRF guard,
|
|
362
|
+
agent scoping, log hygiene). A manual end-to-end pass of the CLI, a custom provider and the live proxy also succeeded using
|
|
363
|
+
a file-backed stand-in for the keychain.
|
|
364
|
+
CI (`.github/workflows/ci.yml`) runs the suite on Linux, macOS and Windows with Node 22 and 24, installs bubblewrap on Linux so
|
|
365
|
+
the real-sandbox test runs, and does a full type-check and `npm audit`. Not covered locally: real keychains on each OS, real provider endpoints, and the Windows shell path for `npx` (logic is unit-tested
|
|
366
|
+
with a simulated platform, not on Windows).
|
|
367
|
+
|
|
368
|
+
## Roadmap
|
|
369
|
+
|
|
370
|
+
1. Compile and sign the Tauri app; bundle Node as a sidecar so no separate install is needed.
|
|
371
|
+
2. E2E-encrypted cloud sync; vault passphrase change; KMS/hardware-token vault backends.
|
|
372
|
+
3. Docker sandbox mode (Windows).
|
|
373
|
+
4. Auto-rotation via provider admin APIs. Honeytoken keys need a hosted callback service, so they belong in a paid cloud tier.
|
|
374
|
+
|
|
375
|
+
## Naming
|
|
376
|
+
|
|
377
|
+
The name is a placeholder pending a trademark check. To rename: edit `packages/core/src/constants.ts` and the `@lllanyard`
|
|
378
|
+
package scope. Changing `SERVICE` orphans keys already stored under the old name.
|
package/dist/index.d.ts
ADDED