diff --git a/frontend-dapp/src/components/layout/Layout.tsx b/frontend-dapp/src/components/layout/Layout.tsx
index 23a5857..5d3df0e 100644
--- a/frontend-dapp/src/components/layout/Layout.tsx
+++ b/frontend-dapp/src/components/layout/Layout.tsx
@@ -18,6 +18,7 @@ export default function Layout() {
{ to: '/bridge', label: 'Bridge', active: isActive('/bridge') },
{ to: '/swap', label: 'Swap', active: isActive('/swap') },
{ to: '/reserve', label: 'Reserve', active: isActive('/reserve') },
+ { href: '/omnl/gru-vault-controls', label: 'Vault Controls' },
{ to: '/history', label: 'History', active: isActive('/history') },
{ to: '/wallets', label: 'Wallets', active: isActive('/wallets') },
{ to: '/admin', label: 'Admin', active: isActive('/admin') },
@@ -55,6 +56,12 @@ export default function Layout() {
{item.label}
))}
+
+ Vault Controls
+
-
+
{item.label}
diff --git a/frontend-dapp/src/components/layout/OmnlProductLayout.tsx b/frontend-dapp/src/components/layout/OmnlProductLayout.tsx
index 56ff748..2acc75d 100644
--- a/frontend-dapp/src/components/layout/OmnlProductLayout.tsx
+++ b/frontend-dapp/src/components/layout/OmnlProductLayout.tsx
@@ -27,6 +27,7 @@ export default function OmnlProductLayout() {
{ to: '/bridge', label: 'Bridge', active: location.pathname === '/bridge' },
{ to: '/admin', label: 'Admin', active: location.pathname === '/admin' },
{ to: '/wallets', label: 'Wallets', active: location.pathname === '/wallets' },
+ { href: '/omnl/gru-vault-controls', label: 'Vault Controls' },
];
const isBanking = location.pathname === '/central-bank';
@@ -75,6 +76,13 @@ export default function OmnlProductLayout() {
Bridge
+
+
+ Vault Controls
+
diff --git a/frontend-dapp/src/pages/ReservePage.tsx b/frontend-dapp/src/pages/ReservePage.tsx
index 107915e..bb355bc 100644
--- a/frontend-dapp/src/pages/ReservePage.tsx
+++ b/frontend-dapp/src/pages/ReservePage.tsx
@@ -30,7 +30,7 @@ export default function ReservePage() {
}, []);
const reserveLinks = OPERATOR_CONSOLE_LINKS.filter((l) =>
- ['reserve-dash', 'compliance', 'omnl-dash'].includes(l.id),
+ ['reserve-dash', 'compliance', 'omnl-dash', 'gru-controls'].includes(l.id),
);
return (
diff --git a/services/token-aggregation/public/gru-vault-controls.css b/services/token-aggregation/public/gru-vault-controls.css
new file mode 100644
index 0000000..f67b731
--- /dev/null
+++ b/services/token-aggregation/public/gru-vault-controls.css
@@ -0,0 +1,209 @@
+:root {
+ --bg: #090d12;
+ --panel: #111822;
+ --panel-2: #0f1520;
+ --border: #223146;
+ --text: #e8eef8;
+ --muted: #8aa0b8;
+ --accent: #65d6ff;
+ --accent-2: #8cffc1;
+ --warn: #f59e0b;
+ --danger: #ef4444;
+}
+
+* { box-sizing: border-box; }
+body {
+ margin: 0;
+ background:
+ radial-gradient(circle at top left, rgba(101, 214, 255, 0.14), transparent 28%),
+ radial-gradient(circle at top right, rgba(140, 255, 193, 0.12), transparent 24%),
+ var(--bg);
+ color: var(--text);
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
+}
+
+.shell {
+ max-width: 1180px;
+ margin: 0 auto;
+ padding: 24px 18px 40px;
+}
+
+.hero {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 16px;
+ margin-bottom: 16px;
+}
+
+.eyebrow {
+ margin: 0 0 6px;
+ color: var(--accent);
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ font-size: 12px;
+}
+
+h1 {
+ margin: 0;
+ font-size: clamp(2rem, 3vw, 3.2rem);
+}
+
+.sub {
+ color: var(--muted);
+ margin: 8px 0 0;
+ line-height: 1.5;
+}
+
+.toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: center;
+}
+
+button,
+.btn {
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.02);
+ color: var(--text);
+ border-radius: 12px;
+ padding: 11px 14px;
+ font-size: 14px;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+button.primary {
+ background: linear-gradient(135deg, rgba(101, 214, 255, 0.9), rgba(140, 255, 193, 0.86));
+ color: #04111b;
+ border-color: transparent;
+ font-weight: 700;
+}
+
+.banner {
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 14px 16px;
+ margin-bottom: 18px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.banner.loading { color: var(--muted); }
+.banner.ok { border-color: rgba(140, 255, 193, 0.35); }
+.banner.error { border-color: rgba(239, 68, 68, 0.5); color: #fecaca; }
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 16px;
+}
+
+.card {
+ background: linear-gradient(180deg, rgba(17, 24, 34, 0.96), rgba(11, 16, 25, 0.96));
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ padding: 18px;
+ margin-bottom: 16px;
+}
+
+.card h2 {
+ margin: 0 0 14px;
+ font-size: 18px;
+}
+
+.kv {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.kv div,
+.form-grid label {
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 14px;
+ padding: 12px;
+}
+
+.kv span,
+.form-grid span {
+ display: block;
+ color: var(--muted);
+ font-size: 12px;
+ margin-bottom: 6px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.kv strong {
+ font-size: 16px;
+}
+
+.note,
+.form-grid small {
+ color: var(--muted);
+ display: block;
+ margin-top: 10px;
+ line-height: 1.4;
+}
+
+.form-grid {
+ display: grid;
+ gap: 14px;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+label input,
+label select,
+label textarea {
+ width: 100%;
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ background: rgba(5, 8, 13, 0.9);
+ color: var(--text);
+ padding: 10px 12px;
+ font-size: 14px;
+}
+
+label textarea {
+ resize: vertical;
+ min-height: 118px;
+}
+
+label.switch {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+label.switch input {
+ width: 18px;
+ height: 18px;
+ margin: 0;
+}
+
+pre {
+ background: rgba(5, 8, 13, 0.95);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 14px;
+ overflow: auto;
+ max-height: 360px;
+ font-size: 12px;
+}
+
+.hidden { display: none; }
+
+@media (max-width: 900px) {
+ .hero,
+ .grid,
+ .form-grid,
+ .kv {
+ grid-template-columns: 1fr;
+ }
+
+ .hero {
+ flex-direction: column;
+ }
+}
diff --git a/services/token-aggregation/public/gru-vault-controls.html b/services/token-aggregation/public/gru-vault-controls.html
new file mode 100644
index 0000000..d399702
--- /dev/null
+++ b/services/token-aggregation/public/gru-vault-controls.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
GRU Smart Vault Controls
+
+
+
+
+
+
+
Initializing…
+
+
+
+
+ Canonical ladder
+
+
Expression1 M00 = 5 M0 = 25 M1
+
M00 → M05x fixed
+
M0 → M15x fixed
+
+
+ The dashboard can tune runtime policy references and component selections, but the canonical
+ ratio is hard-coded and cannot be changed here.
+
+
+
+
+
+
+
+
+
+ Component families
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/token-aggregation/public/gru-vault-controls.js b/services/token-aggregation/public/gru-vault-controls.js
new file mode 100644
index 0000000..ea9967c
--- /dev/null
+++ b/services/token-aggregation/public/gru-vault-controls.js
@@ -0,0 +1,147 @@
+(function () {
+ const apiBaseResolver = window.resolveOmnlApiBase || (() => '/api/v1');
+ const apiBase = apiBaseResolver().replace(/\/$/, '');
+ const params = new URLSearchParams(window.location.search);
+ const accessToken = params.get('access_token')?.trim() || '';
+ const authQuery = accessToken ? `?access_token=${encodeURIComponent(accessToken)}` : '';
+
+ const els = {
+ status: document.getElementById('status'),
+ updatedAt: document.getElementById('updated-at'),
+ profile: document.getElementById('profile'),
+ withdrawFraction: document.getElementById('withdraw-fraction'),
+ reserveMultiplier: document.getElementById('reserve-multiplier'),
+ refreshInterval: document.getElementById('refresh-interval'),
+ cstarEnabled: document.getElementById('cstar-enabled'),
+ expandedEnabled: document.getElementById('expanded-enabled'),
+ bondEnabled: document.getElementById('bond-enabled'),
+ liComponents: document.getElementById('li-components'),
+ bondComponents: document.getElementById('bond-components'),
+ expandedComponents: document.getElementById('expanded-components'),
+ rawSection: document.getElementById('raw-section'),
+ rawJson: document.getElementById('raw-json'),
+ btnRefresh: document.getElementById('btn-refresh'),
+ btnSave: document.getElementById('btn-save'),
+ btnReset: document.getElementById('btn-reset'),
+ btnRaw: document.getElementById('btn-raw'),
+ };
+
+ function listToText(list) {
+ return Array.isArray(list) ? list.join(', ') : '';
+ }
+
+ function textToList(text) {
+ return String(text || '')
+ .split(/[\n,]/)
+ .map((value) => value.trim())
+ .filter(Boolean);
+ }
+
+ function setStatus(type, message) {
+ els.status.className = `banner ${type}`;
+ els.status.textContent = message;
+ }
+
+ function apiUrl(path) {
+ return `${apiBase}${path}${authQuery}`;
+ }
+
+ function readForm() {
+ return {
+ profile: els.profile.value,
+ smartVault: {
+ liXauWithdrawalFraction: Number(els.withdrawFraction.value),
+ cStarSettlementEnabled: Boolean(els.cstarEnabled.checked),
+ expandedAssetClassesEnabled: Boolean(els.expandedEnabled.checked),
+ approvedBondBasketEnabled: Boolean(els.bondEnabled.checked),
+ },
+ componentFamilies: {
+ liIndexComponents: textToList(els.liComponents.value),
+ approvedBondComponents: textToList(els.bondComponents.value),
+ expandedAssetComponents: textToList(els.expandedComponents.value),
+ },
+ dashboard: {
+ reserveReferenceMultiplier: Number(els.reserveMultiplier.value),
+ refreshIntervalSeconds: Number(els.refreshInterval.value),
+ },
+ };
+ }
+
+ function fillForm(snapshot) {
+ const controls = snapshot.controls || snapshot;
+ els.profile.value = controls.profile || 'live';
+ els.withdrawFraction.value = controls.smartVault?.liXauWithdrawalFraction ?? 0.2;
+ els.reserveMultiplier.value = controls.dashboard?.reserveReferenceMultiplier ?? 1.2;
+ els.refreshInterval.value = controls.dashboard?.refreshIntervalSeconds ?? 30;
+ els.cstarEnabled.checked = Boolean(controls.smartVault?.cStarSettlementEnabled);
+ els.expandedEnabled.checked = Boolean(controls.smartVault?.expandedAssetClassesEnabled);
+ els.bondEnabled.checked = Boolean(controls.smartVault?.approvedBondBasketEnabled);
+ els.liComponents.value = listToText(controls.componentFamilies?.liIndexComponents);
+ els.bondComponents.value = listToText(controls.componentFamilies?.approvedBondComponents);
+ els.expandedComponents.value = listToText(controls.componentFamilies?.expandedAssetComponents);
+ els.updatedAt.textContent = `Updated: ${controls.updatedAt || snapshot.updatedAt || '—'} · Profile: ${controls.profile || 'live'}`;
+ els.rawJson.textContent = JSON.stringify(snapshot, null, 2);
+ }
+
+ async function loadControls() {
+ setStatus('loading', 'Loading GRU vault controls…');
+ try {
+ const res = await fetch(apiUrl('/omnl/gru-vault-controls'), { cache: 'no-store' });
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data?.error || `HTTP ${res.status}`);
+ }
+ fillForm(data);
+ setStatus('ok', 'GRU vault controls loaded. Changes are persisted server-side.');
+ } catch (error) {
+ setStatus('error', `Load failed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ async function saveControls() {
+ setStatus('loading', 'Saving runtime controls…');
+ try {
+ const payload = readForm();
+ const res = await fetch(apiUrl('/omnl/gru-vault-controls'), {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ });
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data?.error || `HTTP ${res.status}`);
+ }
+ fillForm(data);
+ setStatus('ok', 'Runtime controls saved without redeploying.');
+ } catch (error) {
+ setStatus('error', `Save failed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ async function resetControls() {
+ setStatus('loading', 'Resetting controls to defaults…');
+ try {
+ const res = await fetch(apiUrl('/omnl/gru-vault-controls/reset'), {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ });
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data?.error || `HTTP ${res.status}`);
+ }
+ fillForm(data);
+ setStatus('ok', 'Controls reset to defaults.');
+ } catch (error) {
+ setStatus('error', `Reset failed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ els.btnRefresh?.addEventListener('click', loadControls);
+ els.btnSave?.addEventListener('click', saveControls);
+ els.btnReset?.addEventListener('click', resetControls);
+ els.btnRaw?.addEventListener('click', () => {
+ els.rawSection?.classList.toggle('hidden');
+ });
+
+ loadControls();
+})();
diff --git a/services/token-aggregation/public/reserve-dashboard.html b/services/token-aggregation/public/reserve-dashboard.html
index bc7db2e..b157514 100644
--- a/services/token-aggregation/public/reserve-dashboard.html
+++ b/services/token-aggregation/public/reserve-dashboard.html
@@ -17,6 +17,7 @@
diff --git a/services/token-aggregation/reports/audit/omnl-audit.jsonl b/services/token-aggregation/reports/audit/omnl-audit.jsonl
index 0b900d3..1d86ba0 100644
--- a/services/token-aggregation/reports/audit/omnl-audit.jsonl
+++ b/services/token-aggregation/reports/audit/omnl-audit.jsonl
@@ -4,3 +4,17 @@
{"id":"4d21094d-56fd-4324-a75b-1075f9a5dd59","ts":"2026-07-06T00:23:58.822Z","category":"reconcile","action":"triple_state_reconcile","traceId":"0xfefb7411285a639b00be06bbffe897eb802ee817983aa57facf5bde6a1b52ff4","status":"error","metadata":{"breakCount":3,"aligned":false}}
{"id":"9b28b030-b396-4e07-9aa8-47854797096c","ts":"2026-07-06T00:28:58.771Z","category":"reconcile","action":"triple_state_reconcile","traceId":"0xfefb7411285a639b00be06bbffe897eb802ee817983aa57facf5bde6a1b52ff4","status":"error","metadata":{"breakCount":3,"aligned":false}}
{"id":"d7ea5f0c-5827-4795-ae98-23f958b92646","ts":"2026-07-06T00:29:30.710Z","category":"reconcile","action":"triple_state_reconcile","traceId":"0xfefb7411285a639b00be06bbffe897eb802ee817983aa57facf5bde6a1b52ff4","status":"error","metadata":{"breakCount":3,"aligned":false}}
+{"id":"c284835f-23c1-4f7f-9648-0558da9fd540","ts":"2026-07-12T22:29:52.477Z","category":"api","action":"GET /omnl/gru-vault-controls","sourceSystem":"http","traceId":"e9598ce7-448f-44e0-bbea-0366d9d4108f","status":"ok","metadata":{"statusCode":200,"durationMs":9,"query":{}}}
+{"id":"93b1b6c2-58c5-4379-841c-8568f8c85524","ts":"2026-07-12T22:29:52.578Z","category":"api","action":"update_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"conservative","liIndexComponents":2,"approvedBondComponents":2,"expandedAssetComponents":2,"liXauWithdrawalFraction":0.15,"reserveReferenceMultiplier":1.35,"refreshIntervalSeconds":45}}
+{"id":"e02b9819-3040-458d-828e-9a045d818667","ts":"2026-07-12T22:29:52.582Z","category":"api","action":"PUT /omnl/gru-vault-controls","sourceSystem":"http","traceId":"6967adc2-e7c8-4d8d-b05a-b727f43cbc82","status":"ok","metadata":{"statusCode":200,"durationMs":10,"query":{}}}
+{"id":"b85e62ba-1cd1-41e7-a3ce-f7b32db46808","ts":"2026-07-12T22:29:52.609Z","category":"api","action":"update_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"emergency","liIndexComponents":5,"approvedBondComponents":0,"expandedAssetComponents":4,"liXauWithdrawalFraction":0.3,"reserveReferenceMultiplier":1.2,"refreshIntervalSeconds":30}}
+{"id":"0847e9a4-6d0d-44b2-9024-52653806cd02","ts":"2026-07-12T22:29:52.611Z","category":"api","action":"PUT /omnl/gru-vault-controls","sourceSystem":"http","traceId":"399f9edb-15f0-4dad-9b96-faf579336f4e","status":"ok","metadata":{"statusCode":200,"durationMs":3,"query":{}}}
+{"id":"fbc5fc45-7ded-4174-b316-49a990b01120","ts":"2026-07-12T22:29:52.624Z","category":"api","action":"reset_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"live"}}
+{"id":"420b3ed9-88e6-4748-a9b8-2697589e8c1c","ts":"2026-07-12T22:29:52.626Z","category":"api","action":"POST /omnl/gru-vault-controls/reset","sourceSystem":"http","traceId":"ae0ec963-aca9-461c-833c-b7d2dab1c7db","status":"ok","metadata":{"statusCode":200,"durationMs":4,"query":{}}}
+{"id":"f05dfe1e-46b1-451e-af96-9bf35b0e32c5","ts":"2026-07-13T02:44:19.551Z","category":"api","action":"GET /omnl/gru-vault-controls","sourceSystem":"http","traceId":"57ea31ed-9a77-4b64-9632-65aca80f9790","status":"ok","metadata":{"statusCode":200,"durationMs":1,"query":{}}}
+{"id":"862adb2f-3dad-4c23-bb38-a757e5230c5c","ts":"2026-07-13T02:44:19.564Z","category":"api","action":"update_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"conservative","liIndexComponents":2,"approvedBondComponents":2,"expandedAssetComponents":2,"liXauWithdrawalFraction":0.15,"reserveReferenceMultiplier":1.35,"refreshIntervalSeconds":45}}
+{"id":"d67a4596-d810-49b3-9114-26b1432c05be","ts":"2026-07-13T02:44:19.564Z","category":"api","action":"PUT /omnl/gru-vault-controls","sourceSystem":"http","traceId":"2884e0c9-4bae-427f-a92c-4155b65315aa","status":"ok","metadata":{"statusCode":200,"durationMs":2,"query":{}}}
+{"id":"09b79e2a-57d3-4181-b5fc-80fc192241e6","ts":"2026-07-13T02:44:19.571Z","category":"api","action":"update_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"emergency","liIndexComponents":5,"approvedBondComponents":0,"expandedAssetComponents":4,"liXauWithdrawalFraction":0.3,"reserveReferenceMultiplier":1.2,"refreshIntervalSeconds":30}}
+{"id":"190eb004-8f6c-416f-987b-0cfb8e0ad0aa","ts":"2026-07-13T02:44:19.572Z","category":"api","action":"PUT /omnl/gru-vault-controls","sourceSystem":"http","traceId":"7f4d8191-f591-42f0-8aa5-7697f16fcfc2","status":"ok","metadata":{"statusCode":200,"durationMs":2,"query":{}}}
+{"id":"8e26662c-e63a-40c7-8e5d-304309b580a6","ts":"2026-07-13T02:44:19.575Z","category":"api","action":"reset_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"live"}}
+{"id":"c0a112aa-569c-47e6-a610-87d6372401aa","ts":"2026-07-13T02:44:19.576Z","category":"api","action":"POST /omnl/gru-vault-controls/reset","sourceSystem":"http","traceId":"0e51cf72-c161-4b6c-98a5-f96a5bdafa20","status":"ok","metadata":{"statusCode":200,"durationMs":2,"query":{}}}
diff --git a/services/token-aggregation/src/api/routes/gru-vault-controls.test.ts b/services/token-aggregation/src/api/routes/gru-vault-controls.test.ts
new file mode 100644
index 0000000..572c5cc
--- /dev/null
+++ b/services/token-aggregation/src/api/routes/gru-vault-controls.test.ts
@@ -0,0 +1,149 @@
+import { createServer } from 'http';
+import express from 'express';
+import fs from 'fs/promises';
+import path from 'path';
+import gruVaultControlsRoutes from './gru-vault-controls';
+
+jest.mock('../middleware/cache');
+
+function createApp() {
+ const app = express();
+ app.use(express.json());
+ app.use('/api/v1', gruVaultControlsRoutes);
+ return app;
+}
+
+async function startServer(app: express.Application): Promise<{ server: ReturnType
; baseUrl: string }> {
+ const server = createServer(app);
+ await new Promise((resolve) => server.listen(0, () => resolve()));
+ const port = (server.address() as { port: number }).port;
+ return { server, baseUrl: `http://127.0.0.1:${port}` };
+}
+
+describe('GRU vault controls API', () => {
+ let server: ReturnType;
+ let baseUrl: string;
+ const originalApiKey = process.env.OMNL_API_KEY;
+ const originalControlsPath = process.env.GRU_VAULT_CONTROLS_PATH;
+
+ beforeAll(async () => {
+ process.env.OMNL_API_KEY = 'test-omnl-key';
+ process.env.GRU_VAULT_CONTROLS_PATH = path.join('/tmp', `gru-vault-controls-${Date.now()}.json`);
+ const started = await startServer(createApp());
+ server = started.server;
+ baseUrl = started.baseUrl;
+ });
+
+ afterEach(async () => {
+ await fs.rm(process.env.GRU_VAULT_CONTROLS_PATH || '', { force: true }).catch(() => undefined);
+ process.env.GRU_VAULT_CONTROLS_PATH = path.join('/tmp', `gru-vault-controls-${Date.now()}-${Math.random()}.json`);
+ });
+
+ afterAll((done) => {
+ if (originalApiKey === undefined) {
+ delete process.env.OMNL_API_KEY;
+ } else {
+ process.env.OMNL_API_KEY = originalApiKey;
+ }
+ if (originalControlsPath === undefined) {
+ delete process.env.GRU_VAULT_CONTROLS_PATH;
+ } else {
+ process.env.GRU_VAULT_CONTROLS_PATH = originalControlsPath;
+ }
+ server.close(done);
+ });
+
+ it('returns canonical defaults when no runtime file exists', async () => {
+ const res = await fetch(`${baseUrl}/api/v1/omnl/gru-vault-controls`, {
+ headers: { Authorization: 'Bearer test-omnl-key' },
+ });
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as Record;
+ expect(body.controls.canonicalLadder).toEqual({
+ expression: '1 M00 = 5 M0 = 25 M1',
+ m00ToM0Ratio: 5,
+ m0ToM1Ratio: 5,
+ });
+ expect(body.controls.smartVault.liXauWithdrawalFraction).toBeCloseTo(0.2);
+ expect(body.controls.componentFamilies.liIndexComponents).toEqual(
+ expect.arrayContaining(['LiXAU', 'LiPMG', 'LiBMG1', 'LiBMG2', 'LiBMG3'])
+ );
+ });
+
+ it('persists runtime adjustments without changing the canonical ladder', async () => {
+ const payload = {
+ profile: 'conservative',
+ smartVault: {
+ liXauWithdrawalFraction: 0.15,
+ cStarSettlementEnabled: false,
+ expandedAssetClassesEnabled: true,
+ approvedBondBasketEnabled: true,
+ },
+ componentFamilies: {
+ liIndexComponents: ['LiXAU', 'LiPMG'],
+ approvedBondComponents: ['Bond-A', 'Bond-B'],
+ expandedAssetComponents: ['LiCARB', 'LiRES1'],
+ },
+ dashboard: {
+ reserveReferenceMultiplier: 1.35,
+ refreshIntervalSeconds: 45,
+ },
+ canonicalLadder: {
+ expression: 'bad',
+ m00ToM0Ratio: 7,
+ m0ToM1Ratio: 9,
+ },
+ };
+
+ const res = await fetch(`${baseUrl}/api/v1/omnl/gru-vault-controls`, {
+ method: 'PUT',
+ headers: {
+ Authorization: 'Bearer test-omnl-key',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as Record;
+ expect(body.controls.profile).toBe('conservative');
+ expect(body.controls.smartVault.liXauWithdrawalFraction).toBeCloseTo(0.15);
+ expect(body.controls.smartVault.cStarSettlementEnabled).toBe(false);
+ expect(body.controls.componentFamilies.liIndexComponents).toEqual(['LiXAU', 'LiPMG']);
+ expect(body.controls.canonicalLadder).toEqual({
+ expression: '1 M00 = 5 M0 = 25 M1',
+ m00ToM0Ratio: 5,
+ m0ToM1Ratio: 5,
+ });
+
+ const stored = JSON.parse(await fs.readFile(process.env.GRU_VAULT_CONTROLS_PATH || '', 'utf8')) as Record;
+ expect(stored.profile).toBe('conservative');
+ expect(stored.canonicalLadder).toEqual({
+ expression: '1 M00 = 5 M0 = 25 M1',
+ m00ToM0Ratio: 5,
+ m0ToM1Ratio: 5,
+ });
+ });
+
+ it('can reset controls to defaults', async () => {
+ await fetch(`${baseUrl}/api/v1/omnl/gru-vault-controls`, {
+ method: 'PUT',
+ headers: {
+ Authorization: 'Bearer test-omnl-key',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ profile: 'emergency',
+ smartVault: { liXauWithdrawalFraction: 0.3 },
+ }),
+ });
+
+ const res = await fetch(`${baseUrl}/api/v1/omnl/gru-vault-controls/reset`, {
+ method: 'POST',
+ headers: { Authorization: 'Bearer test-omnl-key' },
+ });
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as Record;
+ expect(body.controls.profile).toBe('live');
+ expect(body.controls.smartVault.liXauWithdrawalFraction).toBeCloseTo(0.2);
+ });
+});
diff --git a/services/token-aggregation/src/api/routes/gru-vault-controls.ts b/services/token-aggregation/src/api/routes/gru-vault-controls.ts
new file mode 100644
index 0000000..27c2804
--- /dev/null
+++ b/services/token-aggregation/src/api/routes/gru-vault-controls.ts
@@ -0,0 +1,80 @@
+import { Router, Request, Response } from 'express';
+import { omnlRateLimiter } from '../middleware/rate-limit';
+import { omnlAuditMiddleware } from '../middleware/omnl-audit-middleware';
+import { omnlSensitiveRouteGuard, omnlRequireApiKeyInProduction } from '../middleware/omnl-guards';
+import {
+ getDefaultGruVaultControls,
+ getGruVaultControlsPath,
+ loadGruVaultControls,
+ resetGruVaultControls,
+ saveGruVaultControls,
+} from '../../services/gru-vault-controls';
+import { appendOmnlAudit } from '../../services/omnl-audit-log';
+
+const router = Router();
+
+router.use(omnlRateLimiter);
+router.use(omnlAuditMiddleware);
+router.use(omnlRequireApiKeyInProduction);
+
+function extractBody(req: Request): unknown {
+ const body = req.body as Record | undefined;
+ if (!body) return {};
+ if (body.controls && typeof body.controls === 'object') return body.controls;
+ return body;
+}
+
+router.get('/omnl/gru-vault-controls', omnlSensitiveRouteGuard, (_req: Request, res: Response) => {
+ res.json(loadGruVaultControls());
+});
+
+router.get('/omnl/gru-vault-controls/defaults', omnlSensitiveRouteGuard, (_req: Request, res: Response) => {
+ res.json({
+ path: getGruVaultControlsPath(),
+ controls: getDefaultGruVaultControls(),
+ });
+});
+
+router.put('/omnl/gru-vault-controls', omnlSensitiveRouteGuard, (req: Request, res: Response) => {
+ try {
+ const snapshot = saveGruVaultControls(extractBody(req));
+ appendOmnlAudit({
+ category: 'api',
+ action: 'update_gru_vault_controls',
+ actor: String(req.headers['x-omnl-actor'] ?? req.headers['x-forwarded-user'] ?? 'operator'),
+ status: 'ok',
+ metadata: {
+ profile: snapshot.controls.profile,
+ liIndexComponents: snapshot.controls.componentFamilies.liIndexComponents.length,
+ approvedBondComponents: snapshot.controls.componentFamilies.approvedBondComponents.length,
+ expandedAssetComponents: snapshot.controls.componentFamilies.expandedAssetComponents.length,
+ liXauWithdrawalFraction: snapshot.controls.smartVault.liXauWithdrawalFraction,
+ reserveReferenceMultiplier: snapshot.controls.dashboard.reserveReferenceMultiplier,
+ refreshIntervalSeconds: snapshot.controls.dashboard.refreshIntervalSeconds,
+ },
+ });
+ res.json(snapshot);
+ } catch (e) {
+ const msg = e instanceof Error ? e.message : String(e);
+ res.status(400).json({ error: msg });
+ }
+});
+
+router.post('/omnl/gru-vault-controls/reset', omnlSensitiveRouteGuard, (_req: Request, res: Response) => {
+ try {
+ const snapshot = resetGruVaultControls();
+ appendOmnlAudit({
+ category: 'api',
+ action: 'reset_gru_vault_controls',
+ actor: String(_req.headers['x-omnl-actor'] ?? _req.headers['x-forwarded-user'] ?? 'operator'),
+ status: 'ok',
+ metadata: { profile: snapshot.controls.profile },
+ });
+ res.json(snapshot);
+ } catch (e) {
+ const msg = e instanceof Error ? e.message : String(e);
+ res.status(400).json({ error: msg });
+ }
+});
+
+export default router;
diff --git a/services/token-aggregation/src/api/server.ts b/services/token-aggregation/src/api/server.ts
index 6a3347c..92ceb2a 100644
--- a/services/token-aggregation/src/api/server.ts
+++ b/services/token-aggregation/src/api/server.ts
@@ -26,6 +26,7 @@ import omnlIpsasRoutes from './routes/omnl-ipsas';
import omnlPayoutJournalRoutes from './routes/omnl-payout-journal-routes';
import omnlComplianceRoutes from './routes/omnl-compliance-routes';
import omnlTerminalRoutes from './routes/omnl-terminal-routes';
+import gruVaultControlsRoutes from './routes/gru-vault-controls';
import zWalletRoutes from './routes/z-wallet';
import zBotRoutes from './routes/z-bot';
import checkpointRoutes from './routes/checkpoint';
@@ -193,6 +194,7 @@ export class ApiServer {
const settlementTerminalPath = path.join(__dirname, '../../public/omnl-settlement-terminal.html');
const reserveDashboardPath = path.join(__dirname, '../../public/reserve-dashboard.html');
const reserveInstitutionalPath = path.join(__dirname, '../../public/reserve-institutional.html');
+ const gruVaultControlsPath = path.join(__dirname, '../../public/gru-vault-controls.html');
const authorizeOmnlHtml = (req: Request, res: Response): boolean => {
const tok = process.env.OMNL_DASHBOARD_TOKEN?.trim();
@@ -252,6 +254,15 @@ export class ApiServer {
res.type('html').send(readFileSync(file, 'utf8'));
});
+ this.app.get('/omnl/gru-vault-controls', (req: Request, res: Response) => {
+ if (!authorizeOmnlHtml(req, res)) return;
+ if (!existsSync(gruVaultControlsPath)) {
+ res.status(404).type('text/plain').send('gru-vault-controls.html missing');
+ return;
+ }
+ res.type('html').send(readFileSync(gruVaultControlsPath, 'utf8'));
+ });
+
// Public API catalog (register before routers so GET /api/v1 is not swallowed by middleware-only mounts)
const sendApiV1Catalog = (_req: Request, res: Response) => {
res.json({
@@ -321,6 +332,7 @@ export class ApiServer {
this.app.use('/api/v1/prices/metamask', metamaskPriceRoutes);
this.app.use('/api/v1', omnlComplianceRoutes);
this.app.use('/api/v1', omnlTerminalRoutes);
+ this.app.use('/api/v1', gruVaultControlsRoutes);
this.app.use('/api/v1', omnlRoutes);
this.app.use('/api/v1', omnlIpsasRoutes);
this.app.use('/api/v1', omnlPayoutJournalRoutes);
diff --git a/services/token-aggregation/src/services/gru-vault-controls.ts b/services/token-aggregation/src/services/gru-vault-controls.ts
new file mode 100644
index 0000000..7fd0e1f
--- /dev/null
+++ b/services/token-aggregation/src/services/gru-vault-controls.ts
@@ -0,0 +1,243 @@
+import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
+import { dirname, resolve } from 'path';
+
+export type GruVaultProfile = 'live' | 'conservative' | 'expanded' | 'emergency';
+
+export interface GruVaultControls {
+ schemaVersion: '1.0.0';
+ updatedAt: string;
+ profile: GruVaultProfile;
+ canonicalLadder: {
+ expression: '1 M00 = 5 M0 = 25 M1';
+ m00ToM0Ratio: 5;
+ m0ToM1Ratio: 5;
+ };
+ smartVault: {
+ liXauWithdrawalFraction: number;
+ cStarSettlementEnabled: boolean;
+ expandedAssetClassesEnabled: boolean;
+ approvedBondBasketEnabled: boolean;
+ };
+ componentFamilies: {
+ liIndexComponents: string[];
+ approvedBondComponents: string[];
+ expandedAssetComponents: string[];
+ };
+ dashboard: {
+ reserveReferenceMultiplier: number;
+ refreshIntervalSeconds: number;
+ notes: string;
+ };
+}
+
+export interface GruVaultControlsSnapshot {
+ source: 'defaults' | 'file';
+ path: string;
+ controls: GruVaultControls;
+}
+
+const DEFAULT_CONTROLS: GruVaultControls = {
+ schemaVersion: '1.0.0',
+ updatedAt: new Date(0).toISOString(),
+ profile: 'live',
+ canonicalLadder: {
+ expression: '1 M00 = 5 M0 = 25 M1',
+ m00ToM0Ratio: 5,
+ m0ToM1Ratio: 5,
+ },
+ smartVault: {
+ liXauWithdrawalFraction: 0.2,
+ cStarSettlementEnabled: true,
+ expandedAssetClassesEnabled: false,
+ approvedBondBasketEnabled: true,
+ },
+ componentFamilies: {
+ liIndexComponents: ['LiXAU', 'LiPMG', 'LiBMG1', 'LiBMG2', 'LiBMG3'],
+ approvedBondComponents: [],
+ expandedAssetComponents: ['LiCARB', 'LiRES1', 'LiRES2', 'LiRES3'],
+ },
+ dashboard: {
+ reserveReferenceMultiplier: 1.2,
+ refreshIntervalSeconds: 30,
+ notes: 'Runtime policy knobs for the smart-vault dashboard; canonical ladder remains fixed.',
+ },
+};
+
+const ALLOWED_PROFILES: GruVaultProfile[] = ['live', 'conservative', 'expanded', 'emergency'];
+
+function projectRoot(): string {
+ return (
+ process.env.PROXMOX_ROOT?.trim() ||
+ process.env.PHOENIX_REPO_ROOT?.trim() ||
+ resolve(__dirname, '../../../../../..')
+ );
+}
+
+function resolveControlsPath(): string {
+ const configured = process.env.GRU_VAULT_CONTROLS_PATH?.trim();
+ if (configured) return resolve(configured);
+ return resolve(projectRoot(), 'reports/status/gru-vault-controls-latest.json');
+}
+
+function cloneDefaults(): GruVaultControls {
+ return JSON.parse(JSON.stringify(DEFAULT_CONTROLS)) as GruVaultControls;
+}
+
+function parseStringList(value: unknown): string[] {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => String(item).trim())
+ .filter((item) => item.length > 0);
+ }
+ if (typeof value === 'string') {
+ return value
+ .split(/[\n,]/)
+ .map((item) => item.trim())
+ .filter((item) => item.length > 0);
+ }
+ return [];
+}
+
+function clampNumber(value: unknown, fallback: number, min: number, max: number): number {
+ const n = Number(value);
+ if (!Number.isFinite(n)) return fallback;
+ return Math.min(max, Math.max(min, n));
+}
+
+function parseBoolean(value: unknown, fallback: boolean): boolean {
+ if (typeof value === 'boolean') return value;
+ if (typeof value === 'string') {
+ const normalized = value.trim().toLowerCase();
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
+ }
+ return fallback;
+}
+
+function normalizeProfile(value: unknown, fallback: GruVaultProfile): GruVaultProfile {
+ if (typeof value === 'string' && ALLOWED_PROFILES.includes(value as GruVaultProfile)) {
+ return value as GruVaultProfile;
+ }
+ return fallback;
+}
+
+function normalizeControls(raw: unknown, updatedAt?: string): GruVaultControls {
+ const defaults = cloneDefaults();
+ const input = (raw && typeof raw === 'object' ? raw : {}) as Partial & {
+ smartVault?: Partial;
+ componentFamilies?: Partial;
+ dashboard?: Partial;
+ };
+
+ const controls: GruVaultControls = {
+ ...defaults,
+ profile: normalizeProfile(input.profile, defaults.profile),
+ updatedAt: updatedAt || defaults.updatedAt,
+ canonicalLadder: defaults.canonicalLadder,
+ smartVault: {
+ liXauWithdrawalFraction: clampNumber(
+ input.smartVault?.liXauWithdrawalFraction,
+ defaults.smartVault.liXauWithdrawalFraction,
+ 0.05,
+ 0.5,
+ ),
+ cStarSettlementEnabled: parseBoolean(
+ input.smartVault?.cStarSettlementEnabled,
+ defaults.smartVault.cStarSettlementEnabled,
+ ),
+ expandedAssetClassesEnabled: parseBoolean(
+ input.smartVault?.expandedAssetClassesEnabled,
+ defaults.smartVault.expandedAssetClassesEnabled,
+ ),
+ approvedBondBasketEnabled: parseBoolean(
+ input.smartVault?.approvedBondBasketEnabled,
+ defaults.smartVault.approvedBondBasketEnabled,
+ ),
+ },
+ componentFamilies: {
+ liIndexComponents: parseStringList(input.componentFamilies?.liIndexComponents).length
+ ? parseStringList(input.componentFamilies?.liIndexComponents)
+ : defaults.componentFamilies.liIndexComponents,
+ approvedBondComponents: parseStringList(input.componentFamilies?.approvedBondComponents),
+ expandedAssetComponents: parseStringList(input.componentFamilies?.expandedAssetComponents).length
+ ? parseStringList(input.componentFamilies?.expandedAssetComponents)
+ : defaults.componentFamilies.expandedAssetComponents,
+ },
+ dashboard: {
+ reserveReferenceMultiplier: clampNumber(
+ input.dashboard?.reserveReferenceMultiplier,
+ defaults.dashboard.reserveReferenceMultiplier,
+ 0.5,
+ 5,
+ ),
+ refreshIntervalSeconds: Math.round(
+ clampNumber(input.dashboard?.refreshIntervalSeconds, defaults.dashboard.refreshIntervalSeconds, 5, 300),
+ ),
+ notes:
+ typeof input.dashboard?.notes === 'string' && input.dashboard.notes.trim().length > 0
+ ? input.dashboard.notes.trim()
+ : defaults.dashboard.notes,
+ },
+ };
+
+ if (!controls.smartVault.cStarSettlementEnabled) {
+ controls.smartVault.cStarSettlementEnabled = false;
+ }
+
+ return controls;
+}
+
+export function getGruVaultControlsPath(): string {
+ return resolveControlsPath();
+}
+
+export function getDefaultGruVaultControls(): GruVaultControls {
+ return cloneDefaults();
+}
+
+export function loadGruVaultControls(): GruVaultControlsSnapshot {
+ const path = resolveControlsPath();
+ if (!existsSync(path)) {
+ return {
+ source: 'defaults',
+ path,
+ controls: cloneDefaults(),
+ };
+ }
+
+ try {
+ const raw = JSON.parse(readFileSync(path, 'utf8')) as unknown;
+ const updatedAt = typeof (raw as { updatedAt?: unknown })?.updatedAt === 'string'
+ ? String((raw as { updatedAt?: unknown }).updatedAt)
+ : new Date().toISOString();
+ return {
+ source: 'file',
+ path,
+ controls: normalizeControls(raw, updatedAt),
+ };
+ } catch {
+ return {
+ source: 'defaults',
+ path,
+ controls: cloneDefaults(),
+ };
+ }
+}
+
+export function saveGruVaultControls(raw: unknown): GruVaultControlsSnapshot {
+ const path = resolveControlsPath();
+ const controls = normalizeControls(raw, new Date().toISOString());
+ mkdirSync(dirname(path), { recursive: true });
+ const tmp = `${path}.tmp`;
+ writeFileSync(tmp, `${JSON.stringify(controls, null, 2)}\n`, 'utf8');
+ renameSync(tmp, path);
+ return {
+ source: 'file',
+ path,
+ controls,
+ };
+}
+
+export function resetGruVaultControls(): GruVaultControlsSnapshot {
+ return saveGruVaultControls(cloneDefaults());
+}
diff --git a/services/token-aggregation/src/services/omnl-api-catalog.ts b/services/token-aggregation/src/services/omnl-api-catalog.ts
index c86a7e8..3bbaf16 100644
--- a/services/token-aggregation/src/services/omnl-api-catalog.ts
+++ b/services/token-aggregation/src/services/omnl-api-catalog.ts
@@ -68,6 +68,10 @@ export function getOmnlApiCatalog(): {
{ method: 'POST', path: '/omnl/terminal/alchemy/rpc', description: 'Alchemy JSON-RPC (read/estimate methods)', body: '{ network, method, params }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/prices', description: 'Alchemy token prices by address', body: '{ network, addresses[] }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/tx-prep', description: 'Prepare unsigned ERC-20 transfer calldata via Alchemy (dry-run default)', body: '{ network, from, to, tokenAddress, amount, dryRun }', auth: 'OMNL_API_KEY' },
+ { method: 'GET', path: '/omnl/gru-vault-controls', description: 'GRU smart-vault runtime control snapshot', auth: 'OMNL_API_KEY' },
+ { method: 'GET', path: '/omnl/gru-vault-controls/defaults', description: 'GRU smart-vault control defaults', auth: 'OMNL_API_KEY' },
+ { method: 'PUT', path: '/omnl/gru-vault-controls', description: 'Persist GRU smart-vault runtime controls', body: '{ profile, smartVault, componentFamilies, dashboard }', auth: 'OMNL_API_KEY' },
+ { method: 'POST', path: '/omnl/gru-vault-controls/reset', description: 'Reset GRU smart-vault runtime controls to defaults', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/journal-entries', description: 'Create payout journal entry draft (IPSAS-validated pair)', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/journal-entries/:id', description: 'Get journal entry by id', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/journal-entries/:id/post', description: 'Post journal entry to ledger path', auth: 'OMNL_API_KEY' },