Skip to content

Commit b5aaa6a

Browse files
committed
feat(kern): kern_get MCP tool, recipients remove, machine payments docs
- Add kern_get MCP tool (direct mode — decrypt and return a credential) - Implement kern recipients remove (walks all .recipients files) - Implement kern recipients list (shows recipients per folder) - Add machine payments section to README (Stripe MPP, x402 roadmap) - Restore CI badge in README
1 parent 93b2804 commit b5aaa6a

3 files changed

Lines changed: 96 additions & 9 deletions

File tree

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# kern
22

3+
[![tests](https://github.com/daslabhq/kern/actions/workflows/test.yml/badge.svg)](https://github.com/daslabhq/kern/actions/workflows/test.yml)
4+
35
The agent wallet. Hold credentials — agents use them without seeing them.
46

57
```bash
@@ -169,6 +171,21 @@ kern secret rewrap
169171
kern secret rotate tokens/github # rotate what they had access to
170172
```
171173

174+
## Machine payments
175+
176+
The same proxy that protects API keys protects payment credentials. An agent that purchases compute, calls paid APIs, or manages subscriptions needs payment keys — but shouldn't hold them.
177+
178+
```typescript
179+
// agent charges a customer — never sees sk_live_*
180+
const resp = await wallet.fetch("tokens/stripe", "https://api.stripe.com/v1/payment_intents", {
181+
method: "POST",
182+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
183+
body: "amount=2000&currency=usd&automatic_payment_methods[enabled]=true",
184+
});
185+
```
186+
187+
Works with [Stripe MPP](https://docs.stripe.com/payments/machine/mpp) for machine-to-machine payments and any API that takes Bearer auth. [x402](https://www.x402.org/) support — auto-negotiating `402 Payment Required` responses — is on the roadmap.
188+
172189
## CLI
173190

174191
```bash
@@ -186,7 +203,7 @@ kern fetch SECRET URL [OPTIONS] # proxy request (credential stays in wallet)
186203
--method POST # HTTP method (default GET)
187204
--body '{"key": "val"}' # request body
188205

189-
kern recipients list # show recipients
206+
kern recipients # list all recipients
190207
kern recipients remove KEY # remove from all folders
191208

192209
kern mcp # start MCP server

bin/kern.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// kern CLI — the agent wallet
33

44
import { generateIdentity, loadIdentityFromHost, openVault, openWallet } from "../src/index.js";
5-
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
5+
import { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync, statSync } from "fs";
66
import { dirname, join } from "path";
77
import { homedir } from "os";
88

@@ -16,7 +16,7 @@ async function main() {
1616
case "identity": return cmdIdentity(sub, args.slice(2));
1717
case "secret":
1818
case "secrets": return cmdSecret(sub, args.slice(2));
19-
case "recipients": return cmdRecipients();
19+
case "recipients": return cmdRecipients(sub, args.slice(2));
2020
case "fetch": return cmdFetch(args.slice(1));
2121
case "mcp": return cmdMcp();
2222
case "serve": return cmdServe();
@@ -40,7 +40,8 @@ function help() {
4040
` kern secret delete NAME\n` +
4141
` kern secret rewrap\n\n` +
4242
` kern fetch SECRET URL [--method METHOD] [--body BODY]\n\n` +
43-
` kern recipients\n\n` +
43+
` kern recipients list all recipients\n` +
44+
` kern recipients remove KEY remove from all folders\n\n` +
4445
`env: KERN_AGE_KEY (private key) KERN_VAULT_DIR (default ./secrets)\n`);
4546
}
4647

@@ -147,14 +148,57 @@ async function cmdFetch(rest: string[]) {
147148
process.exit(resp.ok ? 0 : 1);
148149
}
149150

150-
function cmdRecipients() {
151+
function cmdRecipients(sub?: string, rest?: string[]) {
151152
const dir = process.env.KERN_VAULT_DIR ?? process.env.KORN_VAULT_DIR ?? "./secrets";
152-
const file = join(dir, ".recipients");
153-
if (!existsSync(file)) {
154-
console.error(`no ${file} — create it with one age pubkey per line`);
153+
154+
if (sub === "remove") {
155+
const key = rest?.[0];
156+
if (!key) { console.error("usage: kern recipients remove KEY"); process.exit(1); }
157+
let removed = 0;
158+
const walk = (d: string) => {
159+
const file = join(d, ".recipients");
160+
if (existsSync(file)) {
161+
const lines = readFileSync(file, "utf8").split("\n");
162+
const filtered = lines.filter(l => l.trim() !== key);
163+
if (filtered.length < lines.length) {
164+
writeFileSync(file, filtered.join("\n"));
165+
removed++;
166+
}
167+
}
168+
if (!existsSync(d)) return;
169+
for (const ent of readdirSync(d)) {
170+
if (ent.startsWith(".")) continue;
171+
const full = join(d, ent);
172+
if (statSync(full).isDirectory()) walk(full);
173+
}
174+
};
175+
walk(dir);
176+
if (removed) console.log(`✓ removed from ${removed} .recipients file${removed > 1 ? "s" : ""}`);
177+
else console.log(`key not found in any .recipients file`);
178+
return;
179+
}
180+
181+
if (!existsSync(dir)) {
182+
console.error(`no vault at ${dir}`);
155183
process.exit(1);
156184
}
157-
process.stdout.write(readFileSync(file, "utf8"));
185+
const walk = (d: string, prefix: string) => {
186+
const file = join(d, ".recipients");
187+
if (existsSync(file)) {
188+
const keys = readFileSync(file, "utf8")
189+
.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("#"));
190+
if (keys.length) {
191+
console.log(`${prefix || "."}:`);
192+
for (const k of keys) console.log(` ${k}`);
193+
}
194+
}
195+
for (const ent of readdirSync(d)) {
196+
if (ent.startsWith(".")) continue;
197+
const full = join(d, ent);
198+
if (statSync(full).isDirectory()) walk(full, prefix ? `${prefix}/${ent}` : ent);
199+
}
200+
};
201+
walk(dir, "");
158202
}
159203

160204
async function readSecretInput(prompt: string): Promise<string> {

src/mcp.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// kern_status — wallet health (credential count, recipient count)
55
// kern_list — list credential names (no values)
66
// kern_fetch — proxy HTTP request (credential stays in wallet)
7+
// kern_get — decrypt and return a credential (direct mode)
78
// kern_add — add a credential via browser form (never through LLM)
89
// kern_rotate — rotate a credential via browser form
910
// kern_remove — remove a credential
@@ -111,6 +112,31 @@ export async function startMcpServer() {
111112
}
112113
},
113114
},
115+
{
116+
name: "kern_get",
117+
description: "Decrypt and return a credential value (direct mode). Use kern_fetch instead when possible — it keeps the credential in the wallet.",
118+
inputSchema: {
119+
type: "object",
120+
required: ["name"],
121+
properties: {
122+
name: { type: "string", description: "Credential name (e.g. tokens/openai)" },
123+
},
124+
},
125+
handler: async (args, respond) => {
126+
const name = args.name as string;
127+
try {
128+
const value = await wallet.get(name);
129+
respond(undefined, {
130+
content: [{ type: "text", text: value }],
131+
});
132+
} catch (e: any) {
133+
respond(undefined, {
134+
content: [{ type: "text", text: `Error: ${e.message}` }],
135+
isError: true,
136+
});
137+
}
138+
},
139+
},
114140
{
115141
name: "kern_add",
116142
description: "Add a credential to the wallet. Opens a secure browser form where the user pastes the value. It never passes through the LLM.",

0 commit comments

Comments
 (0)