فهرست منبع

feat: Profile 可勾选启用的 Codex 插件(默认全部不启用,只开放所勾选插件的应用与技能)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang 6 روز پیش
والد
کامیت
8dbe65fdf5

+ 48 - 3
apps/admin/src/views/ProfilesView.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
-import type { KnowledgeRootDto, McpServerDto, PathRule, ProfileDto, ProfileInput, ProfileVersionDto, ProviderStatusDto, ResolvedAccessDto, SkillInfo } from '@agent-server/shared'
+import type { CodexPluginDto, KnowledgeRootDto, McpServerDto, PathRule, ProfileDto, ProfileInput, ProfileVersionDto, ProviderStatusDto, ResolvedAccessDto, SkillInfo } from '@agent-server/shared'
 import PageHeader from '../components/PageHeader.vue'
 import SidePane from '../components/SidePane.vue'
 import { admin, client, errorMessage } from '../lib/api'
@@ -22,6 +22,26 @@ function toggleMcp(id: string) {
   const cur = d.mcpServerIds ?? []
   d.mcpServerIds = cur.includes(id) ? cur.filter(x => x !== id) : [...cur, id]
 }
+// Codex 插件:只列出已安装的;经 API 的会话只开放勾选的插件(Playground 始终全部开放)
+const codexPlugins = ref<CodexPluginDto[]>([])
+const codexPluginsError = ref('')
+function toggleCodexPlugin(id: string) {
+  const d = draft.value
+  if (!d) return
+  const cur = d.codexPluginIds ?? []
+  d.codexPluginIds = cur.includes(id) ? cur.filter(x => x !== id) : [...cur, id]
+}
+const missingCodexPlugins = computed(() => (draft.value?.codexPluginIds ?? []).filter(id => !codexPlugins.value.some(p => p.id === id)))
+async function loadCodexPlugins() {
+  codexPluginsError.value = ''
+  try {
+    const r = await admin.codexPlugins.catalog({ installed: true })
+    codexPlugins.value = r.items
+    if (r.error) codexPluginsError.value = r.error
+  } catch (e) {
+    codexPluginsError.value = errorMessage(e)
+  }
+}
 const providers = ref<ProviderStatusDto[]>([])
 const draft = ref<ProfileInput | null>(null)
 const preview = ref<ResolvedAccessDto | null>(null)
@@ -36,7 +56,7 @@ const modelLabels = computed(() => providers.value.find(p => p.id === draft.valu
 const blank = (): ProfileInput => ({
   name: '', description: '', provider: 'claude', model: providers.value.find(p => p.id === 'claude')?.defaultModel ?? 'sonnet',
   systemPrompt: '', permissionMode: 'ask', allowedTools: [], disallowedTools: [], skills: 'all',
-  fileAccess: { mode: 'restricted', read: [], write: [], deny: [] }, knowledgeRootIds: [], mcpServerIds: [], isDefault: false,
+  fileAccess: { mode: 'restricted', read: [], write: [], deny: [] }, knowledgeRootIds: [], mcpServerIds: [], codexPluginIds: [], isDefault: false,
 })
 
 function toInput(p: ProfileDto): ProfileInput {
@@ -80,6 +100,7 @@ function onProviderChange() {
 
 // 技能列表随引擎切换
 watch(() => draft.value?.provider, async p => {
+  if (p === 'codex' && !codexPlugins.value.length) void loadCodexPlugins()
   if (p) skills.value = await admin.skills(p).catch(() => [])
 })
 
@@ -134,7 +155,7 @@ async function save() {
 // 历史版本
 const FIELD_LABELS: Record<string, string> = {
   name: '名称', description: '说明', provider: '引擎', model: '模型', systemPrompt: '系统提示词', permissionMode: '权限模式',
-  allowedTools: '允许的工具', disallowedTools: '禁用的工具', skills: '技能', fileAccess: '文件访问策略', knowledgeRootIds: '资料库',
+  allowedTools: '允许的工具', disallowedTools: '禁用的工具', skills: '技能', fileAccess: '文件访问策略', knowledgeRootIds: '资料库', mcpServerIds: '第三方 MCP 服务', codexPluginIds: 'Codex 插件',
 }
 const versions = ref<ProfileVersionDto[] | null>(null)
 async function openVersions() {
@@ -262,6 +283,30 @@ onMounted(() => load().catch(err => toast(errorMessage(err), 'error')))
             <div v-if="!mcpServers.length" class="hint">还没有第三方 MCP 服务</div>
           </section>
 
+          <section v-if="draft.provider === 'codex'" class="flex flex-col gap-2">
+            <div class="label m-0">Codex 插件</div>
+            <div class="hint">
+              插件带有应用连接器,使用你的 ChatGPT 账号授权的数据(如邮件、日历、网盘)。默认都不开放;勾选后,经 API 使用此 Profile 的会话可以调用这些插件,其他已安装的插件仍然不可用。
+              应用调用需要确认时会发出审批卡片。后台 Playground 中始终全部开放。插件在"技能与插件"页面安装。
+            </div>
+            <label v-for="p in codexPlugins" :key="p.id" class="flex items-start gap-2 cursor-pointer" :class="{ 'opacity-60': !p.enabled }">
+              <input type="checkbox" class="mt-1" :checked="(draft.codexPluginIds ?? []).includes(p.id)" @change="toggleCodexPlugin(p.id)">
+              <img v-if="p.logoUrl" :src="p.logoUrl" alt="" class="w-4 h-4 mt-0.5 rounded-sm flex-none">
+              <span class="min-w-0">
+                <span>{{ p.displayName }}</span>
+                <code class="font-mono text-[12px] text-fg-muted ml-1">{{ p.id }}</code>
+                <span v-if="!p.enabled" class="badge bg-code text-fg-muted ml-1">已停用</span>
+                <span v-if="p.description" class="block hint truncate">{{ p.description }}</span>
+              </span>
+            </label>
+            <label v-for="mid in missingCodexPlugins" :key="mid" class="flex items-start gap-2 cursor-pointer opacity-60">
+              <input type="checkbox" class="mt-1" checked @change="toggleCodexPlugin(mid)">
+              <span class="min-w-0"><code class="font-mono text-[12px]">{{ mid }}</code><span class="badge bg-code text-fg-muted ml-1">未安装,不会生效</span></span>
+            </label>
+            <div v-if="codexPluginsError" class="hint text-error">读取插件列表失败:{{ codexPluginsError }}</div>
+            <div v-else-if="!codexPlugins.length && !missingCodexPlugins.length" class="hint">还没有安装 Codex 插件</div>
+          </section>
+
           <section class="grid grid-cols-2 gap-4">
             <div>
               <label class="label" for="p-allow">仅允许这些工具(留空不限制)</label>

+ 1 - 0
apps/server/drizzle/0013_profile_codex_plugins.sql

@@ -0,0 +1 @@
+ALTER TABLE `profiles` ADD `codex_plugin_ids` text DEFAULT '[]' NOT NULL;

+ 2740 - 0
apps/server/drizzle/meta/0013_snapshot.json

@@ -0,0 +1,2740 @@
+{
+  "version": "6",
+  "dialect": "sqlite",
+  "id": "658fbfb0-0f4e-4af7-8342-281a25f14477",
+  "prevId": "ab94bbd8-de0b-41e5-a1f9-97bd1752c3e1",
+  "tables": {
+    "admin_sessions": {
+      "name": "admin_sessions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "api_keys": {
+      "name": "api_keys",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_hash": {
+          "name": "key_hash",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_prefix": {
+          "name": "key_prefix",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "allowed_profile_ids": {
+          "name": "allowed_profile_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "default_profile_id": {
+          "name": "default_profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "last_used_at": {
+          "name": "last_used_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "api_keys_key_hash_unique": {
+          "name": "api_keys_key_hash_unique",
+          "columns": [
+            "key_hash"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "audit_logs": {
+      "name": "audit_logs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "integer",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": true
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "actor": {
+          "name": "actor",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "summary": {
+          "name": "summary",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "detail": {
+          "name": "detail",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "audit_ts_idx": {
+          "name": "audit_ts_idx",
+          "columns": [
+            "ts"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "background_calls": {
+      "name": "background_calls",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "task_id": {
+          "name": "task_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "server_id": {
+          "name": "server_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "server_name": {
+          "name": "server_name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "server_title": {
+          "name": "server_title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "tool": {
+          "name": "tool",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "args": {
+          "name": "args",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'running'"
+        },
+        "started_at": {
+          "name": "started_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "result": {
+          "name": "result",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "error": {
+          "name": "error",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "delivered": {
+          "name": "delivered",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        }
+      },
+      "indexes": {
+        "background_calls_session_idx": {
+          "name": "background_calls_session_idx",
+          "columns": [
+            "session_id"
+          ],
+          "isUnique": false
+        },
+        "background_calls_team_idx": {
+          "name": "background_calls_team_idx",
+          "columns": [
+            "team_id"
+          ],
+          "isUnique": false
+        },
+        "background_calls_status_idx": {
+          "name": "background_calls_status_idx",
+          "columns": [
+            "status",
+            "delivered"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "bot_api_keys": {
+      "name": "bot_api_keys",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_hash": {
+          "name": "key_hash",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_prefix": {
+          "name": "key_prefix",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "allowed_bot_ids": {
+          "name": "allowed_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "last_used_at": {
+          "name": "last_used_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "bot_api_keys_key_hash_unique": {
+          "name": "bot_api_keys_key_hash_unique",
+          "columns": [
+            "key_hash"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "bots": {
+      "name": "bots",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "avatar": {
+          "name": "avatar",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'🤖'"
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "bots_name_unique": {
+          "name": "bots_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "custom_skill_versions": {
+      "name": "custom_skill_versions",
+      "columns": {
+        "skill_id": {
+          "name": "skill_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "reason": {
+          "name": "reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "changed": {
+          "name": "changed",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "custom_skill_versions_skill_id_version_pk": {
+          "columns": [
+            "skill_id",
+            "version"
+          ],
+          "name": "custom_skill_versions_skill_id_version_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "custom_skills": {
+      "name": "custom_skills",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "origin": {
+          "name": "origin",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source_id": {
+          "name": "source_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "include_in_all": {
+          "name": "include_in_all",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "published_version": {
+          "name": "published_version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "latest_version": {
+          "name": "latest_version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "working_files": {
+          "name": "working_files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "custom_skills_name_unique": {
+          "name": "custom_skills_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "events": {
+      "name": "events",
+      "columns": {
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "seq": {
+          "name": "seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "type": {
+          "name": "type",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "payload": {
+          "name": "payload",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "events_session_id_seq_pk": {
+          "columns": [
+            "session_id",
+            "seq"
+          ],
+          "name": "events_session_id_seq_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "files": {
+      "name": "files",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "rel_path": {
+          "name": "rel_path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "origin": {
+          "name": "origin",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mime": {
+          "name": "mime",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "size": {
+          "name": "size",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mtime_ms": {
+          "name": "mtime_ms",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "files_session_path_idx": {
+          "name": "files_session_path_idx",
+          "columns": [
+            "session_id",
+            "rel_path"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "interactions": {
+      "name": "interactions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "request": {
+          "name": "request",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "response": {
+          "name": "response",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "resolved_at": {
+          "name": "resolved_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "interactions_session_idx": {
+          "name": "interactions_session_idx",
+          "columns": [
+            "session_id",
+            "status"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "jobs": {
+      "name": "jobs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "no": {
+          "name": "no",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "plan": {
+          "name": "plan",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'running'"
+        },
+        "summary": {
+          "name": "summary",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "blocked_by": {
+          "name": "blocked_by",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "lead_runs": {
+          "name": "lead_runs",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "run_ids": {
+          "name": "run_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "jobs_team_idx": {
+          "name": "jobs_team_idx",
+          "columns": [
+            "team_id",
+            "no"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "knowledge_roots": {
+      "name": "knowledge_roots",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "path": {
+          "name": "path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "access": {
+          "name": "access",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'read'"
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "mcp_servers": {
+      "name": "mcp_servers",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "transport": {
+          "name": "transport",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "url": {
+          "name": "url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "headers": {
+          "name": "headers",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "command": {
+          "name": "command",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "args": {
+          "name": "args",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "env": {
+          "name": "env",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "allow_network": {
+          "name": "allow_network",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "read_paths": {
+          "name": "read_paths",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "write_paths": {
+          "name": "write_paths",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "tool_policies": {
+          "name": "tool_policies",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "background_tools": {
+          "name": "background_tools",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "tools_cache": {
+          "name": "tools_cache",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "tools_updated_at": {
+          "name": "tools_updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "mcp_servers_name_unique": {
+          "name": "mcp_servers_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "profile_versions": {
+      "name": "profile_versions",
+      "columns": {
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "snapshot": {
+          "name": "snapshot",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "profile_versions_profile_id_version_pk": {
+          "columns": [
+            "profile_id",
+            "version"
+          ],
+          "name": "profile_versions_profile_id_version_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "profiles": {
+      "name": "profiles",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "system_prompt": {
+          "name": "system_prompt",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "permission_mode": {
+          "name": "permission_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'ask'"
+        },
+        "allowed_tools": {
+          "name": "allowed_tools",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "disallowed_tools": {
+          "name": "disallowed_tools",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "skills": {
+          "name": "skills",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'\"all\"'"
+        },
+        "file_access": {
+          "name": "file_access",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "knowledge_root_ids": {
+          "name": "knowledge_root_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "mcp_server_ids": {
+          "name": "mcp_server_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "codex_plugin_ids": {
+          "name": "codex_plugin_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "is_default": {
+          "name": "is_default",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "builtin": {
+          "name": "builtin",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "builtin_key": {
+          "name": "builtin_key",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 1
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "runs": {
+      "name": "runs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "started_at": {
+          "name": "started_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_tokens": {
+          "name": "cache_write_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "context_tokens": {
+          "name": "context_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "error": {
+          "name": "error",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "runs_session_idx": {
+          "name": "runs_session_idx",
+          "columns": [
+            "session_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "schedule_runs": {
+      "name": "schedule_runs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "integer",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": true
+        },
+        "schedule_id": {
+          "name": "schedule_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "outcome": {
+          "name": "outcome",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "note": {
+          "name": "note",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "schedule_runs_idx": {
+          "name": "schedule_runs_idx",
+          "columns": [
+            "schedule_id",
+            "id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "schedules": {
+      "name": "schedules",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_by_bot_id": {
+          "name": "created_by_bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "prompt": {
+          "name": "prompt",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "spec": {
+          "name": "spec",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "timezone": {
+          "name": "timezone",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'active'"
+        },
+        "status_note": {
+          "name": "status_note",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "next_run_at": {
+          "name": "next_run_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "last_run_at": {
+          "name": "last_run_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "last_outcome": {
+          "name": "last_outcome",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "run_count": {
+          "name": "run_count",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "schedules_due_idx": {
+          "name": "schedules_due_idx",
+          "columns": [
+            "status",
+            "next_run_at"
+          ],
+          "isUnique": false
+        },
+        "schedules_session_idx": {
+          "name": "schedules_session_idx",
+          "columns": [
+            "session_id"
+          ],
+          "isUnique": false
+        },
+        "schedules_team_idx": {
+          "name": "schedules_team_idx",
+          "columns": [
+            "team_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "sessions": {
+      "name": "sessions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "permission_mode": {
+          "name": "permission_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "system_prompt_append": {
+          "name": "system_prompt_append",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "provider_session_id": {
+          "name": "provider_session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "workspace_dir": {
+          "name": "workspace_dir",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "last_seq": {
+          "name": "last_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "archived": {
+          "name": "archived",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "sessions_source_idx": {
+          "name": "sessions_source_idx",
+          "columns": [
+            "source",
+            "updated_at"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "settings": {
+      "name": "settings",
+      "columns": {
+        "key": {
+          "name": "key",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "value": {
+          "name": "value",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "skill_sources": {
+      "name": "skill_sources",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "path": {
+          "name": "path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "sync_mode": {
+          "name": "sync_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'manual'"
+        },
+        "interval_min": {
+          "name": "interval_min",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 10
+        },
+        "last_sync_at": {
+          "name": "last_sync_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "last_sync_digest": {
+          "name": "last_sync_digest",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'never'"
+        },
+        "last_error": {
+          "name": "last_error",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "tasks": {
+      "name": "tasks",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "job_id": {
+          "name": "job_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "no": {
+          "name": "no",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "instructions": {
+          "name": "instructions",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "depends_on": {
+          "name": "depends_on",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "follow_up": {
+          "name": "follow_up",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "result": {
+          "name": "result",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "started_at": {
+          "name": "started_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "tasks_team_idx": {
+          "name": "tasks_team_idx",
+          "columns": [
+            "team_id",
+            "no"
+          ],
+          "isUnique": false
+        },
+        "tasks_job_idx": {
+          "name": "tasks_job_idx",
+          "columns": [
+            "job_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_bot_sessions": {
+      "name": "team_bot_sessions",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "last_read_seq": {
+          "name": "last_read_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "reset_requested": {
+          "name": "reset_requested",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        }
+      },
+      "indexes": {
+        "team_bot_sessions_session_id_unique": {
+          "name": "team_bot_sessions_session_id_unique",
+          "columns": [
+            "session_id"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_bot_sessions_team_id_bot_id_pk": {
+          "columns": [
+            "team_id",
+            "bot_id"
+          ],
+          "name": "team_bot_sessions_team_id_bot_id_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_bots": {
+      "name": "team_bots",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "role": {
+          "name": "role",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "joined_at": {
+          "name": "joined_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "team_bots_bot_idx": {
+          "name": "team_bots_bot_idx",
+          "columns": [
+            "bot_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_bots_team_id_bot_id_pk": {
+          "columns": [
+            "team_id",
+            "bot_id"
+          ],
+          "name": "team_bots_team_id_bot_id_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_cases": {
+      "name": "team_cases",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "lead_bot_id": {
+          "name": "lead_bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "worker_bot_ids": {
+          "name": "worker_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "stub_bot_ids": {
+          "name": "stub_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "human_name": {
+          "name": "human_name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "text": {
+          "name": "text",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mentions": {
+          "name": "mentions",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_events": {
+      "name": "team_events",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "seq": {
+          "name": "seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "type": {
+          "name": "type",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "payload": {
+          "name": "payload",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_events_team_id_seq_pk": {
+          "columns": [
+            "team_id",
+            "seq"
+          ],
+          "name": "team_events_team_id_seq_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "teams": {
+      "name": "teams",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "human": {
+          "name": "human",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'active'"
+        },
+        "workspace_dir": {
+          "name": "workspace_dir",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "debug": {
+          "name": "debug",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{\"stepMode\":false,\"stubBotIds\":[]}'"
+        },
+        "last_seq": {
+          "name": "last_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "case_id": {
+          "name": "case_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "teams_source_idx": {
+          "name": "teams_source_idx",
+          "columns": [
+            "source",
+            "updated_at"
+          ],
+          "isUnique": false
+        },
+        "teams_case_idx": {
+          "name": "teams_case_idx",
+          "columns": [
+            "case_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "usage_daily": {
+      "name": "usage_daily",
+      "columns": {
+        "date": {
+          "name": "date",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "runs": {
+          "name": "runs",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "failed": {
+          "name": "failed",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "interrupted": {
+          "name": "interrupted",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cache_write_tokens": {
+          "name": "cache_write_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "usage_daily_date_source_provider_model_pk": {
+          "columns": [
+            "date",
+            "source",
+            "provider",
+            "model"
+          ],
+          "name": "usage_daily_date_source_provider_model_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    }
+  },
+  "views": {},
+  "enums": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "indexes": {}
+  }
+}

+ 7 - 0
apps/server/drizzle/meta/_journal.json

@@ -92,6 +92,13 @@
       "when": 1793091344696,
       "tag": "0012_background_calls",
       "breakpoints": true
+    },
+    {
+      "idx": 13,
+      "version": "6",
+      "when": 1793160347080,
+      "tag": "0013_profile_codex_plugins",
+      "breakpoints": true
     }
   ]
 }

+ 1 - 1
apps/server/scripts/try-claude-adapter.ts

@@ -16,7 +16,7 @@ const ac = new AbortController()
 const res = await new ClaudeProvider().run({
   sessionId: 'ses_test', runId: 'run_test', model: 'haiku', permissionMode: mode as PermissionMode,
   providerSessionId: null, workspaceDir: ws, systemPromptAppend: '', allowedTools: [], disallowedTools: [], skills: 'all',
-  plugins: [], codexPlugins: false, skillsExcludedFromAll: [], gateway: { url: 'http://127.0.0.1:8788/mcp', token: 'none' }, input: { text: prompt, images: [], attachments: [] }, access, settings, signal: ac.signal,
+  plugins: [], codexPlugins: { pluginIds: [], appIds: [] }, skillsExcludedFromAll: [], gateway: { url: 'http://127.0.0.1:8788/mcp', token: 'none' }, input: { text: prompt, images: [], attachments: [] }, access, settings, signal: ac.signal,
   emit: (type, payload) => {
     if (type === 'message.delta' || type === 'reasoning.delta') return
     console.log(`[event] ${type}`, JSON.stringify(payload).slice(0, Number(process.env.EVENT_MAX ?? 220)))

+ 1 - 1
apps/server/src/app.ts

@@ -83,7 +83,7 @@ export function createApp(log: AppLogger, opts: { db?: DB; providers?: Partial<R
     summary => audit.log({ actor: 'admin', kind: 'admin', summary }),
   )
   sessions.extraModels = p => providers[p]?.knownModels?.() ?? []
-  const runs = new RunManager({ db, log, settings, sessions, profiles, knowledge, files, events, interactions, audit, usage, plugins, communitySkills, mcpServers, customSkills, gatewayTokens, providers })
+  const runs = new RunManager({ db, log, settings, sessions, profiles, knowledge, files, events, interactions, audit, usage, plugins, communitySkills, mcpServers, customSkills, gatewayTokens, providers, codexPlugins })
   const bots = new BotService(db, profiles)
   ensureLeadProfiles({ profiles, customSkills, bots, settings })
   const botKeys = new BotKeyService(db, bots)

+ 1 - 0
apps/server/src/db/schema.ts

@@ -36,6 +36,7 @@ export const profiles = sqliteTable('profiles', {
   fileAccess: text('file_access', { mode: 'json' }).$type<FileAccessPolicy>().notNull(),
   knowledgeRootIds: text('knowledge_root_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
   mcpServerIds: text('mcp_server_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
+  codexPluginIds: text('codex_plugin_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
   isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
   /** 内置 Profile(通用助手、组长)不可删除 */
   builtin: integer('builtin', { mode: 'boolean' }).notNull().default(false),

+ 4 - 4
apps/server/src/providers/codex/adapter.ts

@@ -5,7 +5,7 @@ import path from 'node:path'
 import type { AppSettings, ProviderQuotaDto, ProviderStatusDto, QuotaWindowDto, UsagePayload } from '@agent-server/shared'
 import { communitySkillsDir, customSkillsDir } from '../../config'
 import { checkAccess } from '../../services/access-policy'
-import { ProviderRunError, type AgentProvider, type RunContext, type RunResult } from '../types'
+import { codexPluginsOpen, ProviderRunError, type AgentProvider, type RunContext, type RunResult } from '../types'
 import { bundledCodexPath, codexHome } from './cli'
 import { GATEWAY_SERVER, gatewayToolTimeoutSec } from '../../mcp-gateway/server'
 import { buildThreadConfig } from './permissions'
@@ -105,7 +105,7 @@ export class CodexProvider implements AgentProvider {
     fs.mkdirSync(path.join(ctx.workspaceDir, '.agent', 'tmp'), { recursive: true })
     const config = buildThreadConfig({
       access: ctx.access, permissionMode: ctx.permissionMode, settings: ctx.settings,
-      codexBin: this.bin(ctx.settings), codexHome: codexHome(), allowPlugins: ctx.codexPlugins,
+      codexBin: this.bin(ctx.settings), codexHome: codexHome(), plugins: ctx.codexPlugins,
     })
     // 按 Profile 的技能选择禁用其余技能(只作用于本次对话)
     const skills = await this.listSkills(ctx.settings, ctx.workspaceDir).catch(() => [])
@@ -531,12 +531,12 @@ class ThreadRun {
         return { answers }
       }
       case 'mcpServer/elicitation/request': {
-        // 应用连接器的调用确认以 elicitation 形式到达:只在开放插件的会话(Playground)中,
+        // 应用连接器的调用确认以 elicitation 形式到达:只在开放了插件的会话中,
         // 且不需要填写内容时转成审批卡片;需要填表或跳转网址的请求一律拒绝
         const p = params as McpServerElicitationRequestParams
         const schema = 'requestedSchema' in p ? p.requestedSchema as { properties?: Record<string, unknown> } | null : null
         const needsInput = p.mode === 'url' || Object.keys(schema?.properties ?? {}).length > 0
-        if (!ctx.codexPlugins || p.serverName === GATEWAY_SERVER || needsInput) {
+        if (!codexPluginsOpen(ctx.codexPlugins) || p.serverName === GATEWAY_SERVER || needsInput) {
           ctx.audit('approval', `拒绝 MCP 请求(${p.serverName},${p.mode}):${p.message}`.slice(0, 500))
           return { action: 'decline', content: null, _meta: null }
         }

+ 17 - 5
apps/server/src/providers/codex/permissions.ts

@@ -6,6 +6,7 @@ import path from 'node:path'
 import type { AppSettings, PermissionMode } from '@agent-server/shared'
 import type { ResolvedAccess } from '../../services/access-policy'
 import { hasGlob, isWithin, realpathLoose } from '../../lib/paths'
+import { codexPluginsOpen, NO_CODEX_PLUGINS, type CodexPluginGrant } from '../types'
 
 export const PROFILE_NAME = 'agent_server'
 
@@ -44,10 +45,11 @@ export function buildThreadConfig(opts: {
   settings: AppSettings
   codexBin: string
   codexHome: string
-  /** Playground 中开放远程插件与应用连接器(插件安装建议、子代理等仍然关闭) */
-  allowPlugins?: boolean
+  /** 开放的远程插件与应用连接器(插件安装建议、子代理等始终关闭) */
+  plugins?: CodexPluginGrant
 }): CodexThreadConfig {
   const { access } = opts
+  const plugins = opts.plugins ?? NO_CODEX_PLUGINS
   const fs: FsTable = {
     ':root': access.mode === 'broad' ? 'read' : 'deny',
     ':minimal': 'read',
@@ -89,12 +91,11 @@ export function buildThreadConfig(opts: {
   const py = opts.settings.paths.pythonRuntime
   const agentDir = path.join(access.workspaceDir, '.agent')
   return {
-    features: opts.allowPlugins ? { ...DISABLED_FEATURES, plugins: true, remote_plugin: true, apps: true } : { ...DISABLED_FEATURES },
+    features: codexPluginsOpen(plugins) ? { ...DISABLED_FEATURES, plugins: true, remote_plugin: true, apps: true } : { ...DISABLED_FEATURES },
     // 关闭子代理(collaboration.spawn_agent 等)。已实测(2026-10-23,codex 0.164):只设 features.multi_agent = false 时
     // 模型仍能创建并等待子代理;加上 agents.enabled = false 后工具消失。Playground 同样关闭
     agents: { enabled: false },
-    // 未开放插件时,即使有应用被启用也不暴露连接器工具
-    apps: opts.allowPlugins ? { _default: { enabled: true } } : { _default: { enabled: false, destructive_enabled: false, open_world_enabled: false } },
+    apps: appsConfig(plugins),
     default_permissions: PROFILE_NAME,
     permissions: { [PROFILE_NAME]: { filesystem: fs, network: { enabled: false } } },
     // 只继承基础环境变量(PATH、HOME 等),不把服务进程中的密钥带进命令
@@ -134,3 +135,14 @@ export function denyAround(dir: string, keep: string[]): string[] {
   }
   return out
 }
+
+/**
+ * 应用连接器:全部开放时默认启用;只开放部分插件时默认关闭,逐个启用这些插件的应用
+ * (已实测 2026-10-28,codex 0.164:只启用 Google Calendar 时模型发现不了 Gmail 等其他已授权应用的工具)
+ */
+function appsConfig(plugins: CodexPluginGrant): Record<string, unknown> {
+  if (plugins === 'all') return { _default: { enabled: true } }
+  const apps: Record<string, unknown> = { _default: { enabled: false, destructive_enabled: false, open_world_enabled: false } }
+  for (const id of plugins.appIds) apps[id] = { enabled: true }
+  return apps
+}

+ 4 - 3
apps/server/src/providers/codex/skills.ts

@@ -3,6 +3,7 @@ import fs from 'node:fs'
 import path from 'node:path'
 import { COMMUNITY_SKILL_PLUGIN, CUSTOM_SKILL_PLUGIN } from '@agent-server/shared'
 import type { SkillMetadata } from './protocol/v2'
+import { NO_CODEX_PLUGINS, type CodexPluginGrant } from '../types'
 
 /** 服务场景下始终禁用的系统技能:会联网下载或写入 Codex 自身目录 */
 export const ALWAYS_DISABLED = new Set(['skill-installer', 'skill-creator', 'plugin-creator'])
@@ -29,17 +30,17 @@ export function skillDisplayId(s: Pick<SkillMetadata, 'name' | 'scope' | 'path'>
 
 /**
  * 计算需要在本次对话中禁用的技能(按路径选择);选择"全部技能"时第三方技能仍需单独勾选。
- * 远程插件带来的技能(pluginId 非空)不由 Profile 选择:开放插件时(Playground)全部保留,否则全部禁用
+ * 远程插件带来的技能(pluginId 非空)跟随插件授权:全部开放时(Playground)全部保留,否则只保留开放的插件的技能
  */
 export function skillsToDisable(
-  all: SkillMetadata[], selected: string[] | 'all', codexHome: string, localPlugin: string, allowPlugins = false, excludedFromAll: string[] = [],
+  all: SkillMetadata[], selected: string[] | 'all', codexHome: string, localPlugin: string, plugins: CodexPluginGrant = NO_CODEX_PLUGINS, excludedFromAll: string[] = [],
 ): { path: string; enabled: false }[] {
   const excluded = new Set(excludedFromAll)
   const wanted = selected === 'all' ? null : new Set(selected.map(s => (s.includes(':') ? s : `${localPlugin}:${s}`)))
   return all
     .filter(s => {
       if (ALWAYS_DISABLED.has(s.name)) return true
-      if (s.pluginId) return !allowPlugins
+      if (s.pluginId) return plugins !== 'all' && !plugins.pluginIds.includes(s.pluginId)
       const id = skillDisplayId(s, codexHome)
       return wanted === null ? id.startsWith(`${COMMUNITY_SKILL_PLUGIN}:`) || excluded.has(id) : !wanted.has(id)
     })

+ 10 - 2
apps/server/src/providers/types.ts

@@ -34,8 +34,8 @@ export interface RunContext {
   interact(req: NewApproval | NewQuestion): Promise<InteractionOutcome>
   setProviderSessionId(id: string): void
   audit(kind: 'command' | 'file_access_denied' | 'network_denied' | 'approval', summary: string, detail?: unknown): void
-  /** 允许 Codex 的远程插件与应用连接器(只在后台 Playground 的会话中开启,ADR-0018) */
-  codexPlugins: boolean
+  /** 开放哪些 Codex 远程插件与应用连接器:Playground 全部开放;其他会话只开放 Profile 勾选的插件(ADR-0018) */
+  codexPlugins: CodexPluginGrant
   /** MCP Gateway 地址与本次 Run 的令牌(两个引擎共用的工具) */
   gateway: { url: string; token: string }
   log: { info(obj: object, msg?: string): void; warn(obj: object, msg?: string): void; error(obj: object, msg?: string): void }
@@ -60,3 +60,11 @@ export class ProviderRunError extends Error {
     super(message)
   }
 }
+
+/**
+ * Codex 插件授权:'all' 为全部开放(后台 Playground);否则只开放列出的插件(插件 ID)及其应用连接器(应用 ID),
+ * 两个列表都为空即关闭
+ */
+export type CodexPluginGrant = 'all' | { pluginIds: string[]; appIds: string[] }
+export const NO_CODEX_PLUGINS: CodexPluginGrant = { pluginIds: [], appIds: [] }
+export const codexPluginsOpen = (g: CodexPluginGrant) => g === 'all' || g.pluginIds.length > 0

+ 7 - 4
apps/server/src/services/catalog.ts

@@ -33,20 +33,21 @@ export const profileInputSchema: z.ZodType<ProfileInput> = z.object({
   fileAccess: fileAccessSchema,
   knowledgeRootIds: z.array(z.string()),
   mcpServerIds: z.array(z.string()).default([]),
+  codexPluginIds: z.array(z.string().regex(/^[\w.@-]{1,200}$/, '插件 ID 格式不正确')).max(100).default([]),
   isDefault: z.boolean(),
 })
 
 const toProfileDto = (r: ProfileRow): ProfileDto => ({
   id: r.id, name: r.name, description: r.description, provider: r.provider, model: r.model, systemPrompt: r.systemPrompt,
   permissionMode: r.permissionMode, allowedTools: r.allowedTools, disallowedTools: r.disallowedTools, skills: r.skills,
-  fileAccess: r.fileAccess, knowledgeRootIds: r.knowledgeRootIds, mcpServerIds: r.mcpServerIds, isDefault: r.isDefault, builtin: r.builtin, builtinKey: r.builtinKey ?? null, version: r.version, updatedAt: r.updatedAt,
+  fileAccess: r.fileAccess, knowledgeRootIds: r.knowledgeRootIds, mcpServerIds: r.mcpServerIds, codexPluginIds: r.codexPluginIds, isDefault: r.isDefault, builtin: r.builtin, builtinKey: r.builtinKey ?? null, version: r.version, updatedAt: r.updatedAt,
 })
 
-const SNAPSHOT_KEYS = ['name', 'description', 'provider', 'model', 'systemPrompt', 'permissionMode', 'allowedTools', 'disallowedTools', 'skills', 'fileAccess', 'knowledgeRootIds', 'mcpServerIds'] as const satisfies readonly (keyof ProfileSnapshot)[]
+const SNAPSHOT_KEYS = ['name', 'description', 'provider', 'model', 'systemPrompt', 'permissionMode', 'allowedTools', 'disallowedTools', 'skills', 'fileAccess', 'knowledgeRootIds', 'mcpServerIds', 'codexPluginIds'] as const satisfies readonly (keyof ProfileSnapshot)[]
 
 function snapshotOf(p: ProfileSnapshot): ProfileSnapshot {
-  // 早期版本的快照没有 mcpServerIds,按空列表比较
-  return Object.fromEntries(SNAPSHOT_KEYS.map(k => [k, k === 'mcpServerIds' ? p[k] ?? [] : p[k]])) as ProfileSnapshot
+  // 早期版本的快照没有 mcpServerIds、codexPluginIds,按空列表比较
+  return Object.fromEntries(SNAPSHOT_KEYS.map(k => [k, k === 'mcpServerIds' || k === 'codexPluginIds' ? p[k] ?? [] : p[k]])) as ProfileSnapshot
 }
 
 function changedFields(a: ProfileSnapshot, b: ProfileSnapshot): (keyof ProfileSnapshot)[] {
@@ -118,6 +119,7 @@ export class ProfileService {
 
   create(input: ProfileInput, builtin = false, builtinKey: 'lead' | null = null): ProfileDto {
     const data = parse(profileInputSchema, input)
+    if (data.provider !== 'codex') data.codexPluginIds = []
     this.checkMcp(data.mcpServerIds)
     const id = newId('prf')
     this.db.transaction(tx => {
@@ -132,6 +134,7 @@ export class ProfileService {
   update(id: string, input: ProfileInput): ProfileDto {
     const prev = this.get(id)
     const data = parse(profileInputSchema, input)
+    if (data.provider !== 'codex') data.codexPluginIds = []
     this.checkMcp(data.mcpServerIds)
     if (prev.isDefault && !data.isDefault) throw badRequest('请先把另一个 Profile 设为默认')
     if (prev.builtin && data.provider !== prev.provider) throw badRequest('内置 Profile 的引擎不能修改')

+ 21 - 0
apps/server/src/services/codex-plugins.ts

@@ -7,6 +7,7 @@ import type {
   PluginInstallResponse, PluginInstalledResponse, PluginListResponse, PluginMarketplaceEntry, PluginReadResponse, PluginSummary,
 } from '../providers/codex/protocol/v2'
 import { RpcRequestError } from '../providers/codex/rpc'
+import { NO_CODEX_PLUGINS, type CodexPluginGrant } from '../providers/types'
 
 const CACHE_MS = 10 * 60_000
 const PAGE_MAX = 100
@@ -20,6 +21,8 @@ export interface CodexRpc {
 
 export class CodexPluginService {
   private cache: { at: number; items: CodexPluginDto[] } | null = null
+  /** 插件 ID → 它带的应用连接器 ID */
+  private appCache = new Map<string, { at: number; ids: string[] }>()
 
   constructor(
     private codex: () => CodexRpc | null,
@@ -105,6 +108,24 @@ export class CodexPluginService {
     return { ok: true }
   }
 
+  /** Profile 勾选的插件 → 本次对话开放的插件与应用连接器(已卸载的插件忽略) */
+  async grant(pluginIds: string[]): Promise<CodexPluginGrant> {
+    if (!pluginIds.length) return NO_CODEX_PLUGINS
+    const chosen = (await this.load(false)).filter(p => p.installed && pluginIds.includes(p.id))
+    const appIds = new Set<string>()
+    for (const p of chosen) for (const id of await this.appsOf(p)) appIds.add(id)
+    return { pluginIds: chosen.map(p => p.id), appIds: [...appIds] }
+  }
+
+  private async appsOf(p: CodexPluginDto): Promise<string[]> {
+    const hit = this.appCache.get(p.id)
+    if (hit && Date.now() - hit.at < CACHE_MS) return hit.ids
+    const r = await this.call<PluginReadResponse>('plugin/read', { remoteMarketplaceName: p.marketplace, pluginName: p.name })
+    const ids = r.plugin.apps.map(a => a.id)
+    this.appCache.set(p.id, { at: Date.now(), ids })
+    return ids
+  }
+
   private async find(name: string): Promise<CodexPluginDto> {
     if (!/^[\w.-]{1,120}$/.test(name)) throw badRequest('插件名称格式不正确')
     const p = (await this.load(false)).find(x => x.name === name)

+ 9 - 3
apps/server/src/services/runs.ts

@@ -11,7 +11,7 @@ import { runs, sessions } from '../db/schema'
 import { badRequest, conflict, notFound } from '../lib/errors'
 import { newId, nowIso } from '../lib/ids'
 import type { AgentProvider, RunInput } from '../providers/types'
-import { ProviderRunError } from '../providers/types'
+import { NO_CODEX_PLUGINS, ProviderRunError } from '../providers/types'
 import { resolveAccess } from './access-policy'
 import { parse, type AuditService, type KnowledgeService, type ProfileService } from './catalog'
 import type { EventStore } from './event-store'
@@ -22,6 +22,7 @@ import type { McpServerService } from './mcp-servers'
 import type { CustomSkillService } from '../custom-skills/service'
 import type { McpServerRow } from '../mcp-gateway/upstream'
 import type { PluginService } from './plugins'
+import type { CodexPluginService } from './codex-plugins'
 import type { UsageService } from './usage'
 import type { RunTokenRegistry } from '../mcp-gateway/tokens'
 import type { BuiltinTool } from '../mcp-gateway/builtin'
@@ -79,6 +80,8 @@ export class RunManager {
       customSkills: CustomSkillService
       gatewayTokens: RunTokenRegistry
       providers: Partial<Record<ProviderId, AgentProvider>>
+      /** 按 Profile 勾选的 Codex 插件计算开放范围(未提供时,Playground 以外的会话不开放插件) */
+      codexPlugins?: Pick<CodexPluginService, 'grant'>
     },
   ) {
     const c = deps.settings.get().limits.concurrency
@@ -236,8 +239,11 @@ export class RunManager {
         // 插件只在 Claude 中加载;专有许可的插件不能提供给其他引擎(ADR-0013)。第三方技能 Codex 经 extraRoots 加载(ADR-0017)
         plugins: session.provider === 'claude' ? await this.claudePlugins() : [],
         skillsExcludedFromAll: deps.customSkills.excludedFromAll(),
-        // Codex 插件与应用连接器使用账号绑定的数据,只在后台 Playground(source = admin)中开放
-        codexPlugins: session.provider === 'codex' && session.source === 'admin',
+        // Codex 插件与应用连接器使用账号绑定的数据:后台 Playground(source = admin)中全部开放,
+        // 其他会话只开放 Profile 勾选的插件(默认不勾选)
+        codexPlugins: session.provider !== 'codex' ? NO_CODEX_PLUGINS
+          : session.source === 'admin' ? 'all'
+          : await deps.codexPlugins?.grant(profile.codexPluginIds) ?? NO_CODEX_PLUGINS,
         input: await this.buildInput(session, text, attachments),
         access,
         settings,

+ 24 - 4
apps/server/test/codex.test.ts

@@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'
 import type { AgentEventType, EventPayloads } from '@agent-server/shared'
 import { config } from '../src/config'
 import { CodexProvider } from '../src/providers/codex/adapter'
-import type { RunContext } from '../src/providers/types'
+import { NO_CODEX_PLUGINS, type CodexPluginGrant, type RunContext } from '../src/providers/types'
 import { resolveAccess } from '../src/services/access-policy'
 import type { InteractionOutcome, NewApproval, NewQuestion } from '../src/services/interactions'
 import { defaultSettings } from '../src/services/settings'
@@ -15,7 +15,7 @@ const settings = defaultSettings()
 settings.providers.codex = { ...settings.providers.codex, enabled: true, cliPath: fake }
 const silent = { info: () => {}, warn: () => {}, error: () => {} }
 
-function makeCtx(text: string, opts: { providerSessionId?: string; mode?: 'ask' | 'auto' | 'readonly'; answer?: (r: NewApproval | NewQuestion) => InteractionOutcome; signal?: AbortSignal; codexPlugins?: boolean } = {}) {
+function makeCtx(text: string, opts: { providerSessionId?: string; mode?: 'ask' | 'auto' | 'readonly'; answer?: (r: NewApproval | NewQuestion) => InteractionOutcome; signal?: AbortSignal; codexPlugins?: CodexPluginGrant } = {}) {
   const ws = path.join(config.dataDir, 'workspaces', `ses_codex_${Math.random().toString(36).slice(2)}`)
   fs.mkdirSync(ws, { recursive: true })
   const events: { type: AgentEventType; payload: unknown }[] = []
@@ -24,7 +24,7 @@ function makeCtx(text: string, opts: { providerSessionId?: string; mode?: 'ask'
   let sessionId: string | null = null
   const ctx: RunContext = {
     sessionId: 's', runId: 'r', model: 'gpt-test', permissionMode: opts.mode ?? 'ask', providerSessionId: opts.providerSessionId ?? null,
-    workspaceDir: ws, systemPromptAppend: '说明', allowedTools: [], disallowedTools: [], skills: 'all', plugins: [], codexPlugins: opts.codexPlugins ?? false, skillsExcludedFromAll: [],
+    workspaceDir: ws, systemPromptAppend: '说明', allowedTools: [], disallowedTools: [], skills: 'all', plugins: [], codexPlugins: opts.codexPlugins ?? NO_CODEX_PLUGINS, skillsExcludedFromAll: [],
     gateway: { url: 'http://127.0.0.1:9/mcp', token: 'rt_test' },
     input: { text, images: [], attachments: [] },
     access: resolveAccess({ policy: { mode: 'restricted', read: [], write: [], deny: ['**/*.env'] }, knowledgeRoots: [], selectedKnowledgeRootIds: [], workspaceDir: ws, settings }),
@@ -74,7 +74,7 @@ describe('CodexProvider(模拟 app-server)', () => {
   })
 
   it('Playground:开放插件与应用连接器,应用调用确认转为审批卡片', async () => {
-    const t = makeCtx('调用应用', { codexPlugins: true })
+    const t = makeCtx('调用应用', { codexPlugins: 'all' })
     await provider.run(t.ctx)
     const cfg = JSON.parse((t.events.find(e => (e.payload as { messageId?: string }).messageId === 'cfg')!.payload as { text: string }).text)
     expect(cfg.features).toMatchObject({ plugins: true, remote_plugin: true, apps: true, tool_suggest: false, multi_agent: false })
@@ -86,6 +86,23 @@ describe('CodexProvider(模拟 app-server)', () => {
     expect(t.events.some(e => (e.payload as { text?: string }).text === 'app:accept')).toBe(true)
   })
 
+  it('Profile 勾选的插件:只启用这些插件的应用与技能,应用调用确认转为审批卡片', async () => {
+    const t = makeCtx('调用应用', { codexPlugins: { pluginIds: ['google-drive@openai-curated-remote'], appIds: ['connector_drive'] } })
+    await provider.run(t.ctx)
+    const cfg = JSON.parse((t.events.find(e => (e.payload as { messageId?: string }).messageId === 'cfg')!.payload as { text: string }).text)
+    expect(cfg.features).toMatchObject({ plugins: true, remote_plugin: true, apps: true, tool_suggest: false })
+    expect(cfg.apps).toEqual({ _default: { enabled: false, destructive_enabled: false, open_world_enabled: false }, connector_drive: { enabled: true } })
+    // 勾选的插件带来的技能保留
+    expect(cfg.skills).toEqual({ config: [{ path: '/sys/skill-installer/SKILL.md', enabled: false }] })
+    expect(t.interactions[0]).toMatchObject({ kind: 'approval', tool: 'mcp__codex_apps' })
+
+    const other = makeCtx('调用应用', { codexPlugins: { pluginIds: ['gmail@openai-curated-remote'], appIds: ['connector_gmail'] } })
+    await provider.run(other.ctx)
+    const cfg2 = JSON.parse((other.events.find(e => (e.payload as { messageId?: string }).messageId === 'cfg')!.payload as { text: string }).text)
+    // 未勾选的插件(google-drive)的技能被禁用
+    expect(cfg2.skills.config.map((s: { path: string }) => s.path)).toContain('/plugins/google-drive/skills/google-docs/SKILL.md')
+  })
+
   it('API 会话:应用调用确认直接拒绝', async () => {
     const t = makeCtx('调用应用')
     await provider.run(t.ctx)
@@ -109,6 +126,9 @@ describe('CodexProvider(模拟 app-server)', () => {
       .rejects.toMatchObject({ status: 502, code: 'codex_error', message: expect.stringContaining('Plugin not found') })
     await expect(svc.install('other-plugin')).rejects.toThrow(/不存在/)
     await expect(svc.install('../x')).rejects.toThrow(/格式/)
+    // Profile 勾选的插件 → 开放的插件与应用(未安装的忽略)
+    expect(await svc.grant(['gmail@openai-curated-remote', 'notion@openai-curated-remote'])).toEqual({ pluginIds: ['gmail@openai-curated-remote'], appIds: ['connector_gmail'] })
+    expect(await svc.grant([])).toEqual({ pluginIds: [], appIds: [] })
     await svc.uninstall('gmail')
     expect((await svc.catalog({})).items.find(p => p.name === 'gmail')!.installed).toBe(false)
     expect(audits).toEqual(['安装 Codex 插件 Notion(notion@openai-curated-remote)', '卸载 Codex 插件 GMAIL(gmail@openai-curated-remote)'])

+ 1 - 1
apps/server/test/custom-skills.test.ts

@@ -207,7 +207,7 @@ describe('自定义技能:使用、沙箱与恢复', () => {
       { name: 'custom-skills:alpha', path: path.join(customSkillsDir(), 'skills/alpha/SKILL.md') },
       { name: 'custom-skills:Report Kit', path: path.join(customSkillsDir(), 'skills/report-kit/SKILL.md') },
     ].map(s => ({ ...s, scope: 'user' as const, description: '', enabled: true, pluginId: null }))
-    expect(skillsToDisable(all, 'all', '/nohome', 'agent-skills', false, app.customSkills.excludedFromAll()).map(d => path.basename(path.dirname(d.path)))).toEqual(['alpha'])
+    expect(skillsToDisable(all, 'all', '/nohome', 'agent-skills', undefined, app.customSkills.excludedFromAll()).map(d => path.basename(path.dirname(d.path)))).toEqual(['alpha'])
   })
 
   it('停用后不加载,启用后恢复;指纹随之变化', () => {

+ 1 - 1
apps/server/test/fixtures-fake-codex.mjs

@@ -49,7 +49,7 @@ readline.createInterface({ input: process.stdin }).on('line', async line => {
       return reply({ marketplaces: [{ name: 'openai-curated-remote', path: null, interface: null, plugins }, { name: 'other', path: '/x', interface: null, plugins: [p('other-plugin', false)] }], marketplaceLoadErrors: [], featuredPluginIds: [] })
     }
     // 与真实远程目录一致:plugin/read 给出 remotePluginId,安装接口只认这个 ID
-    case 'plugin/read': return reply({ plugin: { marketplaceName: params.remoteMarketplaceName, summary: { id: `${params.pluginName}@openai-curated-remote`, name: params.pluginName, remotePluginId: `plugin_asdk_${params.pluginName}` }, skills: [], apps: [], mcpServers: [] } })
+    case 'plugin/read': return reply({ plugin: { marketplaceName: params.remoteMarketplaceName, summary: { id: `${params.pluginName}@openai-curated-remote`, name: params.pluginName, remotePluginId: `plugin_asdk_${params.pluginName}` }, skills: [], apps: [{ id: `connector_${params.pluginName}`, name: params.pluginName, description: null, installUrl: null }], mcpServers: [] } })
     case 'plugin/install':
       if (!params.pluginName.startsWith('plugin_asdk_')) return send({ id, error: { code: -32600, message: 'install remote plugin: 404 Not Found: {"detail":"Plugin not found"}' } })
       globalThis.installed = params; return reply({ authPolicy: 'ON_USE', appsNeedingAuth: [{ id: 'connector_n', name: 'Notion', description: null, installUrl: 'https://chatgpt.com/apps/notion', category: null }] })

+ 22 - 0
apps/server/test/profile-codex-plugins.test.ts

@@ -0,0 +1,22 @@
+import pino from 'pino'
+import { describe, expect, it } from 'vitest'
+import { createApp } from '../src/app'
+
+describe('Profile 的 Codex 插件', () => {
+  it('Codex Profile 保存勾选的插件并产生新版本;Claude Profile 一律清空;ID 格式校验', () => {
+    const app = createApp(pino({ level: 'silent' }), { providers: {} })
+    const { id: _i, version: _v, updatedAt: _u, builtin: _b, builtinKey: _k, ...base } = app.profiles.getBuiltin()
+    const codex = app.profiles.create({ ...base, provider: 'codex', model: 'gpt-test', name: '带插件', isDefault: false })
+    expect(codex.codexPluginIds).toEqual([])
+
+    const updated = app.profiles.update(codex.id, { ...base, provider: 'codex', model: 'gpt-test', name: '带插件', isDefault: false, codexPluginIds: ['google-calendar@openai-curated-remote'] })
+    expect(updated.codexPluginIds).toEqual(['google-calendar@openai-curated-remote'])
+    expect(updated.version).toBe(2)
+    expect(app.profiles.versions(codex.id)[0]!.changed).toEqual(['codexPluginIds'])
+
+    const claude = app.profiles.create({ ...base, name: 'Claude 不用插件', isDefault: false, codexPluginIds: ['gmail@openai-curated-remote'] })
+    expect(claude.codexPluginIds).toEqual([])
+
+    expect(() => app.profiles.create({ ...base, provider: 'codex', model: 'gpt-test', name: 'x', isDefault: false, codexPluginIds: ['../etc'] })).toThrow(/校验失败/)
+  })
+})

+ 7 - 0
docs/adr/0018-codex-curated-plugins-playground-only.md

@@ -29,3 +29,10 @@
 ## 后果
 - Claude 侧不受影响(Claude 的插件机制见 ADR-0013)
 - 已加载的 Codex 会话沿用创建时的配置(03 §3.4 已知限制),因此 Playground 会话与 API 会话的配置互不影响
+
+## 修订(2026-10-28):Profile 可以为 API 会话开放指定插件
+有些经 API 的操作需要用到插件。Profile 增加 `codexPluginIds`(仅 Codex 引擎),默认为空,由管理员逐个勾选已安装的插件:
+- **开放范围**:勾选了插件的 Profile,会话开启 `plugins`、`remote_plugin`、`apps` 功能,但 `apps._default.enabled = false`,只逐个启用这些插件带的应用连接器(`plugin/read` 返回的应用 ID,缓存 10 分钟);其他插件带来的技能照常禁用。已实测(codex 0.164):只启用 Google Calendar 时,模型发现不了 Gmail 等其他已授权应用的工具。
+- **确认**:应用调用的确认(elicitation)在开放了插件的会话中转为审批卡片,与 Playground 相同;需要填表或跳转网址的请求仍然拒绝。
+- **Playground** 仍然全部开放;未勾选插件的 Profile 与之前一样全部关闭。卸载的插件在运行时忽略。
+- 勾选变化会改变线程配置,已加载的 Codex 会话会分叉以使用新配置。

+ 1 - 1
docs/design/09-admin-ui.md

@@ -39,7 +39,7 @@
 | 💬 Playground | 会话列表(搜索、按引擎/使用者筛选) | 对话面板 + 右侧可收起的调试面板 |
 | 📊 概览 | — | 运行中任务、今日用量、7 天趋势、错误列表、凭证状态 |
 | 🗂 会话 | 全部会话(所有平台/使用者) | 只读回放(时间线 + 原始事件 + 文件),可"在 Playground 中继续" |
-| 🧩 Profiles | Profile 列表 | 表单编辑:引擎、模型、提示词(Monaco 编辑器)、权限、工具、技能、资料库、**文件访问策略**(模式、可读/可写/禁止路径,右侧显示合并后的生效预览,见 06 §4.3);版本历史 |
+| 🧩 Profiles | Profile 列表 | 表单编辑:引擎、模型(下拉选择当前引擎的模型)、提示词(Monaco 编辑器)、权限、工具、技能、资料库、第三方 MCP 服务、Codex 插件(仅 Codex,默认不开放,ADR-0018 修订)、**文件访问策略**(模式、可读/可写/禁止路径,右侧显示合并后的生效预览,见 06 §4.3);版本历史 |
 | 📚 资料库 | 资料目录列表 | 名称、路径(带目录选择器)、说明、读写属性、启用;可见性检测结果;被哪些 Profile 使用;索引状态(M3) |
 | 🗃 文件管理 | 位置 + 常用目录(各资料库、本地技能目录、已安装技能、Python 运行环境、服务数据、日志) | 以管理员身份浏览与管理文件:会话工作区与资料库可写(新建文件/目录、上传与拖放、重命名/移动、删除、目录打包下载);**整个服务目录**(`/srv/agent-server`:程序、运行环境、技能、数据、日志、引擎 home)只读浏览。文本文件用 Monaco 编辑或查看(Ctrl/⌘+S 保存,打开后被改动则拒绝覆盖),图片预览,其他文件下载。登录凭证与密钥(内置全局禁止项中的 `~/.claude/.credentials.json`、`~/.codex/auth.json`、`~/.ssh` 等)不显示;数据库及其备份能看到但不能读取或下载(含 MCP 服务密钥等只写不读的配置)。路径不能越出位置,外部符号链接不跟随;会话目录本身不能在这里删除或移动;修改会话文件后同步该会话的文件记录。实现:`apps/server/src/services/file-manager.ts`、`/admin/v1/fs/*` |
 | 🔑 引擎状态 | Claude / Codex | 登录状态、账户摘要、额度、CLI 路径与版本、生效的工具清单(用于确认个人插件已被禁用);未登录时提示在终端执行 `claude` / `codex login` |

+ 3 - 1
packages/shared/src/api.ts

@@ -87,6 +87,8 @@ export interface ProfileDto {
   knowledgeRootIds: string[]
   /** 可以使用的第三方 MCP 服务(内置工具始终可用) */
   mcpServerIds: string[]
+  /** Codex 引擎:经 API 的会话可以使用的 Codex 插件(插件 ID,如 google-calendar@openai-curated-remote);默认不开放,后台 Playground 中始终全部开放 */
+  codexPluginIds: string[]
   isDefault: boolean
   /** 内置 Profile(通用助手)不可删除 */
   builtin: boolean
@@ -96,7 +98,7 @@ export interface ProfileDto {
   updatedAt: string
 }
 
-export type ProfileInput = Omit<ProfileDto, 'id' | 'version' | 'updatedAt' | 'builtin' | 'builtinKey' | 'mcpServerIds'> & { mcpServerIds?: string[] }
+export type ProfileInput = Omit<ProfileDto, 'id' | 'version' | 'updatedAt' | 'builtin' | 'builtinKey' | 'mcpServerIds' | 'codexPluginIds'> & { mcpServerIds?: string[]; codexPluginIds?: string[] }
 
 /** Profile 历史版本的内容(不含"是否默认") */
 export type ProfileSnapshot = Omit<ProfileInput, 'isDefault'>