bc84fec21d
A row in custom_tools becomes a ToolDef like any built-in, offered beside the thirteen. The registry had to stop being an import-time constant for that: `resolve_tools` now returns the schemas *and* the runners together, carried to the loop on the ToolContext. That closes a hole on the way. `run_tool` looked names up in the global REGISTRY with no reference to what had been offered, so a model naming a tool its chat was gated out of -- a family switched off, a permission the reader lacks -- had it run anyway. The resolved set is now authoritative. Arguments come from a model, so an argument may fill a hole but never move the target: the scheme and host of a URL template are literal, values are escaped for where they land, and the origin is pinned afterwards. Every redirect hop is checked the way services/fetch.py checks one, and the secret is dropped if a hop leaves the origin it was issued for. Also fixes the tool-activity block claiming every library tool had "searched the web", which it has done since the second family landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
4.1 KiB
HTML
111 lines
4.1 KiB
HTML
{% extends "admin/_layout.html" %}
|
||
{% from "_macros.html" import icon %}
|
||
{% set section = "tools" %}
|
||
|
||
{% block title %}Tools - LLeMbas{% endblock %}
|
||
{% block heading %}Tools{% endblock %}
|
||
|
||
{% block admin_content %}
|
||
<p class="admin-lede">
|
||
HTTP calls a model can make while it answers. Each one is offered to models
|
||
marked <strong>Custom tools</strong>, to people who have the permission, and —
|
||
if it is restricted — only to the groups you choose. The model decides
|
||
<em>when</em> to call it from the description you write, so write that as if
|
||
explaining to a colleague what the tool is for.
|
||
</p>
|
||
|
||
{% if saved %}
|
||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ saved }}</span></div>
|
||
{% endif %}
|
||
|
||
{% if not total %}
|
||
<div class="empty" style="padding: var(--sp-10) 0">
|
||
{{ icon("link", "empty__mark") }}
|
||
<p class="empty__text">
|
||
No tools yet. <a href="/admin/tools/new">Define one</a> — a name, a
|
||
description, and a URL with <code>{{ '{{placeholders}}' }}</code> in it.
|
||
</p>
|
||
</div>
|
||
{% else %}
|
||
|
||
{# Filters are links, so a filtered view is a real URL you can keep or share. #}
|
||
<div class="filter-bar">
|
||
<div class="filter-tabs">
|
||
{% for key, label in filters.items() %}
|
||
<a class="filter-tab {{ 'is-active' if key == active_filter }}"
|
||
href="/admin/tools?filter={{ key }}{% if q %}&q={{ q|urlencode }}{% endif %}">
|
||
{{ label }} <span class="filter-tab__count">{{ counts[key] }}</span>
|
||
</a>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<form class="filter-form" method="get" action="/admin/tools">
|
||
<input type="hidden" name="filter" value="{{ active_filter }}">
|
||
<input class="input" type="search" name="q" value="{{ q }}"
|
||
placeholder="Search tools…" aria-label="Search tools">
|
||
<button class="btn" type="submit">{{ icon("search", "icon--sm") }} Filter</button>
|
||
{% if q or active_filter != "all" %}
|
||
<a class="btn btn--ghost" href="/admin/tools">Clear</a>
|
||
{% endif %}
|
||
<a class="btn btn--primary" href="/admin/tools/new">{{ icon("plus", "icon--sm") }} New tool</a>
|
||
</form>
|
||
</div>
|
||
|
||
{% if not tools %}
|
||
<div class="empty" style="padding: var(--sp-8) 0">
|
||
<p class="empty__text">Nothing matches that filter.</p>
|
||
</div>
|
||
{% else %}
|
||
|
||
<div class="model-rows">
|
||
{% for tool in tools %}
|
||
<div class="model-row {{ 'is-off' if not tool.enabled }}">
|
||
<span class="model-row__pos">{{ page_start + loop.index }}</span>
|
||
|
||
<div class="model-row__main">
|
||
<div class="model-row__title">
|
||
<a class="model-row__name" href="/admin/tools/{{ tool.id }}/edit">{{ tool.name }}</a>
|
||
{% if not tool.enabled %}<span class="badge badge--danger">disabled</span>{% endif %}
|
||
{% if not tool.public %}<span class="badge">restricted</span>{% endif %}
|
||
{% if tool.allow_private %}<span class="badge">private network</span>{% endif %}
|
||
{% if tool.last_error %}<span class="badge badge--danger">last call failed</span>{% endif %}
|
||
</div>
|
||
<code class="model-row__id">
|
||
{{ tool.slug }} · {{ tool.method }} {{ tool.url_template }}
|
||
</code>
|
||
</div>
|
||
|
||
<div class="model-row__actions">
|
||
<a class="btn btn--sm" href="/admin/tools/{{ tool.id }}/edit">Edit</a>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div class="list-footer">
|
||
<span class="text-xs faint">
|
||
Showing {{ page_start + 1 }}–{{ page_start + tools|length }} of {{ matched }}
|
||
{%- if matched != total %} (filtered from {{ total }}){% endif %}
|
||
</span>
|
||
|
||
{% if pages > 1 %}
|
||
{% set base = "/admin/tools?filter=" ~ active_filter ~ ("&q=" ~ q|urlencode if q else "") %}
|
||
<div class="btn-row">
|
||
{% if page > 1 %}
|
||
<a class="btn btn--sm" href="{{ base }}&page={{ page - 1 }}">Previous</a>
|
||
{% else %}
|
||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">Previous</span>
|
||
{% endif %}
|
||
<span class="text-xs faint">Page {{ page }} of {{ pages }}</span>
|
||
{% if page < pages %}
|
||
<a class="btn btn--sm" href="{{ base }}&page={{ page + 1 }}">Next</a>
|
||
{% else %}
|
||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">Next</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
{% endif %}
|
||
{% endblock %}
|