Add AISBF broker settings UI and docs

parent dfe27069
...@@ -247,6 +247,45 @@ Config files live in `~/.coderai/` (or `--config` path): ...@@ -247,6 +247,45 @@ Config files live in `~/.coderai/` (or `--config` path):
└── secret_key # Session signing key (auto-generated) └── secret_key # Session signing key (auto-generated)
``` ```
### AISBF Broker Client
CoderAI includes an AISBF broker websocket client that can register this instance
with a broker and receive brokered requests.
You can configure it either by editing `config.json` directly or from the admin
Settings page under `AISBF Broker`.
Example broker configuration:
```json
{
"broker": {
"enabled": true,
"base_url": "https://broker.example.com",
"scope": "user",
"username": "alice",
"provider_id": "coderai-local",
"client_id": "workstation-01",
"registration_token": "your-registration-token",
"advertised_endpoint": "http://127.0.0.1:8776",
"transport": "websocket",
"heartbeat_interval_seconds": 30,
"connect_timeout_seconds": 10,
"request_timeout_seconds": 30,
"reconnect_initial_delay_seconds": 1,
"reconnect_max_delay_seconds": 60
}
}
```
Broker notes:
- `base_url` accepts `http`, `https`, `ws`, or `wss`; the websocket route is derived automatically.
- `scope: "user"` requires a non-global `username`.
- `scope: "global"` requires `username: "global"`.
- When `enabled` is `true`, `provider_id`, `client_id`, and `registration_token` are required.
- `advertised_endpoint` is optional and is sent to the broker as the externally reachable endpoint for this instance.
- Restart CoderAI after changing broker settings so the background broker service reconnects with the new configuration.
### config.json ### config.json
```json ```json
......
...@@ -1624,6 +1624,22 @@ async def api_get_settings(username: str = Depends(require_admin)): ...@@ -1624,6 +1624,22 @@ async def api_get_settings(username: str = Depends(require_admin)):
"directory": c.archive.directory, "directory": c.archive.directory,
"retention": c.archive.retention, "retention": c.archive.retention,
}, },
"broker": {
"enabled": c.broker.enabled,
"base_url": c.broker.base_url,
"scope": c.broker.scope,
"username": c.broker.username,
"provider_id": c.broker.provider_id,
"client_id": c.broker.client_id,
"registration_token": c.broker.registration_token,
"advertised_endpoint": c.broker.advertised_endpoint,
"transport": c.broker.transport,
"heartbeat_interval_seconds": c.broker.heartbeat_interval_seconds,
"connect_timeout_seconds": c.broker.connect_timeout_seconds,
"request_timeout_seconds": c.broker.request_timeout_seconds,
"reconnect_initial_delay_seconds": c.broker.reconnect_initial_delay_seconds,
"reconnect_max_delay_seconds": c.broker.reconnect_max_delay_seconds,
},
"system_prompt": c.system_prompt, "system_prompt": c.system_prompt,
"tools_closer_prompt": c.tools_closer_prompt, "tools_closer_prompt": c.tools_closer_prompt,
"grammar_guided": c.grammar_guided, "grammar_guided": c.grammar_guided,
...@@ -1714,6 +1730,28 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad ...@@ -1714,6 +1730,28 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
resolved = raw_dir if raw_dir and _os.path.isabs(raw_dir) else _os.path.join(cfg_dir, raw_dir or "archive") resolved = raw_dir if raw_dir and _os.path.isabs(raw_dir) else _os.path.join(cfg_dir, raw_dir or "archive")
archive_manager.configure(c.archive.enabled, resolved, c.archive.retention) archive_manager.configure(c.archive.enabled, resolved, c.archive.retention)
if "broker" in data:
bro = data["broker"]
c.broker.enabled = bool(bro.get("enabled", c.broker.enabled))
c.broker.base_url = (bro.get("base_url") or "").strip()
c.broker.scope = (bro.get("scope") or c.broker.scope or "user").strip()
c.broker.username = (bro.get("username") or "").strip()
c.broker.provider_id = (bro.get("provider_id") or "").strip()
c.broker.client_id = (bro.get("client_id") or "").strip()
c.broker.registration_token = (bro.get("registration_token") or "").strip()
c.broker.advertised_endpoint = (bro.get("advertised_endpoint") or "").strip()
c.broker.transport = (bro.get("transport") or c.broker.transport or "websocket").strip()
c.broker.heartbeat_interval_seconds = max(1, int(bro.get("heartbeat_interval_seconds", c.broker.heartbeat_interval_seconds)))
c.broker.connect_timeout_seconds = max(1, int(bro.get("connect_timeout_seconds", c.broker.connect_timeout_seconds)))
c.broker.request_timeout_seconds = max(1, int(bro.get("request_timeout_seconds", c.broker.request_timeout_seconds)))
c.broker.reconnect_initial_delay_seconds = max(1, int(bro.get("reconnect_initial_delay_seconds", c.broker.reconnect_initial_delay_seconds)))
c.broker.reconnect_max_delay_seconds = max(
c.broker.reconnect_initial_delay_seconds,
int(bro.get("reconnect_max_delay_seconds", c.broker.reconnect_max_delay_seconds)),
)
from codai.broker.config import build_broker_runtime_config
request.app.state.broker_runtime = build_broker_runtime_config(c.broker)
config_manager.save_config() config_manager.save_config()
return {"success": True} return {"success": True}
......
...@@ -101,6 +101,81 @@ ...@@ -101,6 +101,81 @@
<span class="form-hint">Archived entries older than this are automatically deleted. Takes effect immediately on save.</span> <span class="form-hint">Archived entries older than this are automatically deleted. Takes effect immediately on save.</span>
</div> </div>
</div> </div>
<div class="card mb-0" style="margin-top:1rem">
<div class="card-title">AISBF Broker</div>
<div class="form-row">
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
<input type="checkbox" id="s-broker-enabled" onchange="toggleBrokerFields()">
<span style="font-size:13px;font-weight:500">Enable broker client</span>
</label>
<span class="form-hint">Registers this CoderAI instance with an AISBF broker so it can receive brokered requests.</span>
</div>
<div id="broker-fields" style="display:none">
<div class="form-row">
<label class="form-label">Broker base URL</label>
<input type="text" id="s-broker-base-url" class="form-input" placeholder="https://broker.example.com">
<span class="form-hint">Supports `http`, `https`, `ws`, or `wss`. The websocket endpoint is derived automatically.</span>
</div>
<div style="display:grid;grid-template-columns:180px 1fr;gap:1rem;align-items:start">
<div class="form-row" style="margin:0">
<label class="form-label">Scope</label>
<select id="s-broker-scope" class="form-input">
<option value="user">user</option>
<option value="global">global</option>
</select>
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Username</label>
<input type="text" id="s-broker-username" class="form-input" placeholder="alice or global">
<span class="form-hint">Use `global` when scope is `global`; otherwise provide the AISBF username.</span>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start">
<div class="form-row" style="margin:0">
<label class="form-label">Provider ID</label>
<input type="text" id="s-broker-provider-id" class="form-input" placeholder="coderai-local">
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Client ID</label>
<input type="text" id="s-broker-client-id" class="form-input" placeholder="workstation-01">
</div>
</div>
<div class="form-row">
<label class="form-label">Registration token</label>
<input type="password" id="s-broker-registration-token" class="form-input" placeholder="Broker registration token">
</div>
<div class="form-row">
<label class="form-label">Advertised endpoint</label>
<input type="text" id="s-broker-advertised-endpoint" class="form-input" placeholder="http://127.0.0.1:8776">
<span class="form-hint">Optional external URL advertised to the broker for this instance.</span>
</div>
<div style="display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:1rem;align-items:start">
<div class="form-row" style="margin:0">
<label class="form-label">Heartbeat seconds</label>
<input type="number" id="s-broker-heartbeat" class="form-input" min="1" placeholder="30">
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Connect timeout</label>
<input type="number" id="s-broker-connect-timeout" class="form-input" min="1" placeholder="10">
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Request timeout</label>
<input type="number" id="s-broker-request-timeout" class="form-input" min="1" placeholder="30">
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:1rem;align-items:start">
<div class="form-row" style="margin:0">
<label class="form-label">Reconnect initial delay</label>
<input type="number" id="s-broker-reconnect-initial" class="form-input" min="1" placeholder="1">
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Reconnect max delay</label>
<input type="number" id="s-broker-reconnect-max" class="form-input" min="1" placeholder="60">
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
...@@ -110,6 +185,11 @@ function toggleHttps(){ ...@@ -110,6 +185,11 @@ function toggleHttps(){
document.getElementById('s-https').checked ? 'block' : 'none'; document.getElementById('s-https').checked ? 'block' : 'none';
} }
function toggleBrokerFields(){
document.getElementById('broker-fields').style.display =
document.getElementById('s-broker-enabled').checked ? 'block' : 'none';
}
function showAlert(type, msg){ function showAlert(type, msg){
const el = document.getElementById('settings-alert'); const el = document.getElementById('settings-alert');
el.className = 'alert alert-' + (type === 'error' ? 'error' : 'info'); el.className = 'alert alert-' + (type === 'error' ? 'error' : 'info');
...@@ -143,6 +223,21 @@ async function loadSettings(){ ...@@ -143,6 +223,21 @@ async function loadSettings(){
if (hint && as.default_directory) hint.textContent = `(default: ${as.default_directory})`; if (hint && as.default_directory) hint.textContent = `(default: ${as.default_directory})`;
document.getElementById('s-arc-dir').placeholder = as.default_directory || '(default)'; document.getElementById('s-arc-dir').placeholder = as.default_directory || '(default)';
} catch(_){} } catch(_){}
const broker = d.broker || {};
document.getElementById('s-broker-enabled').checked = !!broker.enabled;
document.getElementById('s-broker-base-url').value = broker.base_url ?? '';
document.getElementById('s-broker-scope').value = broker.scope ?? 'user';
document.getElementById('s-broker-username').value = broker.username ?? '';
document.getElementById('s-broker-provider-id').value = broker.provider_id ?? '';
document.getElementById('s-broker-client-id').value = broker.client_id ?? '';
document.getElementById('s-broker-registration-token').value = broker.registration_token ?? '';
document.getElementById('s-broker-advertised-endpoint').value = broker.advertised_endpoint ?? '';
document.getElementById('s-broker-heartbeat').value = broker.heartbeat_interval_seconds ?? 30;
document.getElementById('s-broker-connect-timeout').value = broker.connect_timeout_seconds ?? 10;
document.getElementById('s-broker-request-timeout').value = broker.request_timeout_seconds ?? 30;
document.getElementById('s-broker-reconnect-initial').value = broker.reconnect_initial_delay_seconds ?? 1;
document.getElementById('s-broker-reconnect-max').value = broker.reconnect_max_delay_seconds ?? 60;
toggleBrokerFields();
}catch(e){ showAlert('error','Failed to load settings: '+e.message); } }catch(e){ showAlert('error','Failed to load settings: '+e.message); }
} }
...@@ -169,6 +264,22 @@ async function saveSettings(){ ...@@ -169,6 +264,22 @@ async function saveSettings(){
directory: document.getElementById('s-arc-dir').value.trim(), directory: document.getElementById('s-arc-dir').value.trim(),
retention: document.getElementById('s-arc-retention').value, retention: document.getElementById('s-arc-retention').value,
}, },
broker:{
enabled: document.getElementById('s-broker-enabled').checked,
base_url: document.getElementById('s-broker-base-url').value.trim(),
scope: document.getElementById('s-broker-scope').value,
username: document.getElementById('s-broker-username').value.trim(),
provider_id: document.getElementById('s-broker-provider-id').value.trim(),
client_id: document.getElementById('s-broker-client-id').value.trim(),
registration_token: document.getElementById('s-broker-registration-token').value.trim(),
advertised_endpoint: document.getElementById('s-broker-advertised-endpoint').value.trim(),
heartbeat_interval_seconds: parseInt(document.getElementById('s-broker-heartbeat').value) || 30,
connect_timeout_seconds: parseInt(document.getElementById('s-broker-connect-timeout').value) || 10,
request_timeout_seconds: parseInt(document.getElementById('s-broker-request-timeout').value) || 30,
reconnect_initial_delay_seconds: parseInt(document.getElementById('s-broker-reconnect-initial').value) || 1,
reconnect_max_delay_seconds: parseInt(document.getElementById('s-broker-reconnect-max').value) || 60,
transport: 'websocket',
},
}; };
try{ try{
const r = await fetch(ROOT_PATH + '/admin/api/settings',{ const r = await fetch(ROOT_PATH + '/admin/api/settings',{
......
...@@ -841,3 +841,15 @@ def test_settings_template_keeps_queue_size_control(): ...@@ -841,3 +841,15 @@ def test_settings_template_keeps_queue_size_control():
assert "Request queue max size" in template assert "Request queue max size" in template
assert "s-queue-max" in template assert "s-queue-max" in template
def test_settings_template_includes_broker_controls():
template = Path("codai/admin/templates/settings.html").read_text()
assert "AISBF Broker" in template
assert "s-broker-enabled" in template
assert "s-broker-base-url" in template
assert "s-broker-provider-id" in template
assert "s-broker-client-id" in template
assert "s-broker-registration-token" in template
assert "toggleBrokerFields()" in template
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment