Purposes v7

A purpose maps a name to a Postgres role. Registering, changing, and retiring purposes requires the aidb_governance role:

GRANT aidb_governance TO your_admin;

Registering a purpose

SELECT aidb.create_purpose(
    name        => 'sales_support',
    role        => 'sales_tickets_ro',
    description => 'Sales support agent'
);

create_purpose doesn't check that sales_tickets_ro exists or that you hold it. The existence of the resolved role, and whether the caller of the agent is a member, is checked at tool invocation time when the role is actually assumed — see Enforcement.

Grants the resolved role needs

A purpose's resolved role starts out as a plain Postgres role, with none of AIDB's own access by default. What it needs beyond that depends on which tool type the agent calls under that purpose — the two types are not symmetric.

Native tools

Calling a native tool runs a Postgres function call into aidb.<tool_name> under the resolved role, so that role needs EXECUTE on every native tool the agent is configured to call, plus USAGE on the schema to reference it at all:

GRANT USAGE ON SCHEMA aidb TO sales_tickets_ro;
GRANT EXECUTE ON FUNCTION aidb.run_sql_query(TEXT) TO sales_tickets_ro;

This is genuinely per-tool: granting EXECUTE on run_sql_query doesn't cover catalog_list_objects — an agent configured with several native tools needs a grant for each one its purpose role should be allowed to run.

If telemetry is enabled, the trace for a native tool call is also recorded under the resolved role, so that role needs USAGE and EXECUTE on the aidb_otel schema:

GRANT USAGE ON SCHEMA aidb_otel TO sales_tickets_ro;
GRANT EXECUTE ON FUNCTION
    aidb_otel.store_metrics(JSONB[]),
    aidb_otel.store_logs(JSONB[]),
    aidb_otel.store_traces(JSONB[])
    TO sales_tickets_ro;

Grant the per-tool and aidb_otel privileges to every role you register a purpose against, before you invoke any agent that uses it.

Without these grants, the tool call fails with a permission error, and the agent tells the caller of aidb.agent_converse that it couldn't access the data. The exact wording depends on the model. The error itself is recorded in the response_payload column of aidb_internal.action_log, on the tool_response action, with the decision ID appended. For example, a missing aidb_otel grant produces:

Tool error: permission denied for function store_traces [decision_id=<decision_id>]

A missing aidb schema grant or per-tool grant produces the same form of error, naming the schema or function, for example permission denied for schema aidb. To see the errors for a conversation, use the query in A refusal doesn't end the conversation.

Custom SQL tools

A custom SQL tool needs none of this. It runs its stored query directly, without calling a function in schema aidb, so the resolved role needs no function-level grants and no aidb_otel grants. It needs only ordinary privileges on the tables or views its SQL references.

Repointing a purpose takes effect immediately

SELECT aidb.update_purpose('sales_support', role => 'sales_tickets_ro_v2');

The role a purpose maps to is resolved at invocation, not cached when the agent was created. Repointing it takes effect on the next call of every agent holding that purpose — with no edit to any agent.

NULL means leave unchanged, not clear. Passing NULL for role or description leaves the current value as it is:

SELECT aidb.update_purpose('sales_support', description => 'Updated description');
-- role is untouched

Calling update_purpose with nothing to change succeeds without error, even against a name that doesn't exist:

SELECT aidb.update_purpose('never_created');
-- Succeeds. No row is touched, no error is raised.

An actual change against a name that doesn't exist does fail:

SELECT aidb.update_purpose('never_created', role => 'some_role');
Output
ERROR:  purpose 'never_created' not found

update_purpose can't revive a retired purpose.

Retiring a purpose

SELECT aidb.delete_purpose('sales_support');

This is a soft delete. The row stays, with deleted_at set, and remains visible in aidb.purpose_registry for inspection. Every agent still holding this purpose starts failing when invoked directly. As a delegate, an agent runs under the delegating agent's role and its own purpose isn't read. See Delegation. A direct invocation fails with:

purpose 'sales_support' has been retired

Retiring never falls back to running the agent as the caller. An agent with no purpose at all runs as the caller by design — but a purpose that used to confine an agent must not silently stop confining it just because it was retired. That would promote every agent holding it from confined to caller-privileged, the opposite of what retiring a purpose is for.

Deleting an already-retired purpose, or a name that never existed, fails with a distinct error for each case:

SELECT aidb.delete_purpose('sales_support');  -- called a second time
Output
ERROR:  purpose 'sales_support' already deleted
SELECT aidb.delete_purpose('never_created');
Output
ERROR:  purpose 'never_created' not found

Reviving a retired purpose

create_purpose is the only way to bring a retired purpose back into service — call it again with the same name:

SELECT aidb.create_purpose('sales_support', 'sales_tickets_ro');

This overwrites role with the new value and clears deleted_at. Omitting description keeps whatever description the purpose had before it was retired, rather than clearing it.

Calling create_purpose against a name that's still live (not retired) fails instead:

SELECT aidb.create_purpose('sales_support', 'sales_tickets_ro');
Output
ERROR:  purpose 'sales_support' already exists

A purpose pointing at a role that doesn't exist

Neither create_purpose nor update_purpose checks that the role exists. A purpose can be registered against a role you haven't created yet, or one that's later dropped. Invoking an agent under that purpose then fails at invocation, not at registration:

role 'sales_tickets_ro' does not exist

See Purposes reference for the full function signatures and error list.