feat: complete the Unifia rebrand and repair the desync it left behind - #23
Merged
Conversation
added 12 commits
August 10, 2026 15:37
`matchAll(/\((?:[^()\]|\.)*\)\s*Tj/g)` restarts a failed attempt at start+1, so a document made of `(\(\(\(...` has every other offset re-consume the whole tail. Measured on the old code: 10 ms at 2k pairs, 603 ms at 16k — quadratic, on bytes that come straight from an uploaded file (CodeQL `js/polynomial-redos`). The scanner resumes where the failed attempt stopped: 0.43 ms at 16k. Differential fuzzing over 500k random inputs found exactly one divergence class, and it is a fix: JS `.` excludes line terminators, so `\.` could never match a backslash-newline pair and the old regex silently dropped any run whose literal used a line continuation. PDF 32000-1 §7.3.4.2 allows those. Both the continuation and a 20k-pair timing guard are now covered by tests. Affects: PDF preview text extraction only. Does not affect: the active-element refusal, which runs before this and is unchanged.
…n-open Checking `isFile()` on a path and then reading that path is a TOCTOU race (CodeQL `js/file-system-race`). Opening once and using the handle for both assertions keeps the "it is a regular file" check without re-resolving.
`formatMs`, `diffColor` and the `ProviderID`/`ModelID` import have no callers. Deleted rather than underscore-prefixed — git log -S recovers them if they are ever wanted, and `diffColor` would need rewriting anyway: its nested ternary returns a boolean when `betterLower` is true instead of a colour token.
`packages/contracts` is consumed by projects on moduleResolution node16, where a relative import without an extension is TS2835. Checking the package's own tsconfig in isolation passed and hid it — @unifia/workspace-runtime is what compiles those files under node16, and it broke both the typecheck workflow and the quality/typecheck leg of the conformance gate.
…mised The daemon's header says it routes "whitelisted toolchain commands", but no allowlist existed: `MUTATING` only chose whether to push sources back, so any POST to /exec ran arbitrary shell on the developer's PC (CodeQL `js/command-line-injection`, open since 2026-05-05). 127.0.0.1 is not the barrier it appears to be — `adb reverse` exposes the port to the device by design, and a page loaded in the developer's browser can POST to it with Content-Type text/plain, which needs no preflight. Three guards, matching the documented contract: - the leading token must be in ALLOWED_TOOLS and the command must carry no shell control character, so nothing can be chained past it - env may not set PATH, LD_*, DYLD_*, NODE_OPTIONS, BASH_ENV, SHELL or IFS, which would otherwise redirect which binary `cargo` resolves to - requests carrying Origin or Sec-Fetch-* are refused and the content type must be application/json, which together no browser can satisfy Tests run the real daemon: each guard refuses before anything is spawned, and a legitimate `cargo build --release` still reaches the deviceCwd check. Affects: script/cargo-proxy.mjs only — it has no caller in this repository. Does not affect: the adb pull/push paths or the response shape.
After renaming the GitHub repository from Rwanbt/opencode to Rwanbt/unifia, update all internal references to point to the new canonical URL. Excluded intentionally (kept as historical/contextual references): - CHANGELOG.md, docs/autonomy/EXECUTION-LOG.jsonl, docs/autonomy/reports/*: dated before the rename, baseline SHA 207ff45 lives on the old URL - tauri.beta.conf.json: Rwanbt/opencode-beta is a separate repo - packages/mobile/.../opencode-cli.js: compiled bundles, regenerated from updated source (FORK_REPO in installation/index.ts) on next build Also fixes a pre-existing biome noUselessFragments warning in packages/app/src/components/settings-audio.tsx:251 introduced by commit a37f511 (refactor: rename the npm scope to @UNiFia). Validated before commit: - JSON/YAML/XML syntax intact (package.json, tauri.prod.conf.json, UPSTREAM-SOURCES.lock.json, 3 workflow files, appstream.metainfo.xml) - install script: bash syntax, balanced backticks, UNIFIA_RELEASE_REPO var - Workflow gates updated to github.repository == 'Rwanbt/unifia' - Typecheck packages/opencode + packages/app: 0 new errors (1 pre-existing TS2589 in src/config/config.ts:617 unrelated) - biome check --staged: 0 issues (3 pre-existing branch warnings on settings-observability.tsx + orchestrator.ts, not from this commit)
…etch Two guards stood in front of this fetch — a regex anchored to https://github.com/user-attachments/ and a hostname equality check — and a comment written specifically to make CodeQL recognise the second as a barrier. CodeQL kept flagging it (`js/request-forgery`, open since 2026-07-11), and it was right to: it tracks the URL object into fetch, not the property reads. Looking properly turned up a real hole behind it. The regex sees the raw string, so `https://github.com/user-attachments/../../settings/tokens` passes it, and `new URL` resolves that to `/settings/tokens` — the App token went wherever on github.com an attacker's comment pointed. Verified by probe, not by reading. `resolveGithubAttachmentUrl` now rebuilds the URL on a literal host and checks the prefix on the normalised path. The rebuild assigns `.pathname` rather than using `new URL(pathname, base)`: measured, that alternative re-reads a pathname of `//evil.example/x` — the shape `new URL()` yields from `https://github.com//evil.example/x` — as an authority. Extracted to module scope so it is testable at all; it was buried in a closure inside the run handler. `sanitizeForLog` came out with it, unchanged. Also replaces `url.replace(/\/+$/, "")` in remote-connect with a backward walk (`js/polynomial-redos`, high, open since 2026-04-20) — same class as the contracts fix earlier in this branch. Affects: attachment download in the GitHub agent, remote pre-flight URL trimming. Does not affect: the comment-matching regexes or the imgData shape.
…stop overclaiming Self-review of the previous commit. Two things were wrong with it. The env denylist covered the loader and shell variables but not the ones that matter most for a *cargo* proxy: RUSTC, RUSTC_WRAPPER, CARGO_BUILD_RUSTC_WRAPPER and CARGO_TARGET_<triple>_RUNNER each name a program cargo will go on to execute, and a device payload has no reason to set any of them. And the header comment implied the allowlist was a code-execution boundary. It is not, and saying so invites someone to lean on it: `cargo build` runs build.rs by design, so every permitted tool executes project code. `refuseCaller` is the boundary; the allowlist only stops a caller naming a different program. The comment now says which is which.
…g it Extracting the control-character strip into `sanitizeForLog` turned one unrecognised log-injection sanitizer into two NEW CodeQL alerts (373, 374): the taint tracker does not follow a barrier through a helper call, so both console.error sites became sinks and the PR check went red. Teaching the tracker to trust the helper is the wrong fix. Neither message needs the URL: the position in the match list says which attachment, and the reason or HTTP status says what happened. A number cannot carry a newline, so CWE-117 is gone by construction instead of by a sanitizer someone has to keep convincing an analyser about. `sanitizeForLog` had no other caller and is deleted with its test. Also removes three dead declarations CodeQL surfaced in files this branch touched — `stat` in install-coexistence (my own leftover, replaced by the file handle) and `mockPort`/`mockServer` in android-pty, which were already dead before this branch — and records why `waitFor` stays silent on timeout: every call site asserts the same condition on the next line, so the expect gives the real message and no test can pass vacuously.
ci(mobile): bust the cargo cache the repository rename invalidated `runtime.rs unit tests` went red again after the opencode -> unifia rename, with "failed to read plugin permissions: failed to read file /home/runner/work/opencode/opencode/packages/mobile/src-tauri/target/debug/build/ tauri-.../out/permissions/app/autogenerated/commands/app_hide.toml". Not a code failure. Actions caches are keyed to the repository id rather than its name, so the rename kept them, and cargo bakes absolute paths into build-script output under target/. The restored cache pointed at the old workspace path while the checkout now lives at /home/runner/work/unifia/unifia. Adding an `-r2` marker to both key and restore-keys drops every pre-rename entry. mobile-runtime-tests is the only workflow caching a Rust target/ directory; the turbo, apt and playwright caches carry no absolute build output, and the Swatinem cache in the android job is already green. @
test(pty): wait on conditions instead of fixed sleeps in android-pty `unit (windows)` alternated pass and fail across commits that could not have affected it — including one that only edited a workflow cache key. Every wait in this file was a bare `sleep()` covering a TCP connect, a JSON handshake and a response parse. 200 ms is generous on an idle machine and short on a loaded Windows runner, so the spawn test read pid -1 instead of 1234. Ten waits now poll for the state each assertion is about, with a 5 s ceiling. The helper returns rather than throwing on timeout, so the assertion that follows still produces the real diagnostic instead of a generic timeout. Also faster, not just steadier: the file runs in 2.6 s once warm, against the ~3.5 s of fixed sleeping it used to spend regardless. 10 pass, 0 fail on three consecutive local runs. @
`e2e (linux)` has been red on every commit of this branch. The cause was not PATH resolution, which is what my previous commit assumed and what the comment it left behind asserted: the cwd handed to Bun.spawn was `packages/unifia`, a directory that does not exist. posix_spawn reports a missing cwd as ENOENT *on the executable*, so the failure read `ENOENT: posix_spawn 'bun'` and looked like a missing interpreter. That comment is now corrected rather than left standing. Grepping the pattern found the same edit in `e2e/backend.ts` and, in both directions, in publish.yml. Ground truth from build.ts: pkg.name is `unifia`, so the layout is `packages/opencode/dist/unifia-<os>-<arch>/bin/unifia`. The signing job asked for `packages\unifia\dist` (directory renamed, artifacts not) while the uploads asked for `dist/opencode-*` (artifacts renamed, directory not). Neither half matches what the build writes. The uploads now set `if-no-files-found: error`. That default — warn — is why this was invisible: a release would have published empty CLI artifacts and reported success. Both e2e sites likewise fail with a sentence naming the directory instead of a misdirection. Affects: e2e backend startup, CLI artifact upload and Windows CLI signing. Does not affect: the build itself, which was always writing the right layout. Untested: the publish workflow, which has no PR trigger — reviewed against build.ts, not observed running.
|
|
||
| export function activate(context: vscode.ExtensionContext) { | ||
| let openNewTerminalDisposable = vscode.commands.registerCommand("opencode.openNewTerminal", async () => { | ||
| let openNewTerminalDisposable = vscode.commands.registerCommand("unifia.openNewTerminal", async () => { |
6 tasks
…nbook 2026-08-10) (#25) * feat(npm): make the @UNiFia scope publishable `@unifia/plugin` is injected into the package.json of every config directory the CLI manages, pinned to the running CLI version, so the scope has to resolve on npm. Three things stopped any of it from being installable: - `@unifia/sdk-shared` was `private: true`, had no build and no publish script, while both `@unifia/sdk` and `@unifia/plugin` depend on it. - All three declared `files: ["dist"]` against an `exports` map pointing at `./src/*.ts`. The publish scripts rewrote the map in place, but left it rewritten on failure and emitted `import` ahead of `types`. - `tsc` is incremental here (composite). With a stale tsconfig.tsbuildinfo it reports success while emitting nothing, so `rm -rf dist && tsc` could produce an empty tarball. The tsbuildinfo now goes with dist, via a `compile` script. The export rewrite moves to `@unifia/script/npm`, shared by the three publish scripts, and restores the manifest byte-for-byte in a `finally`. Adds `script/publish-npm.ts` to publish the libraries alone, in dependency order, with UNIFIA_NPM_DRY_RUN for a pack-only run. Each package gains a README, a LICENSE and the repository/homepage/bugs metadata an npm page needs. `@types/node` is declared an optional peer of the plugin: its `$` declarations name `Buffer` and `BufferEncoding`. Verified against a local Verdaccio: publish in order, then a clean project outside the workspace resolves `@unifia/plugin@1.3.15` through npm's own installer, typechecks strict with skipLibCheck off, and imports at runtime. * feat(release): cut the CLI's distribution loose from upstream The fork already had its own release pipeline (fork-release.yml, gated on Rwanbt/unifia) while upstream's publish.yml is gated on anomalyco/opencode and cannot fire here. What was missing was npm, and what was left behind was a set of paths still resolving to upstream. Distribution now: GitHub Releases + the install script, the desktop updaters, the Android APK, npm, and a container on ghcr.io/<owner>/unifia. Homebrew, Chocolatey, Scoop, the AUR and the Zed extension are removed rather than re-pointed — publishing to none of them, detecting them only produced upgrade offers computed from someone else's version line. Removed a release-triggered path that reached a third party: no repository gate on sync-zed-extension.yml, so publishing a release here opened a pull request on zed-industries/extensions carrying a manifest whose download URLs pointed at anomalyco/opencode. Upgrade path, all in installation/index.ts: - the npm check read upstream's `opencode-ai` and then installed `unifia-ai` at whatever version that returned. Upstream is at 1.18.16 against this fork's 1.3.15, so every npm install was told to upgrade to a release that does not exist. - `unifia upgrade` on a curl install fetched and ran https://opencode.ai/install — upstream's installer, on a domain this fork does not control — replacing Unifia with the upstream product. It now runs the installer this repo ships as a release asset. - curl installs were detected by `.opencode/bin` only, while `install` deploys to `.unifia/bin`, so a current install fell through to "unknown" and had no upgrade method at all. packages/opencode/script/publish-npm.ts publishes the 12 platform packages and the unifia-ai stub, reusing the binaries build-cli already cross-compiles on one Linux runner. Both inherited publish scripts now refuse to run without UNIFIA_ALLOW_UPSTREAM_PUBLISH, so a manual invocation cannot push to ghcr.io/ anomalyco, the AUR or upstream's Homebrew tap; they are otherwise untouched, so the monthly upstream sync stays conflict-free. Also: the Linux .deb registered its AppStream metadata as ai.opencode.opencode.metainfo.xml while the file declares ai.unifia.desktop — both an upstream identity and a spec violation, since the filename must match the component id. Beta updater pointed at the pre-rename Rwanbt/opencode-beta. Nix homepages and the installer's closing link pointed at opencode.ai. Verified: typecheck clean, 7/7 installation tests (three of them new, asserting no code path reaches upstream's registries), 185/185 CLI tests, both guards fire, and publish-npm.ts dry-runs idempotently against a real build. * fix(release): derive the version from unifia-ai, not upstream's opencode-ai A release cut without UNIFIA_VERSION read upstream's latest npm version and bumped that. Upstream is at 1.18.16 against this product's 1.3.x, so the next tag would have jumped an entire version line that is not ours. * refactor(release): name the product, not what it descends from Unifia released as `fork-release.yml` on `v*-fork*` tags with UNIFIA_CHANNEL=fork. That is the naming a fork gives itself; nothing downstream required it — Installation.isPreview(), the only reader of the channel, has no callers. The workflow is now `release.yml` on `v*` (still matching the `v*-fork*` tags cut before this) with the channel on `latest`, which is also what `npm i -g unifia-ai` resolves. Deleted the stub it collides with: a second `release.yml`, added with the Docker scaffolding and never wired to a build, fired on `v*.*.*` with no repository gate and published an empty draft from `dist/*` and `*.tar.gz` at the repo root — paths nothing produces. `opencode.yml` becomes `unifia.yml` and runs the composite action this repo ships in github/ rather than `anomalyco/opencode/github@<sha>`: every `/unifia` comment was executing upstream's agent at a revision this fork does not control. Trigger words follow (`/uni`, `/unifia`). The `opencode/claude-opus-4-5` model stays — `opencode` there is a provider id (provider/schema.ts:27), not branding. Cleared the `@unifia-ai` spelling left by an intermediate rebrand pass. One of them was load-bearing: file-viewer.spec.ts asserted the file viewer shows `"@unifia-ai/app"` while the package is `@unifia/app`, so the assertion could never have matched. Verified: typecheck clean, 148/148 config and installation tests. * feat(config): read `.opencode`, write `.unifia` The project config directory — skills, themes, agents, commands, plugins, learnings, plans — was `.opencode` on both the read and the write side. That is the same directory the separately-installed OpenCode uses, so this product was loading the other one's project configuration and, through installDependencies on every config load, writing a package.json, a .gitignore and a node_modules tree into it. Reads now cover both names, writes only `.unifia`. The merge order is the rule projectFiles() already applies to filenames — legacy first, current brand last so it wins where both exist — extended to the directory name, which is the one place it had never been applied. Nothing a user wrote before the rename disappears: skills, themes, agents and commands keep loading from `.opencode`, learnings are read from both directories and sorted together by date, and a plan already written to the old path still resolves there rather than reading as empty at a new one. The global config directory needed nothing — Global.Path is XDG-based and has been `unifia` since the rebrand. One deliberate consequence: a tool sitting in a legacy `.opencode` directory that declares npm dependencies no longer gets them installed, because that directory is no longer written to. It still loads; only dependency installation moves. Moving the directory to `.unifia` restores it. Verified: typecheck clean, 682 tests across config, tool, session and coexistence. Three new tests in test/config/paths.test.ts pin the discovery order, the legacy-only case, and the read-only contract; the pre-existing registry test that loads a tool from `.opencode/tool` now doubles as the legacy-read regression. The single failure in the combined run (processor-observability, 5082ms against a 5000ms limit) reproduces identically on the base branch and passes in isolation — run contention, not this change. * feat(storage): carte C8-A - rename opencode.db to unifia.db with one-shot copy - TS: storage/db.ts exports DATABASE_FILE and LEGACY_DATABASE_FILE plus migrateLegacyDatabaseFile() that copies the legacy opencode.db (and its -wal / -shm siblings) to unifia.db on first access. Idempotent, never moves the source. getChannelPath() now returns the unifia path. - TS: mobile-entry.ts:196 uses the new file as the JsonMigration marker. - Rust: lib.rs sidecar_db_path() mirrors the TS migration with the same copy-not-move semantics. New migrate_legacy_db() and append_suffix() helpers. tracing::info! on copy, tracing::error! + Err on failure. - Tests: db-migration.test.ts covers 5 cases (copy, -wal/-shm, idempotent, no source, both present). Rust db_migration_tests covers the 4 main cases (compile OK, runtime blocked by a Windows DLL env issue). - db.test.ts expectations aligned to the new file naming. Refs: Runbook-Autonome-Independance-Unifia-2026-08-10 carte C8-A Gate: 5/5 - content identical, legacy preserved, idempotent, cargo check, tests cover all three cases (legacy present / absent / both) * feat(keyring): carte C8-B - namespace keychain opencode.* -> unifia.* - New crate unifia-keyring-shim owns the brand-prefixed keyring calls (get / set / delete) so the migration logic is unit-testable on a workstation without WebView2 / DirectML installed. It exposes a KeyringBackend trait; RealKeyringBackend delegates to the keyring crate, MockBackend is an in-process HashMap for tests. The keyring crate's own mock module cannot be used here because it does not persist data between Entry::new calls. - auth_storage.rs now uses the shim and only the Tauri-side index management stays in the lib. The four Tauri commands keep the same public signature. - Migration semantics match the runbook: get tries new first, falls back to legacy and rewrites on hit (legacy kept as backup); set writes to new only; delete removes BOTH prefixes so a logout cannot leave a phantom credential under the legacy namespace. - Tests in keyring-shim/tests/keychain_namespace.rs cover 7 cases: rewrite on legacy hit, prefer new when both present, set writes to new only, delete clears both, delete is a no-op when nothing exists, get returns None when both empty, second read uses the rewritten new prefix. Refs: Runbook-Autonome-Independance-Unifia-2026-08-10 carte C8-B Gate: 3/3 - legacy credential becomes visible under new prefix, logout clears both prefixes (verified by 7 tests), cargo check passes * refactor(packages): carte C9 - rename packages/opencode to packages/unifia + opencode-cli to unifia-cli Directory rename: - git mv packages/opencode packages/unifia (1380 files affected) Path references: 245+31 files updated from packages/opencode to packages/unifia, excluding the docs/autonomy/ provenance files (UPSTREAM-*, BASELINE.md, MIGRATION-PLAN.md, REPO-INVENTORY.md) which the runbook C9 gate explicitly preserves as historical record. Sidecar rename: opencode-cli -> unifia-cli in producer and consumer: - .github/workflows/{android,publish,release}.yml — CI artifact names - packages/desktop-electron/{electron-builder.config.ts, .gitignore, scripts/{prepare,utils}.ts, src/main/cli.ts} — Electron packaging - crates/unifia-supervisor/src/tests.rs — left alone: the /opt/opencode/ opencode-cli path there is a forged-lease fixture the supervisor must reject, not a real install path. - packages/unifia/src/account/index.ts — left alone: clientId is the OAuth client identifier registered with the provider. The build script packages/unifia/script/build.ts already derives the binary basename from pkg.name (= unifia) without hardcoding the sidecar suffix; the copy-sidecar step in packages/desktop/scripts/ adds the -cli suffix when staging the artifact. Tauri config (sidecars/unifia-cli-{triple}) was already aligned with the new name; no change required there. Bonus: pre-existing shellcheck warnings on test-tools.sh (SC2086 on the basename call, SC2164 on a bare cd) were caught by the pre-commit hook on the touched file and are fixed here to unblock the commit. Validation: - cargo check (packages/desktop/src-tauri): passes - tsc --noEmit (packages/unifia): 1 pre-existing error in config.ts:634 (TS2589) confirmed to be present on the base branch before this commit; no new errors introduced - grep -rn 'packages/opencode' (excl. node_modules): 3 files, all in docs/autonomy/ provenance, as required by the gate * fix(build): repair relative path references missed by C9 rename The C9 mass-rename caught all 'packages/opencode' absolute references but missed the 'relative' imports and shell cd invocations that point into the renamed directory. These broke three build steps the C9 gate did not exercise: - packages/sdk/js/script/openapi.ts:26 — SDK build fetches 'bun run dev generate' from the renamed CLI package. - packages/app/e2e/fixtures.ts:4-5 and packages/app/script/e2e-local.ts: 167-176 — e2e harness imports 'log', 'installation', 'flag', 'server', 'instance' from the renamed CLI. - packages/desktop-electron/scripts/utils.ts:14,118 and packages/desktop/scripts/{copy-sidecar,predev,utils}.ts — sidecar staging reads the CLI manifest, cds into the CLI directory for the build, and locates 'dist/' under the renamed path. - packages/console/app/package.json:10 — Vite build runs the CLI schema generator at the renamed path. - packages/mobile/scripts/prepare-android-runtime.sh:145 — Android bundle locates the CLI at 'mobile/../<cli>' for the runtime. Caught by the 'unit (linux)' and 'unit (windows)' CI failures on PR #25 — the SDK build is a transitive dep of 'bun turbo test:ci'. The compliance-close bot had not yet acted; this is a real bug, not a bot-driven noise. Pushing the fix re-triggers CI. Refs: PR #25, Runbook-Autonome-Independance-Unifia-2026-08-10 C9 * fix(test): align plugin install tests with config-dir-migration write target c1d76cf (config-dir-migration, in the lineage of feat/unifia-c8-c9) changed packages/unifia/src/config/paths.ts to write to .unifia/ and read from .opencode/ for backward compatibility. The plugin.install.task tests in test/plugin/install.test.ts still read from .opencode/, so after that commit the tests could not find the config the install wrote — 17 ENOENT failures, caught by the 'unit (linux)' check on PR #25. Bug was pre-existing in c1d76cf but invisible there because the test branch (feat/unifia-rebrand-complete) does not include the config-dir-migration commit. Surfaced only now that feat/unifia-c8-c9 stacks on top of feat/unifia-config-dir-migration. Aligned all 30 .opencode -> .unifia references in the test to the new write target. * fix(test): align remaining plugin install tests with config-dir-migration Same root cause as the previous install.test.ts fix: c1d76cf wrote plugin config to .unifia/ but the install-concurrency and auth-override tests still read from .opencode/. Three concurrent test cases were failing with ENOENT on Linux (and Windows, same file). The 30+ other .opencode references in test/ are intentional: install-coexistence / uninstall-coexistence verify coexistence with a real OpenCode install, config.test.ts exercises the legacy .read path, agent.test.ts tests the .opencode/plans permission glob, plugin-loader.test.ts verifies theme resolution. All correctly preserved as-is per the c1d76cf 'legacy read' contract. * refactor(mobile): finish Unifia runtime rename * fix(workspace): clear rebrand follow-up debt * fix(android): enforce release signing identity * fix(e2e): follow renamed Unifia server package * fix(e2e): start isolated Unifia backends --------- Co-authored-by: MM2-B02-WORKER <mm2-b02@team-v3.local>
Rwanbt
pushed a commit
that referenced
this pull request
Aug 11, 2026
It was selected because it appeared on PRs #23, #24 and #25 — which turned out not to prove what it looked like. `observability-sdk-drift.yml` filters its `pull_request` trigger by `paths`, so it never started on #28, and a required check that does not start stays "Expected — waiting for status" forever. Requiring it would have permanently blocked every pull request that does not touch the SDK. Seven checks remain, each verified in its own workflow file to have an unfiltered `pull_request` (or `pull_request_target`) trigger rather than inferred from a sample of pull requests. The ledger records the trap so the eighth is not added back on the same reasoning.
Rwanbt
pushed a commit
that referenced
this pull request
Aug 11, 2026
The previous entry said every alert path sat under `packages/opencode/` and concluded the scan had to be replayed before review. That reasoning was wrong in both halves, and it understated how much of the gate is already done. The alerts are reported against `refs/heads/dev`, and `dev` has not received the C9 rename, so those paths are correct for the branch they describe rather than left over from an older tree. The scan is also current: CodeQL completed successfully on 6cca33b, which is the head of PR #23. More usefully, both `critical` findings are already fixed on feat/unifia-rebrand-complete and will close when it merges — cargo-proxy.mjs now carries the ALLOWED_TOOLS allowlist its own comment claimed but never implemented, and github-run.ts checks the prefix after URL normalisation instead of before. What is left for a human is two high and two medium findings. Nothing was reviewed, modified or dismissed here.
* feat(npm): make the @UNiFia scope publishable `@unifia/plugin` is injected into the package.json of every config directory the CLI manages, pinned to the running CLI version, so the scope has to resolve on npm. Three things stopped any of it from being installable: - `@unifia/sdk-shared` was `private: true`, had no build and no publish script, while both `@unifia/sdk` and `@unifia/plugin` depend on it. - All three declared `files: ["dist"]` against an `exports` map pointing at `./src/*.ts`. The publish scripts rewrote the map in place, but left it rewritten on failure and emitted `import` ahead of `types`. - `tsc` is incremental here (composite). With a stale tsconfig.tsbuildinfo it reports success while emitting nothing, so `rm -rf dist && tsc` could produce an empty tarball. The tsbuildinfo now goes with dist, via a `compile` script. The export rewrite moves to `@unifia/script/npm`, shared by the three publish scripts, and restores the manifest byte-for-byte in a `finally`. Adds `script/publish-npm.ts` to publish the libraries alone, in dependency order, with UNIFIA_NPM_DRY_RUN for a pack-only run. Each package gains a README, a LICENSE and the repository/homepage/bugs metadata an npm page needs. `@types/node` is declared an optional peer of the plugin: its `$` declarations name `Buffer` and `BufferEncoding`. Verified against a local Verdaccio: publish in order, then a clean project outside the workspace resolves `@unifia/plugin@1.3.15` through npm's own installer, typechecks strict with skipLibCheck off, and imports at runtime. * feat(release): cut the CLI's distribution loose from upstream The fork already had its own release pipeline (fork-release.yml, gated on Rwanbt/unifia) while upstream's publish.yml is gated on anomalyco/opencode and cannot fire here. What was missing was npm, and what was left behind was a set of paths still resolving to upstream. Distribution now: GitHub Releases + the install script, the desktop updaters, the Android APK, npm, and a container on ghcr.io/<owner>/unifia. Homebrew, Chocolatey, Scoop, the AUR and the Zed extension are removed rather than re-pointed — publishing to none of them, detecting them only produced upgrade offers computed from someone else's version line. Removed a release-triggered path that reached a third party: no repository gate on sync-zed-extension.yml, so publishing a release here opened a pull request on zed-industries/extensions carrying a manifest whose download URLs pointed at anomalyco/opencode. Upgrade path, all in installation/index.ts: - the npm check read upstream's `opencode-ai` and then installed `unifia-ai` at whatever version that returned. Upstream is at 1.18.16 against this fork's 1.3.15, so every npm install was told to upgrade to a release that does not exist. - `unifia upgrade` on a curl install fetched and ran https://opencode.ai/install — upstream's installer, on a domain this fork does not control — replacing Unifia with the upstream product. It now runs the installer this repo ships as a release asset. - curl installs were detected by `.opencode/bin` only, while `install` deploys to `.unifia/bin`, so a current install fell through to "unknown" and had no upgrade method at all. packages/opencode/script/publish-npm.ts publishes the 12 platform packages and the unifia-ai stub, reusing the binaries build-cli already cross-compiles on one Linux runner. Both inherited publish scripts now refuse to run without UNIFIA_ALLOW_UPSTREAM_PUBLISH, so a manual invocation cannot push to ghcr.io/ anomalyco, the AUR or upstream's Homebrew tap; they are otherwise untouched, so the monthly upstream sync stays conflict-free. Also: the Linux .deb registered its AppStream metadata as ai.opencode.opencode.metainfo.xml while the file declares ai.unifia.desktop — both an upstream identity and a spec violation, since the filename must match the component id. Beta updater pointed at the pre-rename Rwanbt/opencode-beta. Nix homepages and the installer's closing link pointed at opencode.ai. Verified: typecheck clean, 7/7 installation tests (three of them new, asserting no code path reaches upstream's registries), 185/185 CLI tests, both guards fire, and publish-npm.ts dry-runs idempotently against a real build. * fix(release): derive the version from unifia-ai, not upstream's opencode-ai A release cut without UNIFIA_VERSION read upstream's latest npm version and bumped that. Upstream is at 1.18.16 against this product's 1.3.x, so the next tag would have jumped an entire version line that is not ours. * refactor(release): name the product, not what it descends from Unifia released as `fork-release.yml` on `v*-fork*` tags with UNIFIA_CHANNEL=fork. That is the naming a fork gives itself; nothing downstream required it — Installation.isPreview(), the only reader of the channel, has no callers. The workflow is now `release.yml` on `v*` (still matching the `v*-fork*` tags cut before this) with the channel on `latest`, which is also what `npm i -g unifia-ai` resolves. Deleted the stub it collides with: a second `release.yml`, added with the Docker scaffolding and never wired to a build, fired on `v*.*.*` with no repository gate and published an empty draft from `dist/*` and `*.tar.gz` at the repo root — paths nothing produces. `opencode.yml` becomes `unifia.yml` and runs the composite action this repo ships in github/ rather than `anomalyco/opencode/github@<sha>`: every `/unifia` comment was executing upstream's agent at a revision this fork does not control. Trigger words follow (`/uni`, `/unifia`). The `opencode/claude-opus-4-5` model stays — `opencode` there is a provider id (provider/schema.ts:27), not branding. Cleared the `@unifia-ai` spelling left by an intermediate rebrand pass. One of them was load-bearing: file-viewer.spec.ts asserted the file viewer shows `"@unifia-ai/app"` while the package is `@unifia/app`, so the assertion could never have matched. Verified: typecheck clean, 148/148 config and installation tests. * feat(config): read `.opencode`, write `.unifia` The project config directory — skills, themes, agents, commands, plugins, learnings, plans — was `.opencode` on both the read and the write side. That is the same directory the separately-installed OpenCode uses, so this product was loading the other one's project configuration and, through installDependencies on every config load, writing a package.json, a .gitignore and a node_modules tree into it. Reads now cover both names, writes only `.unifia`. The merge order is the rule projectFiles() already applies to filenames — legacy first, current brand last so it wins where both exist — extended to the directory name, which is the one place it had never been applied. Nothing a user wrote before the rename disappears: skills, themes, agents and commands keep loading from `.opencode`, learnings are read from both directories and sorted together by date, and a plan already written to the old path still resolves there rather than reading as empty at a new one. The global config directory needed nothing — Global.Path is XDG-based and has been `unifia` since the rebrand. One deliberate consequence: a tool sitting in a legacy `.opencode` directory that declares npm dependencies no longer gets them installed, because that directory is no longer written to. It still loads; only dependency installation moves. Moving the directory to `.unifia` restores it. Verified: typecheck clean, 682 tests across config, tool, session and coexistence. Three new tests in test/config/paths.test.ts pin the discovery order, the legacy-only case, and the read-only contract; the pre-existing registry test that loads a tool from `.opencode/tool` now doubles as the legacy-read regression. The single failure in the combined run (processor-observability, 5082ms against a 5000ms limit) reproduces identically on the base branch and passes in isolation — run contention, not this change. * feat(storage): carte C8-A - rename opencode.db to unifia.db with one-shot copy - TS: storage/db.ts exports DATABASE_FILE and LEGACY_DATABASE_FILE plus migrateLegacyDatabaseFile() that copies the legacy opencode.db (and its -wal / -shm siblings) to unifia.db on first access. Idempotent, never moves the source. getChannelPath() now returns the unifia path. - TS: mobile-entry.ts:196 uses the new file as the JsonMigration marker. - Rust: lib.rs sidecar_db_path() mirrors the TS migration with the same copy-not-move semantics. New migrate_legacy_db() and append_suffix() helpers. tracing::info! on copy, tracing::error! + Err on failure. - Tests: db-migration.test.ts covers 5 cases (copy, -wal/-shm, idempotent, no source, both present). Rust db_migration_tests covers the 4 main cases (compile OK, runtime blocked by a Windows DLL env issue). - db.test.ts expectations aligned to the new file naming. Refs: Runbook-Autonome-Independance-Unifia-2026-08-10 carte C8-A Gate: 5/5 - content identical, legacy preserved, idempotent, cargo check, tests cover all three cases (legacy present / absent / both) * feat(keyring): carte C8-B - namespace keychain opencode.* -> unifia.* - New crate unifia-keyring-shim owns the brand-prefixed keyring calls (get / set / delete) so the migration logic is unit-testable on a workstation without WebView2 / DirectML installed. It exposes a KeyringBackend trait; RealKeyringBackend delegates to the keyring crate, MockBackend is an in-process HashMap for tests. The keyring crate's own mock module cannot be used here because it does not persist data between Entry::new calls. - auth_storage.rs now uses the shim and only the Tauri-side index management stays in the lib. The four Tauri commands keep the same public signature. - Migration semantics match the runbook: get tries new first, falls back to legacy and rewrites on hit (legacy kept as backup); set writes to new only; delete removes BOTH prefixes so a logout cannot leave a phantom credential under the legacy namespace. - Tests in keyring-shim/tests/keychain_namespace.rs cover 7 cases: rewrite on legacy hit, prefer new when both present, set writes to new only, delete clears both, delete is a no-op when nothing exists, get returns None when both empty, second read uses the rewritten new prefix. Refs: Runbook-Autonome-Independance-Unifia-2026-08-10 carte C8-B Gate: 3/3 - legacy credential becomes visible under new prefix, logout clears both prefixes (verified by 7 tests), cargo check passes * refactor(packages): carte C9 - rename packages/opencode to packages/unifia + opencode-cli to unifia-cli Directory rename: - git mv packages/opencode packages/unifia (1380 files affected) Path references: 245+31 files updated from packages/opencode to packages/unifia, excluding the docs/autonomy/ provenance files (UPSTREAM-*, BASELINE.md, MIGRATION-PLAN.md, REPO-INVENTORY.md) which the runbook C9 gate explicitly preserves as historical record. Sidecar rename: opencode-cli -> unifia-cli in producer and consumer: - .github/workflows/{android,publish,release}.yml — CI artifact names - packages/desktop-electron/{electron-builder.config.ts, .gitignore, scripts/{prepare,utils}.ts, src/main/cli.ts} — Electron packaging - crates/unifia-supervisor/src/tests.rs — left alone: the /opt/opencode/ opencode-cli path there is a forged-lease fixture the supervisor must reject, not a real install path. - packages/unifia/src/account/index.ts — left alone: clientId is the OAuth client identifier registered with the provider. The build script packages/unifia/script/build.ts already derives the binary basename from pkg.name (= unifia) without hardcoding the sidecar suffix; the copy-sidecar step in packages/desktop/scripts/ adds the -cli suffix when staging the artifact. Tauri config (sidecars/unifia-cli-{triple}) was already aligned with the new name; no change required there. Bonus: pre-existing shellcheck warnings on test-tools.sh (SC2086 on the basename call, SC2164 on a bare cd) were caught by the pre-commit hook on the touched file and are fixed here to unblock the commit. Validation: - cargo check (packages/desktop/src-tauri): passes - tsc --noEmit (packages/unifia): 1 pre-existing error in config.ts:634 (TS2589) confirmed to be present on the base branch before this commit; no new errors introduced - grep -rn 'packages/opencode' (excl. node_modules): 3 files, all in docs/autonomy/ provenance, as required by the gate * fix(build): repair relative path references missed by C9 rename The C9 mass-rename caught all 'packages/opencode' absolute references but missed the 'relative' imports and shell cd invocations that point into the renamed directory. These broke three build steps the C9 gate did not exercise: - packages/sdk/js/script/openapi.ts:26 — SDK build fetches 'bun run dev generate' from the renamed CLI package. - packages/app/e2e/fixtures.ts:4-5 and packages/app/script/e2e-local.ts: 167-176 — e2e harness imports 'log', 'installation', 'flag', 'server', 'instance' from the renamed CLI. - packages/desktop-electron/scripts/utils.ts:14,118 and packages/desktop/scripts/{copy-sidecar,predev,utils}.ts — sidecar staging reads the CLI manifest, cds into the CLI directory for the build, and locates 'dist/' under the renamed path. - packages/console/app/package.json:10 — Vite build runs the CLI schema generator at the renamed path. - packages/mobile/scripts/prepare-android-runtime.sh:145 — Android bundle locates the CLI at 'mobile/../<cli>' for the runtime. Caught by the 'unit (linux)' and 'unit (windows)' CI failures on PR #25 — the SDK build is a transitive dep of 'bun turbo test:ci'. The compliance-close bot had not yet acted; this is a real bug, not a bot-driven noise. Pushing the fix re-triggers CI. Refs: PR #25, Runbook-Autonome-Independance-Unifia-2026-08-10 C9 * fix(test): align plugin install tests with config-dir-migration write target c1d76cf (config-dir-migration, in the lineage of feat/unifia-c8-c9) changed packages/unifia/src/config/paths.ts to write to .unifia/ and read from .opencode/ for backward compatibility. The plugin.install.task tests in test/plugin/install.test.ts still read from .opencode/, so after that commit the tests could not find the config the install wrote — 17 ENOENT failures, caught by the 'unit (linux)' check on PR #25. Bug was pre-existing in c1d76cf but invisible there because the test branch (feat/unifia-rebrand-complete) does not include the config-dir-migration commit. Surfaced only now that feat/unifia-c8-c9 stacks on top of feat/unifia-config-dir-migration. Aligned all 30 .opencode -> .unifia references in the test to the new write target. * fix(test): align remaining plugin install tests with config-dir-migration Same root cause as the previous install.test.ts fix: c1d76cf wrote plugin config to .unifia/ but the install-concurrency and auth-override tests still read from .opencode/. Three concurrent test cases were failing with ENOENT on Linux (and Windows, same file). The 30+ other .opencode references in test/ are intentional: install-coexistence / uninstall-coexistence verify coexistence with a real OpenCode install, config.test.ts exercises the legacy .read path, agent.test.ts tests the .opencode/plans permission glob, plugin-loader.test.ts verifies theme resolution. All correctly preserved as-is per the c1d76cf 'legacy read' contract. * refactor(mobile): finish Unifia runtime rename * fix(workspace): clear rebrand follow-up debt * fix(android): enforce release signing identity * fix(e2e): follow renamed Unifia server package * fix(e2e): start isolated Unifia backends * fix(identity): read the isolated UNIFIA_* environment names Every shell that launches the sidecar — the mobile Rust runtime (runtime/server.rs), the Tauri desktop (cli.rs) and Electron (main/cli.ts) — exports UNIFIA_CLIENT, UNIFIA_AUTH_STORAGE and UNIFIA_SERVER_*. These are the `isolated` class in config/identity.json, so Flag deliberately refuses to satisfy them from the OPENCODE_ spelling: an environment prepared for the separately-installed OpenCode must not reach this product's keychain or credentials. Six consumers still read the legacy names straight from process.env, so the value the shells emit never arrived and each silently took its default: - github/auth.ts picked plaintext `file` storage on mobile instead of `encrypted-file` - github/credentials.ts skipped the mobile git-credential passthrough - local-llm-server/index.ts took the desktop spawn path on Android, where LlamaService owns llama-server - tool/bash.ts dropped the --init-file workaround the musl bash build needs - mobile-entry.ts *set* OPENCODE_CLIENT, which isolatedValue ignores, leaving Flag.UNIFIA_CLIENT reading "cli" inside the mobile sidecar - cli/cmd/team.ts authenticated with OPENCODE_SERVER_* and defaulted the username to "opencode", while server/auth-jwt.ts validates UNIFIA_SERVER_* against "unifia" — the Team CLI could never authenticate Also names the Team CLI surface Unifia in its help text. * fix(config): write project config to the current brand directory ConfigPaths.PROJECT_DIRECTORY is `.unifia` and its doc comment states that `.opencode` is read-only by contract: the same directory belongs to the separately-installed OpenCode, so writing there drops files into that product's project. plugin/install.ts already respects this; three write paths did not. - cli/cmd/init.ts created `.opencode/unifia.jsonc` while telling the user it had written `.opencode/opencode.jsonc` — neither path was the real one - cli/cmd/agent.ts created project agents under `.opencode/agent` - local-models/index.ts wrote `.opencode/opencode.jsonc`, legacy in both the directory and the file name All three now resolve through ConfigPaths.PROJECT_DIRECTORY, and init derives its user-facing label from the same constant so the message cannot drift from the path again. * docs(brand): name the product Unifia in system prompts and templates The ten system prompts introduced the agent as OpenCode and pointed users at anomalyco/opencode and opencode.ai for feedback and documentation, so a Unifia session told the user it was a different product and sent bug reports upstream. Feedback now goes to github.com/Rwanbt/unifia/issues and the docs reference is the repository, which is the only location this fork controls — no domain is invented. initialize.txt likewise asked the model to write AGENTS.md for "future OpenCode sessions" and to reference `opencode.json`, which is the legacy config name rather than the one `unifia init` now writes. * chore(governance): make the fork own its own docs and review rules Six inherited or mechanically-rewritten statements described a repository this is not. Upstream is named OpenCode, not Unifia. A global rename had rewritten the *upstream* references too, so README and AGENTS.md called it "upstream Unifia" and described this repo as a fork of itself. Restored in six places; the fork notice now names the two products distinctly, which is the whole point of it. - SECURITY.md sent vulnerability reports to anomalyco/opencode advisories. Reports about fork-only code (desktop/mobile shells, local model orchestration, Team, identity gates) reached maintainers who cannot act on them. Now routes to this repository, with a table for deciding which project owns a defect, and an escalation path sized for a single maintainer instead of upstream's 6-business-day organisational SLA. - .github/CODEOWNERS assigned this fork's packages to @adamdotdevin and @Brendonovich, who have no access here, and routed `packages/tauri/`, which does not exist. Rewritten around the only account with access, so "require Code Owners review" becomes enforceable rather than unsatisfiable. - README's build badge pointed at fork-release.yml, deleted when the workflow was renamed to release.yml, so it rendered as a permanent "no status". - README still said "The final product name is not fixed yet" and called this a "working-name fork" — contradicted by config/identity.json, which the identity gate enforces on every surface. - AGENTS.md claimed the default branch is `dev` and that a local `main` may not exist. `gh api repos/Rwanbt/unifia --jq .default_branch` returns `main`, and the two branches have diverged (27 / 32 commits). - CHANGELOG linked Rwanbt/opencode, the repository's pre-rename name. It only resolves through a GitHub redirect; the canonical name is now used and the former one is stated once as history. Also drops two placeholder root scripts (`random`, `hello`) with no callers. * docs(readiness): make one gate ledger authoritative Three documents claimed to describe release readiness and disagreed. PROD_READINESS.md already called itself the single entry point but was last assessed on 2026-04-19, so it predated the whole rebrand and named none of its gates; PRODUCTION_READINESS.md was a never-filled checklist dated 2026-07-31 whose empty boxes could be read as "not applicable" rather than "never verified". PROD_READINESS.md keeps the role and gains the ledger the decision actually needs, with each gate separated by nature — local, CI, human security, GitHub governance, signing and device, packaging, product identity, domain, external publication — and a stated rule for what may be promoted. Every entry cites the run, the count or the API response it rests on; nothing is checked from a summary. The 2026-04-19 UX verdict is kept and stated as open in addition to, not instead of, the rebrand gates. Also records the classification of the remaining `opencode` references, so a future pass can tell a legitimate external contract from an unfinished rename without re-deriving it, and the reason the branch-protection JSON is prepared but not applied: a single-maintainer repository cannot satisfy a required self-approval, so requiring one alongside enforce_admins would lock both branches permanently. PRODUCTION_READINESS.md is marked superseded rather than deleted — it remains a usable thematic checklist, it just does not decide anything. * docs(readiness): record the branch protection actually applied `main` and `dev` now carry 8 required status checks, one approval, dismiss-stale-reviews, linear history, and the force-push/deletion bans they already had. `enforce_admins` stays false on purpose and the ledger now says why: GitHub forbids approving your own pull request, so requiring an approval while also binding admins would leave a single-maintainer repository with no one able to merge anything. The same reasoning keeps require_code_owner_reviews off even though CODEOWNERS is now correct. The previous configuration is saved outside the repository, so the change is reversible. * fix(governance): drop sdk-drift from the required checks It was selected because it appeared on PRs #23, #24 and #25 — which turned out not to prove what it looked like. `observability-sdk-drift.yml` filters its `pull_request` trigger by `paths`, so it never started on #28, and a required check that does not start stays "Expected — waiting for status" forever. Requiring it would have permanently blocked every pull request that does not touch the SDK. Seven checks remain, each verified in its own workflow file to have an unfiltered `pull_request` (or `pull_request_target`) trigger rather than inferred from a sample of pull requests. The ledger records the trap so the eighth is not added back on the same reasoning. * fix(brand): point fork-owned links at the repository, not a domain Applies the domain decision of 2026-08-11: the fork controls no domain, so every link it owns points at github.com/Rwanbt/unifia. No domain is invented; `unifia.ai` appears only in the comment forbidding it. Deliberately narrow. `opencode.ai` occurs 3749 times in this repository, and roughly 3300 of those are external contracts a rename would break: 2291 `$schema` URLs that make editors validate config files, ~700 Zen API endpoints the `opencode` provider actually calls, and ~200 theme schemas. Also left pointing upstream are the docs site's `console`, `email` and `discord` — Zen's console, anoma.ly's enterprise contact and upstream's Discord are their services, described on pages documenting them, so redirecting those would replace a working instruction with a wrong one. What changed instead: - packages/web/config.mjs is the single producer every one of the 21 locales reads for `config.github`, so `edit this page` and the SDK type links stopped sending contributors to anomalyco/opencode. `url` becomes overridable through UNIFIA_SITE_URL and otherwise names the repository; while the site is undeployed there is no origin to name, and the file now says so. - astro.config.mjs called `../opencode/script/schema.ts`, a path the C9 rename missed. spawnSync reports a missing program through `error` rather than throwing and nothing read it, so the docs build kept succeeding while emitting no config schema at all. Path corrected and both failure modes are now raised. - The CLI's own help — the TUI docs action, the comment `unifia init` writes into every generated config, and two config-schema descriptions users see as editor tooltips — pointed at upstream documentation. config-schema.ts feeds packages/sdk, so `./script/generate.ts` was re-run and the regenerated openapi.json and v2 types are included; committing the schema change alone would have failed the `sdk in sync with server` gate. Verified: packages/unifia and packages/sdk/js typecheck clean; 329 tests pass across test/config and test/cli; biome clean on 1247 files. * docs(readiness): correct the CodeQL gate — the alerts were not stale The previous entry said every alert path sat under `packages/opencode/` and concluded the scan had to be replayed before review. That reasoning was wrong in both halves, and it understated how much of the gate is already done. The alerts are reported against `refs/heads/dev`, and `dev` has not received the C9 rename, so those paths are correct for the branch they describe rather than left over from an older tree. The scan is also current: CodeQL completed successfully on 6cca33b, which is the head of PR #23. More usefully, both `critical` findings are already fixed on feat/unifia-rebrand-complete and will close when it merges — cargo-proxy.mjs now carries the ALLOWED_TOOLS allowlist its own comment claimed but never implemented, and github-run.ts checks the prefix after URL normalisation instead of before. What is left for a human is two high and two medium findings. Nothing was reviewed, modified or dismissed here. * fix(lsp): add UNIFIA_DISABLE_LSP and use it in the e2e runner `UNIFIA_DISABLE_LSP_DOWNLOAD`, which the e2e runner already set, only stops *fetching* a language server that is missing. The servers the runner image already carries still start, and rust, typescript and julials each spend the full 45 s `initialize` timeout before giving up, concurrently with Playwright's own timers. The suite exercises no LSP feature, so it was paying that for nothing. Config `lsp: false` already expressed exactly this, so the new flag routes to the same branch rather than adding a second mechanism; it exists for callers with no config file to edit, which is the runner's situation. Fork-owned, so no OPENCODE_ spelling is accepted. A test pins that, alongside the one that matters most: DISABLE_LSP and DISABLE_LSP_DOWNLOAD stay independent in both directions, since conflating them is what hid this. What this measurably does: the runner logs `reason=UNIFIA_DISABLE_LSP all LSPs are disabled`, with zero `spawned lsp server` and zero 45 s timeouts against four before, and the run is shorter. What it does NOT do, stated because the first draft of this claimed otherwise: it does not stabilise the suite. Three local runs of the same two tests gave LSP on -> pass (43.7 s), LSP off -> fail, LSP off -> pass (37.3 s). The tests flake independently of the language servers, so the LSP timeouts were a real cost but not the cause. The remaining cause is not identified, and this commit should not be read as closing the e2e gate. Verified: packages/unifia typecheck clean, 10 flag tests pass, packages/app typecheck clean. --------- Co-authored-by: MM2-B02-WORKER <mm2-b02@team-v3.local>
This was referenced Aug 11, 2026
* fix(app,mobile): trim trailing slashes in linear time, from one owner CodeQL js/polynomial-redos (anomalyco#278) reports `replace(/\/+$/, "")` in remote-connect.tsx. The anchored `+` makes the engine retry from every offset when the slash run never reaches `$`, so the cost is quadratic in a length the user chooses by pasting. Measured on Bun 1.3.14 with `"https://h/" + "/".repeat(n) + "x"`: n regex backward walk 10 000 222 ms 0.004 ms 50 000 2 914 ms 0.004 ms 100 000 8 001 ms 0.004 ms 8 s on the main thread is a frozen WebView, so this is a real defect and not only a rule firing. The alert named one site; the pattern is the class. Every place that normalizes a URL the user typed carries it: - app context/server.tsx normalizeServerUrl, serverName - app connect/remote-connect.tsx - mobile entry.tsx a verbatim copy of normalizeServerUrl - mobile notifications.ts NotificationBridge's constructor `trimTrailingSlashes` now lives in packages/app/src/utils/url.ts and is exported from @unifia/app, and mobile's inline duplicate of normalizeServerUrl calls the shared function instead of repeating its two lines — the duplicate is how the fix would have had to be found twice. The remaining `replace(/\/+$/, "")` occurrences normalize filesystem paths and config keys (file/index.ts, worker-runtime.ts, autocomplete, dialog-select-directory). They are not reachable from a pasted string of attacker-chosen length and are left alone. Test: packages/app/src/utils/url.test.ts asserts equivalence with the regex it replaces and bounds the 100 000-slash case to 100 ms. * fix(team,github): make both flagged file writes exclusive-create Two CodeQL findings, one root shape: a write that assumes it is creating the file it is about to harden, without saying so to the kernel. js/file-system-race (anomalyco#334) — team/worktree-manager.ts `existsSync(marker)` then `writeFileSync(marker)` is a TOCTOU, and this module exists precisely so several workers can act on worktrees at once: two of them both observe "absent" and both write, and the second silently overwrites a marker the first is already being read for. Replaced by the `wx` flag (O_CREAT|O_EXCL) so the kernel picks the winner in one call. EEXIST is the idempotent path and the only error swallowed. js/http-to-file-access (anomalyco#333) — github/auth.ts The taint the rule follows — a GitHub HTTP response reaching a file write — is not exploitable: the destination is the module constant on line 22, never the network data, and the content round-trips through JSON.parse. Reported as such rather than closed; that call is Erwan's. But the flagged line held a real defect. `mode` on writeFile only applies when writeFile creates the file, and the temp path was the fixed `github-auth.json.tmp`. Left behind by a crash — or planted by anything that can write the data dir — it was reused as-is and the OAuth access token was persisted with whatever permissions it already had. The write now uses a unique name and `wx`, so `mode: 0o600` is always the mode of the file that receives the token. Tests - worktree-manager.test.ts: creation, no-clobber, and a POSIX-only case proving `wx` refuses to write through a planted symlink. That last one is skipped on Windows on measurement, not on principle — CreateFile with CREATE_NEW follows the reparse point and creates the target there, so the flag buys nothing and the assertion would fail for an unrelated reason. - github/auth.test.ts: a stale 0644 `.tmp` is left untouched and the persisted session is 0600. Verified decisive — reverting only auth.ts fails it with ENOENT on the stale file (the old code truncated and renamed it away). * fix(e2e): stop disabling the LSP by default, and drop the claim that justified it The e2e runner defaulted UNIFIA_DISABLE_LSP to "true" on the stated grounds that language servers were "the mechanism behind the suite's flakiness". That claim was never measured, and measurement refutes it: three runs of the same two tests give LSP on -> 2 passed (43.7 s), LSP off -> 2 failed, LSP off -> 2 passed (37.3 s). The suite flakes either way. What the flag does buy is real — four 45 s `initialize` timeouts and every `spawned lsp server` line disappear — but that is a cost removed, not a cause. Left on by default it would have hidden any LSP regression from the suite permanently, in exchange for a shorter run. The flag itself stays: it is a legitimate opt-in, it has four tests in test/flag/disable-lsp.test.ts, and its gate sits at the same point as `cfg.lsp === false` (lsp/index.ts:303). It is simply no longer defaulted — `serverEnv` already spreads process.env, so setting it in the environment still works. The same false claim was duplicated in the flag's own doc comment; it is corrected there too, with an explicit "do not reach for this to stabilise a test". Nothing in .github/workflows sets UNIFIA_DISABLE_LSP, so CI picks the language servers back up from this change alone. * fix(e2e): submit only once the composer has an agent and a model Root cause of the prompt-family failures, read off the Playwright call log of run 31480610511 rather than inferred: - waiting for getByRole('button', { name: 'Send' }).first() - attempting click action - <div data-slot="toast-description">Choose an agent and model before sending a prompt.</div> ... intercepts pointer events 208 x retrying click action `submit.ts:306` refuses the prompt outright when either the agent or the model is missing — it shows that toast and returns, so nothing is sent and no request ever reaches the mock LLM. The helper in fixtures.ts typed and pressed Enter as soon as the prompt input was *visible*, but agent and model are resolved from the provider list the backend serves, which lands after the input renders. Typing before then is the race. That is the exact trap e2e/AGENTS.md names: "Do not treat a visible element as proof that the app will route the next action to it." Everything else in those failures is cascade. `started` never moves, so the helper falls back to clicking Send; the click is intercepted; the test burns its 120 s timeout; the queued mock response is never consumed and the `llm` fixture teardown reports "TestLLMServer still has 1 queued response(s)". Four of the twelve common failures were that teardown message — a symptom reported as if it were the defect. `waitPromptReady` polls the committed `__opencode_e2e.model.current` probe for both `agent` and `model`, which is the same state submit.ts checks. Semantic app state, no wall-clock wait, no new probe: the one it needs already existed and was already used by the noReply branch two functions above. Second defect, independent and also from the same call log: the debug bar `<aside aria-label="Development performance diagnostics">` is `fixed bottom-3 right-3 z-50 pointer-events-auto` and 324 px wide, which parks it on top of the composer's Send button. That is what the 208 retries were hitting once the toast faded. It is dev-only telemetry, never shipped, and nothing in the suite exercises it, so layout.tsx no longer renders it when the e2e harness is present. Without this the fallback click can never land, and a fast, legible failure becomes a timeout. `e2eActive()` reads the `__opencode_e2e` marker the harness installs from an init script. Nothing else writes it, so a normal page always reads false and the bar is unchanged for developers. * fix(e2e): seed the model the backend actually serves This is the first causal defect behind the prompt-family failures, and it is the repo's signature shape: a producer whose consumer was never wired to it. `fixtures.ts` derived the model it seeds into the browser from OPENCODE_E2E_MODEL, defaulting to `opencode/gpt-5-nano`. That variable is set by `script/e2e-local.ts` for `seed-e2e.ts`, which writes a message record on the *shared* backend. The browser does not talk to that backend — it talks to the isolated per-worker backend started by `e2e/backend.ts`. Asked what it serves: HTTP 200 provider=local-llm models=1 gemma-4-E4B-it One provider. No `opencode`, no `gpt-5-nano`: the Zen provider is dropped for having zero models without credentials, which is the "provider removed: zero models after filtering" warning printed on every test. So the composer was seeded with a model that does not exist in the list it reads, `local.model.current()` stayed undefined, and `submit.ts:306` refused every prompt before sending it. Which model it is does not matter for routing — with OPENCODE_E2E_LLM_URL set, `provider.ts:797` sends every model to the mock regardless of provider. What matters is only that the id is in the list. The seed is now resolved from `/config/providers` on the backend the browser will use, once per worker. One authoritative source instead of a constant that had drifted away from the thing it was meant to describe. OPENCODE_E2E_MODEL is still honoured when the backend actually serves it, so pointing the suite at a specific model keeps working; `seedStorage` now requires a model rather than defaulting to one, so this cannot silently drift again. Measured before this branch, on the base commit 8c3e08f (run 31495152621): 13 failures / 119, after 3 attempts each. * fix(test): stop the sanitizer fuzz budget failing on scheduler noise `unit (windows)` went red on run 31501895233 with one failure: `sanitizer-fuzz.test.ts:51`, the per-iteration wall-clock assertion. The same file passes in isolation on the same machine — twice, 45.4 s cold and 4.3 s warm, 6 pass — and the base commit's own `unit (windows)` was green. Nothing in this branch touches the sanitizer. That is contention, and the test was built to catch it: 120 iterations each asserted against a 200 ms ceiling gives 120 chances per run for a GC pause or a preemption on a shared runner to fail a check that proves nothing about the code under test. The budget exists to catch catastrophic backtracking, which blows up by orders of magnitude — seconds on a 4 KB input. So the threshold only has to sit clearly above scheduler noise and clearly below a blow-up, and 200 ms sat in the noise. The per-iteration ceiling moves to ReDoS scale (2 s) and a total budget across the 120 iterations is added next to it. One stalled iteration now trips neither; a single pathological input still trips the ceiling, and a sanitizer that got slow across the board still trips the total. Strictly more sensitive to the thing being tested, and no longer sensitive to the runner. Propagation grep: the sibling budgets in `sanitizer.test.ts` (50 ms / 200 ms) are single-shot, not loops, so they carry 1/120th of the exposure and have not been observed failing. Left alone rather than churned without evidence, and reported. * fix(test): write the sanitizer fuzz control chars as escapes so the file diffs A literal NUL in CONTROL_CHARS made git classify this file as binary. Every diff on it showed as `0 insertions, 0 deletions` and could not be reviewed — including the budget change in the previous commit. Same six code points, written as \u escapes: identical values, identical behaviour, reviewable diff. * docs(readiness): record the measured e2e root cause and the CodeQL verdicts G3: the LSP hypothesis is refuted and the real cause named — the browser was seeded with a model the isolated e2e backend does not serve, so submit.ts:306 refused every prompt. Common-failure count corrected from 11 to 12. G4: the four remaining alerts are classified with cited evidence, two fixed at source, one already fixed on the branch, and js/http-to-file-access left open for a human decision. None closed by the agent. G7: the build chain is proven — unsigned APK produced and apksigner confirms no key was used — so only the keystore blocks it. G1: figures re-measured on this branch, including the packages/mobile trap where bun test and bun run test disagree by 31 failures. * fix(test): harden E2E provider and test isolation * fix(test): align E2E assertions with persisted UI state * fix(lsp): drain responses before stopping servers --------- Co-authored-by: MM2-B02-WORKER <mm2-b02@team-v3.local>
| const crypto = await import("node:crypto") | ||
| const tmp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp` | ||
| try { | ||
| await fs.writeFile(tmp, JSON.stringify(session, null, 2), { mode: 0o600, flag: "wx" }) |
Rwanbt
pushed a commit
that referenced
this pull request
Aug 11, 2026
G9 decision by the owner (2026-08-11): no domain is acquired, the project publishes no website, and its canonical location is github.com/Rwanbt/unifia. The starting point was worse than "packages/web quotes opencode.ai". The rebrand had *fabricated* unifia.ai — a domain nobody has registered, and therefore claimable by a third party — inside executable surfaces: - infra/stage.ts used it as the production domain. It now sits behind UNIFIA_ALLOW_UPSTREAM_DEPLOY, mirroring script/publish.ts, and the production domain reverts to upstream's real one, reachable only under that opt-in. - deploy.yml fired on `push: [dev, production]`. Merging PR #23 into `dev` would therefore have launched `sst deploy` against Cloudflare hostnames, a Stripe webhook and PlanetScale branches that are not ours. It is now workflow_dispatch with an explicit confirmation input. - CODE_OF_CONDUCT.md and SECURITY-INCIDENT-RESPONSE.md routed abuse and vulnerability reports to conduct@/security@unifia.ai — addresses that cannot receive anything. They now use GitHub private reporting. - Both desktop menus opened https://unifia.ai/docs. - 17 app locales labelled the link "unifia.ai/zen" while the href pointed at opencode.ai/zen. - entry.tsx branched on hostname "unifia.ai": dead code, since the fork serves from no domain. - Synthetic git identities used @unifia.ai; they now use .invalid (RFC 2606), which cannot resolve by construction. One pattern recurred: the rebrand renamed the *text* without the *link*. Check both directions when renaming. packages/web and packages/console stay out of scope by decision — they are website surfaces the fork does not publish, and their deploy is neutralised above. Remaining mentions elsewhere are descriptive (audit reports, plans, explicit prohibitions) and direct nobody to the domain. Evidence: typecheck 35/35; identity gate "7 surfaces agree"; brand gate "30/30 masters, 208/208 generated"; packages/app 686 pass / 0 fail.
…34) * fix(deps): patch direct security vulnerabilities * fix(deps): bump second security batch and resync generated SDK Second isolated supply-chain batch: hono, turbo, axios, postcss, ip-address, undici, @remix-run/router, @hono/node-server and esbuild overrides, plus sharp, ws, @babel/core and srvx. vitest moves to 3.2.7 in packages/contracts and @hey-api/openapi-ts to 0.99.0 in the SDK. The generator bump changes emitted output, so packages/sdk is regenerated here; committing the bump without it would fail the "sdk in sync with server" gate. openapi.json itself is unchanged — only the generator's reading of it moved. That new reading surfaced two latent defects, both real: - RequestResult now models `response` as optional, because a failure can predate any reply (request construction or network error). The workspace dialog dereferenced it unguarded, so a network error could throw instead of reporting. It now falls through to the existing error path rather than looping. - The generator now propagates required-ness from the spec. The spec marks `extra` required on experimental.workspace.create, and the TUI call omitted it, violating the server contract. It now passes `extra: null`, matching the two existing call sites in team/opencode-application.ts. Evidence: typecheck 35/35; conformance gate 8/8 including packages/contracts 32/32 under vitest 3; packages/app 694 pass / 0 fail; packages/unifia 4236 pass / 10 skip / 0 fail across 363 files. bun audit: 47 alerts remaining (12 high), down from 101. * fix(app): finish the contract fixes the SDK generator exposed Four more instances of the same two defect classes already fixed in packages/unifia. I had missed them because turbo and the `tsgo -b` build outputs under node_modules/.ts-dist were caching a stale green result: the monorepo typecheck reported 35/35 locally while CI, starting from a clean checkout, failed @unifia/mobile#typecheck. Clearing .ts-dist reproduces it. Optional `response` (a failure can precede any reply): - context/sync.tsx and pages/layout/prefetch.ts both read `messages.response.headers` unguarded. Optional chaining preserves the existing semantics exactly, since both already coalesce to undefined and treat a missing cursor as "complete". Required-ness propagated from the spec: - openapi.json marks `method` required on provider.oauth.callback, and two of the three call sites in dialog-connect-provider.tsx passed `store.methodIndex`, which is `number | undefined`. The third call site at line 352 already guarded with `if (store.methodIndex === undefined) return`; the two others now do the same rather than inventing a different shape. Evidence, with every build cache cleared first: typecheck 35/35; conformance gate 8/8; packages/app 694 pass / 0 fail. --------- Co-authored-by: MM2-B02-WORKER <mm2-b02@team-v3.local>
| record = Object.create(null) as Record<string, unknown> | ||
| params[slot] = record | ||
| } | ||
| record[key] = value |
G9 decision by the owner (2026-08-11): no domain is acquired, the project publishes no website, and its canonical location is github.com/Rwanbt/unifia. The starting point was worse than "packages/web quotes opencode.ai". The rebrand had *fabricated* unifia.ai — a domain nobody has registered, and therefore claimable by a third party — inside executable surfaces: - infra/stage.ts used it as the production domain. It now sits behind UNIFIA_ALLOW_UPSTREAM_DEPLOY, mirroring script/publish.ts, and the production domain reverts to upstream's real one, reachable only under that opt-in. - deploy.yml fired on `push: [dev, production]`. Merging PR #23 into `dev` would therefore have launched `sst deploy` against Cloudflare hostnames, a Stripe webhook and PlanetScale branches that are not ours. It is now workflow_dispatch with an explicit confirmation input. - CODE_OF_CONDUCT.md and SECURITY-INCIDENT-RESPONSE.md routed abuse and vulnerability reports to conduct@/security@unifia.ai — addresses that cannot receive anything. They now use GitHub private reporting. - Both desktop menus opened https://unifia.ai/docs. - 17 app locales labelled the link "unifia.ai/zen" while the href pointed at opencode.ai/zen. - entry.tsx branched on hostname "unifia.ai": dead code, since the fork serves from no domain. - Synthetic git identities used @unifia.ai; they now use .invalid (RFC 2606), which cannot resolve by construction. One pattern recurred: the rebrand renamed the *text* without the *link*. Check both directions when renaming. packages/web and packages/console stay out of scope by decision — they are website surfaces the fork does not publish, and their deploy is neutralised above. Remaining mentions elsewhere are descriptive (audit reports, plans, explicit prohibitions) and direct nobody to the domain. Evidence: typecheck 35/35; identity gate "7 surfaces agree"; brand gate "30/30 masters, 208/208 generated"; packages/app 686 pass / 0 fail. Co-authored-by: MM2-B02-WORKER <mm2-b02@team-v3.local>
…36) The selector persisted every toggle, but the server schema requires at least two distinct models and answers 400 below that. Since `selected()` only advanced after a successful save, the first pick of an empty selection failed forever: reaching two models required passing through one. On a fresh Android install the Team selector was therefore permanently unusable, showing only "Failed to save global Team configuration" on every tap. Extract the toggle decision into a pure `planTeamToggle()` that stages sub-minimal selections locally and only persists once MIN_TEAM_MODELS is reached. `MIN_TEAM_MODELS` replaces the `2` that was hardcoded on both sides of the network boundary; the server schema stays the source of truth. The decision is pure because packages/app has no testing-library, so an inline guard in the JSX would have shipped without any test. Also in this commit: - Surface the server error detail when saving a Team or Debate configuration fails. A validation 400 and a network failure previously raised the same opaque message, which is what made this bug unreadable on device. - Relax `validateTeamSelection` to require two available models rather than all of them, and filter the persisted selection against connected providers on mount, mirroring DebateModelSelector. A single retired model no longer invalidates an otherwise usable selection. - Replace the raw NUL byte in the FAVORITES_GROUP sentinel with the `\0` escape. Same string value, but git no longer classifies the file as binary, so the component is diffable and reviewable again. Verified on a Mi 10 Pro (aarch64 release build): first pick no longer errors, the second persists, and the selection survives a restart. 695 app tests pass, including 8 new ones pinning that a sub-minimal selection is never sent. Co-authored-by: MM2-B02-WORKER <mm2-b02@team-v3.local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@
Issue for this PR
Closes #
Type of change
What does this PR do?
Brings the Unifia rebrand to a working state and repairs what it broke.
The rebrand repeatedly renamed one side of a pair and left the other. None of it
failed to compile: an unsubstituted
defineis still a valid global fortypeof,truthy()returns false for a name nobody sets,Config.withDefaultsupplies the default, and
??falls through. Every instance was silent, whichis why so much of it survived until now.
UNIFIA_MIGRATIONSwas injected butOPENCODE_MIGRATIONSwas read, so a compiled binary fell back to scanning adirectory that does not exist inside it. This is the "cannot reach the local
server" both desktop shells reported.
CI, and the
extraResourcesfilter then matched zero files without warning.opencode.jsonstopped being read, so project config, MCP servers, agents andpermissions in that file were silently ignored. MIGRATION-PLAN.md 4.2 asks for
the legacy name to keep working.
@unifia/pluginisinjected into every tool manifest and is unpublished, and one unresolvable
spec aborts the whole tree — while
Npm.installswallowed the error.another, so it reached the provider on every compaction summary.
UNIFIA_*settings did nothing, including the one the WindowsCI job uses to disable the file watcher, plus 21 consumer sites still reading
the legacy export.
bricked the app for good; the review and file-tree toggles were hidden below
768px while the review panel opened by default, leaving the conversation
unreachable.
android.ymlcloned llama.cpp unpinned and has been red since 2026-07-28;storybook-solidjs-vitewas an unpinned range that moved to a release whoseSolid detection does not see a Bun-hoisted dependency. Both pinned.
How did you verify your code works?
packages/opencodesuite: 4188 pass, 10 skip, 0 fail (4149 / 39before).
typecheck35/35,brand:check30/30 + 208/208,identity:check7/7.endpoint answering, session created and persisted, 401 on a wrong password.
the wrapped key being regenerated on disk, not by reading the code.
the live mobile sidecar over
/experimental/tool/ids.files against the pre-branch commit, rather than assumed.
Not verified: the Nix install and fixup paths (CI only evaluates, never builds),
and the storybook build past the point of the fixed error — it then hits a V8
heap limit on my machine.
Screenshots / recordings
Android, before and after: the app opened straight into the review panel with no
control able to close it; it now opens on the chat with the five-button toolbar
restored (server status, terminal, review, file tree, overflow).
Checklist
@