@kybernesis/payments 0.0.0-stage → 0.2.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/README.md +48 -2
- package/dist/extension.d.ts +10 -0
- package/dist/extension.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +12 -0
- package/dist/link-cli-auth.d.ts +34 -0
- package/dist/link-cli-auth.js +55 -0
- package/dist/link-tools.d.ts +29 -0
- package/dist/link-tools.js +71 -0
- package/dist/pay-on-computer.d.ts +51 -0
- package/dist/pay-on-computer.js +73 -0
- package/package.json +48 -4
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kybernesis/payments
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Link wallet for an eve agent, on its owner's behalf.
|
|
4
|
+
|
|
5
|
+
Stripe's Link eve integration gives an agent `link__create_spend_request` and
|
|
6
|
+
friends: the agent names a merchant and a total, the person approves it in the
|
|
7
|
+
Link app, and Link issues a one-time card for exactly that purchase. This
|
|
8
|
+
package adds the two pieces that make it usable by a Kybernesis agent:
|
|
9
|
+
|
|
10
|
+
- **`linkTool(name)`** — Link's tool catalog (`@stripe/link-sdk/tools`) as eve tools, one per file. `create_spend_request` asks the person in eve before Link asks them again; every result is stripped of card fields.
|
|
11
|
+
- **`linkCliAuth()`** — the owner's `link-cli` sign-in as the agent's wallet
|
|
12
|
+
credential. The owner runs `npx link-cli auth login --client-name <agent>` on
|
|
13
|
+
the agent's host once (a device-code flow approved in the Link app); the CLI
|
|
14
|
+
keeps and refreshes the tokens in its own config file, and this provider reads
|
|
15
|
+
that file for each call. No token in env, none in source. Single-owner by
|
|
16
|
+
design: this is a personal agent paying from its owner's wallet.
|
|
17
|
+
- **`payOnComputerTool()`** (`pay_on_computer`) — once a spend request is
|
|
18
|
+
approved, types the one-time card into the checkout form open in the agent's
|
|
19
|
+
own browser via `@kybernesis/computer`'s fill primitive. The model supplies CSS
|
|
20
|
+
selectors; the card goes Link → page and comes back only as brand + last four.
|
|
21
|
+
|
|
22
|
+
```ts title="agent/tools/create_spend_request.ts"
|
|
23
|
+
import { linkTool } from "@kybernesis/payments";
|
|
24
|
+
export default linkTool("create_spend_request"); // one file per Link tool; see LINK_TOOL_NAMES
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ts title="agent/tools/pay_on_computer.ts"
|
|
28
|
+
import { payOnComputerTool } from "@kybernesis/payments";
|
|
29
|
+
export default payOnComputerTool();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`PAYMENTS_INSTRUCTIONS` is the order of operations the agent follows: agree the
|
|
33
|
+
exact total from the real checkout page, create one spend request, send the
|
|
34
|
+
approval link, wait, fill, screenshot, submit, report with `link__create_report`.
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- `@kybernesis/computer` ≥ 0.2.0 mounted as the root sandbox (that is where the
|
|
39
|
+
browser is).
|
|
40
|
+
- `link-cli` signed in on the host. Without it every wallet tool fails with a
|
|
41
|
+
message saying exactly that; the agent cannot complete the sign-in itself.
|
|
42
|
+
- The install is `eve add @kybernesis/payments` from the Kybernesis registry.
|
|
43
|
+
|
|
44
|
+
## Gotchas
|
|
45
|
+
|
|
46
|
+
- Stripe's own eve extension (`@stripe/link-integrations-eve`, built with eve 0.66) does not mount on eve 0.68 — that is why the tools are wrapped here from the SDK instead.
|
|
47
|
+
- Link may require 3-D Secure or a verification step the agent cannot complete.
|
|
48
|
+
The instructions tell it to stop and describe the screen; the person can take
|
|
49
|
+
over the computer.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type LinkCliAuthOptions } from "./link-cli-auth.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stripe's Link eve extension, mounted behind the owner's link-cli sign-in.
|
|
4
|
+
*
|
|
5
|
+
* ```ts title="agent/extensions/link.ts"
|
|
6
|
+
* import { linkWallet } from "@kybernesis/payments";
|
|
7
|
+
* export default linkWallet();
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export declare function linkWallet(options?: LinkCliAuthOptions): import("eve/extension").MountedExtension;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import link from "@stripe/link-integrations-eve";
|
|
2
|
+
import { linkCliAuth } from "./link-cli-auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* Stripe's Link eve extension, mounted behind the owner's link-cli sign-in.
|
|
5
|
+
*
|
|
6
|
+
* ```ts title="agent/extensions/link.ts"
|
|
7
|
+
* import { linkWallet } from "@kybernesis/payments";
|
|
8
|
+
* export default linkWallet();
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export function linkWallet(options = {}) {
|
|
12
|
+
return link({ auth: linkCliAuth(options) });
|
|
13
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { linkCliAuth, linkCliAuthFile, readStoredAuth, NOT_SIGNED_IN, type LinkCliAuthOptions } from "./link-cli-auth.js";
|
|
2
|
+
export { payOnComputerTool } from "./pay-on-computer.js";
|
|
3
|
+
export { PAYMENTS_INSTRUCTIONS } from "./instructions.js";
|
|
4
|
+
export { linkTool, linkClient, sanitizeLinkOutput, LINK_TOOL_NAMES, type LinkToolName } from "./link-tools.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { linkCliAuth, linkCliAuthFile, readStoredAuth, NOT_SIGNED_IN } from "./link-cli-auth.js";
|
|
2
|
+
export { payOnComputerTool } from "./pay-on-computer.js";
|
|
3
|
+
export { PAYMENTS_INSTRUCTIONS } from "./instructions.js";
|
|
4
|
+
export { linkTool, linkClient, sanitizeLinkOutput, LINK_TOOL_NAMES } from "./link-tools.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PAYMENTS_INSTRUCTIONS = "## Paying for things\n\nYou can buy things with the person's Link wallet. Every purchase is their decision twice over: creating a spend request asks them here, and Link asks them again in their Link app before any card exists. Never work around either step.\n\nThe order, every time:\n1. Agree the exact purchase first: merchant, items, final total including tax and shipping, in the right currency. Get these from the actual checkout page in your own browser (open_browser, then look), not from memory.\n2. `create_spend_request` with that merchant, those line items and that total. If it comes back without an approval link, call `request_spend_approval`. Send the person the approval link as a plain link, and wait. Do not create a second request for the same purchase.\n3. When `retrieve_spend_request` says approved, use `pay_on_computer` to type the one-time card into the checkout. You give it the selectors of the card fields you can see; the card itself never comes to you. Check the filled form in a screenshot, confirm the total still matches, then submit.\n4. Report the outcome with `create_report`: succeeded, failed, or what the merchant said. If the merchant needs more (3-D Secure, a code), stop and tell the person exactly what is on the screen; they can take over your screen.\n\nNever read card numbers or CVCs aloud, never paste them into chat or a file, never guess a total. If Link asks for verification you cannot complete, say so.\n";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const PAYMENTS_INSTRUCTIONS = `## Paying for things
|
|
2
|
+
|
|
3
|
+
You can buy things with the person's Link wallet. Every purchase is their decision twice over: creating a spend request asks them here, and Link asks them again in their Link app before any card exists. Never work around either step.
|
|
4
|
+
|
|
5
|
+
The order, every time:
|
|
6
|
+
1. Agree the exact purchase first: merchant, items, final total including tax and shipping, in the right currency. Get these from the actual checkout page in your own browser (open_browser, then look), not from memory.
|
|
7
|
+
2. \`create_spend_request\` with that merchant, those line items and that total. If it comes back without an approval link, call \`request_spend_approval\`. Send the person the approval link as a plain link, and wait. Do not create a second request for the same purchase.
|
|
8
|
+
3. When \`retrieve_spend_request\` says approved, use \`pay_on_computer\` to type the one-time card into the checkout. You give it the selectors of the card fields you can see; the card itself never comes to you. Check the filled form in a screenshot, confirm the total still matches, then submit.
|
|
9
|
+
4. Report the outcome with \`create_report\`: succeeded, failed, or what the merchant said. If the merchant needs more (3-D Secure, a code), stop and tell the person exactly what is on the screen; they can take over your screen.
|
|
10
|
+
|
|
11
|
+
Never read card numbers or CVCs aloud, never paste them into chat or a file, never guess a total. If Link asks for verification you cannot complete, say so.
|
|
12
|
+
`;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ToolAuthProvider } from "eve/tools";
|
|
2
|
+
/**
|
|
3
|
+
* The owner's Link sign-in, as the agent's wallet credential.
|
|
4
|
+
*
|
|
5
|
+
* `link-cli auth login` on the agent's host is a device-code flow the owner
|
|
6
|
+
* approves in the Link app; the CLI keeps the resulting tokens in its config
|
|
7
|
+
* file and refreshes them itself. This provider reads that file for the live
|
|
8
|
+
* access token and, when it is about to expire, asks the CLI to refresh by
|
|
9
|
+
* running one harmless command. No token is ever copied into env or source.
|
|
10
|
+
*
|
|
11
|
+
* Single-owner by design: every tool call through this mount spends from the
|
|
12
|
+
* wallet signed in on this host. That is what Sid is — Ian's agent paying from
|
|
13
|
+
* Ian's wallet, with Ian approving each purchase in Link. A shared agent needs
|
|
14
|
+
* per-person OAuth through the control plane instead.
|
|
15
|
+
*/
|
|
16
|
+
export interface LinkCliAuthOptions {
|
|
17
|
+
/** The CLI's config file. Defaults to LINK_AUTH_FILE, else the CLI's own default location. */
|
|
18
|
+
authFile?: string;
|
|
19
|
+
/** How close to expiry triggers a refresh. Default 2 minutes. */
|
|
20
|
+
refreshSkewMs?: number;
|
|
21
|
+
/** The CLI binary. Defaults to the one this package depends on. */
|
|
22
|
+
cliPath?: string;
|
|
23
|
+
}
|
|
24
|
+
interface StoredAuth {
|
|
25
|
+
access_token?: string;
|
|
26
|
+
refresh_token?: string;
|
|
27
|
+
expires_at?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function linkCliAuthFile(explicit?: string): string;
|
|
30
|
+
export declare function readStoredAuth(file: string): StoredAuth | null;
|
|
31
|
+
export declare const NOT_SIGNED_IN = "Link is not signed in on this host. The owner runs `npx link-cli auth login --client-name <agent>` here and approves it in the Link app; the agent cannot do this step.";
|
|
32
|
+
/** An eve tool auth provider: the owner's current Link access token, refreshed through the CLI when near expiry. */
|
|
33
|
+
export declare function linkCliAuth(options?: LinkCliAuthOptions): ToolAuthProvider;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export function linkCliAuthFile(explicit) {
|
|
6
|
+
if (explicit)
|
|
7
|
+
return explicit;
|
|
8
|
+
if (process.env.LINK_AUTH_FILE)
|
|
9
|
+
return process.env.LINK_AUTH_FILE;
|
|
10
|
+
// `conf` with projectName "link-cli": ~/.config/link-cli-nodejs/config.json on Linux and macOS.
|
|
11
|
+
const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
12
|
+
return join(base, "link-cli-nodejs", "config.json");
|
|
13
|
+
}
|
|
14
|
+
export function readStoredAuth(file) {
|
|
15
|
+
if (!existsSync(file))
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
19
|
+
return parsed.auth ?? null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export const NOT_SIGNED_IN = "Link is not signed in on this host. The owner runs `npx link-cli auth login --client-name <agent>` here and approves it in the Link app; the agent cannot do this step.";
|
|
26
|
+
function runCli(cliPath, args, env) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const child = spawn(cliPath, args, { stdio: ["ignore", "ignore", "ignore"], env });
|
|
29
|
+
child.on("error", () => resolve(1));
|
|
30
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** An eve tool auth provider: the owner's current Link access token, refreshed through the CLI when near expiry. */
|
|
34
|
+
export function linkCliAuth(options = {}) {
|
|
35
|
+
const file = linkCliAuthFile(options.authFile);
|
|
36
|
+
const skew = options.refreshSkewMs ?? 120_000;
|
|
37
|
+
const cli = options.cliPath ?? "link-cli";
|
|
38
|
+
return {
|
|
39
|
+
principalType: "app",
|
|
40
|
+
displayName: "Link wallet (owner's link-cli sign-in)",
|
|
41
|
+
async getToken() {
|
|
42
|
+
let auth = readStoredAuth(file);
|
|
43
|
+
if (!auth?.access_token)
|
|
44
|
+
throw new Error(NOT_SIGNED_IN);
|
|
45
|
+
if (auth.expires_at !== undefined && auth.expires_at - Date.now() < skew) {
|
|
46
|
+
// Any authenticated command makes the CLI refresh and persist. user-info is read-only.
|
|
47
|
+
await runCli(cli, ["user-info", "retrieve", "--format", "json"], { ...process.env, LINK_AUTH_FILE: file, NO_UPDATE_NOTIFIER: "1" });
|
|
48
|
+
auth = readStoredAuth(file);
|
|
49
|
+
if (!auth?.access_token)
|
|
50
|
+
throw new Error(NOT_SIGNED_IN);
|
|
51
|
+
}
|
|
52
|
+
return { token: auth.access_token, ...(auth.expires_at ? { expiresAt: auth.expires_at } : {}) };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Link } from "@stripe/link-sdk";
|
|
2
|
+
import { type ToolDefinition } from "eve/tools";
|
|
3
|
+
import { type LinkCliAuthOptions } from "./link-cli-auth.js";
|
|
4
|
+
/**
|
|
5
|
+
* Stripe's Link tools, mounted the Kybernesis way.
|
|
6
|
+
*
|
|
7
|
+
* Stripe ships an eve extension, but it is built against an older eve and
|
|
8
|
+
* eve 0.68 refuses to mount it. The same tool catalog is available
|
|
9
|
+
* framework-free from `@stripe/link-sdk/tools`, so each one is wrapped here as
|
|
10
|
+
* an eve tool: the owner's link-cli sign-in supplies the token on every call,
|
|
11
|
+
* creating a spend request asks the person in eve before it asks them in Link,
|
|
12
|
+
* and anything that could carry a card number is stripped from what the model
|
|
13
|
+
* sees — the card has exactly one path, `pay_on_computer`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const LINK_TOOL_NAMES: readonly ["retrieve_user_info", "list_payment_methods", "list_shipping_addresses", "list_spend_requests", "create_spend_request", "update_spend_request", "cancel_spend_request", "request_spend_approval", "retrieve_spend_request", "create_report"];
|
|
16
|
+
export type LinkToolName = (typeof LINK_TOOL_NAMES)[number];
|
|
17
|
+
/** A Link client whose token is the owner's current link-cli sign-in, read at call time. */
|
|
18
|
+
export declare function linkClient(options?: LinkCliAuthOptions): Link;
|
|
19
|
+
/** Remove anything that is or contains a card credential before it reaches the model. */
|
|
20
|
+
export declare function sanitizeLinkOutput<T>(value: T): T;
|
|
21
|
+
/**
|
|
22
|
+
* One Link tool as an eve tool. Mount each in `agent/tools/<name>.ts`:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { linkTool } from "@kybernesis/payments";
|
|
26
|
+
* export default linkTool("create_spend_request");
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function linkTool<N extends LinkToolName>(name: N, options?: LinkCliAuthOptions): ToolDefinition<Record<string, unknown>, unknown>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Link } from "@stripe/link-sdk";
|
|
2
|
+
import { createLinkTools } from "@stripe/link-sdk/tools";
|
|
3
|
+
import { defineTool } from "eve/tools";
|
|
4
|
+
import { always, never } from "eve/tools/approval";
|
|
5
|
+
import { linkCliAuth } from "./link-cli-auth.js";
|
|
6
|
+
/**
|
|
7
|
+
* Stripe's Link tools, mounted the Kybernesis way.
|
|
8
|
+
*
|
|
9
|
+
* Stripe ships an eve extension, but it is built against an older eve and
|
|
10
|
+
* eve 0.68 refuses to mount it. The same tool catalog is available
|
|
11
|
+
* framework-free from `@stripe/link-sdk/tools`, so each one is wrapped here as
|
|
12
|
+
* an eve tool: the owner's link-cli sign-in supplies the token on every call,
|
|
13
|
+
* creating a spend request asks the person in eve before it asks them in Link,
|
|
14
|
+
* and anything that could carry a card number is stripped from what the model
|
|
15
|
+
* sees — the card has exactly one path, `pay_on_computer`.
|
|
16
|
+
*/
|
|
17
|
+
export const LINK_TOOL_NAMES = [
|
|
18
|
+
"retrieve_user_info",
|
|
19
|
+
"list_payment_methods",
|
|
20
|
+
"list_shipping_addresses",
|
|
21
|
+
"list_spend_requests",
|
|
22
|
+
"create_spend_request",
|
|
23
|
+
"update_spend_request",
|
|
24
|
+
"cancel_spend_request",
|
|
25
|
+
"request_spend_approval",
|
|
26
|
+
"retrieve_spend_request",
|
|
27
|
+
"create_report",
|
|
28
|
+
];
|
|
29
|
+
/** A Link client whose token is the owner's current link-cli sign-in, read at call time. */
|
|
30
|
+
export function linkClient(options = {}) {
|
|
31
|
+
const auth = linkCliAuth(options);
|
|
32
|
+
return new Link({ getAccessToken: async () => (await auth.getToken({})).token });
|
|
33
|
+
}
|
|
34
|
+
/** Remove anything that is or contains a card credential before it reaches the model. */
|
|
35
|
+
export function sanitizeLinkOutput(value) {
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return value.map((v) => sanitizeLinkOutput(v));
|
|
38
|
+
if (value && typeof value === "object") {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [k, v] of Object.entries(value)) {
|
|
41
|
+
if (k === "card" || k === "credential" || k === "number" || k === "cvc" || k === "payment_token" || k === "shared_payment_token")
|
|
42
|
+
continue;
|
|
43
|
+
out[k] = sanitizeLinkOutput(v);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One Link tool as an eve tool. Mount each in `agent/tools/<name>.ts`:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { linkTool } from "@kybernesis/payments";
|
|
54
|
+
* export default linkTool("create_spend_request");
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function linkTool(name, options = {}) {
|
|
58
|
+
const catalog = createLinkTools(linkClient(options));
|
|
59
|
+
const tool = catalog[name];
|
|
60
|
+
return defineTool({
|
|
61
|
+
description: tool.description,
|
|
62
|
+
inputSchema: tool.inputSchema,
|
|
63
|
+
// Asking Link for money starts with asking the person here. Reading and
|
|
64
|
+
// reporting do not; Link's own approval step still gates every card.
|
|
65
|
+
approval: name === "create_spend_request" ? always() : never(),
|
|
66
|
+
async execute(input, ctx) {
|
|
67
|
+
const result = await tool.execute(input, ctx);
|
|
68
|
+
return sanitizeLinkOutput(result);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LinkCliAuthOptions } from "./link-cli-auth.js";
|
|
2
|
+
/**
|
|
3
|
+
* Type the one-time card of an APPROVED Link spend request into a checkout
|
|
4
|
+
* form on the agent's own computer. The card number and CVC go from Link to
|
|
5
|
+
* the page and nowhere else: not into this tool's result, not into the
|
|
6
|
+
* transcript, not into a screenshot caption.
|
|
7
|
+
*
|
|
8
|
+
* Eve's approval is deliberately `never()` here — the money decision already
|
|
9
|
+
* happened twice: the agent asked eve's user approval to create the spend
|
|
10
|
+
* request, and the person approved the purchase in Link. Filling the card for
|
|
11
|
+
* a request that is not approved is refused.
|
|
12
|
+
*/
|
|
13
|
+
export declare function payOnComputerTool(options?: LinkCliAuthOptions): import("eve/tools").ToolDefinition<{
|
|
14
|
+
spend_request_id: string;
|
|
15
|
+
page_url: string;
|
|
16
|
+
fields: {
|
|
17
|
+
field: "number" | "cvc" | "name" | "exp_month" | "exp_year" | "expiration" | "postal_code";
|
|
18
|
+
selector: string;
|
|
19
|
+
frameUrl?: string | undefined;
|
|
20
|
+
format?: "MM/YY" | "MM/YYYY" | undefined;
|
|
21
|
+
}[];
|
|
22
|
+
}, {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
filled: number;
|
|
25
|
+
missing: readonly string[];
|
|
26
|
+
card: {
|
|
27
|
+
brand: string;
|
|
28
|
+
last4: string;
|
|
29
|
+
};
|
|
30
|
+
note: string;
|
|
31
|
+
}> & {
|
|
32
|
+
execute(input: {
|
|
33
|
+
spend_request_id: string;
|
|
34
|
+
page_url: string;
|
|
35
|
+
fields: {
|
|
36
|
+
field: "number" | "cvc" | "name" | "exp_month" | "exp_year" | "expiration" | "postal_code";
|
|
37
|
+
selector: string;
|
|
38
|
+
frameUrl?: string | undefined;
|
|
39
|
+
format?: "MM/YY" | "MM/YYYY" | undefined;
|
|
40
|
+
}[];
|
|
41
|
+
}, ctx: import("eve/tools").ToolContext): Promise<{
|
|
42
|
+
ok: boolean;
|
|
43
|
+
filled: number;
|
|
44
|
+
missing: readonly string[];
|
|
45
|
+
card: {
|
|
46
|
+
brand: string;
|
|
47
|
+
last4: string;
|
|
48
|
+
};
|
|
49
|
+
note: string;
|
|
50
|
+
}>;
|
|
51
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { fillOnComputer } from "@kybernesis/computer";
|
|
2
|
+
import { defineTool } from "eve/tools";
|
|
3
|
+
import { never } from "eve/tools/approval";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { linkClient } from "./link-tools.js";
|
|
6
|
+
const fieldSchema = z.object({
|
|
7
|
+
field: z.enum(["name", "number", "exp_month", "exp_year", "expiration", "cvc", "postal_code"]),
|
|
8
|
+
selector: z.string().min(1).max(500).describe("CSS selector of that input on the checkout page."),
|
|
9
|
+
frameUrl: z.string().url().optional().describe("If the input lives in an iframe (common for card fields), that frame's URL prefix."),
|
|
10
|
+
format: z.enum(["MM/YY", "MM/YYYY"]).optional().describe("For a combined expiration field."),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Type the one-time card of an APPROVED Link spend request into a checkout
|
|
14
|
+
* form on the agent's own computer. The card number and CVC go from Link to
|
|
15
|
+
* the page and nowhere else: not into this tool's result, not into the
|
|
16
|
+
* transcript, not into a screenshot caption.
|
|
17
|
+
*
|
|
18
|
+
* Eve's approval is deliberately `never()` here — the money decision already
|
|
19
|
+
* happened twice: the agent asked eve's user approval to create the spend
|
|
20
|
+
* request, and the person approved the purchase in Link. Filling the card for
|
|
21
|
+
* a request that is not approved is refused.
|
|
22
|
+
*/
|
|
23
|
+
export function payOnComputerTool(options = {}) {
|
|
24
|
+
const link = linkClient(options);
|
|
25
|
+
return defineTool({
|
|
26
|
+
description: "Type the one-time card from an APPROVED Link spend request into the checkout form open in your own browser. Give the CSS selector of each card field you can see (name, number, expiration or exp_month/exp_year, cvc, postal code). The card details never come back to you; the result says which fields were filled. Open the checkout with open_browser first and look at it.",
|
|
27
|
+
inputSchema: z.object({
|
|
28
|
+
spend_request_id: z.string().min(1).max(200),
|
|
29
|
+
page_url: z.string().url().describe("The checkout page's URL, as shown in the browser."),
|
|
30
|
+
fields: z.array(fieldSchema).min(2).max(7),
|
|
31
|
+
}),
|
|
32
|
+
approval: never(),
|
|
33
|
+
async execute(input, ctx) {
|
|
34
|
+
const request = await link.spendRequests.retrieve(input.spend_request_id, { include: ["card"] });
|
|
35
|
+
if (!request)
|
|
36
|
+
throw new Error("That spend request does not exist.");
|
|
37
|
+
if (request.status !== "approved") {
|
|
38
|
+
throw new Error(`Spend request is ${request.status}, not approved. Send the approval link and wait for the person to approve it in Link.`);
|
|
39
|
+
}
|
|
40
|
+
const card = request.card;
|
|
41
|
+
if (!card)
|
|
42
|
+
throw new Error("Link did not return a card for this request; it may need a different credential type.");
|
|
43
|
+
const yy = String(card.exp_year).slice(-2);
|
|
44
|
+
const mm = String(card.exp_month).padStart(2, "0");
|
|
45
|
+
const values = {
|
|
46
|
+
name: card.billing_address?.name,
|
|
47
|
+
number: card.number,
|
|
48
|
+
exp_month: mm,
|
|
49
|
+
exp_year: String(card.exp_year),
|
|
50
|
+
cvc: card.cvc,
|
|
51
|
+
postal_code: card.billing_address?.postal_code,
|
|
52
|
+
};
|
|
53
|
+
const fields = [];
|
|
54
|
+
for (const f of input.fields) {
|
|
55
|
+
const value = f.field === "expiration" ? (f.format === "MM/YYYY" ? `${mm}/${card.exp_year}` : `${mm}/${yy}`) : values[f.field];
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
fields.push({ selector: f.selector, value, ...(f.frameUrl ? { frameUrl: f.frameUrl } : {}) });
|
|
59
|
+
}
|
|
60
|
+
const sandbox = await ctx.getSandbox();
|
|
61
|
+
const result = await fillOnComputer(sandbox, { pageOrigin: new URL(input.page_url).origin, fields });
|
|
62
|
+
return {
|
|
63
|
+
ok: result.ok,
|
|
64
|
+
filled: result.filled.length,
|
|
65
|
+
missing: result.missing,
|
|
66
|
+
card: { brand: card.brand ?? "card", last4: card.number.slice(-4) },
|
|
67
|
+
note: result.ok
|
|
68
|
+
? "Card details are in the form. Take a screenshot, check the totals match the spend request, then submit the order and report the outcome with create_report."
|
|
69
|
+
: "Some fields could not be found; look at the page again and correct the selectors.",
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/payments",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Stripe Link as an eve agent's wallet, on its owner's behalf: Link's tool catalog mounted as eve tools behind the owner's link-cli sign-in, the person's approval on every purchase, and a one-time card typed into a checkout on the agent's own computer without the model ever seeing it.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/KybernesisAI/platform.git",
|
|
10
|
+
"directory": "packages/payments"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
|
+
"prepack": "tsc -p tsconfig.build.json",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"prepublishOnly": "node ../../scripts/prepublish.mjs",
|
|
29
|
+
"test": "node --test \"test/**/*.test.mjs\""
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@stripe/link-cli": "^0.26.0",
|
|
33
|
+
"@stripe/link-sdk": "^0.11.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@kybernesis/computer": ">=0.2.0 <0.3.0",
|
|
37
|
+
"eve": ">=0.68.0 <0.69.0",
|
|
38
|
+
"zod": "^4"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@kybernesis/computer": "*",
|
|
42
|
+
"eve": "0.68.0",
|
|
43
|
+
"zod": "^4.0.0",
|
|
44
|
+
"typescript": "^5.9.0",
|
|
45
|
+
"@types/node": "^24.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|