Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: 8f4b37c56cd153a2acc6cfabe27dbbeb43510f00
ref: 199bbd27c8dab2f70e379de52ca9cc910b0e141d
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand All @@ -39,7 +39,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: 8f4b37c56cd153a2acc6cfabe27dbbeb43510f00
ref: 199bbd27c8dab2f70e379de52ca9cc910b0e141d
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand All @@ -65,7 +65,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: inkbox-ai/inkbox
ref: 8f4b37c56cd153a2acc6cfabe27dbbeb43510f00
ref: 199bbd27c8dab2f70e379de52ca9cc910b0e141d
path: .ci/inkbox
- uses: actions/setup-node@v7
with:
Expand Down
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

Initial release.

- Requires `@inkbox/sdk` 0.5.0 or newer.
- 56 `inkbox_*` tools across email, SMS/MMS, iMessage, calls, contacts, notes,
contact rules, access grants, encrypted vault, and diagnostics. 24 are
- Requires `@inkbox/sdk` 0.5.1 or newer.
- 48 `inkbox_*` tools across email, SMS/MMS, iMessage, calls, contacts, notes,
contact rules, note access grants, encrypted vault, and diagnostics. 27 are
enabled by default; the rest are opt-in via the `tools.enable` plugin option
(`inkbox_doctor` reports what is off and how to enable it).
- Outbound sends and calls gate through opencode's native permission prompts,
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ groups are enabled.

## Tools

27 tools are enabled by default; 24 more are opt-in (see
27 tools are enabled by default; 21 more are opt-in (see
[Enabling more tools](#enabling-more-tools)). Names are stable — treat renames
as breaking.

Expand All @@ -124,7 +124,7 @@ as breaking.
| `contacts` | `inkbox_lookup_contact`, `inkbox_get_contact`, `inkbox_list_contacts`, `inkbox_create_contact`, `inkbox_update_contact`, `inkbox_delete_contact` | — |
| `notes` | `inkbox_list_notes`, `inkbox_get_note`, `inkbox_create_note` | `inkbox_update_note`, `inkbox_delete_note` |
| `contact-rules` | `inkbox_list_mail_contact_rules`, `inkbox_list_phone_contact_rules`, `inkbox_list_imessage_contact_rules` (read-only: rule changes are console-only) | — |
| `access` | — | `inkbox_list_contact_access`, `inkbox_grant_contact_access`, `inkbox_revoke_contact_access`, `inkbox_list_note_access`, `inkbox_grant_note_access`, `inkbox_revoke_note_access` |
| `access` | — | `inkbox_list_note_access`, `inkbox_grant_note_access`, `inkbox_revoke_note_access` |
| `vault` | — | `inkbox_credentials_list`, and by exact name only: `inkbox_credentials_get_login`, `inkbox_credentials_get_api_key`, `inkbox_credentials_get_ssh_key`, `inkbox_totp_code` |
| `diagnostics` | `inkbox_whoami`, `inkbox_doctor` | — |

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"opencode": ">=1.15.0 <1.19.0"
},
"dependencies": {
"@inkbox/sdk": ">=0.5.0 <1.0.0",
"@inkbox/sdk": ">=0.5.1 <1.0.0",
"@opencode-ai/sdk": ">=1.17.18 <1.19.0",
"ws": "^8.21.0",
"zod": "4.1.8"
Expand Down
12 changes: 6 additions & 6 deletions skills/inkbox-contact-lookup/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: inkbox-contact-lookup
description: Use when the user asks "who is X", "what's the email for Y", "find a contact named Z", "save this contact", or any request that needs contact context. The agent can read and write Inkbox contacts visible to the configured identity; contact access grants, contact rules, and vCard flows are separate surfaces.
description: Use when the user asks "who is X", "what's the email for Y", "find a contact named Z", "save this contact", or any request that needs contact context. Contacts are shared across the Inkbox organization; contact rules and vCard flows are separate surfaces.
---

# Inkbox contact lookup

Inkbox contacts are an address-book scoped to the configured identity. When the user asks about a person, wants to save or edit a contact, or needs an address or number resolved, use the contact tools below.
Inkbox contacts are an organization-wide address book. When the user asks about a person, wants to save or edit a contact, or needs an address or number resolved, use the contact tools below.

## Required tools

Expand All @@ -18,14 +18,14 @@ All six are enabled by default:
- `inkbox_update_contact` — change an existing contact after you know its UUID
- `inkbox_delete_contact` — delete a contact only after the target is explicit and confirmed

There is no vCard export/import tool. Contact access tools, contact rule tools, and `inkbox_place_call` are opt-in — the user must enable them in your .opencode/plugins/inkbox.ts wrapper and restart opencode:
There is no vCard export/import tool. Contact rule tools, note access tools, and `inkbox_place_call` are opt-in — the user must enable them in your .opencode/plugins/inkbox.ts wrapper and restart opencode:

```ts
// in your .opencode/plugins/inkbox.ts wrapper:
InkboxPlugin(input, { "tools": { "enable": ["inkbox_place_call", "access", "contact-rules"] } })
```

Use access and rule tools only when the user explicitly asks to manage sharing or allow/block rules. `inkbox_doctor` reports which tools are enabled or disabled.
Use note access and contact rule tools only when the user explicitly asks to manage note sharing or allow/block rules. `inkbox_doctor` reports which tools are enabled or disabled.

## Workflow

Expand All @@ -38,9 +38,9 @@ Use access and rule tools only when the user explicitly asks to manage sharing o

## Access semantics

- Contact tools operate only on contacts visible/writable to the configured identity.
- Every identity in the organization can read contacts. Creating, updating, or deleting a contact affects the shared address book.
- Contacts created through `inkbox_create_contact` are Inkbox address-book records, not local notes or session memory.
- Sharing contacts across Inkbox identities is grant management — that requires the opt-in access tools, and only when the user explicitly asks for it.
- Contacts do not have per-identity access grants. The opt-in access tools manage notes only.

## What this skill does NOT cover

Expand Down
22 changes: 9 additions & 13 deletions skills/inkbox-identity-access/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: inkbox-identity-access
description: Use when the user asks which Inkbox agent identities can see a contact or note, or asks to grant/revoke cross-identity access to contacts or notes.
description: Use when the user asks which Inkbox agent identities can see a contact or note, or asks to grant/revoke cross-identity note access. Contacts are organization-wide; notes remain identity-scoped.
---

# Inkbox identity access

Use this skill when managing per-identity visibility for Inkbox contacts and notes.
Use this skill when explaining organization-wide contact visibility or managing per-identity note access.

## Enabling the tools

Expand All @@ -16,20 +16,16 @@ All identity-access tools are opt-in. Enable the `access` group in your .opencod
InkboxPlugin(input, { "tools": { "enable": ["access"] } })
```

This enables `inkbox_list_contact_access`, `inkbox_grant_contact_access`, `inkbox_revoke_contact_access`, `inkbox_list_note_access`, `inkbox_grant_note_access`, and `inkbox_revoke_note_access`. Run `inkbox_doctor` to confirm which tools are currently enabled.
This enables `inkbox_list_note_access`, `inkbox_grant_note_access`, and `inkbox_revoke_note_access`. Run `inkbox_doctor` to confirm which tools are currently enabled.

## Workflow

1. Resolve the contact or note id first. If the user names a person or note, use `inkbox_lookup_contact`, `inkbox_list_contacts`, `inkbox_get_note`, or `inkbox_list_notes`.
2. List current access with `inkbox_list_contact_access` / `inkbox_list_note_access` before changing it when possible.
3. For contacts:
- Grant a specific identity with `identityId`.
- Use `wildcard: true` only when the user wants every active identity to see the contact; it replaces the specific grants. Pass either `identityId` or `wildcard`, never both.
- Revoke by `identityId`.
4. For notes:
- Grant and revoke only by explicit `identityId`; notes do not support wildcard grants.
5. If the user gives an agent handle instead of an identity UUID: `inkbox_whoami` returns the current identity's own id and handle, and access listings may already contain the other identity's id. If neither resolves the handle, explain that you need the identity id.
1. If the request concerns a contact, explain that every identity in the organization can see it and that contact access cannot be granted or revoked per identity.
2. For notes, resolve the note id with `inkbox_get_note` or `inkbox_list_notes`.
3. List current access with `inkbox_list_note_access` before changing it when possible.
4. Grant or revoke note access by explicit `identityId`; notes do not support wildcard grants.
5. If the user gives an agent handle instead of an identity UUID, `inkbox_whoami` returns the current identity's own id and handle, and note access listings may already contain the other identity's id. If neither resolves the handle, explain that you need the identity id.

## Safety

Access changes affect what other Inkbox agent identities can see. Confirm the target identity and object before granting broad or wildcard contact access.
Note access changes affect what other Inkbox agent identities can see. Confirm the target identity and note before changing access.
5 changes: 2 additions & 3 deletions src/gateway/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ export function describeContacts(contacts: Contact[], max = 5): string {
return lines.join("\n");
}

// Resolves inbound sender addresses to Inkbox contacts and derives the
// per-human session key. With an agent-scoped key, lookup results are
// already access-filtered server-side.
// Resolves inbound sender addresses to organization contacts and derives the
// per-human session key.
export function createContactResolver(
deps: { inkbox: InkboxRuntime; logger: GatewayLogger },
opts: { cacheTtlMs?: number } = {},
Expand Down
95 changes: 1 addition & 94 deletions src/tools/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,6 @@ import { runTool } from "../errors.js";
import { formatJson, formatWithHeader } from "../format.js";
import type { RegisteredTool, ToolDeps } from "./types.js";

const listContactAccessArgs = {
contactId: z.string().describe("Contact UUID."),
};

const grantContactAccessArgs = {
contactId: z.string().describe("Contact UUID."),
identityId: z.string().describe("Agent identity UUID to grant.").optional(),
wildcard: z
.boolean()
.describe("Set true to replace specific grants with wildcard access.")
.optional(),
};

const revokeContactAccessArgs = {
contactId: z.string().describe("Contact UUID."),
identityId: z.string().describe("Agent identity UUID to revoke."),
};

const listNoteAccessArgs = {
noteId: z.string().describe("Note UUID."),
};
Expand All @@ -35,89 +17,14 @@ const revokeNoteAccessArgs = {
identityId: z.string().describe("Agent identity UUID to revoke."),
};

type ListContactAccessArgs = z.infer<z.ZodObject<typeof listContactAccessArgs>>;
type GrantContactAccessArgs = z.infer<z.ZodObject<typeof grantContactAccessArgs>>;
type RevokeContactAccessArgs = z.infer<z.ZodObject<typeof revokeContactAccessArgs>>;
type ListNoteAccessArgs = z.infer<z.ZodObject<typeof listNoteAccessArgs>>;
type GrantNoteAccessArgs = z.infer<z.ZodObject<typeof grantNoteAccessArgs>>;
type RevokeNoteAccessArgs = z.infer<z.ZodObject<typeof revokeNoteAccessArgs>>;

function hasString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}

// Access grants control which identities can see a contact or a note.
// Contacts support wildcard grants (every active identity sees the contact);
// note grants are strictly per-identity.
// Notes remain identity-scoped even though contacts are organization-wide.
export function accessTools(deps: ToolDeps): RegisteredTool[] {
const { runtime } = deps;
return [
{
name: "inkbox_list_contact_access",
group: "access",
defaultEnabled: false,
definition: {
description:
"List which Inkbox identities can see a contact. Use before granting or revoking cross-identity contact access.",
args: listContactAccessArgs,
async execute(args: ListContactAccessArgs, _ctx) {
return runTool(async () => {
const inkbox = await runtime.getClient();
const grants = await inkbox.contacts.access.list(args.contactId);
return formatWithHeader(`Returned ${grants.length} contact access grant(s).`, grants);
});
},
},
},
{
name: "inkbox_grant_contact_access",
group: "access",
defaultEnabled: false,
definition: {
description:
"Grant an Inkbox identity access to a contact, or grant wildcard access so every active identity can see it.",
args: grantContactAccessArgs,
async execute(args: GrantContactAccessArgs, _ctx) {
return runTool(async () => {
const identityId = hasString(args.identityId) ? args.identityId.trim() : undefined;
if (args.wildcard === true && identityId) {
throw new Error("Pass either identityId or wildcard=true, not both.");
}
if (args.wildcard !== true && !identityId) {
throw new Error("identityId is required unless wildcard=true.");
}
const inkbox = await runtime.getClient();
const grant = await inkbox.contacts.access.grant(args.contactId, {
identityId,
wildcard: args.wildcard === true,
});
return {
title: `Granted access to contact ${args.contactId}`,
output: formatWithHeader("Granted contact access.", grant),
};
});
},
},
},
{
name: "inkbox_revoke_contact_access",
group: "access",
defaultEnabled: false,
definition: {
description: "Revoke one Inkbox identity's access to a contact.",
args: revokeContactAccessArgs,
async execute(args: RevokeContactAccessArgs, _ctx) {
return runTool(async () => {
const inkbox = await runtime.getClient();
await inkbox.contacts.access.revoke(args.contactId, args.identityId);
return {
title: `Revoked access to contact ${args.contactId}`,
output: `Revoked identity ${args.identityId} access to contact ${args.contactId}.`,
};
});
},
},
},
{
name: "inkbox_list_note_access",
group: "access",
Expand Down
17 changes: 7 additions & 10 deletions src/tools/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,7 @@ function buildContactWritePayload(args: ContactWriteArgs): Record<string, unknow
return payload;
}

// Contacts are an org-level address book filtered server-side by per-identity
// access grants. With an agent-scoped key, list/lookup/get already return only
// the contacts this identity has access to — we don't filter client-side.
// Grant management lives in the opt-in "access" tool group.
// Contacts are shared across every identity in the organization.
export function contactTools(deps: ToolDeps): RegisteredTool[] {
const { runtime } = deps;
return [
Expand All @@ -141,7 +138,7 @@ export function contactTools(deps: ToolDeps): RegisteredTool[] {
defaultEnabled: true,
definition: {
description:
"Reverse-lookup contacts by email or phone. Exactly one filter must be provided — email, phone, emailDomain, emailContains, or phoneContains. Returns contacts this identity has access to.",
"Reverse-lookup organization contacts by email or phone. Exactly one filter must be provided — email, phone, emailDomain, emailContains, or phoneContains.",
args: lookupContactArgs,
async execute(args: LookupContactArgs, _ctx) {
return runTool(async () => {
Expand Down Expand Up @@ -174,8 +171,7 @@ export function contactTools(deps: ToolDeps): RegisteredTool[] {
group: "contacts",
defaultEnabled: true,
definition: {
description:
"List contacts this identity has access to. Optional free-text search via `q`; results scoped by per-identity grants.",
description: "List organization-wide contacts. Optional free-text search via `q`.",
args: listContactsArgs,
async execute(args: ListContactsArgs, _ctx) {
return runTool(async () => {
Expand All @@ -197,7 +193,7 @@ export function contactTools(deps: ToolDeps): RegisteredTool[] {
defaultEnabled: true,
definition: {
description:
"Create an Inkbox address-book contact. Use when the user asks to save a person/contact in Inkbox. Include phone/email when known; notes can hold free-form context.",
"Create an organization-wide Inkbox address-book contact. Use when the user asks to save a person/contact in Inkbox. Include phone/email when known; notes can hold free-form context.",
args: createContactArgs,
async execute(args: CreateContactArgs, _ctx) {
return runTool(async () => {
Expand All @@ -217,7 +213,7 @@ export function contactTools(deps: ToolDeps): RegisteredTool[] {
defaultEnabled: true,
definition: {
description:
"Update an Inkbox address-book contact by UUID. Use after lookup/get when the user asks to add or correct contact details.",
"Update an organization-wide Inkbox address-book contact by UUID. Use after lookup/get when the user asks to add or correct contact details.",
args: updateContactArgs,
async execute(args: UpdateContactArgs, _ctx) {
return runTool(async () => {
Expand All @@ -237,7 +233,8 @@ export function contactTools(deps: ToolDeps): RegisteredTool[] {
group: "contacts",
defaultEnabled: true,
definition: {
description: "Delete an Inkbox address-book contact by UUID. Irreversible.",
description:
"Delete an organization-wide Inkbox address-book contact by UUID. Irreversible.",
args: deleteContactArgs,
async execute(args: DeleteContactArgs, _ctx) {
return runTool(async () => {
Expand Down
7 changes: 2 additions & 5 deletions tests/contract/tool-vocabulary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ const OPT_IN = [
"inkbox_place_call",
"inkbox_update_note",
"inkbox_delete_note",
"inkbox_list_contact_access",
"inkbox_grant_contact_access",
"inkbox_revoke_contact_access",
"inkbox_list_note_access",
"inkbox_grant_note_access",
"inkbox_revoke_note_access",
Expand Down Expand Up @@ -111,10 +108,10 @@ const GROUPS = [
describe("tool vocabulary", () => {
const all = buildAllTools(stubDeps());

it("ships exactly the expected 51 tools", () => {
it("ships exactly the expected 48 tools", () => {
const names = all.map((t) => t.name).sort();
expect(names).toEqual([...DEFAULT_ENABLED, ...OPT_IN].sort());
expect(names).toHaveLength(51);
expect(names).toHaveLength(48);
});

it("has no duplicate tool names", () => {
Expand Down
Loading