Files
LLeMbas/src/lembas/web/templates/admin/tools.html
T
Jaroslav Beneš bc84fec21d Custom HTTP tools an administrator defines
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>
2026-08-01 16:26:47 +02:00

111 lines
4.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}