Skip to content

Release: tool permission system + webui refont + TON tx confirmations#22

Merged
TONresistor merged 151 commits into
mainfrom
dev
Jun 2, 2026
Merged

Release: tool permission system + webui refont + TON tx confirmations#22
TONresistor merged 151 commits into
mainfrom
dev

Conversation

@TONresistor
Copy link
Copy Markdown
Owner

Brings dev up to main — 151 commits, no divergence (clean fast-forward).

Merge method: use Rebase and merge (linear history, keeps the individual commits) — do not squash (it would crush 151 commits into one).

Highlights

Tool permission system (core)

  • Per-tool access level (all / allowlist / admin / off) replaces the old 6-value scope + enabled flag, enforced in the runtime registry (gates every tool call). dm-only / group-only collapse to all — channel gating now lives only in the global DM/Group policy.
  • DB migration 1.19.0 adds scope_level + backfills from the legacy (enabled, scope); legacy columns kept for downgrade safety. Runs automatically at startup.

WebUI refont (Telegram-iOS design language)

  • Every page redesigned: Dashboard, Tools, Plugins, System Prompt, Memory, Chats, Wallet, Hooks, MCP, Tasks, Workspace, Config + new Logs page.
  • Light/dark theming; shared component library (List, Segmented, PillTabs, CodeBlock, Menu, toast/confirm/skeleton/empty-state); pill input DA.
  • Access-control dashboard: provider/model in the status hero, Access Policy + tabbed Allow Lists.

TON

  • Token unit renamed TON → GRAM (currency unit only; the network stays TON).
  • On-chain confirmation for transfers / jetton / swap / DNS / plugin-SDK operations, returning real tx hashes.

Fixes

  • Timezone-proof task scheduling, LLM WebSocket-close retry, setup-wizard config write order, obsolete provider cleanup.

Replace the optional requiredMode flag with a required ToolMode field on
ToolEntry, so the compiler rejects any tool that does not declare which
Telegram mode(s) it runs in. The registry filters on the three-way mode
in passesFilters/execute/setMode.

Honest gap handling: getClient() and the bot bridge's getMessages() now
throw a clear error in bot mode instead of a cryptic TypeError or a
silently empty result.

Move bot_inline_send out of the misnamed tools/bot/ directory into
telegram/messaging/ and drop the now-empty folder.
getRawClient() returned the TelegramUserClient wrapper typed as unknown.
pin.ts (unpin) and deals/executor.ts (gift transfer) cast it and called
.invoke()/.getInputEntity() — methods the wrapper does not expose — which
would throw at runtime. The as-any casts masked the mismatch.

- drop getRawClient() and getPeer() from the ITelegramBridge interface
- reach the GramJS client through the typed getClient() + isUserBridge guard
- pin.ts and deals/executor.ts now use the getClient() helper (real client)
- verify-payment / verification-poller / task-resolver use interface methods
  (getOwnUserId, sendMessage) instead of the loosely-typed raw client
…dup() capability

The handler's offset-store dedup is needed when the bridge can redeliver
the same message (GramJS user mode). Bot mode dedupes via update_id and
does not need it. Express this as a bridge capability instead of checking
getMode() at four sites in handlers.ts. The streaming gate also drops a
redundant mode check — bridge.streamResponse alone is enough since it
exists only on bridges that can stream.

Declare streamDraft / clearDraft / finalizeDraft / resetDraft as optional
methods on ITelegramBridge so future user-mode streaming (Phase 3) will
not need new interface surface.
- npm update in-range: hono, vitest, @vitest/coverage-v8, tsx, jscpd,
  @typescript-eslint/{eslint-plugin,parser} 8.60, @inquirer/prompts 8.5
- @ston-fi/api 0.31 -> 0.32
- @hono/node-server 1.19 -> 2.0.4 (drop Node18/Vercel, API serve inchangee)
- typescript 5.9 -> 6.0.3
- tsconfig: retrait baseUrl/paths (alias @/ inutilise, 0 usage repo) ;
  ignoreDeprecations 6.0 sur les 2 tsconfig (tsup force baseUrl au build dts)

Verifie: tsc ✓, eslint ✓, build (sdk+backend+web) ✓, knip ✓,
dupcheck 4.9% ✓, madge ✓, 1588 tests ✓, npm audit --omit=dev 0 vuln,
overrides axios/fast-xml-parser/xml-builder intacts.
knip 5 -> 6.14.2 (dev-only): $schema -> knip@6, drop now-redundant
ignoreDependencies (pino-pretty, @teleton-agent/sdk) per knip6 hints.

Installing knip 6 surfaced a latent peer conflict introduced by f615dec:
madge@8 (latest) declares peerOptional typescript@^5.4.4, incompatible
with TS 6 — every future npm install would then need --legacy-peer-deps.
TS 6 also forced ignoreDeprecations:6.0 (tsup injects a deprecated baseUrl
at the dts build). TS 6 brings no functional gain here (tsconfig already
strict). => revert typescript to ^5.7.0 (5.9.3), drop ignoreDeprecations.
The baseUrl/paths cleanup from f615dec is kept (alias @/ unused, 0 usage).

Verifie: tsc 5.9 OK, eslint OK, build (sdk+backend+web) OK, knip6 clean,
dupcheck 4.9%, madge no cycle.
- prix dual $IN/$OUT (verifies registre pi-ai 0.73.1) sur anthropic/
  openai/google/xai ; ex Opus $5/$25, Sonnet $3/$15, Gemini 2.5 Pro $1.25/$10
- label Opus 4.7 'Most capable' -> 'Most capable available' (4.8 hors pi-ai)
- ajout gemini-3.1-flash-lite-preview ($0.25/$1.50, 1M) + grok-4-1-fast-non-reasoning ($0.20/$0.50, 2M)
- consomme via getModelsForProvider() -> remonte auto dans WebUI + onboard ;
  schema.model = z.string() libre, IDs valides

Verifie: tsc, eslint, prettier, build backend.
…es dont burn iterations

- pin.ts: remove silent/both_sides (never passed to bridge.pinMessage)
- send-dice.ts: remove reply_to (bridge.sendDice ignores it, SDK drops as _replyToId)
- gift bot-tools: wrap return in data:{} (runtime serializes result.data only —
  total/gifts were invisible to the LLM) + category data-bearing (sibling parity)
- runtime loop: LLM retries (overflow/rate-limit/server) decrement iteration so
  transient errors no longer consume the productive max_agentic_iterations budget
  (still bounded by RATE_LIMIT/SERVER_ERROR_MAX_RETRIES)

Verifie: tsc, eslint, build backend, 1588 tests.
…/Conversations

- Tools.tsx: anonymous <> with keys on children -> <Fragment key> on the
  mapped root (fixes React key warning); drop now-redundant inner tr keys
- SearchInput: optional wrapperStyle prop (backward-compatible)
- Memory/Conversations: replace raw flex filter inputs with shared SearchInput
  (adds clear button + Escape-to-clear, consolidates duplicated markup)

Verifie: tsc --noEmit -p web/tsconfig.json (mes 4 fichiers OK), build:web.
Note: 8 erreurs de type WebUI PRE-EXISTANTES non liees (AgentControl, Config,
Dashboard, Setup, setup/*) — blind-spot #1, a traiter separement.
- parseRetryAfterMs() in runtime-utils (caps 60s, mirrors flood-retry.ts)
- rate-limit retry: honor server Retry-After as a floor, else exponential
  backoff; add positive jitter (0-500ms) to de-sync concurrent retries
- server-error retry: add jitter

Verifie: tsc, eslint, 1588 tests.
… a11y, single SSE

- App: Suspense fallback null -> loading div (no white flash); ErrorBoundary
  keyed off useLocation via resetKey so a crashed page clears on navigation
- ErrorBoundary: resetKey prop + componentDidUpdate (no remount, no Layout flash)
- Tasks: conditional poll (5s) only while a task is pending/in_progress
- a11y: aria-labels on icon buttons (Tasks cancel/delete, Workspace rename/delete)
  + SearchInput input/clear-button aria-labels, type=button
- SearchInput: optional wrapperStyle (already used by Memory/Conversations)
- agentStatusStore: single shared EventSource (ref-counted) — useAgentStatus
  now consumes it via useSyncExternalStore (was 2 connections: badge + control)

Verifie: tsc --noEmit -p web/tsconfig.json (8 erreurs PRE-EXISTANTES inchangees,
0 dans mes fichiers), build:web, build:backend.
…ge, drop dead getForProvider

- move summarizeToolParams + enrichRAGQuery (pure) from runtime.ts to
  runtime-utils.ts (shrinks the 1.3k-line runtime; no cycle, dupcheck 4.89%)
- import isBotBridge statically (bridge-guards is type-only -> no runtime cycle);
  drop the 2 repeated await import() calls
- accumulate token usage before the error branch so retried error responses
  still count toward cost metrics
- clamp max_agentic_iterations to >=1 (schema untouched)
- remove dead getForProvider() (0 prod consumers; knip misses class methods)
  + its 3 tests

Defere (risque > gain sur le chemin critique, prio 4-5):
classifyLlmError (hook errorCode et branches retry ont des conditions server
differentes), getModelResponse (intrique avec l'etat streaming).

Verifie: tsc, eslint, circular OK, knip, dupcheck 4.89%, 1585 tests.
…allet tx)

- formatDateTime (date + hour:minute, fr-FR) in lib/utils
- Tasks detail panel: created/scheduled/started/completed -> formatDateTime
  (compact table cells keep date-only formatDate)
- Wallet transactions -> formatDateTime

Verifie: tsc --noEmit -p web/tsconfig.json (7 erreurs pre-existantes, 0 dans
mes fichiers), build:web.
setActiveToolset() was never called at bootstrap — the toolset profiles were
entirely dead (0 consumers of the setter/getters; config.agent.toolset never
read). User chose removal over wiring.

- registry: drop activeToolset field, TOOLSET_PROFILES, setActiveToolset,
  getActiveToolset, getAvailableToolsets, and the profile filter in passesFilters
  (per-tool tags kept — still surfaced as tool metadata)
- schema: drop agent.toolset field (z.object strips unknown keys, so existing
  configs with toolset: are silently ignored — no break)
- onboard: drop toolset:"full" from both generated config templates

Verifie: tsc, eslint, knip, circular, dupcheck 4.9%, 1585 tests.
- Memory page: the search box now runs a debounced (300ms) semantic search via
  the existing GET /memory/search (api.searchKnowledge was dead code). Empty box
  browses sources as before; a query shows ranked SearchResult chunks (source +
  score) in the existing card/pre style. User chose wiring over deletion.
- drop unused .skeleton / @Keyframes shimmer CSS (no consumers in web/src)

Verifie: tsc --noEmit -p web/tsconfig.json (Memory clean; 7 pre-existing errors
unchanged), build:web. Note: api.tasksGet/tasksCleanDone remain unused (not
memory-search; left per user decision scope).
The WebUI is built with vite/esbuild (no type emit) and not typechecked in CI,
so these latent errors had accumulated. tsc --noEmit -p web/tsconfig.json now
passes clean, making it a usable local guard (blind-spot #1).

- AgentControl: drop unused 'config' (badge computes its own)
- setup/ConfigStep: drop unused Select import
- setup/WelcomeStep: drop orphaned handleAccept + initDone (risk checkbox and
  initWorkspace moved to Setup.tsx — verified Setup.tsx:55-60 handles both)
- Setup: drop unused STEPS import
- Dashboard: bottomRef prop RefObject<HTMLDivElement|null> -> RefObject<HTMLDivElement>
  (matches useRef<HTMLDivElement>(null) under @types/react 18.3 + the div ref)
- Config: drop dead '=== true' branch on getLocal() (returns string; TS2367) —
  behavior identical, '=== "true"' was the only effective check

Verifie: tsc --noEmit -p web/tsconfig.json (0 erreur), build:web.
Hook errorCode and retry dispatch shared the same intent but used divergent
string-matching: a 529/overloaded was reported errorCode=UNKNOWN to the
response:error hook while being retried as a server error internally.

Extract classifyLlmError() + isRateLimitError/isServerError predicates into
runtime-utils (single source of truth, overflow->rate->server precedence).
Hook and dispatch now consume one classification.

Closes audit finding #1.
Collapse the claude-code and codex retry-on-401 blocks (verbatim except
provider/refresh) into RETRY_401_PROVIDERS + isUnauthorizedError predicate.

Closes audit finding #2.
Both paths duplicated the <think>-strip + persist + text-extract + context-append
sequence (and the thinkRe literal). Hoist THINK_RE to module scope and extract
finalizeResponse(response, context, options).

Closes audit finding #3.
The agentic loop branched on provider===cocoon to wrap tool results as a user
message. Move that into buildToolResultMessage() in the cocoon tool-adapter so
the loop emits one message type without knowing the provider quirk.

Closes audit finding #7.
Replace the inline 'user'|'bot' literal on the registry mode field, constructor
and setMode with a named RuntimeMode = Exclude<ToolMode,'both'> alias.

Closes audit finding #15.
The three insertion sites (register, registerPluginTools, replacePluginTools)
each set the parallel tool/scope/mode/tags/module Maps by hand and had drifted
(register set toolTags, plugin paths omitted them). Extract private insertTool()
as the single insertion point; callers keep their collision policy and cache
invalidation.

Closes audit finding #9.
PluginModule.tools() already allows a per-tool mode, but registerPluginTools/
replacePluginTools dropped it and hardcoded 'both'. Thread the declared mode
through (default 'both' when omitted) so mode restrictions apply to plugin tools.

Closes audit finding #16.
loadConfigFromDB, registerPluginTools and replacePluginTools each repeated the
seed-missing-then-reload-once loop. Extract private seedConfigs(names) as the
single seeding path.

Closes audit finding #10.
escapeFts5Query (3 sites) and bm25ToScore (2 sites) were byte-identical clones.
Centralise both in memory/search/fts-utils.ts so the FTS5 escape list — a
syntax-injection surface — has a single definition.

Closes audit finding #13.
USERNAME_REGEX + the canonical error message + @-strip were triplicated across
set/check/create-channel. Add cleanUsername + validateChannelUsername (with an
allowEmpty option for the remove-username case) to sdk/telegram-utils. The
intentionally-different profile/contacts validators are left untouched.

Closes audit finding #18.
The getEntity + className!=='Channel' guard + 'as Api.Channel' cast was verbatim
in 4 channel tools. Add resolveChannel(bridge, channelId) to sdk/telegram-utils
(throws a readable Error caught by each executor's existing catch).

Closes audit finding #19.
Add mapTelegramError + a canonical code->message table to sdk/telegram-utils and
adopt it in the set/check-channel-username error tails, unifying the 3 divergent
phrasings of CHANNELS_ADMIN_PUBLIC_TOO_MUCH. Success-mapped codes
(USERNAME_NOT_MODIFIED, USERNAME_PURCHASE_AVAILABLE) stay explicit before the call.

The ~30 heterogeneous remaining catch sites are deliberately NOT mechanically
swept (each carries tool-specific success mappings) — the helper is the canonical
path for them going forward.

Closes audit finding #17.
Move toUnits/fromUnits to a dependency-free src/ton/units.ts (re-exported from
dedust/asset-cache for compat) and replace the manual string-conversion copies in
stonfi/quote, stonfi/swap and ton/jetton-send with toUnits() — financial decimal
math now has one definition, importable without coupling to DeDust.

Closes audit finding #24.
The pTON proxy address was hardcoded in 4 places under two names. Add
STONFI_PTON_ADDRESS in the neutral src/ton/dex-constants.ts; dedust/constants,
stonfi/quote, stonfi/swap and sdk/ton-dex now source it.

Closes audit finding #22.
Balance hero with TON-diamond watermark, full address + copy/explorer
icons, segmented In/Out filter, iOS list transactions with direction-
colored icons. Token unit renamed TON→GRAM (Durov rebrand; network stays TON).
Blocklist card with header toggle + chip-field keywords, dimmed when off.
Context triggers as an iOS list: tap a row to expand into an inline edit
form (keyword/context, save/delete), plus a New-trigger add row.
Controls row (summary + refresh + add), cleaned add-server card with
field labels and env-var rows. Servers as an iOS list: plug icon colored
by connection, type/scope/disabled badges, tool count; expand for status,
env-key/tool chips, and remove.
PillTabs status filter w/ counts, SearchBar + Clean menu + refresh.
Tasks as an iOS list: status dot (pulsing when running), status-colored
subtitle, priority dots; expand for a kv grid + CodeBlock payload/result/
error and cancel/delete actions. Confirms via useConfirm (drop bespoke modal).
Status hero (pulsing orb + model/Running + provider/uptime/platform),
metrics as a grid of tappable stat-cards (navigate to their page) incl.
GRAM balance, restyled live-logs panel. Keep settings panels. Drop dead
status-bar/metric CSS.
Replace the stat-card grid (too tall) with a single thin pill strip of
inline value+label items (macOS menu-bar style), keeping click-through
navigation. Frees vertical space so hero + settings + logs fit one screen.
- Per-tool access level (all/allowlist/admin/off) replaces the 6-value scope + enabled flag; DB migration 1.19.0 backfills and keeps legacy columns for safety. Global DM/Group response policy shown as pills.
- Dashboard: Provider/Model moved into the status hero; Access Policy + tabbed Allow Lists cards.
- New Logs page (level filter, search, copy, pause-on-scroll); dropped the inline dashboard logs panel.
- Single-line inputs use pill radius to match buttons; ArrayInput add button resized.
@TONresistor TONresistor merged commit 0dee104 into main Jun 2, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant