@paydlux/agent 0.1.11 → 0.1.12
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/README.md +23 -2
- package/dist/cli.js +48 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,11 +213,32 @@ if (res.ok) console.log(res.data, "paid in tx", res.tx);
|
|
|
213
213
|
`npx tsx stellar/onboard.ts` creates the vault and prints that key. The key is
|
|
214
214
|
**one of two signatures** — it cannot spend alone.
|
|
215
215
|
|
|
216
|
+
### Your spending mandate (one command, enforced at the approve button)
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
paydlux sell --price 4.50 --max 10 --daily 20
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
No payment above 10, nothing past 20 in a day. The desk checks this **before**
|
|
223
|
+
it asks your phone, and again at submission — the budget can be spent between
|
|
224
|
+
your approval and the ledger call, so a stale signature is refused rather than
|
|
225
|
+
honoured. Approvals already waiting count against the budget, so a queue of six
|
|
226
|
+
4.50 requests against a 20/day cap sends four to your phone and refuses two at
|
|
227
|
+
the button instead of letting you approve six and watch two fail.
|
|
228
|
+
|
|
229
|
+
A refusal answers the buyer with `error: "mandate_refused"` on the **same**
|
|
230
|
+
handle, so a polite agent cannot park-refuse-park forever. `paydlux sell` also
|
|
231
|
+
refuses to start if `--price` is above `--max` or `--daily`, because that
|
|
232
|
+
combination blocks every payment and looks like a bug in the desk.
|
|
233
|
+
|
|
234
|
+
Verified on the published package: 6 x 4.50 against `--daily 20` -> four
|
|
235
|
+
settled, two refused, vault balance moved exactly 18.00 and stopped.
|
|
236
|
+
|
|
216
237
|
### What is enforced where (said honestly)
|
|
217
238
|
|
|
218
|
-
| enforced by Stellar | enforced by `policy.ts` |
|
|
239
|
+
| enforced by Stellar | enforced by the desk (`policy.ts`) |
|
|
219
240
|
|---|---|
|
|
220
|
-
|
|
|
241
|
+
| payee, token, time window, one-shot execution, per-payment ceiling as a signer rule | per-payment ceiling and daily total at the approve button and again at submission, running daily total, seller allow/deny list |
|
|
221
242
|
|
|
222
243
|
The vault is one payment at a time; counting payments is our job. A rule break
|
|
223
244
|
means we never build the transaction, so nothing reaches the ledger.
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// A seller should type one command and get a price, a link, and a key to paste
|
|
15
15
|
// into their bot.
|
|
16
16
|
import { createInterface } from "node:readline/promises";
|
|
17
|
+
import { createServer } from "node:http";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
17
19
|
import { existsSync, readFileSync, writeFileSync, chmodSync, realpathSync } from "node:fs";
|
|
18
20
|
import { pathToFileURL } from "node:url";
|
|
19
21
|
import { resolve } from "node:path";
|
|
@@ -33,6 +35,39 @@ const log = (...a) => console.log("•", ...a);
|
|
|
33
35
|
/** Print a reason and stop. Declared as a function (not an arrow in a const) so
|
|
34
36
|
* TypeScript's control-flow analysis actually narrows the caller after it. */
|
|
35
37
|
function bad(msg) { console.error("\n" + msg + "\n"); process.exit(1); }
|
|
38
|
+
/**
|
|
39
|
+
* Ask the OS whether a port is free, and if not, who owns it. Returns null when
|
|
40
|
+
* the port is available. Naming the owning process is what turns "EADDRINUSE"
|
|
41
|
+
* into something a seller can act on.
|
|
42
|
+
*/
|
|
43
|
+
function portHolder(port) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const probe = createServer();
|
|
46
|
+
probe.once("error", (e) => {
|
|
47
|
+
if (e.code === "EADDRINUSE")
|
|
48
|
+
resolve(findListener(port) ?? "another process");
|
|
49
|
+
else
|
|
50
|
+
resolve(null); // some other bind problem; let the real listen report it
|
|
51
|
+
});
|
|
52
|
+
probe.once("listening", () => { probe.close(() => resolve(null)); });
|
|
53
|
+
probe.listen(port, "127.0.0.1");
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Best-effort: which command is holding this port, from `lsof`. */
|
|
57
|
+
function findListener(port) {
|
|
58
|
+
try {
|
|
59
|
+
const out = execSync(`lsof -nP -ti tcp:${port} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf8" });
|
|
60
|
+
const pid = out.trim().split("\n")[0];
|
|
61
|
+
if (!pid)
|
|
62
|
+
return null;
|
|
63
|
+
const cmd = execSync(`ps -o command= -p ${pid} 2>/dev/null`, { encoding: "utf8" }).trim();
|
|
64
|
+
const mine = cmd.includes("cli") || cmd.includes("paydlux");
|
|
65
|
+
return `${mine ? "another Paydlux (pid " + pid + ")" : cmd || "pid " + pid}`;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
36
71
|
/** The one file that holds everything. Refuses to read a file that is world-readable. */
|
|
37
72
|
export function loadVault(path = resolve(process.cwd(), arg("file") ?? VAULT_FILE_DEFAULT)) {
|
|
38
73
|
if (!existsSync(path)) {
|
|
@@ -219,6 +254,19 @@ async function sell() {
|
|
|
219
254
|
const publicHost = tls ? PUBLIC_HOST : (arg("host") ?? undefined);
|
|
220
255
|
const deskPort = Number(arg("desk") ?? 4022);
|
|
221
256
|
const gwPort = Number(arg("port") ?? 4021);
|
|
257
|
+
// A taken port used to surface as an unhandled EADDRINUSE stack trace from
|
|
258
|
+
// inside Node. To someone following the README that reads as a broken
|
|
259
|
+
// install, and the stack names neither port nor remedy.
|
|
260
|
+
for (const [label, port] of [["gateway", gwPort], ["approval desk", deskPort]]) {
|
|
261
|
+
const who = await portHolder(port);
|
|
262
|
+
if (who) {
|
|
263
|
+
// Suggest the pair, not the clashing port twice: --port and --desk with
|
|
264
|
+
// the same number would fail on the next line.
|
|
265
|
+
bad(`port ${port} (${label}) is already in use by ${who}.\n` +
|
|
266
|
+
` another Paydlux is probably running. Stop it, or move this one:\n` +
|
|
267
|
+
` paydlux sell --port ${gwPort + 100} --desk ${deskPort + 100}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
222
270
|
const gw = new Gateway({
|
|
223
271
|
vault, network, payee,
|
|
224
272
|
deskPort,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paydlux/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Payments for AI agents that a human approves. Spending mandates enforced on the Stellar ledger: 2-of-2 vault, pre-authorised transaction envelopes, HTTP 402, USDC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|