29 KiB
Executable File
AI-LSC Changelog
v3.1 — 2026-07-07
Codename: Ankh of Jah (continuation)
The v3.1 release is a hardening + polish pass on top of v3.0. It applies the full master code critique (91 of 93 findings addressed), introduces two new UI widgets (Pipeline Ticker + Workspace Tab), and ships three latent-bug fixes that were caught during the post-pass double-check.
New features
Pipeline Ticker
A horizontally scrolling status bar at the top of every workspace tab. Reads the active-tools set from pipeline_state.json, joins it against the STACK_WIRINGS topology in stack/connections.py, and renders:
ollama ──openai_api──▶ litellm ──openai_api──▶ aider ❗ open_webui
- Color-coded arrows by interface type — blue for
openai_api, green forvector/vector_search/embedding, orange forredis_pubsub/redis_cache, purple forpostgresql/mariadb/mysql, teal forhttp_api/websocket/grpc, slate forfilesystem/tmux_socket/systemd_unit, red forcuda_driver. - Orphan detection — any active tool that has no in-stack wiring to another active tool is flagged red with an
❗prefix. This surfaces real registry gaps (e.g. theopen_webuivsopenwebuitool_id collision). - Running-state coloring — pills are blue when the tool is running, white when stopped. The ticker refreshes on every 2 s service-status poll so state changes propagate immediately.
- Hover-pause — mouse over the ticker to stop the scroll so you can read a long flow.
- Click-to-jump — click any tool pill to switch to the Tools tab; the matching row is selected, scrolled to center, and flashed amber for 1.5 s.
Files: src/ai_lsc/ui/widgets/pipeline_ticker.py (new, ~330 lines), src/ai_lsc/ui/main_window.py (wiring), src/ai_lsc/ui/pages/tools_tab.py (new highlight_tool() method), src/ai_lsc/ui/pages/service_row.py (new is_running_now() cache method).
Workspace Tab
A new Workspace nav entry (between Chat and Git Sources) that provides virt-manager / aqemu–style peek orchestration. Every active tool gets its own sub-tab:
- 🌐 Web tools (
has_web=True) embed viaQWebEngineViewathttp://127.0.0.1:{port}. A URL bar at the top shows the loaded URL; ⟳ reloads, ↗ opens in an external browser. No need to leave the app for a browser. - ⌨ CLI tools (
has_cli=True, no web) attach to their tmux session viatmux capture-pane -t <session>::<tool_id> -p -S -200polled at 4 Hz. The terminal is read-only — use a real terminal app for interactive input. - 📦 Passive / library tools get a placeholder explaining they have no interactive surface.
- ⏸ Not-yet-running tools show a Start tool button that wires back to the existing
ServiceRow.start_service()flow. After 1.5 s the workspace tab auto-refreshes so the placeholder → live view switch happens automatically. - Sub-tabs are closable; closing a sub-tab stops the polling but does NOT stop the underlying tool (use the Stack Editor for that).
Servo note: Web embedding uses PySide6.QtWebEngineWidgets.QWebEngineView by default. Swapping in Mozilla's servo engine later is a one-line change to _make_web_view() in src/ai_lsc/ui/widgets/workspace_tab.py — the rest of the WorkspaceTab code only depends on the setUrl() / url() / load() API.
Files: src/ai_lsc/ui/widgets/workspace_tab.py (new, ~310 lines), src/ai_lsc/ui/main_window.py (new _build_workspace_orchestration_page + nav entry + refresh hooks).
Security & reliability hardening (the critique pass)
91 of 93 findings from the master code critique were addressed. See whatremains.txt for the two intentionally-skipped items and deferred polish. Highlights:
CRITICAL (6 of 7 fixed; C-05 skipped per user)
- C-01
runtime/process.py—shell=Trueremoved fromlaunch_desktop,launch_terminal,kill_by_name,docker_compose_down. New_to_arg_list()helper accepts either a pre-split argv list or a single shell-style string (split viashlex.split). - C-02
runtime/systemd.py—shell=Trueremoved fromstart,stop,is_active.is_activenow has a 5 s timeout. - C-03
runtime/tmux.py— 5shell=Truesites converted to list-form argv. New_validate_name()rejects shell-metacharacter session/window names. New_socket_path_for()moves tmux sockets out of/tmpinto$XDG_RUNTIME_DIR/ai-lsc/.SESSIONis now user-scoped:ai_lsc_<uid>. - C-04
runtime/installer.py— 15+shell=Truesites converted to list-form argv. Post-install hooks and script-type installers now run via["bash", "-c", cmd](still a shell, but the registry string is passed verbatim as a single argv element so it can't break out of the subprocess call itself). - C-05
curl|shremote installers — SKIPPED per user instruction. The Ollama / Grafana Alloy / Meilisearchcurl … | shpatterns remain inruntime/installer.py:361,registry/layers/inference.py:30,registry/layers/observability.py:122,registry/layers/data_knowledge.py:164, andregistry/defaults.py:642. - C-06
agents/librechat_config.py— hardcodedsk-ai-lsc-localandsk-localAPI keys removed. Keys now read fromAI_LSC_LITELLM_KEY/AI_LSC_OPENWEBUI_KEYenvironment variables via a new_env_api_key()helper. - C-07
agents/orchestrator.py— missingimport osadded (was a guaranteedNameErroron everyAgentOrchestratorinstantiation).
HIGH (24 of 24 fixed)
- H-01 Path-traversal via unsanitized
tool_id— new_validate_tool_id()inruntime/executor.pyrejects..,.,/, and shell metacharacters. - H-02
os.getcwd()for config —_load_configandsave_configinui/main_window.pynow resolve againstself.base_dirinstead of the cwd the app was launched from. - H-03 Atomic JSON writes — new
_atomic_write_json()helper inui/main_window.pyusestempfile.mkstemp+os.fsync+os.replace. Applied to stack export, config save, and stack-wizard state save. - H-04 Popen reference tracking —
ProcessManagernow tracks every launched child inself._launchedand exposesreap()+shutdown()methods. Zombie accumulation in long-lived GUI sessions is fixed. - H-05
pull_modelPopen not killed on thread crash —agents/dispatcher.py:_pull_modelnow wrapsproc.communicate()intry/exceptwithproc.kill()in the handler. - H-06 JSON parse crash from malformed LLM output —
agents/agent_loop.pynow wrapsjson.loads(func.get("arguments", "{}"))intry/except json.JSONDecodeError. The parse error is fed back to the LLM as a tool result so the model can self-correct on the next round. - H-07
_pull_modelNone guard —dispatcher._pull_modelnow handles the case whereruntime.pull_model()returnsNone. - H-08 Duplicate
list_available_toolsschema —agents/tool_bridge.py:generate_all_schemasnow filters out the staticlist_available_toolsschema before appending the annotated version. - H-09 Broad
except Exception— 12+ sites acrossguardrails.py,loader.py,manifest/support.py,agent_loop.py,ollama_tools.py,service_row.py,chat/api.py,model_pool.py,qdrant_bridge.pynow catch specific exceptions (OSError,ValueError,json.JSONDecodeError,subprocess.SubprocessError,urllib.error.URLError, etc.). - H-10 Exception messages expose internal details —
chat/api.pynow sanitizes error bodies via_short_reason()and_categorize_url_error(). Full detail is logged server-side. - H-11 LXC container name from unsanitized
tool_id— new_validate_lxc_name()and_validate_tool_id()inruntime/lxc.py. - H-12 LXC
attach_execdestroys quoting —command.split()replaced withshlex.split(command). - H-13 Redis lock silently bypassed when down —
agents/redis_bridge.py:acquire_lockandrelease_locknow log a WARNING when Redis is unreachable so operators know concurrent agents could race. - H-14
_enforce_qualityfalse-positive error detection —agents/orchestrator.pynow uses a word-boundary regex with a negative lookahead(?![\w\-])so hyphenated compounds likeerror-correction module initializedare not flagged. - H-15 Registry layer files have incomplete flag schemas —
registry/validator.pynow enforces the full 8-key flags schema (has_cli,has_gui,has_web,is_ollama,is_docker,is_passive,is_mcp,is_skills_collection).scripts/backfill_layer_flags.pybackfilled 123 flag blocks across all 13 layer files. - H-16
_inject_skill_stubreturns fake success —agents/dispatcher.pynow validates the skill exists before reporting success. - H-17 Placeholder resolution triple-copy in
export.py— new_resolve_placeholders()helper replaces three duplicated 6-line.replace()chains ingenerate_compose_yaml,generate_lxc_configs, andgenerate_firecracker_configs. - H-18 Ollama
/api/toolsendpoint does not exist —agents/ollama_tools.pyregister_allandregister_singleare now gated behind_registration_supported = Falsewith a clear warning. Tool schemas are passed inline to/api/chatinstead. - H-19 No port validation on user input — new
_validate_port()inruntime/executor.pyenforces1 ≤ port ≤ 65535. UI surfaces a cleanValueErrormessage instead of a cryptic URLError. - H-20 No URL scheme validation in
install_custom— new_validate_url()inruntime/installer.pyrejects non-http(s) schemes. - H-21 No signal handling on parent exit —
ui/main_window.py:closeEventnow callsruntime._process.shutdown()to terminate every tracked child. - H-22
install_customopens arbitrary URLs — same fix as H-20. - H-23 Thread-unsafe pull lock in model pool —
agents/model_pool.pyself._pull_lock = False(plain boolean) replaced withthreading.Lock(). Non-blocking acquire so concurrent agent threads don't both enter the pull branch. - H-24 Qdrant collection dimension hardcoded —
agents/qdrant_bridge.pycreate_collectionnow probes the live embedding dimension via_probe_embedding_dimension()instead of hardcoding768. Existing collections with a different dimension are surfaced (not silently hidden).
MEDIUM (42 of 42 fixed)
- M-01 Missing
encoding="utf-8"onopen()— fixed in 15+ sites acrossui/main_window.py,runtime/lxc.py,ui/dialogs/stack_wizard.py. - M-02
os.path.joinmixed withpathlib—utils/filesystem.py:walk_treenow usesPath.rglob. - M-03 TOCTOU race in log file operations —
ui/main_window.pylog readers now wrap stat + read in a singletry/except OSError. - M-04 No file locking on shared JSON files —
_atomic_write_json()now usesfcntl.flockfor cross-process serialization. - M-05 No file lock / fsync on LXC config append —
runtime/lxc.py:_apply_confignow flushes + fsyncs. - M-06
/tmpsocket path without cleanup —runtime/tmux.py:_socket_path_formoves sockets under$XDG_RUNTIME_DIR/ai-lsc/with sanitized tool_id. - M-07 through M-13 Nested-if flattening — applied via guard clauses, dict dispatch, reverse-lookup dicts, and predicate extraction across
installer.py,orchestrator.py,model_pool.py,guardrails.py,main_window.py. - M-14 through M-17 Duplicate code extraction —
_iter_py_files/_read_sourceinguardrails.py,_scan_and_import/_match_tagspatterns,_cache_set/_cache_getinredis_bridge.py,_resolve_placeholdersinexport.py. - M-18 through M-23 Loop → comprehension / builtin —
dict.fromkeys()for dedup, dict comprehension forpreflight_batch,next()fordetect_terminal, list comprehension for LXC config lines,extend()for skills_loaded. - M-24 Dead variable
exposedinget_consumers— removed. - M-25
Anytype annotations —ui/protocol.pynow usesTYPE_CHECKINGimports;agents/orchestrator.pydispatcherparam now typed as"AgentDispatcher". - M-26 Variable shadowing in
generate_env_file—linesrenamed toollama_ep. - M-27 Dead if/else block in
guardrails.py— removed (PARENT_ALLOWED_DIRS was always empty). - M-28 Redundant
import jsonin orchestrator method — removed. - M-29 Sorted-set member collision in task queue —
redis_bridge.pynow usestask_idas the sorted-set member and stores the payload in a separate hash. - M-30
_LAYERS_DIRdefined but never used — removed fromregistry/loader.py. - M-31
use_modeltautological assignment — cleaned up. - M-32 Missing error handling on
install_pip— addedtry/except subprocess.CalledProcessError. - M-33 / M-34 Timeouts on
systemctl is-activeandpkill— both now havetimeout=5. - M-35 File handle leak in
verify_and_watch—open(log_file, "a").close()replaced withPath(log_file).touch(). - M-36 Recursive directory traversal —
utils/filesystem.py:walk_treerewritten to usePath.rglob. - M-37 Model pool pull timeout applies to entire stream —
for line in resp: passreplaced withresp.read(). - M-38 Embed batch is sequential —
qdrant_bridge.py:embed_batchnow usesThreadPoolExecutor.map. - M-39 Hardcoded
pacmaninstall hint —_install_hintinruntime/lxc.pynow lists pacman + apt + dnf. - M-40 Nested ternary in
_build_payload_history— deferred (chatbot_console.py), see whatremains.txt. - M-41 Duplicate
OpenEngineerImporterimport — removed fromstack_templates/manager.py. - M-42
_load_skillsdocuments unimplemented Qdrant feature — converted to explicitTODO(security)comment.
LOW (19 of 20 fixed; 1 deferred)
See whatremains.txt for the full list. Highlights:
- L-02 Line-ending normalization on log reads —
rstrip("\r\n")applied. - L-03 TOCTOU on symlink creation —
os.symlinkwrapped intry/except FileExistsError. - L-05 Tmux session name uniqueness —
SESSION = f"ai_lsc_{os.getuid()}". - L-06 Port range check in
chat/api.py—_validate_portapplied toport_idup-front. - L-08
create_collectionreports success for existing collection — now checks the existing collection's dimension and surfaces mismatches.
SaaS-only tool blocklist (v3.1 hardening follow-up)
After the v3.1 release, the user identified a policy gap: SaaS-only tools (closed-source desktop apps with restrictive ToS, hosted LLM routers with no local binary, managed inference services) must not be addable to the registry. The audit found that the actual code registry was already clean — the SaaS names (OpenRouter, LM Studio, Groq, Codestral) only appeared as stale references in README.md and docs/ADR-001-capability-architecture.md. They were removed from the code at some earlier point but the docs were never updated.
What changed:
-
Stale doc references fixed.
README.mdL6 row now listsLiteLLM Proxy, 9Router Proxy, Odysseus, LangChain, LangFlow, OpenAI Swarm, Agno(matching the actual registry).README.mdL8 row replacesCodestral(Mistral SaaS model) withOpenHandsandCodex.docs/ADR-001-capability-architecture.mdLLM Gateway providers now listLiteLLM · 9Router Proxy · Local proxy(wasLiteLLM · OpenRouter · Local proxy); Inference Engine providers replaceLM StudiowithSGlang. -
SaaS blocklist added to the registry validator (
src/ai_lsc/registry/validator.py). The following tool_ids are now rejected at validation time with a clear error message pointing to this section:openrouter,lm_studio,lmstudio,groq,together_ai,together,fireworks_ai,fireworks,replicate,runpod,modal,anyscale,perplexity,cohere,mistral_api,deepseek_api,openai_api,huggingface_inference -
SaaS host regex added to the validator. Even if a tool_id isn't on the blocklist, the validator now rejects any launcher cmd or installer cmd that references a known SaaS provider hostname (api.openai.com, api.anthropic.com, api.openrouter.ai, api.groq.com, api.together.xyz, api.fireworks.ai, api.replicate.com, api.perplexity.ai, api.cohere.ai, api.mistral.ai, api.deepseek.com, generativelanguage.googleapis.com, api.lmstudio.ai, endpoint.huggingface.com). Localhost URLs (127.0.0.1, localhost, 0.0.0.0) are always allowed.
-
Localhost-only env forced for CLI tools that CAN call SaaS.
claude_code,aider,openhands,fabric, and the newcodexentry now have their launcher cmds prepended with the appropriate localhost env vars:Tool Env vars forced claude_codeANTHROPIC_BASE_URL=http://127.0.0.1:4000 ANTHROPIC_API_KEY=sk-ai-lsc-localaiderOPENAI_API_BASE=http://127.0.0.1:4000/v1 OPENAI_API_KEY=sk-ai-lsc-localopenhandsOPENAI_API_BASE=http://127.0.0.1:4000/v1 OPENAI_API_KEY=sk-ai-lsc-localfabricOPENAI_API_BASE=http://127.0.0.1:4000/v1 OPENAI_API_KEY=sk-ai-lsc-localcodex(new)OPENAI_BASE_URL=http://127.0.0.1:4000/v1 OPENAI_API_KEY=sk-ai-lsc-localThis breaks SaaS routing by default — the user must explicitly override the env var (via the service row port or a custom launcher cmd) to call a SaaS endpoint. Each tool's
depsnow includeslitellmso the Stack Editor flags a missing local proxy if the user hasn't staged one. -
New
codextool entry added toregistry/defaults.pyandregistry/layers/endpoints.py— OpenAI's open-source Codex CLI (@openai/codexnpm package), classified as L6 AI Endpoints, with the localhost-only launcher shown above. The user explicitly approved this addition with the caveat: "you can add codex and claude code but only if mapped by force to a localhost port for the engine or endpoint."
Verification: defaults.py validates with 0 errors (125 tools); layer files validate with 0 non-curl|sh errors (124 tools); all 5 localhost-mapped tools confirmed to have 127.0.0.1 in their launcher cmd; the SaaS blocklist correctly rejects openrouter and lm_studio with the documented error message; the SaaS host regex correctly rejects https://api.openai.com/v1 in a launcher cmd while allowing http://127.0.0.1:4000/v1.
License acceptance gate (v3.1 hardening follow-up #2)
After the SaaS blocklist landed, the user asked for a layered license-acceptance system: ToS/disclaimer warnings for proprietary tool pulls, per-tool license acceptance for open-source tools, and a license auto-approval registry where the user can pre-approve entire license types (accept all GPL, all AGPL, all MIT, all BSD, etc.) so they don't have to accept each tool individually. The auto-approval registry must NOT contain any SaaS or proprietary tools. lmstudio stays on the blocklist for aggressive ToS.
What changed:
-
New license catalog (
src/ai_lsc/registry/licenses.py) — 20 SPDX IDs across three categories:Category Auto-approvable? Disclaimer? Licenses OSI (open-source) ✅ Yes ❌ No MIT, Apache-2.0, GPL-2.0, GPL-3.0, AGPL-3.0, LGPL-3.0, BSD-2-Clause, BSD-3-Clause, MPL-2.0, ISC, PostgreSQL, Python SOURCE_AVAILABLE (fair-code) ❌ No ✅ Yes BSL-1.1, SSPL, RSALv2, Sustainable-Use, Dify-OSL PROPRIETARY (ToS-governed) ❌ No ✅ Yes (prominent) Proprietary, Anthropic-ToS, LMStudio-ToS (blocked) Each license entry includes: SPDX ID, human-readable name, category, URL to full text, one-paragraph summary, and an optional disclaimer shown in the acceptance dialog.
-
New
LicenseGateclass (src/ai_lsc/registry/license_gate.py) — sits between the user's "Install" click and the installer dispatch. For every tool installation, the gate checks (in order):- SaaS blocklist — if the tool_id is blocked, raises
LicenseBlockedimmediately (no dialog, no acceptance, no install). - Auto-approval registry (
config/license_approvals.json) — user-editable list of OSI-approved SPDX IDs. If the tool's license is in this list AND the license category is OSI, install proceeds without a dialog. Non-OSI licenses in the file are ignored + logged (defensive against hand-editing). - Per-tool acceptance registry (
config/license_acceptances.json) — auto-managed; records every per-tool acceptance so the user isn't prompted twice. - If none of the three cover the tool, raises
LicenseAcceptanceRequired(carrying theLicenseInfofor the dialog).
LicenseGate.add_auto_approval(spdx)rejects non-OSI licenses with a clearValueError— source-available and proprietary licenses cannot be auto-approved, period. - SaaS blocklist — if the tool_id is blocked, raises
-
New
LicenseAcceptanceDialog(src/ai_lsc/ui/dialogs/license_dialog.py) — Qt dialog shown when the gate raisesLicenseAcceptanceRequired. Shows:- Tool name + tool_id + license name + SPDX ID
- Category banner (green ✓ for OSI, orange ⚠ for source-available, red ⛔ for proprietary)
- License summary (one paragraph)
- Disclaimer (only for source-available + proprietary — prominent red box)
- "Open in browser ↗" button linking to the full license text
- Confirmation checkbox (pre-checked for OSI, unchecked for non-OSI — the user must explicitly check it before Accept is enabled)
- Three buttons: Accept & Install (records per-tool acceptance), Accept all <license> (only for OSI — adds the SPDX to the auto-approval registry), Cancel
Also includes a
LicenseBlockedDialogfor the blocked case (just an OK button + suggestion to use a local alternative). -
licensefield added to the registry schema — every tool entry must now declare its license SPDX ID. The validator (registry/validator.py) enforces this:- Missing/empty
licensefield → error - Unknown SPDX ID (not in the license catalog) → error
- The
_REQUIRED_FIELDSset now includes"license"
Two backfill scripts added:
scripts/backfill_tool_licenses.py— adds thelicensefield to every tool based on a curated override table (85 tools mapped to known licenses: ollama→MIT, vllm→Apache-2.0, grafana→AGPL-3.0, redis→RSALv2, terraform→BSL-1.1, n8n→Sustainable-Use, claude_code→Anthropic-ToS, etc.)scripts/backfill_default_licenses.py— fills any remaining tools with"Proprietary"as the defensive default (83 tools defaulted — the user can review and update these to their actual licenses later)
- Missing/empty
-
License gate wired into the installer —
InstallerManager.__init__now accepts alicense_gateparameter.InstallerManager.run()andinstall_with_preflight()callself._check_license(tool_id, license_spdx)before any subprocess dispatch. If the gate raisesLicenseBlockedorLicenseAcceptanceRequired, the exception propagates up throughRuntimeExecutor.install_tool()to the UI. -
ServiceRowcatches license exceptions — the install thread'sexcept Exceptionhandler (which runs on the main thread viaQTimer.singleShot) calls_handle_license_exception()which:- For
LicenseBlocked→ showsLicenseBlockedDialog(just an OK button) - For
LicenseAcceptanceRequired→ showsLicenseAcceptanceDialogand connects the dialog'saccepted_individual/accepted_all_of_typesignals to handlers that record the acceptance and retry the install
- For
-
lmstudioblocklist comment — theSAAS_BLOCKLISTdefinition invalidator.pynow includes an explicit comment: "lm_studio / lmstudio: BLOCKED for aggressive ToS — the user considers LM Studio's Terms of Service restrictive enough to be equivalent to a SaaS offering, so it is auto-banned regardless of any per-tool acceptance the user might try to grant."
License distribution after backfill (135 unique tool_ids across defaults.py + layer files):
Proprietary 50 tools (defensive default for tools whose license is unknown)
MIT 49 tools
Apache-2.0 17 tools
AGPL-3.0 5 tools
GPL-3.0 3 tools
MPL-2.0 2 tools
Anthropic-ToS 1 tool (claude_code)
Python 1 tool (python)
BSL-1.1 1 tool (terraform)
PostgreSQL 1 tool (postgresql)
GPL-2.0 1 tool (mariadb)
RSALv2 1 tool (redis)
LGPL-3.0 1 tool (glances)
Dify-OSL 1 tool (dify)
Sustainable-Use 1 tool (n8n)
The 50 tools defaulted to "Proprietary" include some that are actually open-source (tmux, git, podman, docker) — the user can review and update these in defaults.py / the layer files. The gate will require individual acceptance for each until the license is corrected.
Verification:
- defaults.py: 125 tools, 0 validation errors
- Layer files: 124 tools, 0 non-curl|sh validation errors
- All 135 unique tool_ids have a
licensefield LicenseGate.check()correctly returnsneeds_acceptancefor fresh tools,acceptedafteraccept(),blockedfor SaaS-blocklist tool_idsLicenseGate.add_auto_approval()correctly rejects BSL-1.1 (source-available) and Proprietary withValueErrorlmstudioandlm_studioboth onSAAS_BLOCKLISTclaude_code(Anthropic-ToS) andn8n(Sustainable-Use) both surfaceneeds_disclaimer=Truevia their category- Auto-approval + acceptance files are created in
config/with the expected JSON structure
Bugs caught during the post-pass double-check (fixed)
Three latent bugs were introduced during the initial fix wave and caught by the double-check's 19 functional spot-checks. All three are now fixed:
installer._validate_tool_idregex allowed/— the regex wasr"^[A-Za-z0-9_.:\-/]+$"(with trailing/), so../../etc/passwdwould have passed. Tightened tor"^[A-Za-z0-9_.:\-]+$"(no/).- All three
_validate_tool_idvalidators accepted bare..and.—os.path.normpath('..')returns'..'unchanged, so the normpath check missed these. Added explicittool_id in {".", ".."}rejection in installer.py, executor.py, and lxc.py. - H-14
_ERROR_REstill flaggederror-correction—\b(word boundary) treats-as a non-word char, soerror-correctionhad a boundary betweenerrorandcorrection. Changed regex to\b(?:error|failed|not found|timeout|exception|traceback)(?![\w\-])— the negative lookahead rejects matches where the next character is a word char OR a hyphen.
Verification
- All 89 Python source files parse cleanly via
ast.parse. - All 124 tools in
defaults.pypass the strengthened validator (0 errors). - All 123 tools across the 13 layer files pass the strengthened validator (3 expected curl|sh-related warnings only).
- All 134 unique tool_ids (defaults + layers merged) pass every
_validate_tool_idvalidator (installer, executor, LXC) and every_validate_lxc_namecheck. - 19/19 functional spot-checks pass (after the 3 double-check fixes).
- Ticker edge + orphan detection simulated against the real
STACK_WIRINGSdata across 6 test scenarios — all behaved as expected.
Known issues
- PySide6 not available in some test environments. The new widget modules (
pipeline_ticker.py,workspace_tab.py) AST-parse cleanly and pass structural checks, but were not exercised by a live Qt instantiation test in the sandbox where v3.1 was finalized. Verify by running the app. open_webuivsopenwebuitool_id collision — the Pipeline Ticker surfaces this real registry inconsistency. Recommend a follow-up pass to reconcile.docs/screenshots/still reflects v3.0 — a screenshot refresh pass to capture the new Pipeline Ticker + Workspace Tab is overdue.
Upgrade notes
- If you have a v3.0
pipeline_state.json, it will continue to work — the schema is unchanged. - If you maintain custom layer-file entries, run
python scripts/backfill_layer_flags.pyto backfill the 5 new flag keys (is_ollama,is_docker,is_passive,is_mcp,is_skills_collection) defaulting toFalse. The validator will reject entries missing these keys. - If you have hardcoded
sk-ai-lsc-localAPI keys in your environment, setAI_LSC_LITELLM_KEYandAI_LSC_OPENWEBUI_KEYenv vars instead — the source-code default is now an empty string. - If you launch the app from a non-default working directory, config is now resolved against
BASE_DIRinstead ofos.getcwd()— this may move yourconfig.jsonto a new location on first v3.1 launch.
Artifacts
- Master tarball:
ai-lsc-master-2026-07-07.tar.gz - New scripts:
scripts/backfill_layer_flags.py - New widgets:
src/ai_lsc/ui/widgets/pipeline_ticker.py,src/ai_lsc/ui/widgets/workspace_tab.py - New docs:
CHANGES.md(this file),docs/ADR-002-pipeline-ticker.md,docs/ADR-003-workspace-tab.md - Skipped-items register:
whatremains.txt
v3.0 — earlier
See git history for the v3.0 release notes. v3.0 introduced the 13-layer architecture, the capability model, the OpenEngineer importer, and the Firecracker microVM export backend.