github lukilabs/craft-agents-oss v0.9.1

5 hours ago

v0.9.1 — Telegram bot whitelisting + access control, mid-stream send modes, scoped image-support toggle

Features

  • Telegram bot whitelisting + access control — Solves "anyone who finds the bot can talk to my Agent". Two-tier access model: (1) workspace owners at the platform level (accessMode: 'open' | 'owner-only'), (2) per-binding access with 'inherit' | 'allow-list' | 'open' modes. The first user who runs /pair in a paired supergroup is auto-seeded as workspace owner. Locking the bot to owner-only in Settings → Messaging → Telegram flips the workspace switch and migrates every legacy 'open' binding to 'inherit' so no public binding is left dangling. A new "Allowed users" collapsible (Users icon) lists owners with remove controls; a "Pending requests" panel surfaces senders the gateway already rejected so the operator can Allow or Allow for this chat (per-row decision, never auto-promotes binding-level rejects to global owner). Inline buttons (bind:/perm:/plan:) run through the same access evaluator as text commands so a non-owner can't tap their way past the gate; bot senders are silent-dropped at the platform layer. The unlock path lives in the platform context menu (next to Reconfigure, only when locked). Composite-key pending-store (platform, userId, reason, bindingId) keeps a sender's workspace-level reject separate from their per-binding rejects so each can be resolved independently. Closes #672. (fd68c070, aa46043a, 3f4fe418)

  • Per-connection mid-stream send behavior (Steer vs Queue) — A new submenu in the Settings → AI connection context menu lets you choose what happens when you send a follow-up while the agent is mid-stream. Steer keeps today's behavior (redirect attempts to inject the new message into the live turn); Queue finishes the current turn cleanly and replays the queued message after onProcessingStopped. The two modes have meaningfully different failure shapes — Claude's emulated steer can produce steer_undelivered if no tool fires before the turn ends, paying tokens for nothing; Pi's .steer() is native and has no such failure mode. Defaults are picked per-provider at connection-create time (anthropic → queue, pi/pi_compat → steer) and stored in connection.midStreamBehavior. 4 new i18n keys × 7 locales. Closes the queue-vs-steer half of conversations around mid-stream UX. (35a4ca2e, f346035a)

  • Per-model image support toggle in the chat-input picker — Custom-endpoint (pi_compat) connections now expose a one-click image toggle next to each model in the chat-input model picker. When you stage an image but the active model resolves to supportsImages !== true, an inline pre-flight banner appears above the attachment preview with an "Enable image support" action. Both surfaces resolve through shared helpers (modelSupportsImages / setModelSupportsImages) so they can't drift. The toggle now also renders in the single-model picker branch (used for pi_compat connections with 0 or 1 models + a defaultModel). Storage schema unchanged — flips connection.models[i].supportsImages. Built-in anthropic / pi catalogs stay SDK-owned and not editable from this surface. Closes #679. (f36c0833, bba2d726, 31ddd5aa)

  • Tool-result threshold scales with model context window — The fixed 15k-token threshold for large-response handling burned 25 % of context per tool result on a 64k-window model, and three or four moderate Read/Grep results could overflow even after Pi's auto-compaction. New tokenLimitFor(contextWindow) helper: floor 2k, ceiling 15k, linear contextWindow * 0.10 between (64k → 6.4k, 128k → 12.8k). Pi-side tool wrapper sources agent.state.model.contextWindow per call so the threshold tracks set_model mid-session. ClaudeAgent reads from the usage tracker. Addresses gap 1 from #666. (0d3c2796)

Improvements

  • Settings → Messaging i18n consolidation — Settings → Messaging mixed English fragments into every non-en locale (Hungarian showed "Reconfigure" / "Disconnect" / "Connect" alongside translated copy) because shared verbs were duplicated across per-platform namespaces and three keys resolved via t(..., { defaultValue: ... }) with no real entry. Added 7 common.* action verbs (configure / connect / disable / disconnect / more / reconfigure / reconnect) translated to all 6 non-en locales, replaced 8 call sites in MessagingSettingsPage.tsx, deleted 9 redundant per-platform keys × 7 locales (63 cells removed), and routed the hardcoded PLATFORM_API_DESCRIPTION constant through new apiType keys. Backfilled 8 platform-specific gap-fills that had been left as English. Net: 14–17 untranslated values per locale → 2–5 (only legitimate cognates like "Region" remain). Adding a new platform no longer requires duplicating verb translations. (bd575ed1)

  • Locale-file sort guard — Across en/de/es/hu/ja/pl/zh-Hans.json, 96 of 1393 keys per locale were out of alphabetical order — identical disorder confirmed past mass-translate scripts preserved insertion order. New scripts/sort-locales.ts (run via bun run sort-locales, or bun run lint:i18n:sorted for CI/check mode) normalizes in place. The pre-commit hook (scripts/lint-i18n-staged.sh) now blocks commits that stage unsorted locales with a "Fix: bun run sort-locales" hint, and validate:ci includes the same check. Net change: zero keys added/removed; the locale-parity test suite goes 58 / 7 → 65 / 0. (3a40b5a5, 3f4fe418)

  • Pi SDK uplift to 0.72.1 — Bumps @mariozechner/pi-ai, pi-agent-core, and pi-coding-agent from 0.70.2 to 0.72.1 across all four package.json files. Drops the now-removed google-gemini-cli and google-antigravity entries from PI_EXCLUDED_PROVIDERS. The 0.72.0 release replaced compat.reasoningEffortMap with model-level thinkingLevelMap; verified Craft's buildCustomEndpointModelDef sets neither field so existing custom-endpoint registrations are unaffected. Adjacent fixes picked up: undici body/headers idle timeouts disabled for long streams (#3715), Anthropic stream-end-as-error handling (#3936), DeepSeek V4 reasoning compat, GPT-5.5 Codex support, websocket-cached transport for OpenAI Codex (#4083). (522a727f)

  • HTTP MCP validation no longer spawns a subprocessvalidateMcpConnection() previously spawned a full Claude Code subprocess via the Agent SDK's query() just to call mcpServerStatus(). On macOS the Electron sandbox killed that subprocess with SIGKILL before validation could finish, so every HTTP MCP source_test failed even when the server was healthy. Now uses CraftMcpClient directly (which already supports HTTP transport and runs a listTools() health check inside connect()); drops the subprocess, the Anthropic env-var swapping, the Claude credential pre-check, and the dead claudeApiKey/claudeOAuthToken/model fields from the validation config. Net −231 / +119 lines. Fixes #697. (f0a12c4e)

  • API source probe honors testEndpoint.body and method; tool descriptions slimmed — POST-only API endpoints reliably 500'd from an empty probe body because the configured testEndpoint.body and testEndpoint.headers were ignored. The basic probe also now honors testEndpoint.method instead of the HEAD→GET-on-405 dance that silently passed POST-only endpoints. Separately, guide.md is no longer inlined into the API tool description on every LLM request — the agent's PrerequisiteManager already forces a Read of sources/{slug}/guide.md before any api_{slug} call, so the guide always lands in conversation history once. For OpenAPI-imported sources this saves tens of KB per request and removes the bloat that pushed #683's payload past the relay limit. (589a51bf)

  • IPC inventory script auto-updates the test snapshotscripts/ipc-inventory.ts now rewrites the auto-generated EXPECTED_CHANNELS block in ipc-channels.test.ts in place instead of just printing a migration report. Run it after adding/removing/renaming any RPC_CHANNELS entry. (aa46043a)

Bug Fixes

  • Custom-endpoint supportsImages toggle now propagates to the live Pi subprocess — Toggling per-model image support wrote to disk but never reached the running Pi subprocess, so the model kept seeing attached images replaced by Pi SDK's "(image omitted: model does not support images)" placeholder. Two compounding issues: getOrCreateAgent gated the in-place runtime refresh on managed.isProcessing (which sendMessage had just flipped to true, making the refresh branch dead code), and the llmConnections.SAVE handler had no notification path to active sessions. Now SAVE fires the runtime push detached (so config persistence isn't gated on N × 15s per-session timeouts), the gate uses agent.isProcessing() only, and a per-session agentRefreshLocks mutex serializes concurrent refreshes so agent.chat() can't fire against a still-applying update. Restart-required field changes (piAuthProvider, slug, auth/provider routing — fields the update_runtime_config IPC can't propagate) now route straight to dispose + recreate via a separate buildRestartRequiredSignature. (81a6f195, 6c0fec02, ebf01805, f0bbfe96)

  • Vision toggle no longer blanks the trigger button on string-shaped models — Promoting a models: ['gpt-4o']-shaped string entry to an object via the new image toggle dropped the model name (no name field), so the picker's trigger button rendered blank. setModelSupportsImages now seeds name: id, shortName: id when promoting; defensive m.name ?? stripPiPrefixForDisplay(m.id) fallback at the three picker display sites also recovers any pre-existing partial entries from hand-edited config.json. (bba2d726)

  • Cold-session metadata changes survive app restartpersistSession previously silently no-op'd when called on a session whose messages hadn't been lazy-loaded yet (the post-restart state of every session in the sidebar). Status / labels / rename changes made before the session was opened were never written to disk and were lost on the next restart. The cold path now hydrates messages + token usage synchronously from the JSONL before enqueueing — keeps the original guard's intent (don't overwrite real messages with []) while removing the silent drop. setSessionLabels is now async and awaits the flush to match setSessionStatus. (d5a31774)

  • Pi+GPT context overflow no longer cascades into a 3-error pile — Pi+GPT context overflow produced Codex error: context_length_exceededAuto-compaction failed: undefined is not an objectPi subprocess error: Agent is already processing a prompt in the UI. Three independent root causes: an upstream race in Pi SDK's _runAutoCompaction (separate PR pending), a wrapper-side duplicate compact in pi-agent-server's handlePrompt racing with the SDK's internal _runAutoCompaction, and PiEventAdapter completing its iterator on every agent_end so the recovered turn from agent.continue() arrived in a closed iterator. Now: a 5-state overflow recovery state machine in PiEventAdapter (none/held/awaiting/compacting/recovering) holds the queue open across the SDK's compaction → continue flow with a 5s drain fallback; the duplicate manual compact path in pi-agent-server is removed; waitForCompaction default timeout matches the RPC compact timeout (60s → 300s); a friendly fallback message replaces the raw AbortController stack until the upstream SDK fix ships. (f70d2b56)

  • Browser-pane hide() no longer crashes mid-load — The 'close' listener could re-enter hide() via keepAliveOnWindowClose, and emitStateChange() ran synchronously after win.hide() while Chromium was still tearing down — particularly fatal when the BrowserView was mid-load. Net effect: black-screen crash on "Hide Window" during agent tool execution. Adds an isHiding guard, defers the state-change callback to a microtask, and calls webContents.stop() before win.hide() when a load is in flight. Fixes #695. (a4038a28)

  • RoutedClient no longer crashes on synchronous workspace switchonConnectionStateChanged fires its callback synchronously when the new client is already in the 'connected' state, so the previous const unsub = … form put unsub in the TDZ and threw ReferenceError: Cannot access 'unsub' before initialization. Production-impacting on any make-before-break workspace switch that lands on an already-connected new client. Switches to let + optional-chaining so the callback can self-unsubscribe in both the synchronous and async cases. (44f8f001)

Documentation

  • Google OAuth setup uses Web application client type, not Desktop app — Issue #585: users following the docs got redirect_uri_mismatch from Google when using the recommended "Desktop app" client. Newer Google Cloud projects and Workspace accounts with stricter controls reject the loopback redirect URI on Desktop-app clients even though the desktop flow uses http://localhost:<port>/callback. The "Web application" client type works in both cases. Updated apps/online-docs/source-guides/google-oauth-setup.mdx to a single Web-application setup with both redirect URIs documented (http://localhost:6477/callback for desktop loopback; https://agents.craft.do/auth/callback for WebUI/headless). (70828cbc, 34521a7d)

  • Mid-stream send and chat-input image toggle docscore-concepts/interactions.mdx gains a "Sending While the Agent Is Responding" section explaining Steer vs Queue modes, the Queued chip cue, and the per-backend defaults. reference/config/llm-connections.mdx adds the midStreamBehavior field to the schema table. reference/config/custom-endpoint.mdx documents the chat-input image-support toggle for pi_compat connections. (f346035a, 31ddd5aa)

Breaking Changes

  • None. The Telegram access-control work migrates legacy 'open' bindings to 'inherit' automatically when the workspace is locked down — no manual config required. connection.midStreamBehavior falls back to a per-provider default for connections created before this release.

Notes

  • A bot pre-paired with a workspace will not have any owners until the next /pair command, at which point the first user is auto-seeded. To audit current state from the UI, open Settings → Messaging → Telegram and check the "Allowed users" collapsible.
  • The new bun run sort-locales (and bun run lint:i18n:sorted in CI) replaces ad-hoc manual locale sorting. If you contribute translations, append the new key anywhere in the file and run the sort script before committing — the pre-commit hook will block on unsorted locales otherwise.
  • Pi SDK uplift to 0.72.1 is internal — no config changes required. If you previously had google-gemini-cli or google-antigravity configured (both removed upstream), those entries are no longer recognized.

Don't miss a new craft-agents-oss release

NewReleases is sending notifications on new releases.