Skip to content

Commit 023abd2

Browse files
✨ feat: gateway speech, an Aura navigable without touch, a live tier for agentic apps, and a declared indexing plan
## Features ### packages/mewbo_speech A new package for synthesis and transcription against an OpenAI-compatible gateway, with model capability discovery so a surface can ask what a deployment can actually do rather than assume. It is optional in both senses the layering rule requires: a PEP 621 extra, and an import guard at every call site. ### packages/mewbo_core - **A declared step plan and an observed progress ledger** (`contracts/progress.py`). Long jobs previously reported progress as a register of ad hoc counters, so a value belonging to a phase that had already ended was read as describing the current one. Work is now DECLARED as a plan of steps and OBSERVED against it, which is what lets a reader tell "not started" from "finished and reset" — two states a bare counter spells identically. - Tool refusals carry a machine-readable code and close the gap that made a refusal indistinguishable from an empty result. Generative UI ships as a first-party plugin suite with a canonical identity for built-ins, and `SessionToolRegistry` gains a deny set, so an operator can switch a built-in tool off without removing the plugin that provides it. ### packages/mewbo_graph Per-project embedding models, and an indexing read failure that used to end a run early now surfaces as a typed fact instead of silent truncation. Every indexing step opens a scope of its own, so a log line that escapes its declared step is labelled as unattributed rather than rendered anonymously — an invisible gap is how one survives a green suite. ### apps/mewbo_api - **A live tier for Mewbo Apps** (`apps/pipeline_runner.py`, `pipeline_tracker.py`, `models.py`, `plugin/submit_app.py`). An app declares a result contract; a post-response verifier checks what it actually returned, and auto-repair reaches on-demand pipelines rather than only scheduled ones. A code pipeline's `ctx` is scoped to the app's own staging directory. - Speech routes (`speech/routes.py`) expose synthesis and transcription over the gateway, gated by the same capability header every other client-side capability travels through. - **Device control is a session** (`device_tools.py`): started, held, shown and ended, with the binding durable across re-engagement instead of rebuilt per request. - An operator-owned execution allowlist for pipelines, and a concurrency bound on live pipelines — a long-lived request spends a process-wide budget, so the ceiling is stated rather than discovered when the API stops answering. ### apps/mewbo_console The declared plan renders as an outline that follows the run and collapses what is done; the indexing screen is rebuilt as four bounded regions so a loading screen can no longer grow a scrollbar and push its own Cancel control off-screen. Live and average tokens-per-second appear in the trajectory and the turn footer. The model and fallback pickers become one tabbed panel. ### apps/mewbo_aura **Aura is operable without a touchscreen.** Anyone driving the app by D-pad, remote or keyboard could not reach most of it, and the cause was not missing focus targets: `clickable` is already focusable in Compose and the accessibility tree held eight focusable nodes. Focus was TRAPPED. A focused text field consumed all four arrow keys to move its caret, the composer took focus on the first frame, and eight consecutive arrow presses moved focus not once. So the fix is a navigation model rather than a stylesheet, and adding `Modifier.focusable` anywhere would have changed nothing. Four parts, all in `ui/common/`. `Modifier.dpadFocusEscape` lets Up/Down always leave a text field while keeping in-text caret movement. `Modifier.auraFocusRing` gives a control a visible focus state, and must precede the click modifier in the chain — `onFocusChanged` observes only focus targets that follow it, so ringing from behind compiles, draws nothing and warns about nothing. `focusProperties { down = FocusRequester.Cancel }` contains focus at the bottom edge, where a move onto a node the IME then destroyed left the tree with no focused node in any direction and no key events to recover with. `Modifier.imeOnConfirmOnly` stops the keyboard covering the screen for someone merely traversing past a field, and is named for the behaviour rather than the device. Settings rows now carry their own click and render the switch as a display-only indicator, because a trailing `Switch` was focusable and still unreachable from all four directions. Television is where this becomes unavoidable rather than what it is for: with no touchscreen at all, each of the above is a hard failure instead of an inconvenience. Android TV and Fire TV admission, a permanent navigation rail in place of the modal drawer, remote speech and in-app updating ride along, and the API floor drops to 30 — `RuntimeShader`, not haptics, was what stood in the way. ## Fixes - **`packages/mewbo_core`** — a reasoning model's bare-string answer is wrapped as a text block before it reaches history, where a strict OpenAI-shaped backend rejected it on replay. Schema upper bounds that break grammar-constrained backends are dropped, an activated skill is no longer truncated in half, and a session stays on its own model. Every turn gets its own trace and every tool call a span. - **`packages/mewbo_graph`** — the progress ledger gets a denominator that cannot move, settles at the end, and says when settling hid something; the ledger is read from whichever transport is ahead. - **`apps/mewbo_api`** — the Web IDE app-staging tier no longer drops sessions bound only by tag; the external-cwd gate can see paths the server already owns; poisoned app workspace bindings are repaired on resubmit; the code-server image is pulled before a container is created. - **`apps/mewbo_console`** — a citation badge annotates its sentence instead of erasing it, and the injected theme no longer collapses embedded-app sidebar spacing or leaves the auto-generated multipage sidebar nav unthemed. - **`apps/mewbo_aura`** — the stop-reading-aloud control gets its focus ring, the one focusable box the sweep missed. - **build and CI** — every package README resolves inside its own project so package metadata can reach it; the Docker and Android release pipelines run on both forges; the docs upload timeout is raised above the real transfer time, where the old cap cut the archive mid-stream and reported a truncated build rather than a timeout. ## Removals `public_endpoint` is dropped — a declaration nothing read. Co-authored-by: Mewbo AI <268600793+mewbo-ai[bot]@users.noreply.github.com>
1 parent 3227d66 commit 023abd2

687 files changed

Lines changed: 63870 additions & 5523 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.devcontainer/Dockerfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,36 @@ RUN groupadd --gid $USER_GID $USERNAME \
7676
# monorepo root.
7777
RUN pip install --no-cache-dir uv
7878

79+
# ---------- Simple Secrets Manager (ssm / ssm-cli) ----------
80+
# The workspace agent is launched through `ssm run`, so an image without it on
81+
# PATH boots the agent straight into exit 127. Installed here (Tier 0) rather
82+
# than init.d because it is toolchain, not project state.
83+
#
84+
# All three UV_* paths are load-bearing, because this installs as root while
85+
# the agent runs as $USERNAME:
86+
# BIN_DIR — uv's default (~/.local/bin) is NOT on a non-login shell's
87+
# PATH, and the agent pane runs `sh -c`, so a default install
88+
# is present and still unfindable (exit 127).
89+
# TOOL_DIR — the shim is a symlink INTO the tool venv; left at the default
90+
# it points inside /root and the agent gets exit 126.
91+
# PYTHON_... — the venv's interpreter is likewise fetched under /root by
92+
# default, so the venv resolves to an unreadable python.
93+
# `a+rX` then grants traverse/read without marking data files executable. The
94+
# python dir is only created when uv had to FETCH an interpreter; this image
95+
# already ships one it can reuse, so the loop skips what does not exist rather
96+
# than failing the build on it.
97+
#
98+
# The null keyring backend is required at RUNTIME, not just here: a container
99+
# has no secret service, and keyring's search for one hangs a headless client.
100+
# The CLI reads SSM_BASE_URL/SSM_TOKEN from the environment, which the
101+
# workspace supervisor already injects — so no credential file is baked in.
102+
ENV PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
103+
RUN UV_PYTHON_INSTALL_DIR=/opt/uv-python \
104+
UV_TOOL_DIR=/opt/uv-tools \
105+
UV_TOOL_BIN_DIR=/usr/local/bin \
106+
uv tool install git+https://github.com/bearlike/Simple-Secrets-Manager.git \
107+
&& for d in /opt/uv-python /opt/uv-tools; do [ -d "$d" ] && chmod -R a+rX "$d"; done
108+
79109
# ---------- Shared cache mount points ----------
80110
# Named volumes are mounted here by devcontainer.json. Create and chown them at
81111
# build time: a volume is created empty and root-owned on first use, and the

.devcontainer/docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ services:
2626
init: true
2727
command: sleep infinity
2828

29+
# Compose does NOT add host.docker.internal for you the way a plain
30+
# `docker run` on Linux does with `--add-host=…:host-gateway` — this is
31+
# that same mapping, spelled for Compose. It is what lets an agent in here
32+
# reach the host's own grove-mcp (networked MCP transport, :7431) by a
33+
# name that also resolves for a host-side Claude session, so ONE .mcp.json
34+
# URL serves both. `host-gateway` is Docker's own sentinel, resolved to
35+
# whatever bridge gateway this container actually gets — never hardcode
36+
# the IP, a fresh network is minted per workspace.
37+
extra_hosts:
38+
- "host.docker.internal:host-gateway"
39+
2940
volumes:
3041
# Relative to THIS file, so it resolves correctly in any git worktree
3142
# rather than being pinned to one checkout path.

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
# API authentication token — generate a strong random string.
4040
MEWBO_MASTER_API_TOKEN=CHANGE_ME
4141

42+
# Published image source. Set a private registry as git.example.com/bearlike.
43+
MEWBO_REGISTRY=ghcr.io/bearlike
44+
MEWBO_TAG=latest
45+
4246
# Exposed ports (host networking)
4347
MEWBO_API_PORT=5125
4448

.github/workflows/agent-pickup.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,14 @@ jobs:
124124
shell: bash
125125
run: |
126126
set -euo pipefail
127+
# A self-hosted forge behind a private CA is invisible to curl's system
128+
# trust store: the runner exports that CA for node and for git, and
129+
# nothing else. Trust the CA narrowly; AGENT_TLS_NO_VERIFY remains an
130+
# explicit escape hatch only when it is configured.
127131
curl_flags=()
132+
if [ -n "${NODE_EXTRA_CA_CERTS:-}" ] && [ -r "${NODE_EXTRA_CA_CERTS}" ]; then
133+
curl_flags+=(--cacert "${NODE_EXTRA_CA_CERTS}")
134+
fi
128135
case "${AGENT_TLS_NO_VERIFY,,}" in true|1|yes) curl_flags+=(-k) ;; esac
129136
# Gitea act_runner may leave github.api_url empty; derive it.
130137
api="$API_URL"
@@ -163,6 +170,9 @@ jobs:
163170
# pull_request events carry head/base inline; comment- and
164171
# dispatch-triggered PR pickups must look them up.
165172
curl_flags=()
173+
if [ -n "${NODE_EXTRA_CA_CERTS:-}" ] && [ -r "${NODE_EXTRA_CA_CERTS}" ]; then
174+
curl_flags+=(--cacert "${NODE_EXTRA_CA_CERTS}")
175+
fi
166176
case "${AGENT_TLS_NO_VERIFY,,}" in true|1|yes) curl_flags+=(-k) ;; esac
167177
if [[ "${ITEM_IS_PR:-false}" == "true" && -z "${HEAD_REF:-}" ]]; then
168178
pr=$(curl "${curl_flags[@]}" --fail-with-body --silent --show-error \
@@ -179,6 +189,9 @@ jobs:
179189
run: |
180190
set -euo pipefail
181191
curl_flags=()
192+
if [ -n "${NODE_EXTRA_CA_CERTS:-}" ] && [ -r "${NODE_EXTRA_CA_CERTS}" ]; then
193+
curl_flags+=(--cacert "${NODE_EXTRA_CA_CERTS}")
194+
fi
182195
case "${AGENT_TLS_NO_VERIFY,,}" in true|1|yes) curl_flags+=(-k) ;; esac
183196
provider="gitea"
184197
[[ "$SERVER_URL" == "https://github.com" ]] && provider="github"

.github/workflows/android-ci.yml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#@doc
2+
# * PR gate for Mewbo Aura (apps/mewbo_aura): lints and unit-tests the `public`
3+
# distribution flavor ONLY. The `enterprise` flavor needs a private CA cert
4+
# that CI does not have (see android-release.yml) — never build/test it here.
5+
#
6+
# One job, not two: `:app:lintPublicDebug` and `:app:testPublicDebugUnitTest`
7+
# would otherwise provision JDK/SDK into the SAME persistent
8+
# `/opt/hostedtoolcache` volume in parallel, which is exactly the race
9+
# android-release.yml's concurrency comment documents (colliding unzip/mv →
10+
# mangled repo metadata → "Failed to find package"). Sequential steps in one
11+
# job reuse the already-provisioned toolcache for the second step for free.
12+
13+
name: Android CI
14+
15+
on:
16+
pull_request:
17+
paths:
18+
- "apps/mewbo_aura/**"
19+
- ".github/workflows/android-ci.yml"
20+
workflow_dispatch:
21+
22+
permissions:
23+
contents: read
24+
25+
# TWO axes, and the outer one is not about this PR at all.
26+
#
27+
# Per-PR cancellation is what you want for signal (a new commit supersedes the
28+
# old run), but it CANNOT be the concurrency group: every Android job on this
29+
# runner provisions into the same persistent /opt/hostedtoolcache volume, and
30+
# two concurrent provisioners race on the SDK dir — colliding unzip/mv, mangled
31+
# repo metadata, "Failed to find package". That is why android-release.yml
32+
# serializes on ONE global group rather than per-tag; a per-PR group here would
33+
# re-open the same race between two PRs, and between a PR and a release.
34+
#
35+
# So: share the release workflow's global group. Two Android runs never overlap.
36+
# cancel-in-progress stays FALSE — a PR run must never kill a release build.
37+
#
38+
# ⚠️ The cost is NOT "concurrent PRs queue" — it is sharper than that. A group
39+
# holds at most ONE pending run, so with a release building and PR A waiting, PR
40+
# B's arrival CANCELS A. A cancelled check is neither a pass nor a failure, so
41+
# nobody re-runs it; the PR simply has no Android signal until someone notices.
42+
# Accepted deliberately: PR volume here is low, and a missing gate you can see is
43+
# better than the toolcache race, which corrupts the SDK for every later run.
44+
# Re-push to re-trigger. If this starts biting, the fix is a per-PR group plus a
45+
# lockfile around the provisioning step only — NOT dropping the shared group.
46+
concurrency:
47+
group: android-toolcache
48+
cancel-in-progress: false
49+
50+
defaults:
51+
run:
52+
working-directory: apps/mewbo_aura
53+
54+
jobs:
55+
lint-and-test:
56+
name: Lint + unit test (public flavor)
57+
runs-on: ubuntu-22.04
58+
timeout-minutes: 30
59+
env:
60+
# Same persistent toolcache volume as android-release.yml, so a JDK/SDK
61+
# already provisioned by a release run (or an earlier CI run) is reused
62+
# instead of re-downloaded.
63+
ANDROID_HOME: /opt/hostedtoolcache/android-sdk
64+
GRADLE_USER_HOME: /opt/hostedtoolcache/gradle-home
65+
steps:
66+
- name: Checkout
67+
uses: actions/checkout@v6
68+
69+
- name: Provision Temurin JDK 21 (persistent toolcache, skipped when present)
70+
run: |
71+
set -euo pipefail
72+
JDK_DIR=/opt/hostedtoolcache/temurin-21-jdk
73+
if [ ! -x "$JDK_DIR/bin/java" ]; then
74+
curl -fsSL -o /tmp/jdk.tar.gz "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jdk/hotspot/normal/eclipse"
75+
mkdir -p "$JDK_DIR"
76+
tar -xzf /tmp/jdk.tar.gz -C "$JDK_DIR" --strip-components=1
77+
rm /tmp/jdk.tar.gz
78+
fi
79+
echo "JAVA_HOME=$JDK_DIR" >> "$GITHUB_ENV"
80+
echo "$JDK_DIR/bin" >> "$GITHUB_PATH"
81+
82+
- name: Provision Android SDK (persistent toolcache, skipped when present)
83+
run: |
84+
set -euo pipefail
85+
if [ ! -d "$ANDROID_HOME/platforms/android-37.0" ] || [ ! -d "$ANDROID_HOME/build-tools/37.0.0" ]; then
86+
mkdir -p "$ANDROID_HOME/cmdline-tools"
87+
cd "$ANDROID_HOME/cmdline-tools"
88+
rm -rf latest cmdline-tools.zip
89+
curl -fsSL -o cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
90+
unzip -q cmdline-tools.zip
91+
mv cmdline-tools latest
92+
rm cmdline-tools.zip
93+
(yes || true) | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" --licenses >/dev/null
94+
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" "platform-tools" "platforms;android-37.0" "build-tools;37.0.0"
95+
fi
96+
97+
# Both Gradle invocations below cap the JVM the same way android-release.yml
98+
# does, and for the same measured reason: the project's gradle.properties
99+
# asks for -Xmx4096m, while a job container here is capped at 2.5 GiB with
100+
# no swap, so the daemon is OOM-killed by construction and the build
101+
# reports only "Gradle build daemon disappeared unexpectedly". The jvmargs
102+
# value is quoted as ONE argument because it contains a space — unquoted,
103+
# Gradle receives `-XX:...` as its own flag and rejects it.
104+
- name: Lint (public flavor)
105+
run: |
106+
set -euo pipefail
107+
./gradlew :app:lintPublicDebug \
108+
"-Dorg.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8" \
109+
-Dkotlin.compiler.execution.strategy=in-process \
110+
-Dorg.gradle.caching=true \
111+
--no-daemon --max-workers=2
112+
113+
- name: Unit tests (public flavor; test sources are flavor-agnostic)
114+
run: |
115+
set -euo pipefail
116+
./gradlew :app:testPublicDebugUnitTest \
117+
"-Dorg.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8" \
118+
-Dkotlin.compiler.execution.strategy=in-process \
119+
-Dorg.gradle.caching=true \
120+
--no-daemon --max-workers=2
121+
122+
- name: Upload lint report on failure
123+
if: failure()
124+
uses: actions/upload-artifact@v4
125+
with:
126+
name: android-lint-report
127+
path: apps/mewbo_aura/app/build/reports/lint-results-publicDebug.html
128+
retention-days: 7
129+
# warn, not ignore: a drifted report path must be visible, not a silent green.
130+
if-no-files-found: warn
131+
132+
- name: Upload unit test results on failure
133+
if: failure()
134+
uses: actions/upload-artifact@v4
135+
with:
136+
name: android-unit-test-results
137+
path: |
138+
apps/mewbo_aura/app/build/reports/tests/testPublicDebugUnitTest
139+
apps/mewbo_aura/app/build/test-results/testPublicDebugUnitTest
140+
retention-days: 7
141+
# warn, not ignore: a drifted report path must be visible, not a silent green.
142+
if-no-files-found: warn

0 commit comments

Comments
 (0)