[eric] merge origin/eric/dev into eric/mcp (1.5.4 line + Sonnet 5 under the social MCPs)

This commit is contained in:
ciregenz
2026-07-02 16:07:51 -07:00
10 changed files with 243 additions and 74 deletions
@@ -42,6 +42,9 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = {
# Opus 4.7: SDK currently strips plaintext thinking deltas (encrypted only) so the live "Thought for Ns" pill loses mid-turn text. Final answer + tokens fine.
{"value": "opus-4-7", "label": "Claude Opus 4.7", "context_window": 1_000_000,
"model_id": "claude-opus-4-7", "router_model_id": "cc/claude-opus-4-7", "api": "anthropic", "reasoning": True},
# Sonnet 5 (2026-06-30): cheaper near-Opus-4.8 agentic model. cc/ route assumed to pass through like opus-4-8 did; needs a live sub-route check.
{"value": "sonnet-5", "label": "Claude Sonnet 5", "context_window": 1_000_000,
"model_id": "claude-sonnet-5", "router_model_id": "cc/claude-sonnet-5", "api": "anthropic", "reasoning": True},
{"value": "sonnet", "label": "Claude Sonnet 4.6", "context_window": 1_000_000,
"model_id": "claude-sonnet-4-6", "router_model_id": "cc/claude-sonnet-4-6", "api": "anthropic", "reasoning": True},
{"value": "opus", "label": "Claude Opus 4.6", "context_window": 1_000_000,
@@ -53,6 +56,8 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = {
"model_id": "claude-opus-4-8", "router_model_id": "cc/claude-opus-4-8", "api": "anthropic", "reasoning": True, "route": "cc"},
{"value": "opus-4-7-cc", "label": "Claude Opus 4.7", "context_window": 1_000_000,
"model_id": "claude-opus-4-7", "router_model_id": "cc/claude-opus-4-7", "api": "anthropic", "reasoning": True, "route": "cc"},
{"value": "sonnet-5-cc", "label": "Claude Sonnet 5", "context_window": 1_000_000,
"model_id": "claude-sonnet-5", "router_model_id": "cc/claude-sonnet-5", "api": "anthropic", "reasoning": True, "route": "cc"},
{"value": "sonnet-cc", "label": "Claude Sonnet 4.6", "context_window": 1_000_000,
"model_id": "claude-sonnet-4-6", "router_model_id": "cc/claude-sonnet-4-6", "api": "anthropic", "reasoning": True, "route": "cc"},
{"value": "opus-cc", "label": "Claude Opus 4.6", "context_window": 1_000_000,
@@ -65,6 +70,8 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = {
"model_id": "claude-opus-4-8", "router_model_id": "claude-opus-4-8", "api": "anthropic", "reasoning": True, "route": "api"},
{"value": "opus-4-7-api", "label": "Claude Opus 4.7 (API key)", "context_window": 1_000_000,
"model_id": "claude-opus-4-7", "router_model_id": "claude-opus-4-7", "api": "anthropic", "reasoning": True, "route": "api"},
{"value": "sonnet-5-api", "label": "Claude Sonnet 5 (API key)", "context_window": 1_000_000,
"model_id": "claude-sonnet-5", "router_model_id": "claude-sonnet-5", "api": "anthropic", "reasoning": True, "route": "api"},
{"value": "sonnet-api", "label": "Claude Sonnet 4.6 (API key)", "context_window": 1_000_000,
"model_id": "claude-sonnet-4-6", "router_model_id": "claude-sonnet-4-6", "api": "anthropic", "reasoning": True, "route": "api"},
{"value": "opus-api", "label": "Claude Opus 4.6 (API key)", "context_window": 1_000_000,
@@ -360,6 +367,7 @@ def get_context_window(provider: str, model: str, settings: AppSettings | None =
COST_PER_1M_TOKENS: dict[tuple[str, str], tuple[float, float]] = {
# (provider, model): (input_cost_per_1M, output_cost_per_1M) NOTE: real cost numbers come from 9Router's usage stats. These entries are kept so the table matches BUILTIN_MODELS and can be used by any future native-loop path. Subscription-routed models are zero-cost to the user, but API rates are recorded here for reference where they exist. Anthropic (direct API rates).
("Anthropic", "sonnet"): (3.0, 15.0),
("Anthropic", "sonnet-5"): (3.0, 15.0),
("Anthropic", "opus"): (5.0, 25.0),
("Anthropic", "opus-4-7"): (5.0, 25.0),
("Anthropic", "opus-4-8"): (5.0, 25.0),
+25 -4
View File
@@ -165,21 +165,42 @@ class p_CuratedInstallRequest(BaseModel):
folder: str
def p_cached_curated_fallback(folder: str) -> Optional[dict]:
"""Offline/rate-limited curated-install fallback: rebuild a single-SKILL.md install
payload from the warmed catalog (it already holds the SKILL.md body) so a curated
install still works when GitHub is unreachable, minus the folder's extra files.
Empty version means it's skipped by update checks until re-installed online."""
cached = next((s for s in p_cache.values() if s.get("folder") == folder), None)
if cached is None:
return None
name, description, body = cached.get("name", ""), cached.get("description", ""), cached.get("content", "")
return {
"skill_id": folder.rsplit("/", 1)[-1], "name": name, "description": description,
"files": {"SKILL.md": f"---\nname: {name}\ndescription: {description}\n---\n\n{body}"},
"scripts": [], "source": sources.REPO, "folder": folder, "version": "",
}
@skill_registry.router.post("/install-curated")
async def registry_install_curated(req: p_CuratedInstallRequest):
"""Install a curated (anthropics/skills) skill with its FULL folder, not just
SKILL.md, so scripts/assets land too (the old path wrote only SKILL.md, which
left multi-file skills like pdf/docx with dead script references). Curated is
the vetted source, so this is one-click; files are still written inert, never
executed. Needs network at install time (the catalog only caches SKILL.md)."""
executed. When GitHub is unreachable (offline / rate-limited) it falls back to the
catalog's cached SKILL.md, so the install still works (single file, no folder
extras), restoring the old offline behavior."""
try:
resolved = await sources.resolve_curated_skill(req.folder)
except RegistryRateLimited:
raise HTTPException(status_code=429, detail="GitHub rate limit hit fetching this skill; try again in a few minutes.")
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=502, detail=f"could not fetch skill: {e}")
resolved = p_cached_curated_fallback(req.folder)
if resolved is None:
if isinstance(e, RegistryRateLimited):
raise HTTPException(status_code=429, detail="GitHub rate limit hit and no cached copy of this skill; try again in a few minutes.")
raise HTTPException(status_code=502, detail=f"GitHub unreachable and no cached copy: {e}")
logger.info(f"curated install: GitHub unreachable ({type(e).__name__}); installing '{req.folder}' from cached SKILL.md (single file, no folder extras)")
from backend.apps.skills.skills import write_folder_skill, unique_skill_slug
slug = unique_skill_slug(resolved["skill_id"])
+27
View File
@@ -32,10 +32,19 @@ def atomic_write_json(path: str, payload, *, indent: int = 2) -> None:
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=indent)
# os.replace is atomic for the filename, but the file's data
# may still be sitting in the page cache when the rename
# commits. A power loss between rename and the kernel's next
# writeback can leave a zero-length or torn file even though
# the rename "succeeded". fsync before the rename is what
# actually makes this crash-safe.
f.flush()
os.fsync(f.fileno())
# Windows: Defender can briefly hold the destination open; a couple of retries covers every real case.
for attempt in range(3):
try:
os.replace(tmp, path)
p_fsync_dir(directory)
return
except PermissionError:
if attempt == 2:
@@ -49,6 +58,24 @@ def atomic_write_json(path: str, payload, *, indent: int = 2) -> None:
raise
def p_fsync_dir(directory: str) -> None:
# Best-effort fsync of the parent dir so the rename itself sticks
# across a crash. POSIX needs this (ext4/xfs/btrfs); Windows doesn't
# let you open a directory for fsync, so we just skip there. Failing
# here is non-fatal: the file content is already fsync'd above and
# the rename has happened in memory.
try:
dir_fd = os.open(directory, os.O_RDONLY)
except OSError:
return
try:
os.fsync(dir_fd)
except OSError:
pass
finally:
os.close(dir_fd)
def read_json_or_none(path: str) -> dict | None:
"""Parse `path`; return None (and log) on a missing/garbled file rather than
raising. Schema validation is the caller's job, kept separate so a real
+56
View File
@@ -49,6 +49,62 @@ def test_atomic_write_preserves_existing_when_new_write_fails(tmp_path):
assert read_json_or_none(p) == {"good": 1}
def test_atomic_write_fsyncs_file_before_rename(tmp_path, monkeypatch):
# Without fsync before os.replace, we only get filename-atomicity, not
# data durability. Catch the regression by counting fsync calls before
# the rename happens.
from backend.config import json_store
fsync_count_at_replace = []
fsync_calls = []
real_fsync = os.fsync
real_replace = os.replace
def tracking_fsync(fd):
fsync_calls.append(fd)
return real_fsync(fd)
def tracking_replace(src, dst):
fsync_count_at_replace.append(len(fsync_calls))
return real_replace(src, dst)
monkeypatch.setattr(json_store.os, "fsync", tracking_fsync)
monkeypatch.setattr(json_store.os, "replace", tracking_replace)
atomic_write_json(str(tmp_path / "x.json"), {"k": "v"})
assert fsync_count_at_replace == [1], (
f"expected fsync on the tempfile before os.replace, saw "
f"{fsync_count_at_replace[0] if fsync_count_at_replace else 0} fsync calls"
)
def test_atomic_write_fsyncs_directory_after_rename(tmp_path, monkeypatch):
# POSIX only: the rename is only durable once the parent dir is fsync'd.
if not hasattr(os, "O_RDONLY"):
pytest.skip("directory fsync not applicable on this platform")
from backend.config import json_store
fsync_targets = []
real_fsync = os.fsync
def tracking_fsync(fd):
try:
st = os.fstat(fd)
fsync_targets.append("dir" if (st.st_mode & 0o170000) == 0o040000 else "file")
except OSError:
fsync_targets.append("unknown")
return real_fsync(fd)
monkeypatch.setattr(json_store.os, "fsync", tracking_fsync)
atomic_write_json(str(tmp_path / "x.json"), {"k": "v"})
assert "file" in fsync_targets, "expected fsync on the data file"
assert "dir" in fsync_targets, "expected fsync on the parent directory"
# ---------------- read_json_or_none ----------------
# ---------------- read_json_or_none ----------------
def test_read_missing_returns_none(tmp_path):
+3 -3
View File
@@ -55,7 +55,7 @@ def test_no_file_returns_defaults(settings_file):
s = store.load_settings()
assert isinstance(s, AppSettings)
assert s.default_system_prompt == DEFAULT_SYSTEM_PROMPT
assert s.theme == "dark"
assert s.theme == "light"
def test_minimal_old_file_fills_missing_with_defaults(settings_file):
@@ -130,7 +130,7 @@ def test_corrupt_json_returns_defaults_and_preserves_file(settings_file):
with open(settings_file, "w", encoding="utf-8") as fh:
fh.write("{ this is : not json ,,, ")
s = store.load_settings()
assert s.theme == "dark" # defaults
assert s.theme == "light" # defaults
# Original is moved aside (recoverable), not silently destroyed.
assert os.path.exists(settings_file + ".corrupt")
assert not os.path.exists(settings_file)
@@ -139,7 +139,7 @@ def test_corrupt_json_returns_defaults_and_preserves_file(settings_file):
def test_non_dict_top_level_returns_defaults(settings_file):
p_write(settings_file, ["not", "an", "object"])
s = store.load_settings()
assert s.theme == "dark"
assert s.theme == "light"
assert os.path.exists(settings_file + ".corrupt")
@@ -350,6 +350,55 @@ def test_curated_install_writes_full_folder(skills_dir, monkeypatch):
assert slug in listed and listed[slug]["has_supporting_files"] is True
def test_curated_install_falls_back_to_cached_skill_md_when_offline(skills_dir, monkeypatch):
"""Regression: when GitHub is unreachable (rate-limited/offline), /install-curated
falls back to the catalog's cached SKILL.md so the install still works (single file,
no folder extras) instead of erroring, restoring the old offline behavior."""
import secrets as p_secrets
from fastapi.testclient import TestClient
from backend.main import app
import backend.auth as auth_mod
import backend.apps.skill_registry.skill_registry as sr_routes
from backend.apps.skill_registry.skill_registry_github import RegistryRateLimited
if not auth_mod.TOKEN:
auth_mod.TOKEN = p_secrets.token_urlsafe(32)
client = TestClient(app, headers={"Authorization": f"Bearer {auth_mod.TOKEN}"})
async def boom(folder):
raise RegistryRateLimited()
monkeypatch.setattr("backend.apps.skill_registry.skill_registry_sources.resolve_curated_skill", boom)
monkeypatch.setattr(sr_routes, "p_cache", {"PDF": {
"name": "PDF", "description": "work with pdfs", "content": "Do PDF things.", "folder": "skills/pdf",
}})
r = client.post("/api/skill-registry/install-curated", json={"folder": "skills/pdf"})
assert r.status_code == 200 and r.json()["installed"] is True
assert r.json()["files"] == ["SKILL.md"] and r.json()["scripts"] == []
slug = r.json()["skill"]["id"]
md = (skills_dir / slug / "SKILL.md").read_text()
assert "Do PDF things." in md and "name: PDF" in md
def test_curated_install_no_cache_no_network_errors_honestly(skills_dir, monkeypatch):
"""If GitHub is unreachable AND nothing's cached, surface an honest error, not a
silent half-install."""
import secrets as p_secrets
from fastapi.testclient import TestClient
from backend.main import app
import backend.auth as auth_mod
import backend.apps.skill_registry.skill_registry as sr_routes
if not auth_mod.TOKEN:
auth_mod.TOKEN = p_secrets.token_urlsafe(32)
client = TestClient(app, headers={"Authorization": f"Bearer {auth_mod.TOKEN}"})
async def boom(folder):
raise RuntimeError("connection refused")
monkeypatch.setattr("backend.apps.skill_registry.skill_registry_sources.resolve_curated_skill", boom)
monkeypatch.setattr(sr_routes, "p_cache", {})
r = client.post("/api/skill-registry/install-curated", json={"folder": "skills/pdf"})
assert r.status_code == 502
def test_manual_rm_leaves_no_ghost_blocking_slug(skills_dir):
"""A folder deleted out-of-band (manual rm) must not keep squatting its slug via
a leftover index entry: existence is by files on disk, and a prune cleans the index."""
+44 -46
View File
@@ -2444,16 +2444,50 @@ app.on('web-contents-created', (_event, contents) => {
})();
`).catch(() => {});
// Agent bridge (window.OPENSWARM_APP). Injected into EVERY app's main world
// from the shell so it exists regardless of frontend/src; the lightweight
// App Builder mode deletes frontend/src (and with it the template's own
// agentBridge.ts), so this is the only entry point a trimmed app can't lose.
// Idempotent + guarded: a workspace app that imports its own bridge installs
// first and this no-ops, so we never clobber a registered bridge. An app
// becomes agent-operable by calling OPENSWARM_APP.register({rules, controls,
// getState, invoke}); until it does, describe()/getState() report __ready:false
// (the agent then falls back to native keyboard/mouse). Keep this in sync with
// backend/apps/outputs/webapp_template/frontend/src/agentBridge.ts.
const url = contents.getURL();
if (url.includes('spotify')) {
contents.executeJavaScript(`
(function() {
const origFetch = window.fetch;
window.fetch = async function(...args) {
const resp = await origFetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
if (url.includes('widevine-license') && !resp.ok) {
const clone = resp.clone();
try {
const text = await clone.text();
console.log('[drm-diag] License response ' + resp.status + ': ' + text.substring(0, 500));
} catch(e) {}
}
return resp;
};
// Check EME availability
if (navigator.requestMediaKeySystemAccess) {
navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{
initDataTypes: ['cenc'],
audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}],
}]).then(function(access) {
console.log('[drm-diag] Widevine EME access: ' + access.keySystem);
}).catch(function(err) {
console.log('[drm-diag] Widevine EME FAILED: ' + err.message);
});
} else {
console.log('[drm-diag] EME API not available');
}
})();
`).catch(() => {});
}
});
// Agent bridge (window.OPENSWARM_APP): app webviews ONLY, all platforms. Gated
// off the browser partition so a normal browser card / agent web automation
// never carries this global, since a unique window.OPENSWARM_APP is a one-line
// bot tell on the open web. Shell-injected so a trimmed App-Builder app (its
// frontend/src + the template's agentBridge.ts deleted) still gets a bridge;
// idempotent, so a full app that self-installs its own bridge no-ops here. Keep
// in sync with backend/apps/outputs/webapp_template/frontend/src/agentBridge.ts.
if (contents.session !== session.fromPartition(BROWSER_PARTITION)) contents.on('dom-ready', () => {
contents.executeJavaScript(`
(function() {
if (window.OPENSWARM_APP) return;
@@ -2521,44 +2555,8 @@ app.on('web-contents-created', (_event, contents) => {
},
};
window.OPENSWARM_APP = bridge;
try { console.warn('[openswarm:bridge] shell-injected window.OPENSWARM_APP at', location.href); } catch (_) {}
})();
`).catch(() => {});
const url = contents.getURL();
if (url.includes('spotify')) {
contents.executeJavaScript(`
(function() {
const origFetch = window.fetch;
window.fetch = async function(...args) {
const resp = await origFetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
if (url.includes('widevine-license') && !resp.ok) {
const clone = resp.clone();
try {
const text = await clone.text();
console.log('[drm-diag] License response ' + resp.status + ': ' + text.substring(0, 500));
} catch(e) {}
}
return resp;
};
// Check EME availability
if (navigator.requestMediaKeySystemAccess) {
navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{
initDataTypes: ['cenc'],
audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}],
}]).then(function(access) {
console.log('[drm-diag] Widevine EME access: ' + access.keySystem);
}).catch(function(err) {
console.log('[drm-diag] Widevine EME FAILED: ' + err.message);
});
} else {
console.log('[drm-diag] EME API not available');
}
})();
`).catch(() => {});
}
});
}
});
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openswarm",
"version": "1.5.3",
"version": "1.5.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openswarm",
"version": "1.5.3",
"version": "1.5.4",
"hasInstallScript": true,
"dependencies": {
"electron-updater": "6.8.3",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openswarm",
"version": "1.5.3",
"version": "1.5.4",
"description": "OpenSwarm — AI Agent Orchestrator",
"author": "openswarm-ai",
"main": "main.js",
@@ -33,6 +33,7 @@
"productName": "OpenSwarm",
"afterPack": "./build/after-pack.js",
"electronLanguages": [
"en",
"en-US"
],
"electronDownload": {
+27 -18
View File
@@ -1,12 +1,17 @@
#!/usr/bin/env node
// Guards the empty-locale renderer crash. A packaged build MUST ship Chromium's
// locale .pak files (locales/en-US.pak + the full ~50). If they are missing,
// Electron launches the renderer with an EMPTY --lang, and Blink's
// LCIDFromLocaleInternal (third_party/blink/.../text/locale_win.cc) null-derefs
// (STATUS_ACCESS_VIOLATION 0xC0000005, read of 0x8) the instant a text/agent/
// webview surface mounts - a hard crash on the first real interaction, with no
// JS error to localize it. electron-builder has been observed to drop these on a
// --dir repack, so we assert them explicitly rather than trust the packager.
// Guards the empty-locale crashes on BOTH platforms. A packaged build MUST ship
// Chromium's locale .pak files.
// Windows: missing locales/en-US.pak launches the renderer with an EMPTY --lang
// and Blink's LCIDFromLocaleInternal null-derefs (STATUS_ACCESS_VIOLATION
// 0xC0000005) the instant a text/agent/webview surface mounts.
// macOS: missing en.lproj/locale.pak makes EVERY l10n_util string come back
// empty; the WebAuthn Touch ID path then passes that empty string as
// localizedReason to -[LAContext evaluateAccessControl:...], which raises an
// uncaught NSException and kills the whole app on any passkey prompt (the
// 1.5.3 "OS suicide"). electronLanguages naming is per-platform ("en" for mac
// .lproj dirs, "en-US" for win .pak files) and electron-builder silently
// deletes EVERYTHING when the name doesn't match, so we assert the artifact
// explicitly rather than trust the packager.
'use strict';
const fs = require('fs');
@@ -41,22 +46,25 @@ function checkWinLinux(exe) {
}
function checkMac(exe) {
// mac stores locale paks inside the Electron Framework; layout varies by version,
// and this crash is Windows-specific, so be informational rather than blocking.
// mac stores locale paks as <lang>.lproj/locale.pak inside the Electron
// Framework; chrome_100_percent/resources.pak do NOT count (they survive the
// language strip that causes the crash), so require a real locale.pak.
const appRoot = exe.slice(0, exe.indexOf('.app') + 4);
const found = [];
(function walk(d, depth) {
if (depth > 6) return;
if (depth > 8) return;
let ents = [];
try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
for (const e of ents) {
const full = path.join(d, e.name);
if (e.isDirectory()) walk(full, depth + 1);
else if (e.isFile() && e.name.toLowerCase().endsWith('.pak')) found.push(full);
else if (e.isFile() && e.name.toLowerCase() === 'locale.pak') found.push(full);
}
})(appRoot, 0);
process.stdout.write(` mac: found ${found.length} .pak file(s) under the app bundle\n`);
return { ok: found.length > 0, msg: `${found.length} paks (mac is informational)` , soft: true };
const hasEn = found.some((p) => path.basename(path.dirname(p)).toLowerCase() === 'en.lproj');
process.stdout.write(` mac: ${found.length} locale.pak file(s), en.lproj=${hasEn}\n`);
if (!hasEn) return { ok: false, msg: `en.lproj/locale.pak missing (${found.length} locale paks under the bundle)` };
return { ok: true, msg: `${found.length} locale paks incl en.lproj` };
}
function main() {
@@ -64,13 +72,14 @@ function main() {
const exe = h.packagedAppPath(args.app);
const res = process.platform === 'darwin' ? checkMac(exe) : checkWinLinux(exe);
if (res.ok) { process.stdout.write(`PASS locale paks present (${res.msg})\n`); process.exit(0); }
if (res.soft) { process.stdout.write(`WARN ${res.msg} - could not confirm mac paks; not blocking\n`); process.exit(0); }
process.stderr.write(
`FAIL packaged build is MISSING Chromium locale paks: ${res.msg}\n` +
` The renderer would launch with an empty --lang and crash in Blink's\n` +
` Windows: renderer launches with an empty --lang and crashes in Blink's\n` +
` LCIDFromLocaleInternal (0xC0000005) on the first text/agent/webview mount.\n` +
` Fix: ensure electron-builder copies node_modules/electron/dist/locales/*.pak\n` +
` into the packaged output (this regressed on --dir repacks of the v42 build).\n`);
` macOS: every l10n string is empty and the WebAuthn Touch ID prompt kills\n` +
` the whole app with an uncaught NSException (empty localizedReason).\n` +
` Fix: electronLanguages must list BOTH "en" (mac .lproj) and "en-US"\n` +
` (win locales/*.pak); a name miss makes electron-builder delete them all.\n`);
process.exit(1);
}