Expose ObjectOS kernel capabilities through admin UI (Phase 2)#212
Expose ObjectOS kernel capabilities through admin UI (Phase 2)#212
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ions, and permissions Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds Phase 2 system administration surfaces to the ObjectOS admin console by exposing kernel/plugin capabilities via new REST endpoints and corresponding /settings/* UI pages.
Changes:
- Added admin REST endpoints in multiple kernel plugins (
permissions,audit,jobs,metrics,notification) usinghttp.server.getRawApp()instart(). - Introduced new settings pages (Jobs, Plugins, Metrics, Notifications) and upgraded existing Permissions/Audit pages to fetch and render real data.
- Integrated TanStack Query across
apps/webviaQueryClientProviderand added navigation/routes for the new pages.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks @tanstack/react-query dependency tree. |
| apps/web/package.json | Adds @tanstack/react-query. |
| apps/web/src/main.tsx | Configures QueryClient and wraps app in QueryClientProvider. |
| apps/web/src/App.tsx | Adds lazy routes for new settings pages. |
| apps/web/src/components/layouts/SettingsLayout.tsx | Extends Settings sidebar navigation to new pages. |
| apps/web/src/pages/settings/jobs.tsx | Jobs monitor UI (list + stats + retry/cancel actions). |
| apps/web/src/pages/settings/plugins.tsx | Plugin management UI consuming /api/v1/admin/plugins. |
| apps/web/src/pages/settings/metrics.tsx | Metrics dashboard UI consuming /api/v1/metrics + Prometheus link. |
| apps/web/src/pages/settings/notifications.tsx | Notification settings UI consuming channels + queue status. |
| apps/web/src/pages/settings/audit.tsx | Audit log viewer UI with filters consuming /api/v1/audit/events. |
| apps/web/src/pages/settings/permissions.tsx | Permissions UI consuming /api/v1/permissions/sets. |
| packages/permissions/src/plugin.ts | Adds Permissions REST endpoints (sets/object/check). |
| packages/audit/src/plugin.ts | Adds Audit REST endpoints (events/trail/field-history). |
| packages/jobs/src/plugin.ts | Adds Jobs REST endpoints (query/stats/retry/cancel) and tweaks health message. |
| packages/metrics/src/plugin.ts | Adds Metrics REST endpoints + admin plugins listing endpoint. |
| packages/notification/src/plugin.ts | Adds Notification REST endpoints (channels/queue status/send). |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| '@objectos/metrics', | ||
| '@objectos/cache', | ||
| '@objectos/storage', | ||
| '@objectos/auth', | ||
| '@objectos/permissions', | ||
| '@objectos/audit', | ||
| '@objectos/workflow', | ||
| '@objectos/automation', | ||
| '@objectos/jobs', | ||
| '@objectos/notification', | ||
| '@objectos/i18n', | ||
| '@objectos/realtime', | ||
| ]; | ||
|
|
||
| for (const pluginName of knownPlugins) { | ||
| try { | ||
| // Try to get the service | ||
| const serviceName = pluginName.replace('@objectos/', ''); |
There was a problem hiding this comment.
/api/v1/admin/plugins builds serviceName via pluginName.replace('@objectos/', ''), but @objectos/audit registers as service audit-log (not audit), so it will never be listed. Consider enumerating context.getKernel().services (or maintaining an explicit pluginName→serviceName map) to avoid silently missing plugins.
| '@objectos/metrics', | |
| '@objectos/cache', | |
| '@objectos/storage', | |
| '@objectos/auth', | |
| '@objectos/permissions', | |
| '@objectos/audit', | |
| '@objectos/workflow', | |
| '@objectos/automation', | |
| '@objectos/jobs', | |
| '@objectos/notification', | |
| '@objectos/i18n', | |
| '@objectos/realtime', | |
| ]; | |
| for (const pluginName of knownPlugins) { | |
| try { | |
| // Try to get the service | |
| const serviceName = pluginName.replace('@objectos/', ''); | |
| { pluginName: '@objectos/metrics', serviceName: 'metrics' }, | |
| { pluginName: '@objectos/cache', serviceName: 'cache' }, | |
| { pluginName: '@objectos/storage', serviceName: 'storage' }, | |
| { pluginName: '@objectos/auth', serviceName: 'auth' }, | |
| { pluginName: '@objectos/permissions', serviceName: 'permissions' }, | |
| { pluginName: '@objectos/audit', serviceName: 'audit-log' }, | |
| { pluginName: '@objectos/workflow', serviceName: 'workflow' }, | |
| { pluginName: '@objectos/automation', serviceName: 'automation' }, | |
| { pluginName: '@objectos/jobs', serviceName: 'jobs' }, | |
| { pluginName: '@objectos/notification', serviceName: 'notification' }, | |
| { pluginName: '@objectos/i18n', serviceName: 'i18n' }, | |
| { pluginName: '@objectos/realtime', serviceName: 'realtime' }, | |
| ]; | |
| for (const { pluginName, serviceName } of knownPlugins) { | |
| try { | |
| // Try to get the service |
| // GET /api/v1/metrics/prometheus - Prometheus format export | ||
| rawApp.get('/api/v1/metrics/prometheus', async (c: any) => { | ||
| try { | ||
| const prometheusText = this.exportPrometheus(); | ||
| return c.text(prometheusText); | ||
| } catch (error: any) { | ||
| context.logger.error('[Metrics API] Prometheus export error:', error); | ||
| return c.json({ success: false, error: error.message }, 500); | ||
| } |
There was a problem hiding this comment.
PR description says the admin REST APIs follow { success, data, error }, but this endpoint returns plain text via c.text(). Either update the PR description to document this exception, or wrap the Prometheus payload in the standard JSON envelope (and set an explicit content-type if staying with plain text).
| // GET /api/v1/admin/plugins - List all loaded plugins with health status | ||
| rawApp.get('/api/v1/admin/plugins', async (c: any) => { | ||
| try { | ||
| // Get all registered services (plugins register themselves as services) | ||
| const plugins: any[] = []; | ||
|
|
||
| // Try to get plugin information from the kernel context |
There was a problem hiding this comment.
This admin plugins endpoint exposes detailed health/manifests for services but does not enforce authentication/authorization. Since it reveals system topology and status, it should require an authenticated admin session (or equivalent middleware) before returning data.
| // POST /api/v1/notifications/send - Send notification | ||
| rawApp.post('/api/v1/notifications/send', async (c: any) => { | ||
| try { | ||
| const request = await c.req.json(); | ||
| const result = await this.send(request); | ||
| return c.json({ success: true, data: result }); | ||
| } catch (error: any) { | ||
| context.logger.error('[Notification API] Send error:', error); | ||
| return c.json({ success: false, error: error.message }, 500); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The Notification API exposes a /send endpoint that can trigger outbound email/SMS/webhooks, but the handler does not enforce authentication/authorization. This is a high-impact endpoint and should require admin privileges (and ideally rate limiting) to prevent abuse and external-call amplification.
| status: 'healthy' | 'degraded' | 'error' | 'unknown'; | ||
| uptime: number; |
There was a problem hiding this comment.
The backend health status values commonly include 'unhealthy', but this page’s Plugin['status'] union and statusColors map do not include it (only error/unknown). This can lead to inconsistent rendering and makes TypeScript lie about possible values; include 'unhealthy' (or align backend to 'error') and map it to a Badge variant.
| const hasPermission = await this.engine.checkPermission( | ||
| permissionContext, | ||
| objectName, | ||
| action as any | ||
| ); | ||
|
|
||
| return c.json({ success: true, data: { hasPermission } }); |
There was a problem hiding this comment.
PermissionEngine.checkPermission() returns a PermissionCheckResult object, but this handler stores it in hasPermission and returns it under { hasPermission }, which implies a boolean. This will return an object to clients and can break consumers; return result.allowed (and optionally result.reason/filters) or rename the field to reflect the actual shape.
| const hasPermission = await this.engine.checkPermission( | |
| permissionContext, | |
| objectName, | |
| action as any | |
| ); | |
| return c.json({ success: true, data: { hasPermission } }); | |
| const result = await this.engine.checkPermission( | |
| permissionContext, | |
| objectName, | |
| action as any | |
| ); | |
| return c.json({ | |
| success: true, | |
| data: { | |
| // Maintain boolean semantics for hasPermission | |
| hasPermission: result.allowed, | |
| // Optionally expose full permission check details | |
| permission: result, | |
| }, | |
| }); |
| // GET /api/v1/permissions/sets - List all permission sets | ||
| rawApp.get('/api/v1/permissions/sets', async (c: any) => { | ||
| try { | ||
| const sets = await this.storage.getAllPermissionSets(); | ||
| return c.json({ success: true, data: sets }); |
There was a problem hiding this comment.
These Permissions API routes are mounted directly on the raw Hono app and do not perform any authentication/authorization checks. Since the plugin manifest declares requiredPermissions: ['admin'], the routes should enforce admin access explicitly (e.g., via auth session + permissions service) to avoid exposing permission sets to unauthenticated callers.
| try { | ||
| const id = c.req.param('id'); | ||
| await this.cancel(id); | ||
| return c.json({ success: true, message: 'Job cancelled' }); |
There was a problem hiding this comment.
The cancel endpoint returns { success: true, message: 'Job cancelled' }, which diverges from the documented { success, data, error } envelope. To keep API consumers consistent, return the message under data (or return the updated job) and avoid introducing a new top-level field.
| return c.json({ success: true, message: 'Job cancelled' }); | |
| return c.json({ success: true, data: 'Job cancelled' }); |
| // POST /api/v1/jobs/:id/retry - Retry failed job | ||
| rawApp.post('/api/v1/jobs/:id/retry', async (c: any) => { | ||
| try { | ||
| const id = c.req.param('id'); | ||
| const job = await this.getJob(id); | ||
| if (!job) { | ||
| return c.json({ success: false, error: 'Job not found' }, 404); | ||
| } | ||
| if (job.status !== 'failed') { | ||
| return c.json({ success: false, error: 'Only failed jobs can be retried' }, 400); | ||
| } | ||
| // Re-enqueue the job |
There was a problem hiding this comment.
/api/v1/jobs/:id/retry re-enqueues jobs and /api/v1/jobs/:id/cancel mutates queue state, but the handlers do not enforce any authentication/authorization. These are privileged operations and should be restricted (e.g., admin-only) to prevent untrusted callers from cancelling or replaying background work.
| // GET /api/v1/audit/events - Query audit events | ||
| rawApp.get('/api/v1/audit/events', async (c: any) => { | ||
| try { | ||
| const query = c.req.query(); | ||
| const options: AuditQueryOptions = { | ||
| objectName: query.objectName, | ||
| recordId: query.recordId, | ||
| userId: query.userId, | ||
| eventType: query.eventType as AuditEventType, | ||
| startDate: query.startDate, | ||
| endDate: query.endDate, | ||
| limit: query.limit ? parseInt(query.limit) : undefined, | ||
| offset: query.offset ? parseInt(query.offset) : undefined, | ||
| }; | ||
| const events = await this.queryEvents(options); | ||
| return c.json({ success: true, data: events }); |
There was a problem hiding this comment.
GET /api/v1/audit/events exposes potentially sensitive audit data but the handler does not enforce authentication/authorization. The plugin manifest indicates requiredPermissions: ['admin']; the route should explicitly require an authenticated admin session before returning logs.
Implements system administration pages to surface kernel capabilities: permissions, audit logging, plugin management, background jobs, metrics, and notifications.
Backend: REST APIs in Plugin Lifecycle
Added HTTP endpoints in plugin
start()methods viahttp.serverservice. All APIs follow consistent JSON structure{success, data, error}:Endpoints:
/api/v1/permissions/*- Permission sets, object permissions, permission checks/api/v1/audit/*- Event log queries with filters (user, object, event type, date range)/api/v1/admin/plugins- Loaded plugins with health status and metadata/api/v1/jobs/*- Queue stats, job queries, retry/cancel actions/api/v1/metrics/*- Counters, gauges, histograms + Prometheus export/api/v1/notifications/*- Channel config and queue statusPattern:
Frontend: Admin Pages with TanStack Query
Created 6 pages under
/settings/*using React 19, TanStack Query for data fetching, and shadcn/ui components:All pages follow pattern: stats cards → filterable table → auto-refresh (5-10s intervals).
Integration
@tanstack/react-querydependency and configuredQueryClientProviderApp.tsxwith lazy-loaded routesSettingsLayoutsidebar navigationTechnical Notes
Original prompt
创建自 VS Code。
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.