{% extends "base.html" %}{% from "_icons.html" import icon %}{% block title %}{{ 'Edit' if res else 'Add' }} {{ preset.title if preset else kind }}{% endblock %}{% block body %} {% include "_integration_style.html" %}
{% if not res %}{{ icon('chevron') }} All integrations{% endif %} {% if res %} {% include "_infrastructure_nav.html" %} {% endif %} {% if res %}

Inventory: {% if inventory_asset %}{{ inventory_asset.name }}{% else %}Add this resource to inventory · or link it from an existing inventory record{% endif %}

{% endif %} {% if preset %}
{% if preset.icon == 'notion' %}N{% else %}{{ icon(preset.icon) }}{% endif %}

{{ preset.title }}

{{ preset.instructions if not res else preset.description }}

{% if preset.auth_type == 'oauth' %}
  1. 1 Connection details
  2. 2 {{ 'Connected' if oauth_status == 'connected' else 'Sign in' }}
{% endif %} {% else %}

{{ 'Edit' if res else 'Add' }} {{ {'ssh': 'SSH host', 'mcp': 'MCP server', 'llm': 'LLM gateway'}[kind] }}

{% endif %}
Applies when no access rule or active grant covers an operation.
{% if kind == 'ssh' %}
{% if res %}
{% if key and key.exists %}Key present{% if key.pub %}: {{ key.pub }}{% endif %}{% else %}No key yet. Save, then use Generate key below (recommended), or paste one here.{% endif %}
Paste an existing private key instead
{% else %}
Save first; then generate a key on this Keywright and enrol it on the host from the same page.
{% endif %} {% if res and res.config.host_key %}{{ res.config.host_key }}
{% else %}
Host key not pinned yet: the first successful connection or probe pins it.
{% endif %} {% elif kind == 'llm' %} {% set limits = (res.config.limits if res else {}) or {} %}{% set budget = (res.config.agent_budget if res else {}) or {} %}
Each model is a path: llm/{{ res.name if res else 'litellm' }}/model/<model> (slashes in a name become _). A rule on llm/{{ res.name if res else 'litellm' }}/model/* covers them all. Keys are minted by approvals; an open default or a standing allow rule mints nothing until the LLM proxy ships, so keep this on approval for now.

Per-lease ceiling

Every approval becomes one LiteLLM virtual key with exactly these limits and the lease's lifetime. If the agent goes feral, this is all the money there is.

Per-agent ceiling

Each agent is a LiteLLM internal user; all of its keys on this gateway count against this window as well.

Set a global ceiling too, in litellm.yaml (litellm_settings.max_budget), and keep the vendors' own spend limits on. Keywright is a wall, not the only wall.
{% else %} {% if adapter == 'keywright' %}

Built-in tools for inventory, hosting and network topology, discovery observations, subnets and IP allocations. Agents use the same approval gate and access rules as other resources. No URL or credentials are needed.

{% elif adapter == 'omada' %}

In Omada Global View, open Settings → Platform Integration → Open API and create an app in Client mode with access to the sites you want. This connector reads sites, devices and clients.

Copy the address from Open API settings, including its port. Do not include an API path.
Stored on Keywright. Agents never receive this credential.
{% else %} {% if preset and preset.url %}

You'll authorize access on {{ preset.title }}. Keywright keeps the connection credentials; your agents use the access rules you choose here.

Advanced connection settings {% else %} {% endif %} {% set oauth = res.config.get('oauth', {}) if res else {} %} {% if not (preset and preset.url) %}
Choose the authentication method required by your server. For OAuth, save and then connect in your browser.
{% endif %}
Advanced OAuth settings
Leave blank for automatic client registration. For a pre-registered client, use the callback {{ oauth_callback }} on your configured public HTTPS URL.
Leave blank to use the MCP server's advertised scopes.
The MCP server's origin is trusted by default. Add an origin only when your provider uses a separate authorization server.
{% if preset and preset.url %}
{% endif %} {% endif %}
Advanced access settings {% set inspection_mode = res.config.get('result_inspection', 'off') if res else 'off' %}
Start with Observe to check compatibility: it reports findings and delivers original output. Redact removes configured credentials and recognizable secrets. Suspicious instructions are advisory only. Scanning is local; no external AI receives results. Images, audio, binary resources, linked content and results over 256 KiB cannot be fully inspected and are withheld in Redact mode. Results needing changes to structured data are also withheld to preserve the tool's output schema. The tool has already run when a result is withheld. Findings appear in the audit log and result metadata.
Each key present in a call adds a path such as mcp/{{ res.name if res else 'ha' }}/tools/call_service/service=light.turn_on. A rule or grant on the bare tool still covers every call; a deny on one qualifier blocks just that value. Without keys, calls are gated per tool.
A tool whose name says delete, restart, reset… is flagged medium on its own (shown to you; access unchanged). High needs a fresh once-only approval whatever access exists; critical is refused. A qualifier such as service=… needs that argument key listed above.
{% endif %}
{% if preset and preset.auth_type == 'oauth' and not res %}

Saves this resource, then opens {{ preset.title }} sign-in.

{% else %}{% endif %}
{% if res and kind == 'mcp' and res.config.get('auth_type') == 'oauth' %}

OAuth connection

Status: {{ {'connected': 'Connected', 'connecting': 'Sign-in pending', 'needs_reconnect': 'Reconnect required', 'disconnected': 'Disconnected'}.get(oauth_status, 'Disconnected') }}

Disconnect removes Keywright's saved tokens. To revoke the provider's authorization too, remove Keywright in the provider's connection settings.
{% endif %} {% if res and kind == 'llm' and leases %}

Gateway

{% if leases.enforced %}

Budgets enforced LiteLLM {{ leases.health.version }} · database connected · checked {{ leases.health.checked_at[:16] | replace('T', ' ') }}

{% elif leases.health %}

Budget enforcement unavailable LiteLLM reports its database {{ leases.health.db }}. Budgets are read from that database; without it the proxy serves past every limit, so no key is minted until it is connected.

{% else %}

Not probed yet. Test connection checks the database and refreshes the model list.

{% endif %}

Active leases

{% if leases.leases %} {% for k in leases.leases %}{% endfor %}
AgentModelsSpent / ceilingTime left
{{ k.agent }}{{ k.row.models | join(', ') }} {% if k.spend %}${{ '%.2f' | format(k.spend.spend) }}{% else %}?{% endif %} / ${{ '%.2f' | format(k.row.limits.max_budget or 0) }} {{ k.remaining }}{% if k.row.request_id %} · Approval{% endif %}

Exposure remaining: ${{ '%.2f' | format(leases.exposure) }} · what every active lease on this gateway could still spend if it all went wrong right now

{% else %}

No active leases. Nothing can be spent through this gateway until you approve a request.

{% endif %} {% endif %} {% if res and kind == 'mcp' %}
groups upstream tools by verb (get/list → read-only, call/run → control, remove/restart → destructive, set/create → config-write); adds only names you don't have
{% endif %} {% if res and kind == 'ssh' %}

Key & enrolment

{% if not (key and key.exists) %}
ed25519, stored at {{ keys_dir }}/{{ res.name }}, never leaves this machine
{% else %}
Enrol with a password (used once, not stored)
Connects once, appends the public key to ~/.ssh/authorized_keys, pins the host key, then re-connects with the key to prove it works.
Run a command on the host

As the login user; link valid 30 min.

{{ key.enroll_cmd }}
Then press Test connection below to pin the host key. Set the Keywright outbound IP in Settings to have this script add a from= restriction.
{% endif %} {% endif %} {% if res %}
{% endif %}
{% endblock %}