Expose Connector package trust details and history

Show inspected transport, permissions, credential slots, outbound destinations and durable trust events in Agent settings.

Checks: frontend tests (393 passed); web and desktop builds; desktop pack; git diff --check.
This commit is contained in:
Ryder Sun committed 2026-11-06 22:07:51 +08:00
1 parent 025034de7f
commit f4cb6d1372
4 files changed
+72 -1

No files matched your search

@@ -60,4 +60,22 @@ describe('Connector package settings', () => {
expect(wrapper.text()).toContain(t('connectorPackages.instanceInUse'))
wrapper.unmount()
})
it('displays inspected permissions and durable trust history', async () => {
const item = { packageId: 'package', manifestId: 'manifest', state: 'imported', label: 'Example', connectorVersion: '1.0.0', publisher: { name: 'Publisher' } }
const api = { packages: vi.fn().mockResolvedValue([item]), list: vi.fn().mockResolvedValue([]),
inspect: vi.fn().mockResolvedValue({ packageId: 'package', origin: { filename: 'example.json', sha256: 'abc' }, manifest: {
transport: { type: 'http', protocol: 'json' }, permissionModes: ['read-only'],
credentials: { slots: [{ slotId: 'key' }] }, outboundDestinations: ['https://example.com'],
} }), audit: vi.fn().mockResolvedValue([{ eventId: 'event', action: 'imported', timestamp: '2026-11-06T00:00:00Z' }]) }
window.meldworkDesktop = { localAgentConnector: api }
const wrapper = mount(ConnectorPackagesPanel)
await flushPromises()
await wrapper.findAll('button').find(button => button.text() === t('connectorPackages.inspect')).trigger('click')
await flushPromises()
expect(wrapper.text()).toContain('read-only')
expect(wrapper.text()).toContain('https://example.com')
expect(wrapper.text()).toContain(t('connectorPackages.event.imported'))
wrapper.unmount()
})
})
@@ -10,6 +10,7 @@
<article v-for="item in packages" :key="item.packageId" class="connector-row">
<div><strong>{{ item.label }}</strong> <span>{{ item.connectorVersion }}</span><p>{{ item.publisher.name }} · {{ t(`connectorPackages.state.${item.state}`) }}</p></div>
<div class="connector-actions">
<button type="button" :disabled="busy" @click="inspect(item)">{{ t('connectorPackages.inspect') }}</button>
<button v-if="item.state === 'imported'" type="button" :disabled="busy" @click="perform('approve', item)">{{ t('connectorPackages.approve') }}</button>
<button v-if="item.state === 'approved' || item.state === 'disabled'" type="button" :disabled="busy" @click="perform('install', item)">{{ t('connectorPackages.install') }}</button>
<button v-if="item.state === 'approved' && packages.some(other => other.connectorId === item.connectorId && other.state === 'installed')" type="button" :disabled="busy" @click="perform('upgrade', item)">{{ t('connectorPackages.upgrade') }}</button>
@@ -18,6 +19,15 @@
<button v-if="!['revoked', 'removed'].includes(item.state)" type="button" :disabled="busy" @click="perform('revoke', item)">{{ t('connectorPackages.revoke') }}</button>
<button v-if="['imported', 'approved', 'disabled', 'revoked'].includes(item.state)" type="button" :disabled="busy" @click="perform('remove', item)">{{ t('connectorPackages.remove') }}</button>
</div>
<div v-if="selected?.packageId === item.packageId" class="connector-detail">
<p>{{ t('connectorPackages.origin') }}: {{ selected.origin.filename }} · {{ selected.origin.sha256 }}</p>
<p>{{ t('connectorPackages.transport') }}: {{ selected.manifest.transport.type }} / {{ selected.manifest.transport.protocol }}</p>
<p>{{ t('connectorPackages.permissions') }}: {{ selected.manifest.permissionModes.join(', ') || t('connectorPackages.none') }}</p>
<p>{{ t('connectorPackages.credentials') }}: {{ selected.manifest.credentials.slots.map(slot => slot.slotId).join(', ') || t('connectorPackages.none') }}</p>
<p>{{ t('connectorPackages.destinations') }}: {{ selected.manifest.outboundDestinations.join(', ') || t('connectorPackages.none') }}</p>
<h4>{{ t('connectorPackages.audit') }}</h4>
<ol><li v-for="event in audit" :key="event.eventId">{{ t(`connectorPackages.event.${event.action}`) }} · {{ event.timestamp }}</li></ol>
</div>
</article>
<h3 v-if="instances.length">{{ t('connectorPackages.instances') }}</h3>
<article v-for="instance in instances" :key="instance.instanceId" class="connector-row">
@@ -49,6 +59,8 @@ const busy = ref(false)
const error = ref('')
const notice = ref('')
const confirmingInstance = ref('')
const selected = ref(null)
const audit = ref([])
async function run(action) {
busy.value = true
error.value = ''
@@ -58,7 +70,16 @@ async function run(action) {
finally { busy.value = false }
}
function importPackage() { return run(() => api.import()) }
function perform(action, item) { return run(() => api[action](item.packageId)) }
function perform(action, item) {
selected.value = null
return run(() => api[action](item.packageId))
}
function inspect(item) {
if (selected.value?.packageId === item.packageId) { selected.value = null; return }
return run(async () => {
;[selected.value, audit.value] = await Promise.all([api.inspect(item.packageId), api.audit(item.packageId)])
})
}
function testInstance(instance) {
return run(async () => {
const result = await api.test(instance.instanceId)
@@ -88,6 +109,8 @@ onMounted(() => run(async () => {}))
.connector-heading, .connector-row { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.connector-heading h2 { font-size: 18px; }
.connector-row { padding: 14px 0; border-bottom: 1px solid var(--border); }
.connector-row:has(.connector-detail) { flex-wrap: wrap; }
.connector-detail { flex-basis: 100%; overflow-wrap: anywhere; }
.connector-row p { color: var(--muted); margin: 4px 0 0; }
.connector-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.connector-actions button { border: 0; background: transparent; color: var(--accent-text); cursor: pointer; padding: 8px; }
+15
View File
@@ -20,6 +20,21 @@ export default {
'connectorPackages.delete': 'Delete instance',
'connectorPackages.deletePrompt': 'Delete this instance?',
'connectorPackages.instanceInUse': 'This instance is still used by a conversation. Remove it from the conversation first.',
'connectorPackages.inspect': 'Details and history',
'connectorPackages.origin': 'Origin and hash',
'connectorPackages.transport': 'Transport',
'connectorPackages.permissions': 'Permissions',
'connectorPackages.credentials': 'Credential slots',
'connectorPackages.destinations': 'Outbound destinations',
'connectorPackages.none': 'None',
'connectorPackages.audit': 'Trust history',
'connectorPackages.event.imported': 'Imported',
'connectorPackages.event.approved': 'Approved',
'connectorPackages.event.installed': 'Installed',
'connectorPackages.event.disabled': 'Disabled',
'connectorPackages.event.revoked': 'Trust revoked',
'connectorPackages.event.upgraded': 'Upgraded',
'connectorPackages.event.removed': 'Removed',
'connectorPackages.state.imported': 'Awaiting approval',
'connectorPackages.state.approved': 'Approved',
'connectorPackages.state.installed': 'Installed',
+15
View File
@@ -20,6 +20,21 @@ export default {
'connectorPackages.delete': '删除实例',
'connectorPackages.deletePrompt': '确定删除此实例?',
'connectorPackages.instanceInUse': '此实例仍被会话使用,请先从会话中移除。',
'connectorPackages.inspect': '查看详情和记录',
'connectorPackages.origin': '来源与哈希',
'connectorPackages.transport': '传输方式',
'connectorPackages.permissions': '权限',
'connectorPackages.credentials': '凭据槽',
'connectorPackages.destinations': '外联目标',
'connectorPackages.none': '无',
'connectorPackages.audit': '信任记录',
'connectorPackages.event.imported': '已导入',
'connectorPackages.event.approved': '已批准',
'connectorPackages.event.installed': '已安装',
'connectorPackages.event.disabled': '已停用',
'connectorPackages.event.revoked': '已撤销信任',
'connectorPackages.event.upgraded': '已升级',
'connectorPackages.event.removed': '已移除',
'connectorPackages.state.imported': '待批准',
'connectorPackages.state.approved': '已批准',
'connectorPackages.state.installed': '已安装',