@kybernesis/vault 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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @kybernesis/vault
2
+
3
+ The person's logins, cards and addresses, usable by their agent without ever
4
+ being read by it.
5
+
6
+ Items live in the Kybernesis control plane, added from KYBER Studio
7
+ (Settings → Vault, or a Chrome password CSV import). The summary half — label,
8
+ site, username, a card's brand and last four — is what an agent may list. The
9
+ secret half is sealed, and leaves the control plane through exactly one door: a
10
+ materialize call that carries the agent's credential AND the person's verified
11
+ principal, for one item, audited. The only caller of that door is
12
+ `fill_from_vault`, which types the value into the page open on the agent's own
13
+ computer (`@kybernesis/computer`'s DevTools fill) and returns which selectors
14
+ were filled. The value is never a tool result, so it is never in a transcript.
15
+
16
+ ```ts title="agent/tools/list_vault.ts"
17
+ import { listVaultTool } from "@kybernesis/vault";
18
+ export default listVaultTool();
19
+ ```
20
+
21
+ ```ts title="agent/tools/fill_from_vault.ts"
22
+ import { fillFromVaultTool } from "@kybernesis/vault";
23
+ export default fillFromVaultTool();
24
+ ```
25
+
26
+ ## Who the vault belongs to
27
+
28
+ `personFromContext` is default-deny: only a turn whose verified principal came
29
+ from the control plane (`authenticator: "kybernesis"`, a `user`) has a vault.
30
+ Studio turns and linked chat senders qualify; schedules, eve's local-dev
31
+ principal and a channel's default principal do not, and get an empty list.
32
+
33
+ ## Guards
34
+
35
+ - A **login** only fills on the site it was saved for (same registrable host or
36
+ a subdomain of it). A password typed into a look-alike is the whole phishing
37
+ problem, so the tool refuses rather than asking.
38
+ - A **card** requires the person's approval on every fill (eve `user-approval`).
39
+ - Every materialization writes an `audit_log` row naming the agent and the item.
40
+
41
+ ## Env
42
+
43
+ `KYBERNESIS_ISSUER` (the control plane) and `KYBERNESIS_AGENT_CREDENTIAL` (this
44
+ agent's credential) — the same two every enterprise-governed agent already has.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The agent's view of a person's vault, through the control plane.
3
+ *
4
+ * Two calls. `listItems` returns what the agent may know: a label, a site, a
5
+ * username, a card's brand and last four. `materialize` returns one item's
6
+ * secret, and exists for exactly one caller — the fill tool, which types it into
7
+ * a page and returns nothing. Both name the person from the turn's VERIFIED
8
+ * principal, never from the model: a vault is someone's, and an unattended turn
9
+ * (a schedule, an unlinked chat sender) has no one.
10
+ */
11
+ export type VaultKind = "login" | "card" | "address" | "contact";
12
+ export interface VaultSummary {
13
+ readonly id: string;
14
+ readonly kind: VaultKind;
15
+ readonly label: string;
16
+ readonly origin: string | null;
17
+ readonly summary: Readonly<Record<string, unknown>>;
18
+ readonly updatedAt: string;
19
+ }
20
+ export interface MaterializedItem extends VaultSummary {
21
+ readonly secret: Readonly<Record<string, unknown>>;
22
+ }
23
+ export interface VaultClientOptions {
24
+ /** The control plane. Defaults to KYBERNESIS_ISSUER, then agent.kybernesis.ai. */
25
+ readonly issuer?: string;
26
+ /** This agent's credential. Defaults to KYBERNESIS_AGENT_CREDENTIAL. */
27
+ readonly credential?: string;
28
+ readonly fetchImpl?: typeof fetch;
29
+ }
30
+ /** The shape of eve's tool context this package reads. Kept structural so the package has no opinion about the rest. */
31
+ export interface PrincipalContext {
32
+ readonly session?: {
33
+ readonly auth?: {
34
+ readonly current?: {
35
+ readonly principalId?: string;
36
+ readonly authenticator?: string;
37
+ readonly principalType?: string;
38
+ } | null;
39
+ };
40
+ };
41
+ }
42
+ /**
43
+ * The person this turn is for, or null. Default-deny: only a principal the
44
+ * control plane itself authenticated (Studio, a linked chat sender) counts.
45
+ * eve's own local-dev and channel-default principals are not people the vault
46
+ * knows, so they get nothing.
47
+ */
48
+ export declare function personFromContext(ctx: PrincipalContext): string | null;
49
+ export declare const NO_PERSON = "This conversation has no signed-in person, so there is no vault to use. Ask them to continue from KYBER Studio or a linked channel.";
50
+ export declare function listItems(options: VaultClientOptions, user: string, origin?: string): Promise<VaultSummary[]>;
51
+ export declare function materialize(options: VaultClientOptions, user: string, id: string): Promise<MaterializedItem>;
52
+ /** `accounts.example.com` and `example.com` are the same site for a stored login; `example.com.evil.net` is not. */
53
+ export declare function sameSite(stored: string | null, pageUrl: string): boolean;
54
+ /**
55
+ * Every value a form field may be filled with, by field name. Summary and
56
+ * secret flatten together; a card also gets `expiration` (MM/YY) and
57
+ * `expiration_long` (MM/YYYY), and a zero-padded `exp_month`.
58
+ */
59
+ export declare function valuesOf(item: MaterializedItem): Record<string, string>;
package/dist/client.js ADDED
@@ -0,0 +1,96 @@
1
+ const DEFAULT_ISSUER = "https://agent.kybernesis.ai";
2
+ function base(options) {
3
+ return (options.issuer ?? process.env.KYBERNESIS_ISSUER ?? DEFAULT_ISSUER).replace(/\/$/, "");
4
+ }
5
+ function credentialOf(options) {
6
+ const credential = options.credential ?? process.env.KYBERNESIS_AGENT_CREDENTIAL;
7
+ if (!credential)
8
+ throw new Error("This agent has no control-plane credential, so it cannot reach anyone's vault.");
9
+ return credential;
10
+ }
11
+ /**
12
+ * The person this turn is for, or null. Default-deny: only a principal the
13
+ * control plane itself authenticated (Studio, a linked chat sender) counts.
14
+ * eve's own local-dev and channel-default principals are not people the vault
15
+ * knows, so they get nothing.
16
+ */
17
+ export function personFromContext(ctx) {
18
+ const current = ctx.session?.auth?.current;
19
+ if (!current?.principalId)
20
+ return null;
21
+ if (current.authenticator !== "kybernesis")
22
+ return null;
23
+ if (current.principalType && current.principalType !== "user")
24
+ return null;
25
+ return current.principalId;
26
+ }
27
+ export const NO_PERSON = "This conversation has no signed-in person, so there is no vault to use. Ask them to continue from KYBER Studio or a linked channel.";
28
+ async function post(options, path, body) {
29
+ const f = options.fetchImpl ?? fetch;
30
+ const res = await f(`${base(options)}${path}`, {
31
+ method: "POST",
32
+ headers: { "content-type": "application/json", authorization: `Bearer ${credentialOf(options)}` },
33
+ body: JSON.stringify(body),
34
+ signal: AbortSignal.timeout(20_000),
35
+ });
36
+ if (res.status === 401)
37
+ throw new Error("The control plane did not accept this agent's credential.");
38
+ if (res.status === 404)
39
+ throw new Error("That vault item does not exist (or is not this person's).");
40
+ if (!res.ok)
41
+ throw new Error(`The vault call failed (HTTP ${res.status}).`);
42
+ return (await res.json());
43
+ }
44
+ export async function listItems(options, user, origin) {
45
+ const body = await post(options, "/api/vault/items", { user, ...(origin ? { origin } : {}) });
46
+ return body.items ?? [];
47
+ }
48
+ export async function materialize(options, user, id) {
49
+ const body = await post(options, "/api/vault/materialize", { user, id });
50
+ return body.item;
51
+ }
52
+ /** `accounts.example.com` and `example.com` are the same site for a stored login; `example.com.evil.net` is not. */
53
+ export function sameSite(stored, pageUrl) {
54
+ if (!stored)
55
+ return false;
56
+ try {
57
+ const a = new URL(stored).hostname.replace(/^www\./, "").toLowerCase();
58
+ const b = new URL(pageUrl).hostname.replace(/^www\./, "").toLowerCase();
59
+ return a === b || a.endsWith(`.${b}`) || b.endsWith(`.${a}`);
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ /**
66
+ * Every value a form field may be filled with, by field name. Summary and
67
+ * secret flatten together; a card also gets `expiration` (MM/YY) and
68
+ * `expiration_long` (MM/YYYY), and a zero-padded `exp_month`.
69
+ */
70
+ export function valuesOf(item) {
71
+ const out = {};
72
+ for (const source of [item.summary, item.secret]) {
73
+ for (const [key, value] of Object.entries(source)) {
74
+ if (value === null || value === undefined)
75
+ continue;
76
+ out[key] = String(value);
77
+ }
78
+ }
79
+ if (item.kind === "card") {
80
+ const mm = out.expMonth ? out.expMonth.padStart(2, "0") : undefined;
81
+ const yyyy = out.expYear;
82
+ if (mm)
83
+ out.exp_month = mm;
84
+ if (yyyy) {
85
+ out.exp_year = yyyy;
86
+ out.exp_year_short = yyyy.slice(-2);
87
+ if (mm) {
88
+ out.expiration = `${mm}/${yyyy.slice(-2)}`;
89
+ out.expiration_long = `${mm}/${yyyy}`;
90
+ }
91
+ }
92
+ if (out.cardholder)
93
+ out.name = out.cardholder;
94
+ }
95
+ return out;
96
+ }
@@ -0,0 +1,3 @@
1
+ export { listVaultTool, fillFromVaultTool } from "./tools.js";
2
+ export { VAULT_INSTRUCTIONS } from "./instructions.js";
3
+ export { listItems, materialize, personFromContext, sameSite, valuesOf, NO_PERSON, type MaterializedItem, type PrincipalContext, type VaultClientOptions, type VaultKind, type VaultSummary, } from "./client.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { listVaultTool, fillFromVaultTool } from "./tools.js";
2
+ export { VAULT_INSTRUCTIONS } from "./instructions.js";
3
+ export { listItems, materialize, personFromContext, sameSite, valuesOf, NO_PERSON, } from "./client.js";
@@ -0,0 +1 @@
1
+ export declare const VAULT_INSTRUCTIONS = "## The person's vault\n\nThe person can save logins, cards and addresses in KYBER Studio (Settings \u2192 Vault). You can use them on your own computer without ever seeing them:\n\n- `list_vault` with the page URL tells you what applies: labels, usernames, a card's brand and last four. That is all you get.\n- `fill_from_vault` types an item into the form you are looking at. You give the item id and the CSS selector of each field; the values go straight into the page. Then take a screenshot and check before submitting.\n\nRules: a login only fills on the site it was saved for; if the tool refuses, say so rather than retyping anything by hand. A card asks the person for approval every time. If a site asks for a code or a second factor, stop and tell the person what is on the screen \u2014 they can take over your screen. Never ask the person to paste a password into chat; point them to the vault instead.\n";
@@ -0,0 +1,9 @@
1
+ export const VAULT_INSTRUCTIONS = `## The person's vault
2
+
3
+ The person can save logins, cards and addresses in KYBER Studio (Settings → Vault). You can use them on your own computer without ever seeing them:
4
+
5
+ - \`list_vault\` with the page URL tells you what applies: labels, usernames, a card's brand and last four. That is all you get.
6
+ - \`fill_from_vault\` types an item into the form you are looking at. You give the item id and the CSS selector of each field; the values go straight into the page. Then take a screenshot and check before submitting.
7
+
8
+ Rules: a login only fills on the site it was saved for; if the tool refuses, say so rather than retyping anything by hand. A card asks the person for approval every time. If a site asks for a code or a second factor, stop and tell the person what is on the screen — they can take over your screen. Never ask the person to paste a password into chat; point them to the vault instead.
9
+ `;
@@ -0,0 +1,77 @@
1
+ import { type VaultClientOptions } from "./client.js";
2
+ /** What the person has that could apply here. Labels and handles only. */
3
+ export declare function listVaultTool(options?: VaultClientOptions): import("eve/tools").ToolDefinition<{
4
+ page_url?: string | undefined;
5
+ }, {
6
+ items: {
7
+ id: string;
8
+ kind: import("./client.js").VaultKind;
9
+ label: string;
10
+ site: string | null;
11
+ }[];
12
+ note: string;
13
+ }> & {
14
+ execute(input: {
15
+ page_url?: string | undefined;
16
+ }, ctx: import("eve/tools").ToolContext): Promise<{
17
+ items: {
18
+ id: string;
19
+ kind: import("./client.js").VaultKind;
20
+ label: string;
21
+ site: string | null;
22
+ }[];
23
+ note: string;
24
+ }>;
25
+ };
26
+ /**
27
+ * Type a vault item into the page open on the agent's computer. The secret
28
+ * goes control plane → page; the model sees which selectors were filled.
29
+ *
30
+ * Guards: a login only fills on the site it was saved for (a password typed
31
+ * into a look-alike is the whole phishing problem), and a card asks the
32
+ * person first — logging in is what they saved the login for, spending is a
33
+ * decision each time.
34
+ */
35
+ export declare function fillFromVaultTool(options?: VaultClientOptions): import("eve/tools").ToolDefinition<{
36
+ item_id: string;
37
+ page_url: string;
38
+ fields: {
39
+ field: string;
40
+ selector: string;
41
+ frameUrl?: string | undefined;
42
+ }[];
43
+ submit?: boolean | undefined;
44
+ }, {
45
+ note: string;
46
+ unknown_fields?: string[] | undefined;
47
+ ok: boolean;
48
+ item: {
49
+ id: string;
50
+ kind: import("./client.js").VaultKind;
51
+ label: string;
52
+ };
53
+ filled: readonly string[];
54
+ missing: readonly string[];
55
+ }> & {
56
+ execute(input: {
57
+ item_id: string;
58
+ page_url: string;
59
+ fields: {
60
+ field: string;
61
+ selector: string;
62
+ frameUrl?: string | undefined;
63
+ }[];
64
+ submit?: boolean | undefined;
65
+ }, ctx: import("eve/tools").ToolContext): Promise<{
66
+ note: string;
67
+ unknown_fields?: string[] | undefined;
68
+ ok: boolean;
69
+ item: {
70
+ id: string;
71
+ kind: import("./client.js").VaultKind;
72
+ label: string;
73
+ };
74
+ filled: readonly string[];
75
+ missing: readonly string[];
76
+ }>;
77
+ };
package/dist/tools.js ADDED
@@ -0,0 +1,102 @@
1
+ import { fillOnComputer } from "@kybernesis/computer";
2
+ import { defineTool } from "eve/tools";
3
+ import { z } from "zod";
4
+ import { NO_PERSON, listItems, materialize, personFromContext, sameSite, valuesOf, } from "./client.js";
5
+ /** What the person has that could apply here. Labels and handles only. */
6
+ export function listVaultTool(options = {}) {
7
+ return defineTool({
8
+ description: "List the signed-in person's saved logins, cards and addresses that apply to a site: labels, usernames, card brand and last four. Never the secrets. Give the page URL you are on so logins are filtered to that site.",
9
+ inputSchema: z.object({
10
+ page_url: z.string().url().optional().describe("The page open in your browser; logins are filtered to its site."),
11
+ }),
12
+ async execute(input, ctx) {
13
+ const user = personFromContext(ctx);
14
+ if (!user)
15
+ return { items: [], note: NO_PERSON };
16
+ const items = await listItems(options, user, input.page_url);
17
+ return {
18
+ items: items.map((i) => ({ id: i.id, kind: i.kind, label: i.label, site: i.origin, ...i.summary })),
19
+ note: items.length ? "Use fill_from_vault with an item id and the selectors of the fields you can see." : "Nothing saved for this site. The person can add it in KYBER Studio under Settings → Vault.",
20
+ };
21
+ },
22
+ });
23
+ }
24
+ const fieldSchema = z.object({
25
+ field: z
26
+ .string()
27
+ .min(1)
28
+ .max(40)
29
+ .describe("Which value: username, password, number, cvc, expiration (MM/YY), expiration_long (MM/YYYY), exp_month, exp_year, name/cardholder, or an address/contact key like line1, city, postal_code, email, phone."),
30
+ selector: z.string().min(1).max(500).describe("CSS selector of that input on the page."),
31
+ frameUrl: z.string().url().optional().describe("If the input is inside an iframe, that frame's URL prefix."),
32
+ });
33
+ /**
34
+ * Type a vault item into the page open on the agent's computer. The secret
35
+ * goes control plane → page; the model sees which selectors were filled.
36
+ *
37
+ * Guards: a login only fills on the site it was saved for (a password typed
38
+ * into a look-alike is the whole phishing problem), and a card asks the
39
+ * person first — logging in is what they saved the login for, spending is a
40
+ * decision each time.
41
+ */
42
+ export function fillFromVaultTool(options = {}) {
43
+ return defineTool({
44
+ description: "Type a saved login, card or address from the person's vault into the form open in your own browser. Give the item id (from list_vault), the page URL as shown, and the CSS selector for each field you can see. The values never come back to you; the result says which selectors were filled. A login only fills on its own site. Take a screenshot afterwards before submitting.",
45
+ inputSchema: z.object({
46
+ item_id: z.string().min(1).max(200),
47
+ page_url: z.string().url(),
48
+ fields: z.array(fieldSchema).min(1).max(12),
49
+ submit: z.boolean().optional().describe("Press the form's submit after filling. Default false: look first."),
50
+ }),
51
+ approval: async (ctx) => {
52
+ // Spending needs a yes each time; signing in is what the person saved the
53
+ // login for. If anything about the question fails, ask.
54
+ try {
55
+ const user = personFromContext(ctx);
56
+ const id = ctx.toolInput?.item_id;
57
+ if (!user || !id)
58
+ return "user-approval";
59
+ const items = await listItems(options, user);
60
+ const item = items.find((i) => i.id === id);
61
+ if (!item)
62
+ return "user-approval";
63
+ return item.kind === "card" ? "user-approval" : "not-applicable";
64
+ }
65
+ catch {
66
+ return "user-approval";
67
+ }
68
+ },
69
+ async execute(input, ctx) {
70
+ const user = personFromContext(ctx);
71
+ if (!user)
72
+ throw new Error(NO_PERSON);
73
+ const item = await materialize(options, user, input.item_id);
74
+ if (item.kind === "login" && !sameSite(item.origin, input.page_url)) {
75
+ throw new Error(`That login was saved for ${item.origin}, and this page is ${new URL(input.page_url).origin}. It will not be typed into a different site. If this really is the same service, tell the person; they can save a login for this site.`);
76
+ }
77
+ const values = valuesOf(item);
78
+ const fields = [];
79
+ const unknown = [];
80
+ for (const f of input.fields) {
81
+ const value = values[f.field];
82
+ if (value === undefined) {
83
+ unknown.push(f.field);
84
+ continue;
85
+ }
86
+ fields.push({ selector: f.selector, value, ...(f.frameUrl ? { frameUrl: f.frameUrl } : {}) });
87
+ }
88
+ if (!fields.length)
89
+ throw new Error(`None of those field names exist on this ${item.kind}: ${unknown.join(", ")}. Available: ${Object.keys(values).join(", ")}.`);
90
+ const sandbox = await ctx.getSandbox();
91
+ const result = await fillOnComputer(sandbox, { pageOrigin: new URL(input.page_url).origin, fields, submit: input.submit ?? false });
92
+ return {
93
+ ok: result.ok,
94
+ item: { id: item.id, kind: item.kind, label: item.label },
95
+ filled: result.filled,
96
+ missing: result.missing,
97
+ ...(unknown.length ? { unknown_fields: unknown } : {}),
98
+ note: result.ok ? "Filled. Take a screenshot and check before you submit." : "Some selectors were not found on the page; look again and correct them.",
99
+ };
100
+ },
101
+ });
102
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@kybernesis/vault",
3
+ "version": "0.1.0",
4
+ "description": "The person's password and card vault, usable by their eve agent on its own computer: the agent sees labels and handles, the control plane holds the secrets, and a fill tool types them into the page so they are never in the transcript.",
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/vault"
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
+ "peerDependencies": {
32
+ "@kybernesis/computer": ">=0.2.0 <0.3.0",
33
+ "eve": ">=0.68.0 <0.69.0",
34
+ "zod": "^4"
35
+ },
36
+ "devDependencies": {
37
+ "@kybernesis/computer": "*",
38
+ "eve": "0.68.0",
39
+ "zod": "^4.0.0",
40
+ "typescript": "^5.9.0",
41
+ "@types/node": "^24.0.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }