diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65d99fecb..e356162de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,30 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
## Unreleased
+### The trail says what started a run, not only whose authority it had
+
+A routine runs as the person who set it up, and a Bot handing work to another Bot runs as the person
+who began the conversation. Both are correct, that is whose grants and whose connections are being
+used, and both meant an action taken while somebody slept was written into the audit trail as though
+they had taken it themselves. Telling the two apart meant correlating timestamps against
+`routine_runs` by hand, and there was nothing at all to correlate a hop against.
+
+Every audit row now also names what caused it: a person, a routine, another Bot handing work on, or
+the deployment itself. It travels inside the signed run assertion, so a tool call, a hop, a Bot
+stopping to ask its person and a stalled stream all say it, and a Bot cannot relabel its own run.
+The Audit screen has a **Started by** column and a **Nobody watching** view that answers the
+question directly. An unattended run is the one nobody is there to notice going
+wrong, which is the reason it is worth being able to find.
+
+The fourth of those exists so the column never overclaims. Two rows have no person behind them at
+all: the boundary and isolation rows written at start-up, and the refusal written when a caller
+cannot be identified at all. Those say the deployment, not a person, and they stay out of
+**Nobody watching**, which asks what ran on somebody's authority rather than what the deployment did
+by itself.
+
+Nothing about existing rows changes. Every row already written, and every row a person's own click
+writes from now on, reads as a person, because that is what it was.
+
### A bad `COMPUTER_MEMORY_BYTES` refuses to start the supervisor, instead of capping a computer at 512 bytes
`COMPUTER_MEMORY_BYTES=512m` used to parse as `512` via `parseInt`, which Docker accepts as a memory
diff --git a/app/src/routes/_authed/admin/audit.tsx b/app/src/routes/_authed/admin/audit.tsx
index 18b4d98e5..6bd2cc124 100644
--- a/app/src/routes/_authed/admin/audit.tsx
+++ b/app/src/routes/_authed/admin/audit.tsx
@@ -29,6 +29,9 @@ export const Route = createFileRoute("/_authed/admin/audit")({
type AuditEvent = {
id: string;
actorUserId: string | null;
+ /** Absent on a deployment that has not migrated yet. */
+ initiatorKind?: string;
+ initiatorId?: string | null;
eventType: string;
targetType: string;
targetId: string | null;
@@ -52,6 +55,8 @@ const FILTERS = [
label: "Did not happen",
search: eventTypeFilter(DID_NOT_HAPPEN_EVENT_TYPES),
},
+ // Both unattended kinds: the question is whether anybody was watching, not which of the two.
+ { label: "Nobody watching", search: "?initiatorKind=routine,handoff" },
] as const;
function AuditPage() {
@@ -109,6 +114,7 @@ function AuditPage() {
What |
On |
Bot |
+ Started by |
Decision |
@@ -125,6 +131,41 @@ function AuditPage() {
);
}
+function StartedBy({
+ event,
+ nameFor,
+}: {
+ event: AuditEvent;
+ nameFor: (botId: string) => string;
+}) {
+ if (event.initiatorKind === "routine") {
+ return (
+
+ A routine
+
+ );
+ }
+ if (event.initiatorKind === "handoff") {
+ return (
+
+ {event.initiatorId
+ ? `Handed on by ${nameFor(event.initiatorId)}`
+ : "Handed on"}
+
+ );
+ }
+ if (event.initiatorKind === "deployment") {
+ return This deployment;
+ }
+ return A person;
+}
+
function Row({
event,
nameFor,
@@ -247,6 +288,9 @@ function Row({
"-"
)}
+
+
+ |
statement-breakpoint
+ALTER TABLE "audit_events" ADD COLUMN "initiator_id" text;--> statement-breakpoint
+CREATE INDEX "audit_events_initiator_time_idx" ON "audit_events" USING btree ("initiator_kind","created_at" DESC NULLS LAST,"id" DESC NULLS LAST);
\ No newline at end of file
diff --git a/server/drizzle/meta/0028_snapshot.json b/server/drizzle/meta/0028_snapshot.json
new file mode 100644
index 000000000..3d11a459a
--- /dev/null
+++ b/server/drizzle/meta/0028_snapshot.json
@@ -0,0 +1,3141 @@
+{
+ "id": "2dc825f1-9abb-47e2-9b47-9c6c941c557a",
+ "prevId": "c98643b6-c5da-420f-82b7-ea9b94eb0b06",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.accounts": {
+ "name": "accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "accounts_provider_account_idx": {
+ "name": "accounts_provider_account_idx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "accounts_user_id_users_id_fk": {
+ "name": "accounts_user_id_users_id_fk",
+ "tableFrom": "accounts",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agents": {
+ "name": "agents",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "agent_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "configuration": {
+ "name": "configuration",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "package_id": {
+ "name": "package_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "override": {
+ "name": "override",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "agents_package_id_deployment_packages_id_fk": {
+ "name": "agents_package_id_deployment_packages_id_fk",
+ "tableFrom": "agents",
+ "tableTo": "deployment_packages",
+ "columnsFrom": [
+ "package_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_events": {
+ "name": "audit_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initiator_kind": {
+ "name": "initiator_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'person'"
+ },
+ "initiator_id": {
+ "name": "initiator_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_type": {
+ "name": "target_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_id": {
+ "name": "target_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_events_created_at_idx": {
+ "name": "audit_events_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_events_type_time_idx": {
+ "name": "audit_events_type_time_idx",
+ "columns": [
+ {
+ "expression": "event_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_events_actor_time_idx": {
+ "name": "audit_events_actor_time_idx",
+ "columns": [
+ {
+ "expression": "actor_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_events_target_time_idx": {
+ "name": "audit_events_target_time_idx",
+ "columns": [
+ {
+ "expression": "target_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_events_initiator_time_idx": {
+ "name": "audit_events_initiator_time_idx",
+ "columns": [
+ {
+ "expression": "initiator_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.channel_agents": {
+ "name": "channel_agents",
+ "schema": "",
+ "columns": {
+ "channel_id": {
+ "name": "channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "channel_agents_channel_id_channels_id_fk": {
+ "name": "channel_agents_channel_id_channels_id_fk",
+ "tableFrom": "channel_agents",
+ "tableTo": "channels",
+ "columnsFrom": [
+ "channel_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "channel_agents_agent_id_agents_id_fk": {
+ "name": "channel_agents_agent_id_agents_id_fk",
+ "tableFrom": "channel_agents",
+ "tableTo": "agents",
+ "columnsFrom": [
+ "agent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "channel_agents_channel_id_agent_id_pk": {
+ "name": "channel_agents_channel_id_agent_id_pk",
+ "columns": [
+ "channel_id",
+ "agent_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.channel_memberships": {
+ "name": "channel_memberships",
+ "schema": "",
+ "columns": {
+ "channel_id": {
+ "name": "channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pinned_at": {
+ "name": "pinned_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_read_at": {
+ "name": "last_read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "channel_memberships_channel_id_channels_id_fk": {
+ "name": "channel_memberships_channel_id_channels_id_fk",
+ "tableFrom": "channel_memberships",
+ "tableTo": "channels",
+ "columnsFrom": [
+ "channel_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "channel_memberships_user_id_users_id_fk": {
+ "name": "channel_memberships_user_id_users_id_fk",
+ "tableFrom": "channel_memberships",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "channel_memberships_channel_id_user_id_pk": {
+ "name": "channel_memberships_channel_id_user_id_pk",
+ "columns": [
+ "channel_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.channels": {
+ "name": "channels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "suggested_prompts": {
+ "name": "suggested_prompts",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "allowed_groups": {
+ "name": "allowed_groups",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "package_id": {
+ "name": "package_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "override": {
+ "name": "override",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary_at": {
+ "name": "summary_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_message": {
+ "name": "last_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_message_at": {
+ "name": "last_message_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_message_agent_id": {
+ "name": "last_message_agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "channels_recent_activity_idx": {
+ "name": "channels_recent_activity_idx",
+ "columns": [
+ {
+ "expression": "COALESCE(\"last_message_at\", \"created_at\") DESC",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "channels_awaiting_summary_idx": {
+ "name": "channels_awaiting_summary_idx",
+ "columns": [
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"channels\".\"summary\" is null and \"channels\".\"deleted_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "channels_package_id_deployment_packages_id_fk": {
+ "name": "channels_package_id_deployment_packages_id_fk",
+ "tableFrom": "channels",
+ "tableTo": "deployment_packages",
+ "columnsFrom": [
+ "package_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "channels_last_message_agent_id_agents_id_fk": {
+ "name": "channels_last_message_agent_id_agents_id_fk",
+ "tableFrom": "channels",
+ "tableTo": "agents",
+ "columnsFrom": [
+ "last_message_agent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.credentials": {
+ "name": "credentials",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "kind": {
+ "name": "kind",
+ "type": "credential_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_value": {
+ "name": "encrypted_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key_id": {
+ "name": "key_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "credentials_active_key_idx": {
+ "name": "credentials_active_key_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credentials\".\"revoked_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.deployment_packages": {
+ "name": "deployment_packages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_path": {
+ "name": "source_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "checksum": {
+ "name": "checksum",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "loaded_at": {
+ "name": "loaded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "deployment_packages_tenant_id_unique": {
+ "name": "deployment_packages_tenant_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "tenant_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.intelligence_channel_mappings": {
+ "name": "intelligence_channel_mappings",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "intelligence_channel_mappings_thread_idx": {
+ "name": "intelligence_channel_mappings_thread_idx",
+ "columns": [
+ {
+ "expression": "thread_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "intelligence_channel_mappings_user_id_users_id_fk": {
+ "name": "intelligence_channel_mappings_user_id_users_id_fk",
+ "tableFrom": "intelligence_channel_mappings",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "intelligence_channel_mappings_channel_id_channels_id_fk": {
+ "name": "intelligence_channel_mappings_channel_id_channels_id_fk",
+ "tableFrom": "intelligence_channel_mappings",
+ "tableTo": "channels",
+ "columnsFrom": [
+ "channel_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "intelligence_channel_mappings_user_id_channel_id_pk": {
+ "name": "intelligence_channel_mappings_user_id_channel_id_pk",
+ "columns": [
+ "user_id",
+ "channel_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.revoked_access": {
+ "name": "revoked_access",
+ "schema": "",
+ "columns": {
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "revoked_by": {
+ "name": "revoked_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sessions": {
+ "name": "sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sessions_user_id_users_id_fk": {
+ "name": "sessions_user_id_users_id_fk",
+ "tableFrom": "sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "sessions_token_unique": {
+ "name": "sessions_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sso_providers": {
+ "name": "sso_providers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_config": {
+ "name": "oidc_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "saml_config": {
+ "name": "saml_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sso_providers_user_id_users_id_fk": {
+ "name": "sso_providers_user_id_users_id_fk",
+ "tableFrom": "sso_providers",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "sso_providers_provider_id_unique": {
+ "name": "sso_providers_provider_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "provider_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_instructions": {
+ "name": "user_instructions",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "instructions": {
+ "name": "instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_instructions_user_id_users_id_fk": {
+ "name": "user_instructions_user_id_users_id_fk",
+ "tableFrom": "user_instructions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_roles": {
+ "name": "user_roles",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_roles_user_id_users_id_fk": {
+ "name": "user_roles_user_id_users_id_fk",
+ "tableFrom": "user_roles",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_roles_user_id_role_pk": {
+ "name": "user_roles_user_id_role_pk",
+ "columns": [
+ "user_id",
+ "role"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "groups": {
+ "name": "groups",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "onboarding_step": {
+ "name": "onboarding_step",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "onboarding_completed_at": {
+ "name": "onboarding_completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verifications": {
+ "name": "verifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.action_policy": {
+ "name": "action_policy",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "mode": {
+ "name": "mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deny": {
+ "name": "deny",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "allow": {
+ "name": "allow",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.computer_page_frame": {
+ "name": "computer_page_frame",
+ "schema": "",
+ "columns": {
+ "computer_id": {
+ "name": "computer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_call_id": {
+ "name": "tool_call_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "frame": {
+ "name": "frame",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "captured_at": {
+ "name": "captured_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "computer_page_frame_captured_idx": {
+ "name": "computer_page_frame_captured_idx",
+ "columns": [
+ {
+ "expression": "captured_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "computer_page_frame_computer_id_tool_call_id_pk": {
+ "name": "computer_page_frame_computer_id_tool_call_id_pk",
+ "columns": [
+ "computer_id",
+ "tool_call_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.computer_snapshot": {
+ "name": "computer_snapshot",
+ "schema": "",
+ "columns": {
+ "computer_id": {
+ "name": "computer_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "snapshot_id": {
+ "name": "snapshot_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "elements": {
+ "name": "elements",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "taken_at": {
+ "name": "taken_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "session": {
+ "name": "session",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agent_preferences": {
+ "name": "agent_preferences",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hidden_at": {
+ "name": "hidden_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "agent_preferences_user_id_users_id_fk": {
+ "name": "agent_preferences_user_id_users_id_fk",
+ "tableFrom": "agent_preferences",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "agent_preferences_agent_id_agents_id_fk": {
+ "name": "agent_preferences_agent_id_agents_id_fk",
+ "tableFrom": "agent_preferences",
+ "tableTo": "agents",
+ "columnsFrom": [
+ "agent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "agent_preferences_user_id_agent_id_pk": {
+ "name": "agent_preferences_user_id_agent_id_pk",
+ "columns": [
+ "user_id",
+ "agent_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agent_profiles": {
+ "name": "agent_profiles",
+ "schema": "",
+ "columns": {
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_description": {
+ "name": "role_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "avatar_seed": {
+ "name": "avatar_seed",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "agent_visibility",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "callback_token_hash": {
+ "name": "callback_token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "callback_token_issued_at": {
+ "name": "callback_token_issued_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "agent_profiles_visibility_deleted_idx": {
+ "name": "agent_profiles_visibility_deleted_idx",
+ "columns": [
+ {
+ "expression": "visibility",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "agent_profiles_agent_id_agents_id_fk": {
+ "name": "agent_profiles_agent_id_agents_id_fk",
+ "tableFrom": "agent_profiles",
+ "tableTo": "agents",
+ "columnsFrom": [
+ "agent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "agent_profiles_owner_user_id_users_id_fk": {
+ "name": "agent_profiles_owner_user_id_users_id_fk",
+ "tableFrom": "agent_profiles",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.routine_runs": {
+ "name": "routine_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "routine_id": {
+ "name": "routine_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "routine_run_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "routine_runs_by_routine_idx": {
+ "name": "routine_runs_by_routine_idx",
+ "columns": [
+ {
+ "expression": "routine_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "routine_runs_routine_id_routines_id_fk": {
+ "name": "routine_runs_routine_id_routines_id_fk",
+ "tableFrom": "routine_runs",
+ "tableTo": "routines",
+ "columnsFrom": [
+ "routine_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.routines": {
+ "name": "routines",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "instruction": {
+ "name": "instruction",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cron": {
+ "name": "cron",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'UTC'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "next_run_at": {
+ "name": "next_run_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "routines_due_idx": {
+ "name": "routines_due_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "routines_by_owner_idx": {
+ "name": "routines_by_owner_idx",
+ "columns": [
+ {
+ "expression": "owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "routines_owner_user_id_users_id_fk": {
+ "name": "routines_owner_user_id_users_id_fk",
+ "tableFrom": "routines",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "routines_agent_id_agents_id_fk": {
+ "name": "routines_agent_id_agents_id_fk",
+ "tableFrom": "routines",
+ "tableTo": "agents",
+ "columnsFrom": [
+ "agent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.component_exclusions": {
+ "name": "component_exclusions",
+ "schema": "",
+ "columns": {
+ "component_name": {
+ "name": "component_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "withheld_by": {
+ "name": "withheld_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "component_exclusions_component_name_components_name_fk": {
+ "name": "component_exclusions_component_name_components_name_fk",
+ "tableFrom": "component_exclusions",
+ "tableTo": "components",
+ "columnsFrom": [
+ "component_name"
+ ],
+ "columnsTo": [
+ "name"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "component_exclusions_agent_id_agents_id_fk": {
+ "name": "component_exclusions_agent_id_agents_id_fk",
+ "tableFrom": "component_exclusions",
+ "tableTo": "agents",
+ "columnsFrom": [
+ "agent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "component_exclusions_component_name_agent_id_pk": {
+ "name": "component_exclusions_component_name_agent_id_pk",
+ "columns": [
+ "component_name",
+ "agent_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.component_functions": {
+ "name": "component_functions",
+ "schema": "",
+ "columns": {
+ "component_name": {
+ "name": "component_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "function_name": {
+ "name": "function_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "granted_by": {
+ "name": "granted_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "component_functions_component_name_components_name_fk": {
+ "name": "component_functions_component_name_components_name_fk",
+ "tableFrom": "component_functions",
+ "tableTo": "components",
+ "columnsFrom": [
+ "component_name"
+ ],
+ "columnsTo": [
+ "name"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "component_functions_component_name_function_name_pk": {
+ "name": "component_functions_component_name_function_name_pk",
+ "columns": [
+ "component_name",
+ "function_name"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.components": {
+ "name": "components",
+ "schema": "",
+ "columns": {
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "draft_description": {
+ "name": "draft_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "published_description": {
+ "name": "published_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published": {
+ "name": "published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_servers": {
+ "name": "mcp_servers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vendor": {
+ "name": "vendor",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provenance": {
+ "name": "provenance",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'first-party'"
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tools_refreshed_at": {
+ "name": "tools_refreshed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "added_by": {
+ "name": "added_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mcp_servers_credential_id_credentials_id_fk": {
+ "name": "mcp_servers_credential_id_credentials_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "credentials",
+ "columnsFrom": [
+ "credential_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_tools": {
+ "name": "mcp_tools",
+ "schema": "",
+ "columns": {
+ "server_id": {
+ "name": "server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "input_schema": {
+ "name": "input_schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mcp_tools_server_id_mcp_servers_id_fk": {
+ "name": "mcp_tools_server_id_mcp_servers_id_fk",
+ "tableFrom": "mcp_tools",
+ "tableTo": "mcp_servers",
+ "columnsFrom": [
+ "server_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "mcp_tools_server_id_name_pk": {
+ "name": "mcp_tools_server_id_name_pk",
+ "columns": [
+ "server_id",
+ "name"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_user_credentials": {
+ "name": "mcp_user_credentials",
+ "schema": "",
+ "columns": {
+ "server_id": {
+ "name": "server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connected_at": {
+ "name": "connected_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mcp_user_credentials_user_idx": {
+ "name": "mcp_user_credentials_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_user_credentials_server_id_mcp_servers_id_fk": {
+ "name": "mcp_user_credentials_server_id_mcp_servers_id_fk",
+ "tableFrom": "mcp_user_credentials",
+ "tableTo": "mcp_servers",
+ "columnsFrom": [
+ "server_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_user_credentials_user_id_users_id_fk": {
+ "name": "mcp_user_credentials_user_id_users_id_fk",
+ "tableFrom": "mcp_user_credentials",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_user_credentials_credential_id_credentials_id_fk": {
+ "name": "mcp_user_credentials_credential_id_credentials_id_fk",
+ "tableFrom": "mcp_user_credentials",
+ "tableTo": "credentials",
+ "columnsFrom": [
+ "credential_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "mcp_user_credentials_server_id_user_id_pk": {
+ "name": "mcp_user_credentials_server_id_user_id_pk",
+ "columns": [
+ "server_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.plugin_grants": {
+ "name": "plugin_grants",
+ "schema": "",
+ "columns": {
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ref": {
+ "name": "ref",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "granted_by": {
+ "name": "granted_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "plugin_grants_agent_idx": {
+ "name": "plugin_grants_agent_idx",
+ "columns": [
+ {
+ "expression": "agent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "plugin_grants_agent_id_agents_id_fk": {
+ "name": "plugin_grants_agent_id_agents_id_fk",
+ "tableFrom": "plugin_grants",
+ "tableTo": "agents",
+ "columnsFrom": [
+ "agent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "plugin_grants_kind_ref_agent_id_pk": {
+ "name": "plugin_grants_kind_ref_agent_id_pk",
+ "columns": [
+ "kind",
+ "ref",
+ "agent_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sandboxed_components": {
+ "name": "sandboxed_components",
+ "schema": "",
+ "columns": {
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "draft_description": {
+ "name": "draft_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "draft_html": {
+ "name": "draft_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "draft_css": {
+ "name": "draft_css",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "draft_js_functions": {
+ "name": "draft_js_functions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "draft_argument_schema": {
+ "name": "draft_argument_schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "published_description": {
+ "name": "published_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_html": {
+ "name": "published_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_css": {
+ "name": "published_css",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_js_functions": {
+ "name": "published_js_functions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_argument_schema": {
+ "name": "published_argument_schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sample_arguments": {
+ "name": "sample_arguments",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "published": {
+ "name": "published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "authored_by": {
+ "name": "authored_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.skill_tools": {
+ "name": "skill_tools",
+ "schema": "",
+ "columns": {
+ "skill_id": {
+ "name": "skill_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ref": {
+ "name": "ref",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "declared_by": {
+ "name": "declared_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "skill_tools_ref_idx": {
+ "name": "skill_tools_ref_idx",
+ "columns": [
+ {
+ "expression": "ref",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "skill_tools_skill_id_skills_id_fk": {
+ "name": "skill_tools_skill_id_skills_id_fk",
+ "tableFrom": "skill_tools",
+ "tableTo": "skills",
+ "columnsFrom": [
+ "skill_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "skill_tools_skill_id_ref_pk": {
+ "name": "skill_tools_skill_id_ref_pk",
+ "columns": [
+ "skill_id",
+ "ref"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.skills": {
+ "name": "skills",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "instructions": {
+ "name": "instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "origin": {
+ "name": "origin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'yours'"
+ },
+ "installed_by": {
+ "name": "installed_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "skills_slug_key": {
+ "name": "skills_slug_key",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "skills_owner_idx": {
+ "name": "skills_owner_idx",
+ "columns": [
+ {
+ "expression": "owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "skills_owner_user_id_users_id_fk": {
+ "name": "skills_owner_user_id_users_id_fk",
+ "tableFrom": "skills",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.work_items": {
+ "name": "work_items",
+ "schema": "",
+ "columns": {
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_at": {
+ "name": "run_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "claimed_by": {
+ "name": "claimed_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_until": {
+ "name": "lease_until",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "work_items_claimable_idx": {
+ "name": "work_items_claimable_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "work_items_kind_key_pk": {
+ "name": "work_items_kind_key_pk",
+ "columns": [
+ "kind",
+ "key"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.agent_type": {
+ "name": "agent_type",
+ "schema": "public",
+ "values": [
+ "built_in",
+ "remote_ag_ui"
+ ]
+ },
+ "public.credential_kind": {
+ "name": "credential_kind",
+ "schema": "public",
+ "values": [
+ "model",
+ "connector",
+ "agent",
+ "mcp",
+ "mcp_oauth_client",
+ "mcp_user_token"
+ ]
+ },
+ "public.role": {
+ "name": "role",
+ "schema": "public",
+ "values": [
+ "admin",
+ "user"
+ ]
+ },
+ "public.agent_visibility": {
+ "name": "agent_visibility",
+ "schema": "public",
+ "values": [
+ "public",
+ "private"
+ ]
+ },
+ "public.routine_run_status": {
+ "name": "routine_run_status",
+ "schema": "public",
+ "values": [
+ "succeeded",
+ "failed",
+ "skipped"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json
index 49201434b..040924c34 100644
--- a/server/drizzle/meta/_journal.json
+++ b/server/drizzle/meta/_journal.json
@@ -197,6 +197,13 @@
"when": 1788543624826,
"tag": "0027_user_instructions",
"breakpoints": true
+ },
+ {
+ "idx": 28,
+ "version": "7",
+ "when": 1788548093782,
+ "tag": "0028_audit_initiator",
+ "breakpoints": true
}
]
}
diff --git a/server/src/agents/callback-token.ts b/server/src/agents/callback-token.ts
index 717d04421..8f95f38cd 100644
--- a/server/src/agents/callback-token.ts
+++ b/server/src/agents/callback-token.ts
@@ -1,4 +1,5 @@
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
+import { type AuditInitiator, PERSON_INITIATOR } from "../audit";
import { sign, verify } from "../auth/signed-value";
/**
@@ -102,6 +103,17 @@ export type RunAssertion = {
* Optional on the way in so an assertion minted before this existed still reads, and read as zero.
*/
depth?: number;
+ /**
+ * What started this run, for the audit rows written by whoever holds the assertion.
+ *
+ * IT TRAVELS HERE FOR THE REASON `depth` DOES: a hop is A to B on up to two pods, and the thing
+ * that knows a routine began the run is the process that claimed the routine, not the one writing
+ * the row. Signed with the rest, so a Bot cannot relabel its own run as a person's.
+ *
+ * Optional on the way in so an assertion minted before this existed still reads, and read as a
+ * person, which is what those runs were.
+ */
+ initiator?: AuditInitiator;
};
type SignedRun = RunAssertion & { exp: number };
@@ -121,6 +133,7 @@ export function mintRunAssertion(
const payload: SignedRun = {
...run,
depth: run.depth ?? 0,
+ initiator: run.initiator ?? PERSON_INITIATOR,
exp: now + RUN_TTL_MS,
};
const value = Buffer.from(JSON.stringify(payload)).toString("base64url");
@@ -175,12 +188,30 @@ export function readRunAssertion(
payload.depth >= 0
? payload.depth
: 0,
+ // Read as a person on anything unclear, for the reason depth reads as zero: an assertion
+ // minted before this existed carries none, and a person is what those runs were.
+ initiator: readInitiator(payload.initiator),
};
} catch {
return null;
}
}
+/**
+ * The initiator a signed payload carries, narrowed back to the union.
+ *
+ * A kind that is not one this deployment writes is read as a person rather than kept, so a field
+ * from a future version cannot arrive as a string the Audit screen has no branch for.
+ */
+function readInitiator(value: unknown): AuditInitiator {
+ if (!value || typeof value !== "object") return PERSON_INITIATOR;
+ const kind = (value as { kind?: unknown }).kind;
+ if (kind === "person" || kind === "deployment") return { kind };
+ if (kind !== "routine" && kind !== "handoff") return PERSON_INITIATOR;
+ const id = (value as { id?: unknown }).id;
+ return typeof id === "string" && id ? { kind, id } : PERSON_INITIATOR;
+}
+
export type CallVerdict =
| { ok: true; botId: string; actorId: string }
| { ok: false; status: 401 | 403; reason: string };
diff --git a/server/src/agents/escalation.ts b/server/src/agents/escalation.ts
index 8a00e1047..017c27d73 100644
--- a/server/src/agents/escalation.ts
+++ b/server/src/agents/escalation.ts
@@ -124,6 +124,7 @@ export function escalationTool(options: {
targetType: "agent",
targetId: from.botId,
...(from.actorId ? { actorUserId: from.actorId } : {}),
+ ...(from.initiator ? { initiator: from.initiator } : {}),
payload: {
bot: from.botId,
run: from.runId,
diff --git a/server/src/agents/handoff-delivery.ts b/server/src/agents/handoff-delivery.ts
index 21259bd0d..28128032b 100644
--- a/server/src/agents/handoff-delivery.ts
+++ b/server/src/agents/handoff-delivery.ts
@@ -70,6 +70,8 @@ export function createHandoffDelivery(options: {
agentFor: (input: {
actorId: string;
botId: string;
+ /** The Bot that handed the work on, so the trail says a hop ran this and not the person. */
+ fromBotId: string;
}) => Promise;
/**
* The conversation so far, so the addressed Bot is not answering out of context.
@@ -161,6 +163,7 @@ export function createHandoffDelivery(options: {
const agent = await agentFor({
actorId: work.actorId,
botId: work.toBotId,
+ fromBotId: work.fromBotId,
});
if (!agent) {
/*
diff --git a/server/src/agents/handoff-runner.ts b/server/src/agents/handoff-runner.ts
index c59a3bc76..da3506209 100644
--- a/server/src/agents/handoff-runner.ts
+++ b/server/src/agents/handoff-runner.ts
@@ -299,6 +299,7 @@ export function createHandoffRunner(options: {
targetType: "agent",
targetId: work.toBotId,
...(work.actorId ? { actorUserId: work.actorId } : {}),
+ initiator: { kind: "handoff", id: work.fromBotId },
payload: {
// See the same key on `agent.handoff_delivered` below: the Audit screen's Bot
// column reads `payload.bot`, so a row without it names no Bot.
@@ -376,6 +377,7 @@ export function createHandoffRunner(options: {
targetType: "agent",
targetId: work.toBotId,
...(work.actorId ? { actorUserId: work.actorId } : {}),
+ initiator: { kind: "handoff", id: work.fromBotId },
payload: {
// See the same key on `agent.handoff_delivered` below.
bot: work.fromBotId,
@@ -410,6 +412,7 @@ export function createHandoffRunner(options: {
targetType: "agent",
targetId: work.toBotId,
...(work.actorId ? { actorUserId: work.actorId } : {}),
+ initiator: { kind: "handoff", id: work.fromBotId },
payload: {
// See the same key on `agent.handoff_offered`: the Audit screen's Bot column reads
// `payload.bot`, so a row without it names no Bot.
@@ -460,6 +463,7 @@ export function createHandoffRunner(options: {
targetType: "agent",
targetId: work.toBotId,
...(work.actorId ? { actorUserId: work.actorId } : {}),
+ initiator: { kind: "handoff", id: work.fromBotId },
payload: {
// See the same key on `agent.handoff_delivered` above. This row is the one a
// person's unanswered question ends on, so a Bot column showing a dash on it is
diff --git a/server/src/agents/handoff.ts b/server/src/agents/handoff.ts
index 58c32848f..dad7c3d85 100644
--- a/server/src/agents/handoff.ts
+++ b/server/src/agents/handoff.ts
@@ -109,6 +109,7 @@ export function createHandoffDesk(options: {
targetType: "agent",
targetId: from.botId,
...(from.actorId ? { actorUserId: from.actorId } : {}),
+ ...(from.initiator ? { initiator: from.initiator } : {}),
payload: {
// The same key `agent.handoff_offered` sets below, and for the same reason: the Audit
// screen renders `payload.bot` and nothing else in its Bot column, so a row without it
@@ -388,6 +389,7 @@ export function createHandoffDesk(options: {
targetType: "agent",
targetId: found.id,
...(from.actorId ? { actorUserId: from.actorId } : {}),
+ ...(from.initiator ? { initiator: from.initiator } : {}),
payload: {
// The Bot that did this, under the key the Audit screen reads for its Bot column. `from`
// below says the same thing and is what the payload is read by, but the screen renders
diff --git a/server/src/app.ts b/server/src/app.ts
index d0f38a312..09f2764bc 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -10,6 +10,7 @@ import {
type AuditStore,
AuditQueryError,
auditQueryFromUrl,
+ DEPLOYMENT_INITIATOR,
recordAuditEvent,
} from "./audit";
import { createDevRequireUser } from "./auth/dev-actor";
@@ -836,6 +837,7 @@ export function createApp(
await recordAuditEvent(auditStore, {
eventType: "routines.dispatch_refused",
targetType: "worker",
+ initiator: DEPLOYMENT_INITIATOR,
payload: {
reason: !expected
? "unconfigured"
@@ -1107,6 +1109,7 @@ export function createApp(
await recordAuditEvent(auditStore, {
eventType: "mcp.callback_refused",
targetType: "mcp_tool",
+ initiator: DEPLOYMENT_INITIATOR,
targetId:
typeof body?.name === "string"
? body.name.slice(0, 120)
diff --git a/server/src/audit.ts b/server/src/audit.ts
index e057138dc..b422ddc71 100644
--- a/server/src/audit.ts
+++ b/server/src/audit.ts
@@ -392,11 +392,40 @@ export const auditEventTypes = [
export type AuditEventType = (typeof auditEventTypes)[number];
+/** What caused a row, where `actorUserId` is only whose authority it borrowed. */
+export type AuditInitiator =
+ | { kind: "person" }
+ | { kind: "deployment" }
+ | { kind: "routine"; id: string }
+ | { kind: "handoff"; id: string };
+
+export const PERSON_INITIATOR: AuditInitiator = { kind: "person" };
+
+/** The deployment acting as itself: at start-up, or refusing a caller it could not identify. */
+export const DEPLOYMENT_INITIATOR: AuditInitiator = { kind: "deployment" };
+
+export const auditInitiatorKinds = [
+ "person",
+ "deployment",
+ "routine",
+ "handoff",
+] as const;
+
+export type AuditInitiatorKind = (typeof auditInitiatorKinds)[number];
+
+export function isAuditInitiatorKind(
+ value: string,
+): value is AuditInitiatorKind {
+ return (auditInitiatorKinds as readonly string[]).includes(value);
+}
+
export type AuditEventInput = {
eventType: AuditEventType;
targetType: string;
targetId?: string;
actorUserId?: string;
+ /** Omitted means a person did it. */
+ initiator?: AuditInitiator;
payload: Record;
};
@@ -407,6 +436,9 @@ export type AuditStore = {
export type AuditEvent = {
id: string;
actorUserId: string | null;
+ /** Read back as written, not narrowed to the union. */
+ initiatorKind: string;
+ initiatorId: string | null;
eventType: string;
targetType: string;
targetId: string | null;
@@ -426,6 +458,8 @@ export type AuditEventQuery = {
*/
eventType?: string;
actorUserId?: string;
+ /** One kind, or several separated by commas, the way `eventType` takes several. */
+ initiatorKind?: string;
targetType?: string;
targetId?: string;
from?: string;
@@ -482,11 +516,21 @@ export async function recordAuditEvent(
});
}
+function initiatorColumns(initiator: AuditInitiator | undefined) {
+ if (!initiator)
+ return { initiatorKind: "person" as const, initiatorId: null };
+ if (initiator.kind === "person" || initiator.kind === "deployment") {
+ return { initiatorKind: initiator.kind, initiatorId: null };
+ }
+ return { initiatorKind: initiator.kind, initiatorId: initiator.id };
+}
+
export function createAuditStore(database: Database): AuditStore {
return {
- insert: async (event) => {
+ insert: async ({ initiator, ...event }) => {
await database.insert(auditEvents).values({
...event,
+ ...initiatorColumns(initiator),
payload: redactAuditPayload(event.payload) as Record,
});
},
@@ -519,6 +563,10 @@ export function createAuditReader(database: Database): AuditReader {
.split(",")
.map((type) => type.trim())
.filter(Boolean);
+ const requestedInitiators = (query.initiatorKind ?? "")
+ .split(",")
+ .map((kind) => kind.trim())
+ .filter((kind) => isAuditInitiatorKind(kind));
const conditions = [
requestedTypes.length === 1
? eq(auditEvents.eventType, requestedTypes[0] as string)
@@ -528,6 +576,11 @@ export function createAuditReader(database: Database): AuditReader {
query.actorUserId
? eq(auditEvents.actorUserId, query.actorUserId)
: undefined,
+ requestedInitiators.length === 1
+ ? eq(auditEvents.initiatorKind, requestedInitiators[0] as string)
+ : requestedInitiators.length > 1
+ ? inArray(auditEvents.initiatorKind, requestedInitiators)
+ : undefined,
query.targetType
? eq(auditEvents.targetType, query.targetType)
: undefined,
@@ -611,6 +664,7 @@ export function auditQueryFromUrl(url: URL): AuditEventQuery {
limit,
eventType: optional("eventType"),
actorUserId: optional("actorUserId"),
+ initiatorKind: optional("initiatorKind"),
targetType: optional("targetType"),
targetId: optional("targetId"),
from,
diff --git a/server/src/channels/stall-guard.ts b/server/src/channels/stall-guard.ts
index b6aca7104..6e69297e7 100644
--- a/server/src/channels/stall-guard.ts
+++ b/server/src/channels/stall-guard.ts
@@ -41,7 +41,11 @@
* RUN_ERROR at any point in a stream, including as the very first event, which is what a Bot that
* never spoke produces.
*/
-import { type AuditStore, recordAuditEvent } from "../audit";
+import {
+ type AuditInitiator,
+ type AuditStore,
+ recordAuditEvent,
+} from "../audit";
import { type StalledStream, TurnWatchdog } from "./turn-watchdog";
/** The fetch an `HttpAgent` uses, as @ag-ui/client 0.0.57 declares it. */
@@ -51,7 +55,12 @@ export type AgentFetch = (
) => Promise;
/** Which Bot a watched stream belongs to. The name is for the sentence a person reads. */
-export type WatchedBot = { id: string; name: string };
+export type WatchedBot = {
+ id: string;
+ name: string;
+ /** What started the run this stream belongs to, so a routine's stall is not filed as a person's. */
+ initiator?: AuditInitiator;
+};
export type StallGuardOptions = {
/** Silence this long ends the turn. Zero or less leaves every stream untouched. */
@@ -226,6 +235,7 @@ export function createStallGuard(options: StallGuardOptions): StallGuard {
eventType: "agent.stream_stalled",
targetType: "agent",
targetId: stream.bot.id,
+ ...(stream.bot.initiator ? { initiator: stream.bot.initiator } : {}),
payload: {
bot: stream.bot.id,
silentForMs: stalled.silentForMs,
diff --git a/server/src/copilot.ts b/server/src/copilot.ts
index fb68b0e2d..ee46da013 100644
--- a/server/src/copilot.ts
+++ b/server/src/copilot.ts
@@ -15,6 +15,7 @@ import {
PROVENANCE_GUIDANCE,
} from "../../shared/bot-prompt";
import { sanitizeSeededHistory } from "./agents/history-sanitize";
+import type { AuditInitiator } from "./audit";
import type { AgentActor } from "./agents/profile-types";
import type { AgentFetch, StallGuard } from "./channels/stall-guard";
import type { DeploymentConfig } from "./config";
@@ -361,6 +362,7 @@ export async function buildAgents(
* carry standing instructions, which is what every deployment did before they existed.
*/
loadInstructions?: LoadInstructions,
+ initiator?: AuditInitiator,
): Promise> {
const vendors = await loadVendors().catch(() => [] as readonly string[]);
/*
@@ -392,6 +394,7 @@ export async function buildAgents(
agentFetch,
handoff,
instructions ?? null,
+ initiator,
),
]),
),
@@ -421,6 +424,7 @@ async function buildAgent(
handoff?: HandoffForRun,
/** Already resolved by {@link buildAgents}, so one roster costs one read. */
standingInstructions: string | null = null,
+ initiator?: AuditInitiator,
): Promise {
if (agent.type === "unavailable") {
return new UnavailableAgent(agent);
@@ -495,6 +499,7 @@ async function buildAgent(
connectedVendors,
narrowing ? offeredFor : undefined,
agentFetch,
+ initiator,
);
}
@@ -620,6 +625,7 @@ function remoteAgentWithStandingRole(
narrow?: (input: RunAgentInput) => Promise,
/** The fetch this agent is dialled with. See {@link buildAgents}. */
agentFetch?: AgentFetch,
+ initiator?: AuditInitiator,
) {
const remote = new HttpAgent({
url: agent.endpoint,
@@ -632,7 +638,11 @@ function remoteAgentWithStandingRole(
...(stallGuard
? {
fetch: stallGuard.watch(
- { id: agent.id, name: agent.name },
+ {
+ id: agent.id,
+ name: agent.name,
+ ...(initiator ? { initiator } : {}),
+ },
agentFetch,
),
}
@@ -958,6 +968,8 @@ export async function resolveRuntimeAgents(
* are positional and moving one shifts every existing call site by one.
*/
loadInstructions?: LoadInstructions,
+ /** Appended after `loadInstructions`, for the positional reason it gives. */
+ initiator?: AuditInitiator,
): Promise> {
const all = await loadAgents();
if (all.length === 0) {
@@ -989,6 +1001,7 @@ export async function resolveRuntimeAgents(
agentFetch,
handoff,
loadInstructions,
+ initiator,
);
}
@@ -1036,9 +1049,12 @@ export function createRequestAgents(
*/
stallGuard?: StallGuard,
/** What each Bot may call, resolved for whoever is asking. Absent means no tools. */
- loadToolsForActor?: (actorId: string) => LoadToolsForBot,
+ loadToolsForActor?: (
+ actorId: string,
+ initiator?: AuditInitiator,
+ ) => LoadToolsForBot,
/** Resolved per request, because what it signs is who this request turned out to be. */
- signRunForActor?: (actorId: string) => SignRun,
+ signRunForActor?: (actorId: string, initiator?: AuditInitiator) => SignRun,
/** What every built-in Bot is told about the computer. Absent means this deployment has none. */
computerGuidance?: string,
/** Which vendors this deployment connects to, held by a Bot or not. Absent means none. */
@@ -1186,8 +1202,11 @@ export function mountCopilotRuntime(
* there is no reason for a caller to have to say `undefined` here to reach `basePath`.
*/
stallGuard: StallGuard,
- loadToolsForActor?: (actorId: string) => LoadToolsForBot,
- signRunForActor?: (actorId: string) => SignRun,
+ loadToolsForActor?: (
+ actorId: string,
+ initiator?: AuditInitiator,
+ ) => LoadToolsForBot,
+ signRunForActor?: (actorId: string, initiator?: AuditInitiator) => SignRun,
basePath = "/api/copilotkit",
loadVendors?: () => Promise,
selectionForActor?: (actorId: string) => ToolSelection,
@@ -1236,6 +1255,7 @@ export function mountCopilotRuntime(
*/
actor: AgentActor;
botId: string;
+ initiator?: AuditInitiator;
}): Promise => {
const { actor } = input;
const agents = await resolveRuntimeAgents(
@@ -1243,8 +1263,8 @@ export function mountCopilotRuntime(
model,
resolveModelApiKey,
stallGuard,
- loadToolsForActor?.(actor.id),
- signRunForActor?.(actor.id),
+ loadToolsForActor?.(actor.id, input.initiator),
+ signRunForActor?.(actor.id, input.initiator),
config.computer ? COMPUTER_GUIDANCE : undefined,
loadVendors,
selectionForActor?.(actor.id),
@@ -1255,6 +1275,7 @@ export function mountCopilotRuntime(
// what each of them was granted, on every delivery and again on every retry.
input.botId,
loadInstructionsForActor?.(actor.id),
+ input.initiator,
);
return agents[input.botId] ?? null;
};
diff --git a/server/src/db/schema/core.ts b/server/src/db/schema/core.ts
index e2bcdf80e..5387b54bd 100644
--- a/server/src/db/schema/core.ts
+++ b/server/src/db/schema/core.ts
@@ -419,6 +419,10 @@ export const auditEvents = pgTable(
* user who had done anything could never be deleted.
*/
actorUserId: text("actor_user_id"),
+ /** Defaulted rather than nullable, because every row written before this was a person's. */
+ initiatorKind: text("initiator_kind").notNull().default("person"),
+ /** Which routine, or which Bot handed the work on. Null when a person started it. */
+ initiatorId: text("initiator_id"),
eventType: text("event_type").notNull(),
targetType: text("target_type").notNull(),
targetId: text("target_id"),
@@ -456,6 +460,11 @@ export const auditEvents = pgTable(
table.createdAt.desc(),
table.id.desc(),
),
+ index("audit_events_initiator_time_idx").on(
+ table.initiatorKind,
+ table.createdAt.desc(),
+ table.id.desc(),
+ ),
],
);
diff --git a/server/src/index.ts b/server/src/index.ts
index 667f05bdb..1218ebc0c 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -17,7 +17,14 @@ import { createAgentProfileStore } from "./agents/profile-store";
import type { AgentActor } from "./agents/profile-types";
import { createRuntimeAgentLoader } from "./agents/runtime-agents";
import { createApp } from "./app";
-import { createAuditReader, createAuditStore, recordAuditEvent } from "./audit";
+import {
+ type AuditInitiator,
+ createAuditReader,
+ createAuditStore,
+ DEPLOYMENT_INITIATOR,
+ PERSON_INITIATOR,
+ recordAuditEvent,
+} from "./audit";
import { startRetentionSweeps } from "./audit-retention";
import { createAuth } from "./auth";
import { DEV_ACTOR, initializeDevActorUser } from "./auth/dev-actor";
@@ -374,6 +381,7 @@ const handoffDesk = createHandoffDesk({
void recordAuditEvent(bootAuditStore, {
eventType: "computer.policy_loaded",
targetType: "policy",
+ initiator: DEPLOYMENT_INITIATOR,
payload: {
...policyStore.get(),
source:
@@ -400,6 +408,7 @@ const isolation = describeComputerIsolation(computerProvider);
void recordAuditEvent(bootAuditStore, {
eventType: "computer.isolation_loaded",
targetType: "computer",
+ initiator: DEPLOYMENT_INITIATOR,
payload: {
isolation: isolation.isolation,
note: isolation.note,
@@ -511,8 +520,10 @@ const resolveRuntimeModelApiKey = () =>
// Tools run here, not in the browser. Each one still executes through the plugin store, so the
// grant, the policy and the audit row are exactly where they were.
-const loadToolsForActor = (actorId: string) => (botId: string) =>
- grantedTools({ store: pluginStore, botId, actorId });
+const loadToolsForActor =
+ (actorId: string, initiator: AuditInitiator = PERSON_INITIATOR) =>
+ (botId: string) =>
+ grantedTools({ store: pluginStore, botId, actorId, initiator });
/** One person's standing instructions, for both the /api/settings routes and every run they start. */
const userInstructionsStore = createUserInstructionsStore(database);
@@ -537,9 +548,10 @@ const loadInstructionsForActor = (actorId: string) => () =>
* neither is read out of the request body any more.
*/
const signRunForActor =
- (actorId: string) => (botId: string, runId: string, threadId?: string) =>
+ (actorId: string, initiator: AuditInitiator = PERSON_INITIATOR) =>
+ (botId: string, runId: string, threadId?: string) =>
mintRunAssertion(
- { botId, actorId, runId, threadId },
+ { botId, actorId, runId, threadId, initiator },
config.keyEncryptionKey,
);
@@ -661,9 +673,11 @@ const actorFor = async (ownerUserId: string): Promise => {
const buildAgentFor = async ({
ownerUserId,
agentId,
+ initiator,
}: {
ownerUserId: string;
agentId: string;
+ initiator: AuditInitiator;
}) => {
const actor = await actorFor(ownerUserId);
const agents = await resolveRuntimeAgents(
@@ -671,8 +685,8 @@ const buildAgentFor = async ({
tenantPackage.model,
resolveRuntimeModelApiKey,
stallGuard,
- loadToolsForActor(actor.id),
- signRunForActor(actor.id),
+ loadToolsForActor(actor.id, initiator),
+ signRunForActor(actor.id, initiator),
config.computer ? COMPUTER_GUIDANCE : undefined,
loadVendors,
selectionForActor(actor.id),
@@ -685,6 +699,7 @@ const buildAgentFor = async ({
// The owner's own standing instructions. A routine is their work done while they are asleep, so
// it is written the way they asked for it to be written, exactly as their chat turn would be.
loadInstructionsForActor(actor.id),
+ initiator,
);
const agent = agents[agentId];
if (!agent) {
@@ -783,6 +798,9 @@ const copilotRuntime = mountCopilotRuntime(
runId: input.runId,
threadId: input.threadId,
depth: from?.depth ?? 0,
+ // Read from the assertion for the reason `depth` is: the run is rebuilt from parts here, and
+ // a field left out of this object is a field the desk and the escalation never see.
+ initiator: from?.initiator ?? PERSON_INITIATOR,
};
/*
* The caps are checked BEFORE the grants query, not inside the tool that would discard it.
@@ -898,14 +916,18 @@ if (config.handoff.maxDepth > 0 && config.handoff.maxPerRun > 0) {
* delivery that then rebuilt them as an ordinary user could not find the Bot the desk had just
* agreed to, and the person was told it never answered.
*/
- agentFor: async ({ actorId, botId }) => {
+ agentFor: async ({ actorId, botId, fromBotId }) => {
const actor = await actorFor(actorId).catch(() => null);
if (!actor) {
throw new Error(
"who this is for could not be confirmed, so the Bot was not run",
);
}
- return copilotRuntime.agentFor({ actor, botId });
+ return copilotRuntime.agentFor({
+ actor,
+ botId,
+ initiator: { kind: "handoff", id: fromBotId },
+ });
},
history: copilotRuntime.history,
lock: copilotRuntime.threadLock,
diff --git a/server/src/plugins/store.ts b/server/src/plugins/store.ts
index 11867e4ce..4a65202a0 100644
--- a/server/src/plugins/store.ts
+++ b/server/src/plugins/store.ts
@@ -1,5 +1,9 @@
import { and, asc, eq, inArray, isNull, or, sql } from "drizzle-orm";
-import { type AuditStore, recordAuditEvent } from "../audit";
+import {
+ type AuditInitiator,
+ type AuditStore,
+ recordAuditEvent,
+} from "../audit";
import {
type ActionPolicy,
evaluateActionPolicy,
@@ -2787,6 +2791,7 @@ export function createPluginStore(options: PluginStoreOptions) {
args: Record;
botId: string;
actorId: string;
+ initiator?: AuditInitiator;
}): Promise<{ text: string; isError: boolean }> {
const [serverId, ...rest] = input.ref.split("/");
const toolName = rest.join("/");
@@ -2800,6 +2805,7 @@ export function createPluginStore(options: PluginStoreOptions) {
eventType: "mcp.call_rejected",
targetType: "mcp_tool",
targetId: input.ref,
+ ...(input.initiator ? { initiator: input.initiator } : {}),
payload: {
actor: input.actorId,
bot: input.botId,
@@ -2919,6 +2925,7 @@ export function createPluginStore(options: PluginStoreOptions) {
eventType: "mcp.call_rejected",
targetType: "mcp_tool",
targetId: input.ref,
+ ...(input.initiator ? { initiator: input.initiator } : {}),
payload: decided,
});
}
@@ -2957,6 +2964,7 @@ export function createPluginStore(options: PluginStoreOptions) {
eventType: result.isError ? "mcp.call_failed" : "mcp.call_succeeded",
targetType: "mcp_tool",
targetId: input.ref,
+ ...(input.initiator ? { initiator: input.initiator } : {}),
/*
* The vendor's own words, when it is reporting a failure.
*
@@ -2992,6 +3000,7 @@ export function createPluginStore(options: PluginStoreOptions) {
eventType: "mcp.call_failed",
targetType: "mcp_tool",
targetId: input.ref,
+ ...(input.initiator ? { initiator: input.initiator } : {}),
payload: {
...decided,
failure: (error instanceof Error
diff --git a/server/src/plugins/tools.ts b/server/src/plugins/tools.ts
index d4bb9a2fd..4e54bf20c 100644
--- a/server/src/plugins/tools.ts
+++ b/server/src/plugins/tools.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import type { AuditInitiator } from "../audit";
import type { SelectableSkill } from "./selection";
import { PluginRefusedError, type PluginStore } from "./store";
@@ -158,8 +159,9 @@ export async function grantedTools(options: {
store: PluginStore;
botId: string;
actorId: string;
+ initiator?: AuditInitiator;
}): Promise {
- const { store, botId, actorId } = options;
+ const { store, botId, actorId, initiator } = options;
const granted = await store.listForAgent(botId);
return granted.tools.map((tool) => ({
@@ -179,6 +181,7 @@ export async function grantedTools(options: {
: {},
botId,
actorId,
+ ...(initiator ? { initiator } : {}),
});
/*
* A vendor's error is named as one, not handed over as content.
diff --git a/server/src/routines/run-turn.ts b/server/src/routines/run-turn.ts
index e3d5929f9..aad4793ce 100644
--- a/server/src/routines/run-turn.ts
+++ b/server/src/routines/run-turn.ts
@@ -56,6 +56,7 @@ import type {
} from "@ag-ui/client";
import { EventType } from "@ag-ui/client";
import { sanitizeSeededHistory } from "../agents/history-sanitize";
+import type { AuditInitiator } from "../audit";
import { historyOrEmpty } from "../copilot";
import type { TurnRunner } from "./runner";
@@ -260,6 +261,7 @@ export function createTurnRunner(options: {
buildAgentFor: (input: {
ownerUserId: string;
agentId: string;
+ initiator: AuditInitiator;
}) => Promise;
/** How long one headless turn may take before it is stopped. */
turnTimeoutMs?: number;
@@ -278,7 +280,7 @@ export function createTurnRunner(options: {
abortGraceMs = DEFAULT_ABORT_GRACE_MS,
} = options;
- return async ({ ownerUserId, agentId, threadId, instruction }) => {
+ return async ({ ownerUserId, routineId, agentId, threadId, instruction }) => {
/*
* One id for this turn, minted once.
*
@@ -362,7 +364,11 @@ export function createTurnRunner(options: {
* ownership on every event and pushes them to the gateway, which is the whole reason this file
* exists rather than a bare `runAgent`.
*/
- const agent = await buildAgentFor({ ownerUserId, agentId });
+ const agent = await buildAgentFor({
+ ownerUserId,
+ agentId,
+ initiator: { kind: "routine", id: routineId },
+ });
agent.threadId = threadId;
agent.setMessages(messages);
diff --git a/server/src/routines/runner.ts b/server/src/routines/runner.ts
index 99775aa0e..3455ce5c9 100644
--- a/server/src/routines/runner.ts
+++ b/server/src/routines/runner.ts
@@ -28,6 +28,7 @@ import type { RoutineStore } from "./store";
/** Everything a headless turn needs, injectable so tests never dial a model. */
export type TurnRunner = (input: {
ownerUserId: string; // the actor the run asserts — grants and connections resolve to them
+ routineId: string; // what the trail names as having started this turn, rather than the owner
agentId: string;
threadId: string; // the owner's thread for the routine's channel
instruction: string; // the user message of this turn
@@ -130,6 +131,7 @@ export function createRoutineRunner(options: {
try {
({ replyText } = await runTurn({
ownerUserId,
+ routineId,
agentId,
threadId: channel.threadId,
instruction,
diff --git a/server/tests/agent-callback-token.test.ts b/server/tests/agent-callback-token.test.ts
index dc5ad86e1..26aa0e595 100644
--- a/server/tests/agent-callback-token.test.ts
+++ b/server/tests/agent-callback-token.test.ts
@@ -43,7 +43,11 @@ describe("the run assertion", () => {
test("survives a round trip", () => {
const signed = mintRunAssertion(RUN, KEY);
// A run that began with a person is depth zero, which is what an unstated depth means.
- expect(readRunAssertion(signed, KEY)).toEqual({ ...RUN, depth: 0 });
+ expect(readRunAssertion(signed, KEY)).toEqual({
+ ...RUN,
+ depth: 0,
+ initiator: { kind: "person" },
+ });
});
test("is refused when signed with another key", () => {
@@ -71,6 +75,7 @@ describe("the run assertion", () => {
expect(readRunAssertion(signed, KEY, 60 * 1000)).toEqual({
...RUN,
depth: 0,
+ initiator: { kind: "person" },
});
});
@@ -293,6 +298,47 @@ describe("how deep a run is", () => {
}
});
+ test("what started the run survives a round trip", () => {
+ for (const initiator of [
+ { kind: "person" } as const,
+ { kind: "deployment" } as const,
+ { kind: "routine", id: "routine_7" } as const,
+ { kind: "handoff", id: "research-assistant" } as const,
+ ]) {
+ const signed = mintRunAssertion({ ...RUN, initiator }, KEY);
+ expect(readRunAssertion(signed, KEY)?.initiator).toEqual(initiator);
+ }
+ });
+
+ test("a run that says nothing about what started it reads as a person", () => {
+ expect(
+ readRunAssertion(mintRunAssertion(RUN, KEY), KEY)?.initiator,
+ ).toEqual({ kind: "person" });
+ });
+
+ /*
+ * The point of putting this inside the signature. A Bot cannot relabel its own run, and a kind
+ * this deployment does not write cannot arrive as a string the Audit screen has no branch for.
+ */
+ test("an initiator that is not one reads as a person rather than being kept", () => {
+ for (const nonsense of [
+ { kind: "administrator" },
+ { kind: "routine" },
+ { kind: "handoff", id: "" },
+ { kind: "routine", id: 7 },
+ "routine",
+ null,
+ ]) {
+ const signed = mintRunAssertion(
+ { ...RUN, initiator: nonsense as never },
+ KEY,
+ );
+ expect(readRunAssertion(signed, KEY)?.initiator).toEqual({
+ kind: "person",
+ });
+ }
+ });
+
test("the conversation survives a round trip, and is absent when there is none", () => {
expect(
readRunAssertion(mintRunAssertion({ ...RUN, threadId: "t1" }, KEY), KEY)
diff --git a/server/tests/agent-escalation.test.ts b/server/tests/agent-escalation.test.ts
index 4b2b76ff3..d1c92eefe 100644
--- a/server/tests/agent-escalation.test.ts
+++ b/server/tests/agent-escalation.test.ts
@@ -48,6 +48,36 @@ describe("asking a person", () => {
expect(said).toContain("the person in this conversation");
});
+ /*
+ * A routine's Bot stopping to ask is the case worth finding: nobody is in the conversation to
+ * answer, so the row has to say the question was raised by a schedule rather than by a person.
+ */
+ test("the row says what started the run, not only whose authority it had", async () => {
+ const { written, store } = recorder();
+ const tool = escalationTool({
+ from: { ...FROM, initiator: { kind: "routine", id: "routine_7" } },
+ route: askTheirOwnPerson,
+ auditStore: store,
+ });
+
+ await tool.execute({ question: "which account?" });
+
+ expect(written[0]?.initiator).toEqual({ kind: "routine", id: "routine_7" });
+ });
+
+ test("a run that says nothing leaves the row filed as a person's", async () => {
+ const { written, store } = recorder();
+ const tool = escalationTool({
+ from: FROM,
+ route: askTheirOwnPerson,
+ auditStore: store,
+ });
+
+ await tool.execute({ question: "which account?" });
+
+ expect(written[0]?.initiator).toBe(undefined);
+ });
+
test("the question is on the record", async () => {
const { written, store } = recorder();
const tool = escalationTool({
diff --git a/server/tests/agent-handoff-delivery.test.ts b/server/tests/agent-handoff-delivery.test.ts
index 293f7fbc6..4dc63eea2 100644
--- a/server/tests/agent-handoff-delivery.test.ts
+++ b/server/tests/agent-handoff-delivery.test.ts
@@ -53,15 +53,20 @@ function delivery(
}> = [];
const lockCalls: string[] = [];
const released: string[] = [];
+ const builtFor: { actorId: string; botId: string; fromBotId: string }[] = [];
return {
requests,
lockCalls,
released,
+ builtFor,
delivery: createHandoffDelivery({
...(options.deadlineMs === undefined
? {}
: { deadlineMs: options.deadlineMs }),
- agentFor: async () => agent,
+ agentFor: async (input) => {
+ builtFor.push(input);
+ return agent;
+ },
history: async () => options.history ?? PRIOR,
newRunId: () => "run-2",
mintThreadId: () => "scratch-thread",
@@ -783,3 +788,20 @@ describe("a history read that fails", () => {
expect(lockCalls).toEqual([]);
});
});
+
+describe("what the trail is told started the hop", () => {
+ test("the addressed Bot is built for the Bot that handed the work on", async () => {
+ const { delivery: deliver, builtFor } = delivery(FINISHED);
+
+ await deliver.deliver({
+ work: WORK,
+ message: "assistant has asked you to help",
+ shown: "Assistant asked Researcher for this on your behalf: find it",
+ assertion: "signed",
+ });
+
+ expect(builtFor).toEqual([
+ { actorId: "user-1", botId: "researcher", fromBotId: "assistant" },
+ ]);
+ });
+});
diff --git a/server/tests/agent-handoff.test.ts b/server/tests/agent-handoff.test.ts
index 748e232b7..4a72a4df1 100644
--- a/server/tests/agent-handoff.test.ts
+++ b/server/tests/agent-handoff.test.ts
@@ -51,8 +51,11 @@ function desk(options?: {
role?: "admin" | "user";
}) {
const rows: Array<{ kind: string; key: string; payload: unknown }> = [];
- const events: Array<{ eventType: string; payload: Record }> =
- [];
+ const events: Array<{
+ eventType: string;
+ payload: Record;
+ initiator?: { kind: string; id?: string };
+ }> = [];
const queue = {
offer: async (item: {
@@ -84,6 +87,7 @@ function desk(options?: {
recorded.push({
eventType: event.eventType,
payload: event.payload ?? {},
+ ...(event.initiator ? { initiator: event.initiator } : {}),
});
},
};
@@ -303,6 +307,51 @@ describe("handing work to another Bot", () => {
* a hop that was refused is invisible everywhere else, and "why did it not ask the specialist" is
* the question somebody asks about a thin answer.
*/
+ /*
+ * The row that records a hop beginning. It asserts the person whose authority the run carries, so
+ * without this it reads as an action that person took, which is the whole reason the column exists.
+ */
+ test("the offered row says what started the run, not only whose authority it had", async () => {
+ const started = desk();
+ await started.desk.send({
+ from: { ...FROM, initiator: { kind: "routine", id: "routine_7" } },
+ target: "researcher",
+ envelope: { task: "t" },
+ });
+
+ expect(started.events[0]?.eventType).toBe("agent.handoff_offered");
+ expect(started.events[0]?.initiator).toEqual({
+ kind: "routine",
+ id: "routine_7",
+ });
+ });
+
+ test("a refusal says it too, so a refused hop is not filed as a person's", async () => {
+ const refused = desk({ granted: false });
+ await refused.desk.send({
+ from: { ...FROM, initiator: { kind: "handoff", id: "researcher" } },
+ target: "researcher",
+ envelope: { task: "t" },
+ });
+
+ expect(refused.events[0]?.eventType).toBe("agent.handoff_refused");
+ expect(refused.events[0]?.initiator).toEqual({
+ kind: "handoff",
+ id: "researcher",
+ });
+ });
+
+ test("a run that says nothing leaves the row filed as a person's", async () => {
+ const plain = desk();
+ await plain.desk.send({
+ from: FROM,
+ target: "researcher",
+ envelope: { task: "t" },
+ });
+
+ expect(plain.events[0]?.initiator).toBe(undefined);
+ });
+
test("both outcomes leave a row naming the run and the reason", async () => {
const allowed = desk();
await allowed.desk.send({
diff --git a/server/tests/audit-initiator.integration.test.ts b/server/tests/audit-initiator.integration.test.ts
new file mode 100644
index 000000000..ba0ca3bb8
--- /dev/null
+++ b/server/tests/audit-initiator.integration.test.ts
@@ -0,0 +1,210 @@
+import { describe, expect, test } from "bun:test";
+import { eq } from "drizzle-orm";
+import {
+ auditQueryFromUrl,
+ createAuditReader,
+ createAuditStore,
+} from "../src/audit";
+import { createDatabase } from "../src/db/client";
+import { auditEvents } from "../src/db/schema";
+import { TEST_POOL } from "./support/database";
+
+const databaseUrl =
+ process.env.DATABASE_URL ??
+ "postgres://openbot:openbot@localhost:5432/openbot";
+const database = createDatabase(databaseUrl, TEST_POOL);
+const store = createAuditStore(database);
+const reader = createAuditReader(database);
+
+// Nothing is cleaned up: the retention trigger refuses the delete, so each test takes its own target id.
+const target = () => `initiator-test-${crypto.randomUUID()}`;
+
+async function rowsFor(targetId: string, search = "") {
+ const { events } = await reader.list(
+ auditQueryFromUrl(new URL(`https://openbot.test/audit${search}`)),
+ );
+ return events.filter((event) => event.targetId === targetId);
+}
+
+describe("what started a run", () => {
+ test("a person's action is the default, and needs nothing passed", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "mcp.call_succeeded",
+ targetType: "mcp_tool",
+ targetId: TARGET,
+ payload: { bot: "general-assistant" },
+ });
+
+ const [row] = await rowsFor(TARGET);
+
+ expect(row?.initiatorKind).toBe("person");
+ expect(row?.initiatorId).toBeNull();
+ });
+
+ test("a routine's action names the routine, not only the owner", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "mcp.call_succeeded",
+ targetType: "mcp_tool",
+ targetId: TARGET,
+ actorUserId: null as unknown as undefined,
+ initiator: { kind: "routine", id: "routine-7" },
+ payload: { bot: "general-assistant" },
+ });
+
+ const [row] = await rowsFor(TARGET);
+
+ expect(row?.initiatorKind).toBe("routine");
+ expect(row?.initiatorId).toBe("routine-7");
+ });
+
+ test("a hop names the Bot that handed the work on", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "agent.handoff_delivered",
+ targetType: "agent",
+ targetId: TARGET,
+ initiator: { kind: "handoff", id: "research-assistant" },
+ payload: { bot: "general-assistant" },
+ });
+
+ const [row] = await rowsFor(TARGET);
+
+ expect(row?.initiatorKind).toBe("handoff");
+ expect(row?.initiatorId).toBe("research-assistant");
+ });
+
+ test("the deployment acting as itself is not filed as a person", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "computer.policy_loaded",
+ targetType: "policy",
+ targetId: TARGET,
+ initiator: { kind: "deployment" },
+ payload: { note: "read at start-up" },
+ });
+
+ const [row] = await rowsFor(TARGET);
+
+ expect(row?.initiatorKind).toBe("deployment");
+ expect(row?.initiatorId).toBeNull();
+ });
+
+ test("a boundary refusal is the deployment, and is not swept up by nobody watching", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "routines.dispatch_refused",
+ targetType: "worker",
+ targetId: TARGET,
+ initiator: { kind: "deployment" },
+ payload: { marker: "by-deployment" },
+ });
+ await store.insert({
+ eventType: "mcp.call_succeeded",
+ targetType: "mcp_tool",
+ targetId: TARGET,
+ initiator: { kind: "routine", id: "routine-7" },
+ payload: { marker: "by-routine" },
+ });
+
+ const unattended = await rowsFor(TARGET, "?initiatorKind=routine,handoff");
+ const deployment = await rowsFor(TARGET, "?initiatorKind=deployment");
+
+ expect(unattended.map((event) => event.payload.marker)).toEqual([
+ "by-routine",
+ ]);
+ expect(deployment.map((event) => event.payload.marker)).toEqual([
+ "by-deployment",
+ ]);
+ });
+
+ test("one filter answers what ran with nobody watching", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "mcp.call_succeeded",
+ targetType: "mcp_tool",
+ targetId: TARGET,
+ payload: { marker: "by-hand" },
+ });
+ await store.insert({
+ eventType: "mcp.call_succeeded",
+ targetType: "mcp_tool",
+ targetId: TARGET,
+ initiator: { kind: "routine", id: "routine-7" },
+ payload: { marker: "by-routine" },
+ });
+ await store.insert({
+ eventType: "agent.handoff_delivered",
+ targetType: "agent",
+ targetId: TARGET,
+ initiator: { kind: "handoff", id: "research-assistant" },
+ payload: { marker: "by-hop" },
+ });
+
+ const unattended = await rowsFor(TARGET, "?initiatorKind=routine,handoff");
+
+ expect(unattended.map((event) => event.payload.marker).sort()).toEqual([
+ "by-hop",
+ "by-routine",
+ ]);
+ });
+
+ test("one kind on its own narrows to that kind", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "mcp.call_succeeded",
+ targetType: "mcp_tool",
+ targetId: TARGET,
+ initiator: { kind: "routine", id: "routine-7" },
+ payload: { marker: "by-routine" },
+ });
+ await store.insert({
+ eventType: "agent.handoff_delivered",
+ targetType: "agent",
+ targetId: TARGET,
+ initiator: { kind: "handoff", id: "research-assistant" },
+ payload: { marker: "by-hop" },
+ });
+
+ const routines = await rowsFor(TARGET, "?initiatorKind=routine");
+
+ expect(routines.map((event) => event.payload.marker)).toEqual([
+ "by-routine",
+ ]);
+ });
+
+ test("a kind nothing writes is ignored rather than returning nothing", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "mcp.call_succeeded",
+ targetType: "mcp_tool",
+ targetId: TARGET,
+ payload: { marker: "by-hand" },
+ });
+
+ const rows = await rowsFor(TARGET, "?initiatorKind=nonsense");
+
+ expect(rows.map((event) => event.payload.marker)).toEqual(["by-hand"]);
+ });
+
+ test("the trail stays append-only, so a row cannot be re-attributed later", async () => {
+ const TARGET = target();
+ await store.insert({
+ eventType: "mcp.call_succeeded",
+ targetType: "mcp_tool",
+ targetId: TARGET,
+ initiator: { kind: "routine", id: "routine-7" },
+ payload: { bot: "general-assistant" },
+ });
+
+ const reattribute = async () => {
+ await database
+ .update(auditEvents)
+ .set({ initiatorKind: "person", initiatorId: null })
+ .where(eq(auditEvents.targetId, TARGET));
+ };
+
+ expect(reattribute()).rejects.toThrow();
+ });
+});
diff --git a/server/tests/plugin-store.integration.test.ts b/server/tests/plugin-store.integration.test.ts
index 89e2ef832..04edb7686 100644
--- a/server/tests/plugin-store.integration.test.ts
+++ b/server/tests/plugin-store.integration.test.ts
@@ -126,6 +126,8 @@ async function auditRowsFor(targetId: string) {
.select({
eventType: auditEvents.eventType,
payload: auditEvents.payload,
+ initiatorKind: auditEvents.initiatorKind,
+ initiatorId: auditEvents.initiatorId,
})
.from(auditEvents)
.where(
@@ -267,6 +269,49 @@ describe("a grant is the permission", () => {
);
});
+ test("a refusal names the routine that asked, not only the person it ran as", async () => {
+ await expect(
+ store.callTool({
+ ref,
+ args: {},
+ botId: strangerId,
+ actorId: "someone@openbot.local",
+ initiator: { kind: "routine", id: "routine_standup" },
+ }),
+ ).rejects.toBeInstanceOf(PluginRefusedError);
+
+ const rows = await auditRowsFor(ref);
+ const rejected = rows.filter(
+ (row) =>
+ row.eventType === "mcp.call_rejected" &&
+ (row.payload as { bot?: string }).bot === strangerId &&
+ row.initiatorKind === "routine",
+ );
+ expect(rejected.length).toBeGreaterThan(0);
+ expect(rejected[0].initiatorId).toBe("routine_standup");
+ });
+
+ test("a call nobody said anything about is still filed as a person's", async () => {
+ await expect(
+ store.callTool({
+ ref,
+ args: {},
+ botId: strangerId,
+ actorId: "someone@openbot.local",
+ }),
+ ).rejects.toBeInstanceOf(PluginRefusedError);
+
+ const rows = await auditRowsFor(ref);
+ expect(
+ rows.some(
+ (row) =>
+ row.eventType === "mcp.call_rejected" &&
+ row.initiatorKind === "person" &&
+ row.initiatorId === null,
+ ),
+ ).toBe(true);
+ });
+
test("granting lets the same Bot past the grant check", async () => {
await store.grant("mcp", ref, holderId, "admin@openbot.local");
const decision = await store.decide("mcp", ref, holderId);
diff --git a/server/tests/routine-run-turn.test.ts b/server/tests/routine-run-turn.test.ts
index 05ce5ca9b..07cdcde2e 100644
--- a/server/tests/routine-run-turn.test.ts
+++ b/server/tests/routine-run-turn.test.ts
@@ -24,6 +24,7 @@ import {
*/
const OWNER = "user_owner";
+const ROUTINE_ID = "routine_standup";
const AGENT_ID = "bot_helper";
const THREAD_ID = "thread_owner_channel_1";
const INSTRUCTION = "Post the standup summary.";
@@ -195,12 +196,16 @@ function harness(options: {
},
};
+ const builtFor: { initiator: { kind: string; id?: string } }[] = [];
const runTurn = createTurnRunner({
// biome-ignore lint/suspicious/noExplicitAny: narrow structural fakes, on purpose.
intelligence: intelligence as any,
// biome-ignore lint/suspicious/noExplicitAny: narrow structural fakes, on purpose.
runner: runner as any,
- buildAgentFor: async () => agent,
+ buildAgentFor: async (input) => {
+ builtFor.push(input);
+ return agent;
+ },
...(options.turnTimeoutMs === undefined
? {}
: { turnTimeoutMs: options.turnTimeoutMs }),
@@ -218,12 +223,13 @@ function harness(options: {
const run = () =>
runTurn({
ownerUserId: OWNER,
+ routineId: ROUTINE_ID,
agentId: AGENT_ID,
threadId: THREAD_ID,
instruction: INSTRUCTION,
});
- return { run, agent, calls, order };
+ return { run, agent, calls, order, builtFor };
}
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -880,3 +886,17 @@ describe("a RUN_ERROR through next", () => {
expect(calls.cleaned).toHaveLength(1);
});
});
+
+describe("what the trail is told started the turn", () => {
+ test("the Bot is built for the routine, not for the owner acting by hand", async () => {
+ const { run, builtFor } = harness({});
+
+ await run();
+
+ expect(builtFor).toHaveLength(1);
+ expect(builtFor[0]?.initiator).toEqual({
+ kind: "routine",
+ id: ROUTINE_ID,
+ });
+ });
+});
diff --git a/server/tests/routine-runner.test.ts b/server/tests/routine-runner.test.ts
index 2013419ab..731bf54ff 100644
--- a/server/tests/routine-runner.test.ts
+++ b/server/tests/routine-runner.test.ts
@@ -150,6 +150,8 @@ describe("createRoutineRunner", () => {
expect(recorded.turns).toEqual([
{
ownerUserId: CONTEXT.ownerUserId,
+ // Carried so the turn's audit rows say a routine ran this, not that the owner did.
+ routineId: CONTEXT.routineId,
agentId: CONTEXT.agentId,
threadId: CHANNEL.threadId,
instruction: CONTEXT.instruction,
diff --git a/server/tests/stall-guard.test.ts b/server/tests/stall-guard.test.ts
index 9148db18f..6c9f2d1a2 100644
--- a/server/tests/stall-guard.test.ts
+++ b/server/tests/stall-guard.test.ts
@@ -79,6 +79,24 @@ describe("a Bot that stops streaming", () => {
expect(body.endsWith("\n\n")).toBe(true);
});
+ test("the row says what started the run, so a routine's stall is not a person's", async () => {
+ const audit = collecting();
+ const guard = createStallGuard({ stallMs: 60, auditStore: audit.store });
+ const watched = guard.watch(
+ { ...BOT, initiator: { kind: "routine", id: "routine_7" } },
+ async () => sse(saysNothing()),
+ );
+
+ const response = await watched("http://bot.internal/ag-ui", RUN_REQUEST);
+ await new Response(response.body).text();
+ guard.stop();
+
+ const row = audit.rows.find(
+ (event) => event.eventType === "agent.stream_stalled",
+ );
+ expect(row?.initiator).toEqual({ kind: "routine", id: "routine_7" });
+ });
+
test("leaves a row naming the Bot, the turn and how long the silence was", async () => {
const audit = collecting();
const guard = createStallGuard({ stallMs: 60, auditStore: audit.store });
|