open-managed-agents 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +248 -0
  3. package/bin/oma.mjs +499 -0
  4. package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
  5. package/dist/scripts/alpha-smoke-models.mjs +92 -0
  6. package/dist/scripts/alpha-smoke.mjs +581 -0
  7. package/dist/scripts/oma-doctor.js +230 -0
  8. package/dist/scripts/oma-models.js +299 -0
  9. package/dist/scripts/oma-workspaces.js +133 -0
  10. package/dist/src/control-plane/admin/auth.js +43 -0
  11. package/dist/src/control-plane/admin/routes.js +76 -0
  12. package/dist/src/control-plane/admin/service.js +102 -0
  13. package/dist/src/control-plane/admission.js +90 -0
  14. package/dist/src/control-plane/agents/routes.js +63 -0
  15. package/dist/src/control-plane/agents/service.js +687 -0
  16. package/dist/src/control-plane/agents/store.js +391 -0
  17. package/dist/src/control-plane/agents/types.js +1 -0
  18. package/dist/src/control-plane/api-constants.js +3 -0
  19. package/dist/src/control-plane/app.js +907 -0
  20. package/dist/src/control-plane/console/auth.js +68 -0
  21. package/dist/src/control-plane/console/static.js +112 -0
  22. package/dist/src/control-plane/deployment-runtime-config.js +328 -0
  23. package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
  24. package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
  25. package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
  26. package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
  27. package/dist/src/control-plane/deployment-storage.js +354 -0
  28. package/dist/src/control-plane/egress/image.js +48 -0
  29. package/dist/src/control-plane/egress/policy.js +571 -0
  30. package/dist/src/control-plane/egress/presets.js +77 -0
  31. package/dist/src/control-plane/egress/proxy.js +72 -0
  32. package/dist/src/control-plane/egress/ssrf.js +106 -0
  33. package/dist/src/control-plane/egress/utils/debug.js +14 -0
  34. package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
  35. package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
  36. package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
  37. package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
  38. package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
  39. package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
  40. package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
  41. package/dist/src/control-plane/environments/routes.js +54 -0
  42. package/dist/src/control-plane/environments/service.js +112 -0
  43. package/dist/src/control-plane/environments/store.js +132 -0
  44. package/dist/src/control-plane/environments/types.js +1 -0
  45. package/dist/src/control-plane/errors.js +81 -0
  46. package/dist/src/control-plane/events/broadcaster.js +217 -0
  47. package/dist/src/control-plane/events/constants.js +2 -0
  48. package/dist/src/control-plane/events/persist.js +39 -0
  49. package/dist/src/control-plane/events/request.js +190 -0
  50. package/dist/src/control-plane/events/routes.js +159 -0
  51. package/dist/src/control-plane/events/runtime-helpers.js +134 -0
  52. package/dist/src/control-plane/events/service.js +1985 -0
  53. package/dist/src/control-plane/events/session-guards.js +29 -0
  54. package/dist/src/control-plane/events/sse.js +6 -0
  55. package/dist/src/control-plane/events/store.js +772 -0
  56. package/dist/src/control-plane/events/tool-persistence.js +102 -0
  57. package/dist/src/control-plane/events/types.js +21 -0
  58. package/dist/src/control-plane/files/routes.js +121 -0
  59. package/dist/src/control-plane/files/service.js +65 -0
  60. package/dist/src/control-plane/files/store-common.js +87 -0
  61. package/dist/src/control-plane/files/store-local.js +488 -0
  62. package/dist/src/control-plane/files/store-memory.js +270 -0
  63. package/dist/src/control-plane/files/store.js +2 -0
  64. package/dist/src/control-plane/files/types.js +7 -0
  65. package/dist/src/control-plane/http.js +25 -0
  66. package/dist/src/control-plane/ids.js +43 -0
  67. package/dist/src/control-plane/logging.js +263 -0
  68. package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
  69. package/dist/src/control-plane/models/catalog.js +78 -0
  70. package/dist/src/control-plane/models/config-security.js +376 -0
  71. package/dist/src/control-plane/models/deployment-config.js +93 -0
  72. package/dist/src/control-plane/models/routes.js +32 -0
  73. package/dist/src/control-plane/models/service.js +138 -0
  74. package/dist/src/control-plane/observability/instruments.js +65 -0
  75. package/dist/src/control-plane/observability/metrics.js +125 -0
  76. package/dist/src/control-plane/observability/routes.js +95 -0
  77. package/dist/src/control-plane/openapi/document.js +282 -0
  78. package/dist/src/control-plane/openapi/routes.js +78 -0
  79. package/dist/src/control-plane/request-idempotency.js +85 -0
  80. package/dist/src/control-plane/secrets/envelope.js +132 -0
  81. package/dist/src/control-plane/secrets/master-key.js +44 -0
  82. package/dist/src/control-plane/secrets/routes.js +20 -0
  83. package/dist/src/control-plane/secrets/service.js +71 -0
  84. package/dist/src/control-plane/secrets/store.js +212 -0
  85. package/dist/src/control-plane/secrets/types.js +3 -0
  86. package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
  87. package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
  88. package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
  89. package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
  90. package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
  91. package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
  92. package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
  93. package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
  94. package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
  95. package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
  96. package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
  97. package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
  98. package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
  99. package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
  100. package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
  101. package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
  102. package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
  103. package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
  104. package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
  105. package/dist/src/control-plane/sessions/pi/translator.js +100 -0
  106. package/dist/src/control-plane/sessions/request.js +164 -0
  107. package/dist/src/control-plane/sessions/resources.js +63 -0
  108. package/dist/src/control-plane/sessions/routes.js +88 -0
  109. package/dist/src/control-plane/sessions/serialize.js +24 -0
  110. package/dist/src/control-plane/sessions/service.js +733 -0
  111. package/dist/src/control-plane/sessions/store.js +538 -0
  112. package/dist/src/control-plane/sessions/types.js +1 -0
  113. package/dist/src/control-plane/skills/archive.js +169 -0
  114. package/dist/src/control-plane/skills/routes.js +41 -0
  115. package/dist/src/control-plane/skills/service.js +29 -0
  116. package/dist/src/control-plane/skills/store.js +233 -0
  117. package/dist/src/control-plane/skills/types.js +5 -0
  118. package/dist/src/control-plane/sqlite-transaction.js +15 -0
  119. package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
  120. package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
  121. package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
  122. package/dist/src/control-plane/vaults/routes.js +66 -0
  123. package/dist/src/control-plane/vaults/service.js +553 -0
  124. package/dist/src/control-plane/vaults/store.js +691 -0
  125. package/dist/src/control-plane/vaults/types.js +1 -0
  126. package/dist/src/control-plane/wake-loop.js +65 -0
  127. package/dist/src/control-plane/wiring.js +92 -0
  128. package/dist/src/control-plane/workspace.js +4 -0
  129. package/dist/src/control-plane/workspaces/store.js +158 -0
  130. package/dist/src/egress-proxy-main.js +103 -0
  131. package/dist/src/main.js +124 -0
  132. package/dist/src/types/agents.js +1 -0
  133. package/dist/src/types/common.js +1 -0
  134. package/dist/src/types/environments.js +1 -0
  135. package/dist/src/types/events.js +121 -0
  136. package/dist/src/types/files.js +1 -0
  137. package/dist/src/types/json.js +19 -0
  138. package/dist/src/types/sessions.js +1 -0
  139. package/dist/ui/managed-agents-console/README.md +52 -0
  140. package/dist/ui/managed-agents-console/docs/agents.md +23 -0
  141. package/dist/ui/managed-agents-console/docs/console.md +23 -0
  142. package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
  143. package/dist/ui/managed-agents-console/docs/environments.md +27 -0
  144. package/dist/ui/managed-agents-console/docs/events.md +19 -0
  145. package/dist/ui/managed-agents-console/docs/files.md +17 -0
  146. package/dist/ui/managed-agents-console/docs/github.md +11 -0
  147. package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
  148. package/dist/ui/managed-agents-console/docs/memory.md +11 -0
  149. package/dist/ui/managed-agents-console/docs/migration.md +17 -0
  150. package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
  151. package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
  152. package/dist/ui/managed-agents-console/docs/overview.md +41 -0
  153. package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
  154. package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
  155. package/dist/ui/managed-agents-console/docs/reference.md +27 -0
  156. package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
  157. package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
  158. package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
  159. package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
  160. package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
  161. package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
  162. package/dist/ui/managed-agents-console/docs/skills.md +15 -0
  163. package/dist/ui/managed-agents-console/docs/tools.md +23 -0
  164. package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
  165. package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
  166. package/dist/ui/managed-agents-console/index.html +47 -0
  167. package/dist/ui/managed-agents-console/serve.mjs +116 -0
  168. package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
  169. package/dist/ui/managed-agents-console/src/api.js +1043 -0
  170. package/dist/ui/managed-agents-console/src/app.jsx +633 -0
  171. package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
  172. package/dist/ui/managed-agents-console/src/console.css +733 -0
  173. package/dist/ui/managed-agents-console/src/data.js +188 -0
  174. package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
  175. package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
  176. package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
  177. package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
  178. package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
  179. package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
  180. package/dist/ui/managed-agents-console/src/sse.js +185 -0
  181. package/dist/ui/managed-agents-console/src/states.jsx +90 -0
  182. package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
  183. package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
  184. package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
  185. package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
  186. package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
  187. package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
  188. package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
  189. package/dist/ui/openapi-docs/VENDOR.md +27 -0
  190. package/dist/ui/openapi-docs/index.html +16 -0
  191. package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
  192. package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
  193. package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
  194. package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
  195. package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
  196. package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
  197. package/package.json +53 -0
@@ -0,0 +1,190 @@
1
+ // ui.jsx — shared console chrome (status, sidebar, toolbar bits). → window
2
+ const { useState } = React;
3
+
4
+ const STLABEL = {
5
+ running:'Running',
6
+ rescheduling:'Rescheduling',
7
+ active:'Active',
8
+ idle:'Idle',
9
+ error:'Error',
10
+ archived:'Archived',
11
+ terminated:'Terminated',
12
+ open:'open',
13
+ action:'Needs action'
14
+ };
15
+ function St({ k }) {
16
+ return <span className={'badge st-' + k}><i className="dot" />{STLABEL[k] || k}</span>;
17
+ }
18
+
19
+ function NeedsAction() {
20
+ return <span className="badge st-action"><i className="dot" />Needs action</span>;
21
+ }
22
+
23
+ function Pill({ icon, children }) {
24
+ return <span className="pill">{icon && <Icon name={icon} size={12} />}{children}</span>;
25
+ }
26
+
27
+ function Field({ icon = 'search', placeholder, wide, value, onChange, style }) {
28
+ return (
29
+ <div className="field" style={{ flex: wide ? 1 : undefined, maxWidth: wide ? 360 : undefined, ...style }}>
30
+ {icon && <Icon name={icon} size={15} />}
31
+ <input placeholder={placeholder} value={value || ''} onChange={(e) => onChange && onChange(e.target.value)} />
32
+ </div>
33
+ );
34
+ }
35
+
36
+ function Select({ label, value, w }) {
37
+ return (
38
+ <div className="field select" style={{ width:w }}>
39
+ <span style={{ color:'var(--faint)' }}>{label}</span>
40
+ {value && <b>{value}</b>}
41
+ <Icon name="chevDown" size={14} className="chev" />
42
+ </div>
43
+ );
44
+ }
45
+
46
+ function Kebab() { return <span className="kebab"><Icon name="more" size={16} /></span>; }
47
+ function Pager() {
48
+ return (
49
+ <div className="pager">
50
+ <button className="btn"><Icon name="chevLeft" size={15} /></button>
51
+ <button className="btn"><Icon name="chevRight" size={15} /></button>
52
+ </div>
53
+ );
54
+ }
55
+ function Crumbs({ items }) {
56
+ return (
57
+ <div className="crumbs">
58
+ {items.map((it, i) => (
59
+ <React.Fragment key={i}>
60
+ {i > 0 && <span className="sep">/</span>}
61
+ {it.onClick ? <a onClick={it.onClick}>{it.label}</a> : <span style={{ color:'var(--ink)' }}>{it.label}</span>}
62
+ </React.Fragment>
63
+ ))}
64
+ </div>
65
+ );
66
+ }
67
+
68
+ const NAV = [
69
+ { key:'start', label:'Start', icon:'checkCircle' },
70
+ { key:'sessions', label:'Sessions', icon:'activity' },
71
+ { key:'agents', label:'Agents', icon:'bot' },
72
+ { key:'environments', label:'Environments', icon:'database' },
73
+ { key:'files', label:'Files', icon:'folder' },
74
+ { key:'vaults', label:'Vaults', icon:'database' },
75
+ { key:'skills', label:'Skills', icon:'fileText' },
76
+ ];
77
+
78
+ function WorkspaceSwitcher({ workspace, admin, onSelectWorkspace, onSwitchWorkspace, onSignOut }) {
79
+ const [open, setOpen] = useState(false);
80
+ const [workspaces, setWorkspaces] = useState(null);
81
+ const toggle = () => {
82
+ const next = !open;
83
+ setOpen(next);
84
+ if (next && admin && workspaces === null) {
85
+ OmaConsoleApi.listWorkspaces().then(setWorkspaces).catch(() => setWorkspaces([]));
86
+ }
87
+ };
88
+ const label = workspace?.name || (admin ? 'Choose workspace' : 'Connect workspace');
89
+ return (
90
+ <div className="workspace-switcher">
91
+ <button className="ws" type="button" onClick={toggle} aria-expanded={open}>
92
+ <span className="ws-dot" /><span className="ws-name">{label}</span>
93
+ <Icon name="chevDown" size={14} className="ws-chev" />
94
+ </button>
95
+ {open && <div className="workspace-menu">
96
+ {admin && <>
97
+ <div className="workspace-menu-label">Switch workspace</div>
98
+ {workspaces === null ? <div className="workspace-menu-note">Loading workspaces…</div>
99
+ : workspaces.length === 0 ? <div className="workspace-menu-note">No workspaces yet.</div>
100
+ : workspaces.map((item) => <button key={item.id} type="button" className="workspace-menu-item"
101
+ onClick={() => { setOpen(false); onSelectWorkspace(item.id); }}>
102
+ <span>{item.name}</span><small className="mono">{item.id}</small>
103
+ </button>)}
104
+ </>}
105
+ {!admin && <button type="button" className="workspace-menu-item" onClick={() => { setOpen(false); onSwitchWorkspace(); }}>
106
+ Enter another workspace key
107
+ </button>}
108
+ <button type="button" className="workspace-menu-item workspace-signout" onClick={() => { setOpen(false); onSignOut(); }}>
109
+ Sign out
110
+ </button>
111
+ </div>}
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function Sidebar({ route, go, showAdmin = false, workspace, admin = false, onSelectWorkspace, onSwitchWorkspace, onSignOut }) {
117
+ const top = route === 'session' ? 'sessions' : route === 'agent' ? 'agents' : route;
118
+ return (
119
+ <aside className="sidebar">
120
+ <div className="brand">
121
+ <div className="brand-mark">O</div>
122
+ <div className="brand-name">OMA Console<small>Managed Agents</small></div>
123
+ </div>
124
+ <WorkspaceSwitcher workspace={workspace} admin={admin} onSelectWorkspace={onSelectWorkspace}
125
+ onSwitchWorkspace={onSwitchWorkspace} onSignOut={onSignOut} />
126
+ <div className="nav-label">Managed Agents</div>
127
+ {NAV.map((n) => (
128
+ <div key={n.key} className={'nav-item' + (top === n.key ? ' active' : '')} onClick={() => go(n.key)}>
129
+ <Icon name={n.icon} size={16} />{n.label}
130
+ </div>
131
+ ))}
132
+ {showAdmin && <>
133
+ <div className="nav-label">Operator</div>
134
+ <div className={'nav-item' + (top === 'admin' ? ' active' : '')} onClick={() => go('admin')}>
135
+ <Icon name="database" size={16} />Admin
136
+ </div>
137
+ </>}
138
+ <div className="sidebar-spacer" />
139
+ <div className="sidebar-foot">
140
+ <button type="button" className={'nav-item' + (top === 'documentation' ? ' active' : '')} onClick={() => go('documentation')}>
141
+ <Icon name="fileText" size={16} />Documentation
142
+ </button>
143
+ <div className="nav-item dim"><Icon name="terminal" size={16} />proxy /v1</div>
144
+ </div>
145
+ </aside>
146
+ );
147
+ }
148
+
149
+ function PageHead({ title, sub, action, onAction, readOnly = false, endpoint }) {
150
+ const disabled = Boolean(action && readOnly);
151
+ return (
152
+ <div className="page-head">
153
+ <div>
154
+ <h1 className="page-title">{title}</h1>
155
+ {sub && <div className="page-sub">{sub}</div>}
156
+ </div>
157
+ {action && <div className="action-wrap" title={disabled && endpoint ? `Read-only API mode · ${endpoint}` : undefined}>
158
+ <button className={'btn btn-primary' + (disabled ? ' ro' : '')}
159
+ disabled={disabled}
160
+ onClick={disabled ? undefined : onAction}>
161
+ <Icon name="plus" size={15} />{action}
162
+ </button>
163
+ {disabled && endpoint && <span className="endpoint-hint mono">{endpoint}</span>}
164
+ </div>}
165
+ </div>
166
+ );
167
+ }
168
+
169
+ function ModeBar({ mode, warnings = [] }) {
170
+ const label = mode === 'demo'
171
+ ? 'Demo review mode'
172
+ : mode === 'error'
173
+ ? 'Live API unavailable'
174
+ : 'Live API mode';
175
+ const copy = mode === 'demo'
176
+ ? 'Local interactions mutate bundled demo data only.'
177
+ : mode === 'error'
178
+ ? 'The live request failed; no bundled demo records are being shown.'
179
+ : 'Live agent, environment, session, prompt, interrupt, and confirmation actions are enabled; unsupported writes stay disabled.';
180
+ return (
181
+ <div className={'modebar mode-' + mode}>
182
+ <Icon name={mode === 'api' ? 'database' : 'alert'} size={14} />
183
+ <span className="mode-main">{label}</span>
184
+ <span className="mode-copy">{copy}</span>
185
+ {warnings.length > 0 && <span className="mode-warn">{warnings.join(' ')}</span>}
186
+ </div>
187
+ );
188
+ }
189
+
190
+ Object.assign(window, { St, NeedsAction, Pill, Field, Select, Kebab, Pager, Crumbs, Sidebar, PageHead, ModeBar });
@@ -0,0 +1,89 @@
1
+ // Pure data helpers for the Vaults and Credential health panels. Keep raw API
2
+ // records out of JSX so the browser only receives the display fields it needs.
3
+ export function vaultRow(vault) {
4
+ return {
5
+ id: String(vault?.id ?? ""),
6
+ displayName: String(vault?.display_name || vault?.id || "Unnamed vault"),
7
+ createdAt: vault?.created_at ?? null,
8
+ archivedAt: vault?.archived_at ?? null,
9
+ };
10
+ }
11
+
12
+ export function credentialRow(credential) {
13
+ const auth = credential?.auth || {};
14
+ const refresh = auth?.refresh || null;
15
+ return {
16
+ id: String(credential?.id ?? ""),
17
+ displayName: String(credential?.display_name || credential?.id || "Unnamed credential"),
18
+ authType: auth.type === "mcp_oauth" ? "mcp_oauth" : "static_bearer",
19
+ serverUrl: typeof auth.mcp_server_url === "string" ? auth.mcp_server_url : "",
20
+ expiresAt: auth.expires_at ?? null,
21
+ archivedAt: credential?.archived_at ?? null,
22
+ refresh: refresh && typeof refresh === "object" ? {
23
+ tokenEndpointHost: urlHost(refresh.token_endpoint),
24
+ scope: typeof refresh.scope === "string" ? refresh.scope : null,
25
+ endpointAuth: refresh.token_endpoint_auth?.type ?? null,
26
+ } : null,
27
+ };
28
+ }
29
+
30
+ export function urlHost(value) {
31
+ try { return new URL(value).host || "—"; } catch { return "—"; }
32
+ }
33
+
34
+ export function relativeTime(value, now = Date.now()) {
35
+ const at = typeof value === "string" ? Date.parse(value) : NaN;
36
+ if (!Number.isFinite(at)) return "—";
37
+ const seconds = Math.round((at - now) / 1000);
38
+ const absolute = Math.abs(seconds);
39
+ if (absolute < 60) return seconds >= 0 ? "in under a minute" : "just now";
40
+ const minutes = Math.round(absolute / 60);
41
+ if (minutes < 60) return seconds >= 0 ? `in ${minutes}m` : `${minutes}m ago`;
42
+ const hours = Math.round(minutes / 60);
43
+ if (hours < 48) return seconds >= 0 ? `in ${hours}h` : `${hours}h ago`;
44
+ const days = Math.round(hours / 24);
45
+ return seconds >= 0 ? `in ${days}d` : `${days}d ago`;
46
+ }
47
+
48
+ export function healthState(row) {
49
+ if (!row?.hasRefresh) return { label: "n/a", tone: "neutral" };
50
+ if (row.refreshStatus === "ok") return { label: "ok", tone: "ok" };
51
+ if (row.refreshStatus === "invalid") return { label: "invalid", tone: "error" };
52
+ if (row.refreshStatus === "transient") return { label: "transient", tone: "warn" };
53
+ return { label: "not attempted", tone: "neutral" };
54
+ }
55
+
56
+ // Map a semantic tone onto an existing badge class (console.css): the accent
57
+ // wash is the amber-gold the plan calls for on transient/warn rows.
58
+ const TONE_BADGE = { ok: "st-active", warn: "st-rescheduling", error: "st-error", neutral: "st-idle" };
59
+
60
+ export function toneBadgeClass(tone) {
61
+ return TONE_BADGE[tone] || TONE_BADGE.neutral;
62
+ }
63
+
64
+ export function truncationWarning(kind) {
65
+ const noun = kind === "credentials" ? "Credential" : "Vault";
66
+ return `${noun} list reached the safety cap; this view may be partial.`;
67
+ }
68
+
69
+ export function validationOutcome(result) {
70
+ if (result?.status === "valid") return { tone: "ok", message: "Credential works." };
71
+ if (result?.status === "invalid") return { tone: "error", message: "Re-authorize with the provider and rotate the credential." };
72
+ if (result?.status === "unknown" && result?.refresh?.status === "skipped") {
73
+ return { tone: "neutral", message: "Refresh was skipped; the probe was inconclusive. The credential may have changed or been checked recently." };
74
+ }
75
+ return { tone: "warn", message: "Could not conclude (transient or unreachable); try again later." };
76
+ }
77
+
78
+ // Presentation details for a validation result: the probe status, plus the
79
+ // refresh status/HTTP code the plan requires on invalid/failed outcomes.
80
+ export function validationDetail(result) {
81
+ const probeStatus = result?.mcp_probe?.http_response?.status_code ?? null;
82
+ const refreshStatus = result?.refresh?.status ?? null;
83
+ const refreshHttpStatus = result?.refresh?.http_response?.status_code ?? null;
84
+ return { probeStatus, refreshStatus, refreshHttpStatus };
85
+ }
86
+
87
+ export function isCurrentVaultResult(epoch, currentEpoch, vaultId, currentVaultId) {
88
+ return epoch === currentEpoch && vaultId === currentVaultId;
89
+ }
@@ -0,0 +1,171 @@
1
+ const { useState: useVaultState, useEffect: useVaultEffect, useRef: useVaultRef } = React;
2
+
3
+ const DEMO_VAULTS = [{ id: 'vlt_demo', display_name: 'Demo integrations', created_at: '2026-07-01T00:00:00Z', archived_at: null }];
4
+ const DEMO_CREDENTIALS = [{ id: 'vcrd_demo', display_name: 'Demo MCP OAuth', archived_at: null, auth: { type: 'mcp_oauth', mcp_server_url: 'https://mcp.example.test/mcp', expires_at: '2026-12-01T00:00:00Z', refresh: { token_endpoint: 'https://auth.example.test/token', scope: 'read', token_endpoint_auth: { type: 'none' } } } }];
5
+
6
+ function ToneBadge({ tone, children }) {
7
+ return <span className={'badge ' + VaultsData.toneBadgeClass(tone)}><i className="dot" />{children}</span>;
8
+ }
9
+
10
+ function VaultsView({ mode, initialVaultId, onCreate, onCreateCredential, readOnly = false, onOpenVault, onBackToVaults }) {
11
+ const [vaults, setVaults] = useVaultState(null);
12
+ const [error, setError] = useVaultState(null);
13
+ const [selected, setSelected] = useVaultState(null);
14
+ const [credentials, setCredentials] = useVaultState(null);
15
+ const [detailError, setDetailError] = useVaultState(null);
16
+ const [warning, setWarning] = useVaultState(null);
17
+ const [confirm, setConfirm] = useVaultState(null);
18
+ const [validation, setValidation] = useVaultState(null);
19
+ const [validating, setValidating] = useVaultState(false);
20
+ const epoch = useVaultRef(0);
21
+ const selectedId = selected?.id || null;
22
+
23
+ const refresh = () => {
24
+ const current = ++epoch.current;
25
+ setError(null); setVaults(null); setSelected(null); setCredentials(null); setWarning(null);
26
+ if (mode !== 'api') { setVaults(DEMO_VAULTS.map(VaultsData.vaultRow)); return; }
27
+ OmaConsoleApi.listVaults().then((page) => {
28
+ if (current !== epoch.current) return;
29
+ setVaults(page.data.map(VaultsData.vaultRow));
30
+ if (page.truncated) setWarning(VaultsData.truncationWarning('vaults'));
31
+ }).catch((err) => { if (current === epoch.current) setError(err); });
32
+ };
33
+ useVaultEffect(refresh, [mode]);
34
+
35
+ // The ref is the authority for a detail request: React state updates are
36
+ // asynchronous, so an old response must not win a fast selection change.
37
+ const selectedRef = useVaultRef(null);
38
+ selectedRef.current = selectedId;
39
+ const open = (vault) => {
40
+ const current = ++epoch.current;
41
+ selectedRef.current = vault.id; setSelected(vault); setCredentials(null); setDetailError(null); setValidation(null); setValidating(false); setWarning(null);
42
+ if (mode !== 'api') { setCredentials(DEMO_CREDENTIALS.map(VaultsData.credentialRow)); return; }
43
+ OmaConsoleApi.listVaultCredentials(vault.id).then((page) => {
44
+ if (!VaultsData.isCurrentVaultResult(current, epoch.current, vault.id, selectedRef.current)) return;
45
+ setCredentials(page.data.map(VaultsData.credentialRow));
46
+ if (page.truncated) setWarning(VaultsData.truncationWarning('credentials'));
47
+ }).catch((err) => { if (VaultsData.isCurrentVaultResult(current, epoch.current, vault.id, selectedRef.current)) setDetailError(err); });
48
+ };
49
+ const back = () => { ++epoch.current; setSelected(null); setCredentials(null); setValidation(null); setValidating(false); setWarning(null); onBackToVaults(); };
50
+ const validate = (credential) => {
51
+ // Guard the result the same way the list/detail fetches are guarded: a
52
+ // probe can take seconds, and the operator can navigate to another vault
53
+ // before it resolves. Without this, A's outcome renders under vault B.
54
+ const startedEpoch = epoch.current;
55
+ const vaultId = selected.id;
56
+ const isCurrent = () => VaultsData.isCurrentVaultResult(startedEpoch, epoch.current, vaultId, selectedRef.current);
57
+ setConfirm(null); setValidating(true); setValidation(null);
58
+ if (mode === 'demo') { setValidation({ result: { status: 'valid', mcp_probe: { http_response: { status_code: 200 } } } }); setValidating(false); return; }
59
+ OmaConsoleApi.validateMcpOauthCredential(vaultId, credential.id, mode)
60
+ .then((result) => { if (isCurrent()) setValidation({ result }); })
61
+ .catch((err) => { if (isCurrent()) setValidation({ error: err.message }); })
62
+ .finally(() => { if (isCurrent()) setValidating(false); });
63
+ };
64
+ useVaultEffect(() => {
65
+ if (!initialVaultId || !vaults || selected) return;
66
+ const vault = vaults.find((item) => item.id === initialVaultId);
67
+ if (vault) open(vault);
68
+ }, [initialVaultId, vaults]);
69
+
70
+ const body = selected
71
+ ? <VaultDetail vault={selected} credentials={credentials} error={detailError} warning={warning}
72
+ onBack={back} onRetry={() => open(selected)}
73
+ mode={mode} readOnly={readOnly} onCreateCredential={() => onCreateCredential(selected)} validating={validating} validation={validation} onValidate={(credential) => setConfirm(credential)} />
74
+ : <div className="main-scroll scroll fade-in">
75
+ <PageHead title="Vaults" sub="Manage workspace vaults and validate MCP OAuth credentials."
76
+ action="Create vault" onAction={onCreate} readOnly={readOnly} endpoint="POST /v1/vaults" />
77
+ {warning && <div className="inline-warn"><Icon name="alert" size={14} /><span>{warning}</span></div>}
78
+ {vaults === null && !error ? <SkeletonTable rows={4} cols={[180, 'grow', 120, 100]} />
79
+ : error ? <ErrorState resource="vaults" onRetry={refresh} />
80
+ : vaults.length === 0 ? <EmptyState icon="database" title="No vaults" message="This workspace has no vaults. Create one before adding MCP credentials."
81
+ actionLabel={!readOnly ? 'Create vault' : null} onAction={onCreate} />
82
+ : <div className="panel">{vaults.map((vault) => <div className="trow" key={vault.id} style={{ opacity: vault.archivedAt ? 0.55 : 1 }} onClick={() => { onOpenVault(vault.id); open(vault); }}>
83
+ <span className="td mono" style={{ width:180, fontSize:12 }}>{vault.id}</span>
84
+ <span className="td grow cell-strong">{vault.displayName}</span>
85
+ <span className="td mono" style={{ width:120, color:'var(--faint)', fontSize:12 }}>{VaultsData.relativeTime(vault.createdAt)}</span>
86
+ <span className="td" style={{ width:100 }}>{vault.archivedAt ? <St k="archived" /> : <St k="active" />}</span>
87
+ </div>)}</div>}
88
+ </div>;
89
+
90
+ // The dialog lives at the component root so it renders in both the list and
91
+ // detail branches (an earlier revision nested it under the list only, which
92
+ // made Validate a no-op once a vault was open).
93
+ return <>
94
+ {body}
95
+ {confirm && <ConfirmDialog icon="alert" title="Validate credential"
96
+ message="Validate contacts the MCP server with this credential and may refresh the token at the provider."
97
+ confirmLabel="Validate" endpoint="POST /v1/vaults/:id/credentials/:id/mcp_oauth_validate"
98
+ onClose={() => setConfirm(null)} onConfirm={() => validate(confirm)} />}
99
+ </>;
100
+ }
101
+
102
+ function VaultDetail({ vault, credentials, error, warning, onBack, onRetry, mode, readOnly, onCreateCredential, validating, validation, onValidate }) {
103
+ return <div className="main-scroll scroll fade-in"><PageHead title={vault.displayName} sub={vault.id} />
104
+ <div className="toolbar"><button className="btn" onClick={onBack}>Back to vaults</button>{!readOnly && <button className="btn btn-primary" onClick={onCreateCredential}><Icon name="plus" size={15} />Add credential</button>}</div>
105
+ {warning && <div className="inline-warn"><Icon name="alert" size={14} /><span>{warning}</span></div>}
106
+ {credentials === null && !error ? <SkeletonTable rows={3} cols={[180, 'grow', 120]} />
107
+ : error ? <ErrorState resource="credentials" onRetry={onRetry} />
108
+ : credentials.length === 0 ? <EmptyState icon="database" title="No credentials" message="This vault has no credentials. Add one to authorize a matching MCP server."
109
+ actionLabel={!readOnly ? 'Add credential' : null} onAction={onCreateCredential} />
110
+ : <div className="panel">{credentials.map((c) => <div className="trow" key={c.id} style={{ display:'block', cursor:'default', padding:'14px', opacity: c.archivedAt ? 0.55 : 1 }}>
111
+ <div style={{ display:'flex', gap:12, alignItems:'center' }}><span className="mono" style={{ color:'var(--soft)' }}>{c.id}</span><b>{c.displayName}</b><span className="pill">{c.authType}</span>{c.archivedAt && <St k="archived" />}</div>
112
+ <div className="field-hint" style={{ marginTop:6 }}>{c.serverUrl || 'No server URL'} · expires {VaultsData.relativeTime(c.expiresAt)}</div>
113
+ {c.refresh && <div className="field-hint">token host {c.refresh.tokenEndpointHost} · {c.refresh.scope || 'no scope'} · {c.refresh.endpointAuth || 'no endpoint auth'}</div>}
114
+ {mode !== 'mock' && !c.archivedAt && c.authType === 'mcp_oauth' && <button className="btn" disabled={validating} onClick={() => onValidate(c)} style={{ marginTop:8 }}>{validating ? 'Validating…' : 'Validate'}</button>}
115
+ </div>)}</div>}
116
+ {validation && <ValidationResult value={validation} />}
117
+ </div>;
118
+ }
119
+
120
+ function ValidationResult({ value }) {
121
+ if (value.error) return <div className="inline-warn" style={{ marginTop:12 }}><Icon name="alert" size={14} /><span>{value.error}</span></div>;
122
+ const outcome = VaultsData.validationOutcome(value.result);
123
+ const detail = VaultsData.validationDetail(value.result);
124
+ return <div className="panel" style={{ marginTop:12, padding:14 }}>
125
+ <div style={{ display:'flex', gap:8, alignItems:'center' }}><ToneBadge tone={outcome.tone}>{value.result?.status || 'error'}</ToneBadge><b>{outcome.message}</b></div>
126
+ {detail.probeStatus && <div className="field-hint" style={{ marginTop:6 }}>Probe HTTP {detail.probeStatus}</div>}
127
+ {detail.refreshStatus && <div className="field-hint">Refresh {detail.refreshStatus}{detail.refreshHttpStatus ? ` · HTTP ${detail.refreshHttpStatus}` : ''}</div>}
128
+ <details style={{ marginTop:8 }}><summary>Details</summary><pre className="code" style={{ whiteSpace:'pre-wrap' }}>{JSON.stringify(value.result ?? {}, null, 2)}</pre></details>
129
+ </div>;
130
+ }
131
+
132
+ function CredentialHealthView({ workspaceId, onBack, onReauth }) {
133
+ const [page, setPage] = useVaultState(null);
134
+ const [error, setError] = useVaultState(null);
135
+ const [cursor, setCursor] = useVaultState(null);
136
+ const load = (next = null) => {
137
+ setError(null); setPage(null); setCursor(next);
138
+ OmaConsoleApi.listWorkspaceCredentialHealth(workspaceId, next)
139
+ .then(setPage)
140
+ .catch((err) => { if (err.status === 401) onReauth(); else setError(err); });
141
+ };
142
+ useVaultEffect(() => load(null), [workspaceId]);
143
+ const groups = new Map();
144
+ for (const row of page?.data || []) {
145
+ const key = `${row.vaultId}:${row.vaultDisplayName}`;
146
+ if (!groups.has(key)) groups.set(key, []);
147
+ groups.get(key).push(row);
148
+ }
149
+ return <div className="main-scroll scroll fade-in"><PageHead title="Credential health" sub={`Administrative health for ${workspaceId}.`} />
150
+ <div className="toolbar"><button className="btn" onClick={onBack}>Back to Admin</button><button className="btn" onClick={() => load(cursor)}>Refresh</button></div>
151
+ {page === null && !error ? <SkeletonTable rows={5} cols={['grow', 210, 120, 130]} />
152
+ : error ? <ErrorState resource="credential health" onRetry={() => load(cursor)} />
153
+ : page.data.length === 0 ? <EmptyState icon="database" title="No credentials" message="This workspace has no stored vault credentials." />
154
+ : [...groups.entries()].map(([key, rows]) => <div className="panel" key={key} style={{ marginBottom:12, opacity: rows[0].vaultArchivedAt ? 0.6 : 1 }}>
155
+ <div style={{ padding:'10px 14px', borderBottom:'1px solid var(--border)' }}><b>{rows[0].vaultDisplayName}</b>{rows[0].vaultArchivedAt && <span className="field-hint"> · archived</span>}</div>
156
+ {rows.map((row) => { const state = VaultsData.healthState(row); return <div className="trow" key={row.credentialId} style={{ cursor:'default', opacity: row.credentialArchivedAt ? 0.55 : 1 }}>
157
+ <span className="td grow"><b>{row.credentialDisplayName || row.credentialId}</b><span className="field-hint"> · {row.authType} · {VaultsData.urlHost(row.mcpServerUrl)}</span>{row.credentialArchivedAt && <span className="field-hint"> · archived</span>}</span>
158
+ <span className="td" style={{ width:210, display:'flex', gap:6, alignItems:'center', flexWrap:'wrap' }}>
159
+ <ToneBadge tone={state.tone}>{state.label}</ToneBadge>
160
+ {state.label === 'transient' && <span className="field-hint">{row.refreshAttempts} attempts</span>}
161
+ {row.authHintAt && <span className="badge st-action"><i className="dot" />Auth hint</span>}
162
+ </span>
163
+ <span className="td" style={{ width:120, color:'var(--faint)' }}>exp {VaultsData.relativeTime(row.expiresAt)}</span>
164
+ <span className="td" style={{ width:130, color:'var(--faint)' }}>next {row.nextRefreshAt ? VaultsData.relativeTime(row.nextRefreshAt) : '—'}</span>
165
+ </div>; })}
166
+ </div>)}
167
+ {page?.has_more && page.next_page && <button className="btn" onClick={() => load(page.next_page)}>Next page</button>}
168
+ </div>;
169
+ }
170
+
171
+ Object.assign(window, { VaultsView, CredentialHealthView, VaultsData: window.VaultsData || {} });