Repository navigation
Releases: ImanKari/G9BrowserAgent
Release list
G9BrowserAgent 3.4.0
Download
| System | File | Updates |
|---|---|---|
| Windows 10/11 (x64) | G9BrowserAgent-Setup-3.4.0.exe — per user, no administrator rights |
Automatic: G9BrowserAgent checks these releases, downloads in the background and asks before installing |
| macOS 12+ (Apple silicon) | G9BrowserAgent-3.4.0-mac-arm64.dmg |
G9BrowserAgent tells you when a release is out; download it and replace the app (the app is not signed with an Apple Developer ID yet, so macOS cannot install updates into it) |
| macOS 12+ (Intel) | G9BrowserAgent-3.4.0-mac-x64.dmg |
As above |
| Linux (x64) | G9BrowserAgent-x86_64.AppImage — keep this file name: updates replace the file in place |
Automatic, like Windows |
| Debian/Ubuntu (x64) | G9BrowserAgent_3.4.0_amd64.deb |
G9BrowserAgent tells you; install the new .deb with your package manager |
The installers are not code-signed: Windows SmartScreen and macOS Gatekeeper ask once before the first start. See the README for what to click, and for the browser extension (loaded once, updated by G9BrowserAgent itself).
latest.yml, latest-mac.yml and latest-linux.yml are the update metadata G9BrowserAgent reads; the .zip and .blockmap files are for the updater.
What changed in 3.4.0
Why. Agents now run long jobs unattended through G9BrowserAgent: social-media business tools, a
music service, renders that take minutes inside the page. A field incident on a browser node showed
how such a job dies. An evaluate on a background tab Engine 1 had just opened hung for 120 s. After
that even browser_tabs close timed out, and only a container restart helped. A panel gate, whose
every request is a fresh MCP session, was told "No session named n-bs" on the call right after it
opened that session, and its own tabs were refused as "owned by agent-N". And every step re-read the
whole page. One release fixes all three, in seven parts.
1. Hang-proofing.
- Nothing before a command is unbounded any more (
lib/cdp.js): the debugger attach (10 s), each
domain enable (5 s, sent together),Target.setAutoAttach(5 s) and the state write (5 s). A domain
that did not answer is not marked enabled, and is asked for again when a tool needs it. One attach
per tab runs at a time.platform-extensionnow honourstimeoutMson attach, detach and
sendCommand; it used to ignore it. - CSS left the attach set. No capture needs it, and it is the enable most likely to block on a
background page;inspect stylesenables it when used. Engine 2's list (engine/stealth.js) lost it
too. - The watchdog (§4.10, DAEMON_PROTOCOL §3). A cancelled call still running 1.5 s later has its tab
recovered (recoverTab): every CDP command waiting there is abandoned, so the call ends at once with
[recovered] …. In the extension the debugger is then detached and attached again; a launched
engine re-enables its domains. The extension answers a cancelled call within 5 s whatever happens,
and the daemon waits 8 s for that answer before it frees the tab's queue anyway.browser_status
shows a tab's slow CDP commands underdebugger. - close and focus bypass the tab's queue. Close ends every call on the tab first (cancelled in
its engine too) and abandons stuck CDP commands in the engine, so it works while a call is stuck. - The state chain cannot hold a reply. A step longer than
WRITE_STEP_MS(5 s) no longer blocks
the next one. The activity line is written after the reply. - Background tabs. Reads, evaluate and snapshots on hidden tabs are covered by tests, and
keepAwake:trueon open, orbrowser_tabs action:"keep_awake", keeps a tab working: focus
emulation plusPage.setWebLifecycleState active, applied again after any re-attach. Timer
throttling of a hidden page is a browser switch, not a command, and the result says so. Engine 2
already launches with--disable-background-timer-throttling; Engine 1 relies on the
IntensiveWakeUpThrottlingEnabled=0policy. - Found by the tests: a launched engine's recovery returned early from its re-attach, because the
tab was still listed as attached. Its domains and keep-awake were therefore never applied again. It
now forgets the attachment on both engines.
2. Stateless clients.
- Session names. The daemon's session names were never per agent. The report came from
open
reading onlyname, soopen session:"n-bs"never named the tab.opennow takessessiontoo. - Agent identity. An agent hello may carry
client.identity. Over HTTP that is theX-G9-Agent
header, else the MCP client's name (G9_MCP_AGENT_IDENTITYname|header|off). The stdio shim sends
onlyG9_AGENT_ID, because two editors on one machine may share a client name. - Takeover. A new connection with an identity takes over the claims, leases and current tab of
the older connection with the same identity. If that connection is gone, it takes back what it
owned and nobody has claimed since (remembered 10 min; a departed owner never blocks anyone). The
HTTP endpoint ends the idle older sessions of that identity at once, which releases their claims
instead of after 30 idle minutes.
3. Token economy.
- Snapshot options.
browser_snapshottakesscope(a ref or a CSS selector),roles/name
(regex) filters,interactiveOnlyandincludeText:false, withmaxNodesas before. - Diff.
diff:truereturns only+added,~changed (with what it was) and-removed nodes,
against the caller's previous diff:true snapshot of the page with the same options. - Stable refs (§4.2). An element keeps its ref across snapshots of a page.
- find.
browser_inspect what:"find"(a role and/or a name regex) returns just the matching lines
with refs. - Compact results (
mcp/mcp.js, default on,G9_MCP_COMPACT=0restores the old shape). JSON
carries no indentation, and atreeor longtextcomes as a plain-text block of its own instead of
an escaped string.resultFromContentreverses this for the runner and the harnesses.
4. Waiting and jobs.
- Waits.
browser_navigate waittakes apredicate(a page expression, polled until truthy; the
final value comes back) andref+state(visible, hidden, enabled, disabled, checked, unchecked,
gone, present), up to 2 h. The engine polls every 250 ms, then every 1 s after 10 s and every 2 s
after 5 min. - Evaluate.
browser_console evaluatetakestimeoutMsup to 4 h. - Jobs.
job:trueanswers at once with a jobId (tools/jobs.js). The job lives in the page
(window.__g9jobs), so it outlives the caller, its MCP session and a worker restart, but not a
navigation (state:"lost"). Page code reports progress with__g9job.progress(fraction, note).
job_status(waitMswaits up to 10 min),job_cancel(aborts__g9job.signal) andjobsread
and manage it. Values are bounded at 256 KiB of JSON.
5. Files on the browser's machine (the daemon runs there for both engines; router #finishFiles).
- fetch.
browser_network fetchruns from G9's isolated world, which has universal access: it
sends the browser's cookies for the URL and is not refused by CORS. The method, headers and body are
the caller's; the limit is 40 MiB. The daemon hashes the body and writessavePath(relative paths
go under<G9_HOME>/files/), or returns a body bounded bymaxBytes. - Screenshots.
browser_screenshot savePathwrites the image and returns its path, size and
sha256. Withthumbnail:trueit also returns a small JPEG of the viewport, or a scaled copy of the
capture when there is no time for a second one. - Engine 1 downloads now report the final path and, when the daemon can read the file, its sha256.
6. Batch.
- The action.
browser_recording action:"batch"(tools/batch.js) runs a list of steps in one
call: navigate; input by ref or role+name withnth; wait; evaluate; extract. Values saved with
saveAsfeed{{name}}in later steps. - Results. The list is validated before anything runs. Each step gets one compact line, and the
batch stops at the first failure with an excerpt: the same-role elements when a role+name matched
nothing. - FlowSpec. FlowSpec gained
evaluateandextractsteps (flowspec, replay, outline, Playwright
export; runner/README.md). A FlowSpecevaluateruns throughcdp.evaluateUserExpression, the one
named main-world helper the interaction suite's scan allows.
7. EXT-04 is done (§8.5). The call's deadline travels with it (caller.deadlineAt), so the
screenshot's two attempts are budgeted from the time left. When both stall, the call ends with the
cause before its deadline. The relay-timeout text no longer guesses a dialog, and both the late-reply
and both-attempts-stall cases are tested. EXT-13 stays open: its probed capability model and a
measured worker restart were not part of this work.
Tests.
- New: daemon 112 (+12: identity, the session alias, close past a stuck call, the recovered relay,
files, the deadlines, compact results, a late reply); seam 23 (+7 through the real runtime, with a
node:vmmain world for jobs); extension suite 111 (+4 Engine 1 hang tests, +7 FlowSpec
value steps); mcp-http 9 (+1). - Changed: one seam assertion — your own fifth snapshot no longer retires your first ref; it is
the same element's stable ref. One F7 test now waits for the activity line, which is written after
the reply. - The new live suite
setup/longwork-livetest.mjsruns inside the Engine 1 harness:
G9_TIMEOUT_MS=10000 G9_LIVE_SCRIPT=setup/longwork-livetest.mjs node setup/isolated-livetest.mjs, or
check.mjs --live, which learned a per-checkenv.
Measured on the owner's workstation (2026-10-07), on headless Edge 154, Engine 1 = the real
extension, Engine 2 = a launched headless browser: setup/longwork-livetest.mjs 54 passed, 0 failed.
- Background tab (...
G9BrowserAgent 3.3.0
Download
| System | File | Updates |
|---|---|---|
| Windows 10/11 (x64) | G9BrowserAgent-Setup-3.3.0.exe — per user, no administrator rights |
Automatic: G9BrowserAgent checks these releases, downloads in the background and asks before installing |
| macOS 12+ (Apple silicon) | G9BrowserAgent-3.3.0-mac-arm64.dmg |
G9BrowserAgent tells you when a release is out; download it and replace the app (the app is not signed with an Apple Developer ID yet, so macOS cannot install updates into it) |
| macOS 12+ (Intel) | G9BrowserAgent-3.3.0-mac-x64.dmg |
As above |
| Linux (x64) | G9BrowserAgent-x86_64.AppImage — keep this file name: updates replace the file in place |
Automatic, like Windows |
| Debian/Ubuntu (x64) | G9BrowserAgent_3.3.0_amd64.deb |
G9BrowserAgent tells you; install the new .deb with your package manager |
The installers are not code-signed: Windows SmartScreen and macOS Gatekeeper ask once before the first start. See the README for what to click, and for the browser extension (loaded once, updated by G9BrowserAgent itself).
latest.yml, latest-mac.yml and latest-linux.yml are the update metadata G9BrowserAgent reads; the .zip and .blockmap files are for the updater.
What changed in 3.3.0
Why. Ten approved runner/flow features in one batch: flows carrying credentials could not be
recorded safely, --env was only a label, the known world confused environments, a flow that could
not run either failed or silently "passed", and the device recorder's passive capture had no consumer.
Password masking at record time (extension/tools/record.js). The in-page recorder never emits
a password field's value (input type=password, or autocomplete current-/new-password — the same rule
the input sampler always used): the change event carries value: '', secret: true, so the
credential is not even in the raw session buffer. normalize() turns the marker into
{{secret:password}} (password2, … per distinct field, keyed by the strongest locator), and
stopRecording declares the matching parameters: { password: { type: "secretRef", required: true } }
(secretParametersOf). validateFlowSpec also refuses a secretRef carrying a literal under value,
next to the existing default rule; qa.substitute resolves {{secret:NAME}} exactly like
{{NAME}}.
Secrets at run time (runner/g9.mjs). resolveSecrets: the --data file first, then
G9_SECRET_<NAME> (uppercased, non-alphanumerics → _). A flow whose required secret has neither
is SKIPPED, the reason naming both ways to provide it. Secret VALUES are masked back to their
placeholder everywhere a result echoes them (maskSecretsDeep over the whole flow result — step
echoes, failure messages quoting field contents). Found and fixed on the way: parameterDefaults
(replay.js) handed the secretRef declaration object over as a value, so {{password}} typed
"[object Object]" into the field — a secretRef now contributes no default, and an unresolved one
fails substitution loudly.
Real --env (runner/g9.mjs, daemon/project.js). environments accepts the object form
{ "baseUrl", "features", "build" } beside the string form (a malformed entry is reported in
problems and kept out). With --env <name> (refused when it names no declared environment) every
flow URL — startUrl, navigate steps, url oracles — whose origin is ANY declared environment's origin
is re-pointed at the chosen one before import (rewriteFlowUrls); {{baseUrl}} resolves in URLs
and as a variable. Fixed on the way: the start URL was never substituted at all — {{placeholders}}
in it reached the browser literally (resolveUrlPlaceholders now runs before the goto).
Known worlds per environment (extension/lib/signature.js, approved.js, replay/flowsync/
router/flows). What "normal" looks like on test1 is not test2. knownWorldFor/withKnownWorld:
the flat recording.knownWorld IS the default environment's world (full compatibility — every
existing store, sidecar and panel keeps meaning what it meant), named environments live under
knownWorlds[env]. Replay compares, seeds and saves under the run's environment (the new
environment replay/calibrate argument; the runner passes --env through); approve folds the
reviewed run into the environment its signature names and refuses a contradicting --env. The
sidecar carries knownWorlds beside the flat knownWorld (validateApproved knows the shape;
applyApproved takes the later human approval PER ENVIRONMENT, so a repo approval on test2 never
overwrites a newer local one on test1); knownWorlds joined every local-truth list
(daemon/flows.js LOCAL_ONLY, daemon/router.js LOCAL_TRUTH, the import skip set). The desktop's
Approvals view still shows the default environment's approval only.
urlNormalizers are consumed (extension/lib/signature.js). The project's [{match, as}] rules
now reach the signature twice: buildSignature gets them from the new replay argument (the runner
reads g9.project.json), and renormalizeKnownWorld re-applies them to the STORED world's network
keys (and volatile masks) at comparison — so /api/orders/<guid> collapses to /api/orders/{id} on
both sides even for a world approved before the rule existed. Colliding keys keep the larger count,
never a sum past runs.
requires preflight and SKIPPED (extension/lib/flowspec.js, runner/g9.mjs,
runner/report.mjs). A flow may declare requires: { minBuild?, features?, environments? }
(validated; unknown keys refused; carried through toFlowSpec/fromFlowSpec — they used to drop
everything they did not know, so a Pull → Push would have silently erased it). Before a tab opens or
a device is driven: environments against --env; on maui, qa.ping/qa.capabilities once per
device per run (lenient numeric-dotted minBuild compare); on web, the environment object form —
data it does not declare is a SKIP saying "cannot evaluate requires.X … declare it in
g9.project.json", never a silent pass. SKIPPED is first-class: skipReason in run.json,
<skipped message> in junit.xml (with a testsuite skipped count), a distinct section in
report.html, "SKIPPED" in qa-automation-status.json. <reportDir>/skips.json maps
(flowId + environment) → { firstSkippedAt, lastReason }; an executed flow drops out, an entry
14+ days old warns loudly in the summary and the report («این فلو ۱۴+ روز است اجرا نشده — پوشش در
حال آب رفتن است»). Skips never make a run red.
Suite order and stopOn (runner/g9.mjs). order: "risk-desc" (critical > high > medium >
low; missing = medium, "normal" accepted; stable) and stopOn: "failure" — the suite stops after
FAIL_PRODUCT / FAIL_AUTOMATION / ERROR (not SURPRISE, not SKIPPED), and the rest are reported
NOT_RUN, naming the stopping flow. Any other stopOn value is refused by name.
Exit code 5 (runner/g9.mjs, scheduler, desktop, README). A runner-internal ERROR exits 5,
distinct from product 1; precedence 1 > 2 > 5 > 3 > 0. The scheduler's exit-code fallback and the
desktop's verdict maps learned SKIPPED/NOT_RUN and 5.
Provenance (runner/g9.mjs, report.mjs). run.json/report.html/junit properties carry
provenance: { gitCommit, gitBranch, appBuild } — git asked in the directory holding
g9.project.json (nulls without git), appBuild from the device's qa.ping or the web environment's
declared build.
g9 harvest --platform agripad (runner/g9.mjs). Reaches the device exactly like
run --platform agripad (adb forward, port 8799, lease), calls flow.record.harvest (drains the
app's passive ring buffer) and writes each candidate to
<flowsDir>/agripad/candidates/<yyyyMMdd-HHmm>-<n>.candidate.json (format: "g9/flow-candidate",
schemaVersion 1, tags ["@candidate"], capture time, device, provenance, the device's steps passed
through untouched). An app build without the command gets a friendly message naming its build.
Candidates are NOT flows: the runner's loaders read only *.flow.json, and the daemon's library
walk now skips *.candidate.json by name (it used to list any .json as a flow).
Also found and fixed on the way.
- Device
typesteps typed NOTHING: the driver senttext, the bridge readsargs.value— every
type step "passed" while the field stayed empty (runner/drivers/agripad.mjs). - The device's consent refusal ("Live access has not been granted") was classified FAIL_PRODUCT:
the runner's pattern said "has not granted live access". Both wordings classify as
FAIL_AUTOMATION now. - Device flows were never validated:
loadDeviceFlowsnow runsvalidateFlowSpecwith the action
set widened by the driver's owndismiss(the validator takes{ actions }), so a malformed
file stops the run with its name instead of failing step-by-step on the device. - Device steps never saw
--datavariables at all; they are substituted now (including
{{secret:…}}), and step outcomes are masked like the web path's.
Docs/tests. g9.project.example.json shows the environment object form and marks
operationAliases/reportSink/volatile RESERVED — not implemented; runner/README.md covers all
of the above; the extension suite grew from 88 to 100 tests (masking, secrets, requires, --env
rewriting, urlNormalizers both sides, per-env worlds and sidecars, junit <skipped>, exit
precedence, the skip ledger, candidate exclusion); the version stays 3.2.1 until this ships.
SHA-256
daa9b71ef8b67c1a7337b120a653d8570fdc1c0026715644a72df918c05455cf G9BrowserAgent-3.3.0-mac-arm64.dmg
e7c644f3eee5a923f7240f111abcaa5a4b7be0bd239a90dfbebf99ba241c2d95 G9BrowserAgent-3.3.0-mac-arm64.dmg.blockmap
a81d77f0deced0137bf9daed1b42fdb0d4e3fd73726b4f6c552db570446605f5 G9BrowserAgent-3.3.0-mac-arm64.zip
9a139791b6a646f9b5de8531b612258ab71db4652ce3f3f13adc920adf54971a G9BrowserAgent-3.3.0-mac-arm64.zip.blockmap
aaef3a4d2218154c5a7e7f00a2b0837f8874c0093cddfb4b494c6332830cf9e7 G9BrowserAgent-3.3.0-mac-x64.dmg
7...
G9BrowserAgent 3.2.1
Download
| System | File | Updates |
|---|---|---|
| Windows 10/11 (x64) | G9BrowserAgent-Setup-3.2.1.exe — per user, no administrator rights |
Automatic: G9BrowserAgent checks these releases, downloads in the background and asks before installing |
| macOS 12+ (Apple silicon) | G9BrowserAgent-3.2.1-mac-arm64.dmg |
G9BrowserAgent tells you when a release is out; download it and replace the app (the app is not signed with an Apple Developer ID yet, so macOS cannot install updates into it) |
| macOS 12+ (Intel) | G9BrowserAgent-3.2.1-mac-x64.dmg |
As above |
| Linux (x64) | G9BrowserAgent-x86_64.AppImage — keep this file name: updates replace the file in place |
Automatic, like Windows |
| Debian/Ubuntu (x64) | G9BrowserAgent_3.2.1_amd64.deb |
G9BrowserAgent tells you; install the new .deb with your package manager |
The installers are not code-signed: Windows SmartScreen and macOS Gatekeeper ask once before the first start. See the README for what to click, and for the browser extension (loaded once, updated by G9BrowserAgent itself).
latest.yml, latest-mac.yml and latest-linux.yml are the update metadata G9BrowserAgent reads; the .zip and .blockmap files are for the updater.
What changed in 3.2.1
Why. The product's name is G9BrowserAgent — the repository's, the GitHub releases' — but the app,
the extension, the installer and every screen said "G9". The owner asked for one name everywhere,
before anyone depends on the old one: in the app and the extension, in the files people install and
in what they read, and for the data folder and the AI clients' entry too.
What was renamed. Every mention of the product (1,571, by one script with its exceptions spelled
out, then read through): the app (G9BrowserAgent.exe, G9BrowserAgent.app, the Linux command
g9browseragent, /opt/G9BrowserAgent, the .deb package g9browseragent), the release files
(G9BrowserAgent-Setup-<v>.exe, G9BrowserAgent-<v>-mac-<arch>.dmg/.zip,
G9BrowserAgent-x86_64.AppImage, G9BrowserAgent_<v>_amd64.deb), the extension (its manifest name and
toolbar title; the description was shortened to stay within the store's 132 characters, which the
panel suite checks), the window, tray, notifications, the MCP server's name (G9BrowserAgent), the
AI clients' entry (g9browseragent), the data folder (%LOCALAPPDATA%\G9BrowserAgent,
~/.g9browseragent), the updater's cache, and the guides, both READMEs and their pictures (made
again). Kept, on purpose (docs/INSTALL.md, "Upgrading from G9"): G9_* variables, g9d and its
protocol (a new app must still recognise an old daemon), runner/g9.mjs, g9.project.json, the
export format id, the Docker image's /opt/g9, and this change log's older entries, which describe
what those versions were called.
Existing installs follow by themselves.
- The data folder moves once (
lib/home.mjs, a copy indesktop/lib/paths.mjs): one atomic rename
by one of its owners — the daemon at its start or the desktop app; every other caller only looks,
the old folder until the move and the new one after — only for a folder that is
recognisably G9's, and not at all while something holds files in it (Windows) — then the old folder
stays in use by every component until a later start. Amigrated-from.jsonnote makes the app say
so once and send Setup back to the extension step: the browser knows an unpacked extension by its
folder. - AI clients' entries (
migrateRegistrations): an old-name entry moves to the new name — unchanged
when its files exist (a developer's checkout), pointed at the app when they are gone; an entry naming
a vanished executable is repointed; commented files and deliberate entries are left for Setup. - Start at sign-in follows the new executable (Windows Run key, Linux autostart file).
- The .deb replaces
g9-desktop(fpm --replaces/--conflicts/--provides).
Found on the way.
- Only the folder's owners may move it. The first version moved the old folder from every
resolver, and twice that moved the owner's real%LOCALAPPDATA%\G9: a one-line check of
resolveHome(), and later the update test's own browser lookup (engine/find.js, run by the
harness in the real environment). Both times it was put back at once, unchanged. Now only the
daemon at its start and the desktop app move it (migrate: true); every other caller only looks —
the old folder until the move, the new one after — so a library call, a script or a test never
moves a person's data, and all components still agree. After the change, a fullcheck.mjs --all
and the update test left the real folder where it was. Two daemon tests resolved the real default
home too; they now run against a temporary profile. And the update
end-to-end test started the packaged app with the real user profile — with the new start-up
migration, that would have rewritten this machine's AI-client configs to point at a temporary
install. The test now gives the app a profile of its own (USERPROFILE/HOME/APPDATA/XDG_*)
and seeds an old-styleg9-browserentry there to check that it follows the rename. - The first migration rule repointed every old-name entry at the installed app, which would have
silently replaced a developer's entry naming their repository. Now only a stale entry is repointed. setup/unit/mcp-http.test.mjs(3.2.0) ended withprocess.exit(0)while its sockets were closing;
Node 24 on Windows aborts on that (a libuv assertion). It now exits a moment later.
The 3.2.0 pipeline failure. Its log could not be read from here (the credential available reads
code, not builds). Every stage the pipeline runs was repeated on 3.2.0 instead: all 18 offline checks
on Windows under Node 22 (the pipeline's version), the Linux job in a clean Node 22 container, and the
Windows packages with the update test (20 of 20). All passed, so the failure is in a stage that needs
the pipeline itself — most likely Publish, whose first step validates the Azure DevOps and GitHub
tokens stored in the pipeline: they were to be revoked after 3.1.1.
Runs on 3.2.1 (2026-09-27). check.mjs --all 16 of 18 on the owner's workstation — the two others
fail only there and pass from a clone elsewhere (the self-test's flow library finds the owner's own
g9.project.json in a folder above the repository) and under Node 22 (mcp-http, fixed
above). The update end to end on Windows 11, 3.2.0 → 3.2.1 across the rename, 21 of 21: the
3.2.0 install (G9.exe) updated through its own updater, stayed in its folder as
G9BrowserAgent.exe, and its old g9-browser AI-client entry was moved to g9browseragent and
pointed at it; every earlier scenario passed too.
Version 3.2.1 in package.json, extension/manifest.json, desktop/package.json and
desktop/package-lock.json.
SHA-256
c2d2da183c9b8333a7db046284806c02d29e69ae5d70e2298e9d6fc0ec996099 G9BrowserAgent-3.2.1-mac-arm64.dmg
113eb0fcfd3bc4c8fab148bd18b92d3d6b4af2baa4324cf9cd6bcd7a86a2d63e G9BrowserAgent-3.2.1-mac-arm64.dmg.blockmap
52579108d9d0628c529cad9661b88ae301405964833e61131f702c8ab400bace G9BrowserAgent-3.2.1-mac-arm64.zip
b69080202880063a0e7e9bee1d0a429a6870b0cc7715c3b836748298c5065309 G9BrowserAgent-3.2.1-mac-arm64.zip.blockmap
2421621d34417149fc532322c4179f27becf6fee282bc82d36c1bfe25dbb45ba G9BrowserAgent-3.2.1-mac-x64.dmg
18ae258b2143f5468d13e4ab9962f78628818096d916b640ec8c7ee0b9bf5009 G9BrowserAgent-3.2.1-mac-x64.dmg.blockmap
0dced21dad19afe71833c704879b89f4079012b10367ecd331def0a725bf92bd G9BrowserAgent-3.2.1-mac-x64.zip
f4cb0b2db566af232374dba51a543db787d9d296c2413a93630efc6a2bb16bb4 G9BrowserAgent-3.2.1-mac-x64.zip.blockmap
b11c8b41f294f6130f88a419f0a565c20d011ec30b1fd5a387b331553f66de3b G9BrowserAgent-Setup-3.2.1.exe
b5454aec555cc401558b6f440bc98ed8118fba123dd793ef8144b12f79eef93b G9BrowserAgent-Setup-3.2.1.exe.blockmap
915f011dd27ee1ccf7f0b9993e42f490dcc43b3dba4864bb95bfd3a514f800c2 G9BrowserAgent-x86_64.AppImage
38c5535580f830f53e8d35284de24920b36c3c29fb0223b9cfe7bd4668442114 G9BrowserAgent_3.2.1_amd64.deb
G9 3.1.1
Download
| System | File | Updates |
|---|---|---|
| Windows 10/11 (x64) | G9-Setup-3.1.1.exe — per user, no administrator rights |
Automatic: G9 checks these releases, downloads in the background and asks before installing |
| macOS 12+ (Apple silicon) | G9-3.1.1-mac-arm64.dmg |
G9 tells you when a release is out; download it and replace the app (the app is not signed with an Apple Developer ID yet, so macOS cannot install updates into it) |
| macOS 12+ (Intel) | G9-3.1.1-mac-x64.dmg |
As above |
| Linux (x64) | G9-x86_64.AppImage — keep this file name: updates replace the file in place |
Automatic, like Windows |
| Debian/Ubuntu (x64) | G9_3.1.1_amd64.deb |
G9 tells you; install the new .deb with your package manager |
The installers are not code-signed: Windows SmartScreen and macOS Gatekeeper ask once before the first start. See the README for what to click, and for the browser extension (loaded once, updated by G9 itself).
latest.yml, latest-mac.yml and latest-linux.yml are the update metadata G9 reads; the .zip and .blockmap files are for the updater.
What changed in 3.1.1
Why. 3.1.0 is the first release on GitHub. Two things could only be done after it existed: the
update test against the real feed, and README pictures whose update status is true.
The update from GitHub, end to end (desktop/test/update-e2e.mjs --feed official, real
packages, no settings written): an older build of the 3.1.0 source (3.0.999), freshly installed,
checked github.com/ImanKari/G9BrowserAgent by itself, downloaded the published 3.1.0 from GitHub's
storage (SHA-512 from the release's latest*.yml), installed it and restarted as 3.1.0.
Windows 11, the owner's workstation: 6 of 6 (the extension in Chrome 153 connected before the
update). Linux AppImage, a clean Ubuntu container: 6 of 6 (the published G9-x86_64.AppImage
replaced the installed file byte for byte; Chrome could not be downloaded into the container that
time, so the extension half ran only in the pipeline's local-feed test, 20 of 20 with Edge). Found on
the way: Node on the owner's workstation cannot reach api.github.com (a network timeout; git,
Docker and the app itself can), which is why the check goes through the app, as a person's would.
The pictures. Made again from the 3.1.1 source in the neutral container: the footer and
Settings → Updates now read "up to date (3.1.1)" — a real check against the published releases —
instead of "no G9 release is published yet", and the Engines view shows the launched tabs' titles
(the 3.1.0 fix). The README and README.fa gained the Updates picture.
No runtime behaviour changed. Version 3.1.1 in package.json, extension/manifest.json,
desktop/package.json and desktop/package-lock.json.
SHA-256
492147ca36f7202ec03a4b33c052e48cf1466dc276c0f0a44ce2a95ef3457d5a G9-3.1.1-mac-arm64.dmg
22f044722d548ea8bf2d372d6b30671fa9e1861d5b260743f289f0123b607d01 G9-3.1.1-mac-arm64.dmg.blockmap
adeb1f76b2a7aa6019910a62f78a3a5e19290e4af94cab5915bcf8bdec3af229 G9-3.1.1-mac-arm64.zip
439e364ed3ab3239812e6e255c8a1224418cedce0abbdae9a73bf301b5e780aa G9-3.1.1-mac-arm64.zip.blockmap
83f5b5770953c78ffaca58978700375d510871700485905f725054d004cb7246 G9-3.1.1-mac-x64.dmg
920ca09d0795197088994b75143aaf3a9cdde0c6e861de4f9bc19c768886c58b G9-3.1.1-mac-x64.dmg.blockmap
a7968b4870bc4aba75686782a4fdfd581f9f7d1840a8f63e0c76a53e40abf998 G9-3.1.1-mac-x64.zip
88ad38c8c5d844d553e1c81c3b35caa633e7c7609085859837c0c9a258ea3fd0 G9-3.1.1-mac-x64.zip.blockmap
e5817af7401c512c8f18f673435726bccd5e2de597275399f4503798acc30559 G9-Setup-3.1.1.exe
f21220247d7d8ed6d082e134fd08a9964f27e8e6ed85454f939339f0514e4238 G9-Setup-3.1.1.exe.blockmap
d47c5992e723ad12fb23e1550b8a5d855b2d15323ffe3440169f30774b63fc04 G9-x86_64.AppImage
2ce9cb9713e179bd9ae622120c86a5c43777a0eb2baaf0216a00c23cae29aeef G9_3.1.1_amd64.deb
G9 3.1.0
Download
| System | File | Updates |
|---|---|---|
| Windows 10/11 (x64) | G9-Setup-3.1.0.exe — per user, no administrator rights |
Automatic: G9 checks these releases, downloads in the background and asks before installing |
| macOS 12+ (Apple silicon) | G9-3.1.0-mac-arm64.dmg |
G9 tells you when a release is out; download it and replace the app (the app is not signed with an Apple Developer ID yet, so macOS cannot install updates into it) |
| macOS 12+ (Intel) | G9-3.1.0-mac-x64.dmg |
As above |
| Linux (x64) | G9-x86_64.AppImage — keep this file name: updates replace the file in place |
Automatic, like Windows |
| Debian/Ubuntu (x64) | G9_3.1.0_amd64.deb |
G9 tells you; install the new .deb with your package manager |
The installers are not code-signed: Windows SmartScreen and macOS Gatekeeper ask once before the first start. See the README for what to click, and for the browser extension (loaded once, updated by G9 itself).
latest.yml, latest-mac.yml and latest-linux.yml are the update metadata G9 reads; the .zip and .blockmap files are for the updater.
What changed in 3.1.0
Why. G9 is published on GitHub, so a person should be able to download it for their system, and
an installed G9 should find and install new versions by itself — with nothing to configure, never
in the middle of a run, and with the browser extension following the app.
Packages. desktop/scripts/build.mjs (replacing build-win.mjs) builds for the system it runs on:
Windows G9-Setup-<v>.exe (NSIS, per user, as before); macOS G9-<v>-mac-{arm64,x64}.dmg and .zip
(the ZIP is the updater's format); Linux G9-x86_64.AppImage and G9_<v>_amd64.deb, each with its
latest*.yml. What was Windows-only, found by reading every platform branch: the background policies
(registry; now reported as not applicable elsewhere), the browser search (App Paths in the registry;
macOS and Linux got their own candidate lists), the MCP config folders (%APPDATA%; now the system's
own), the extension picker's wording, the login item (Linux has none in Electron; G9 writes an XDG
autostart entry), the app menu (macOS needs one for Cmd+Q and editing shortcuts) and the Chrome for
Testing pin (now pinned for win64, mac-x64, mac-arm64 and linux64).
- macOS is unsigned (the owner's choice: no Apple Developer ID yet). It is signed ad hoc so it
runs on Apple silicon, Gatekeeper asks once, and — because Squirrel.Mac installs only into a
Developer-ID-signed app — updates are manual: G9 checks, downloads nothing, and says where the
release is. An app started from a disk image or Downloads runs translocated; the wizard refuses to
write that path into MCP configs. - The AppImage keeps one name. electron-updater's
AppImageUpdater(read in its source) replaces
$APPIMAGEin place only when the file name has no version; otherwise it writes a new versioned
file and deletes the old one — breaking every MCP entry that named it. So the AppImage is published
asG9-x86_64.AppImage. - An AppImage cannot be named from inside. It runs from a mount that vanishes when it exits, so
MCP entries, the daemon and scheduled runs start$APPIMAGE <G9_HOME>/bin/g9-run.mjs <script>in
Node mode (lib/runtime.mjs). Found in a clean Ubuntu container: the AppImage's AppRun adds
--no-sandboxin front of the arguments when unprivileged user namespaces are off (Ubuntu 24.04,
containers), and Node then exits "bad option: --no-sandbox" (9). G9 passes--no-sandboxafter the
script instead, where AppRun leaves it, and the bootstrap drops it. - The .deb installs to
/opt/G9; replacing it needs root, so it is updated by hand too.
Updates. The default source is now the official releases (updateMode: 'official',
electron-updater's github provider on ImanKari/G9BrowserAgent: releases.atom, /releases/latest,
then that tag's latest*.yml, read from github.com, not the rate-limited API). custom keeps the old
https feed (a 3.0.x settings file with a URL and no mode stays on it); off never checks. New in the
prompt and in Settings: Skip this version (never downloaded again) and Install when I quit G9;
a manual install mode (macOS, .deb) with Open the release page. A 404 or an empty release list now
reads "no G9 release is published at the official releases yet" instead of a stack trace.
The extension follows the app, and now always. The daemon and its clients carry an installId (the
AppImage file or the resource folder) beside the version, so "same version, different install" is seen
too. Found by the real update test: after an update the app refreshed G9_HOME/extension, but the
extension was reconnecting at that moment, so the reload was sent to nobody (reloadSent: 0) and the
browser kept the old version. The daemon now catches it up: when an extension older than the daemon
says hello and G9_HOME/extension already holds the daemon's version, it asks that extension once to
reload, after its calls in flight (at most 60 s); never in a loop (daemon.test).
The real update test (desktop/test/update-e2e.mjs, no fakes). It installs an older build of the
same source (built with --as-version), gives it a temporary G9_HOME and a random port, serves the new
packages from a local feed that can corrupt or cut off a download, and drives the running app through
its own window over the DevTools protocol. Scenarios: corrupt download refused by SHA-512; a download
cut off 50 times still completes; skip; a busy daemon (a launched browser) defers the install; install
when I quit; the new version starts with a new daemon, G9_HOME/extension refreshed and reloaded, no
mismatch; the reload waits for a call in flight; macOS's manual notice; and, against GitHub, a fresh
install with no update settings finds, downloads and installs the published release. Windows, on the
owner's workstation: 3.0.1 → 3.1.0, 20 of 20 checks (installed silently into a temporary folder,
uninstalled at the end). Linux AppImage, in a clean Ubuntu container: 13 of 13. In the pipeline, on hosted machines
(Azure DevOps run 487, 3.0.999 → 3.1.0 through the local feed): Windows 20 of 20, Linux AppImage 20 of
20 (with Edge 153 for the extension), macOS 6 of 6 (the manual path). It also found:
build.mjs staging node_modules through a junction lost electron-updater's dependencies in the
packed app (fs-extra; the older build could not update at all) — now copied, and
verify-artifacts.mjs checks every production dependency is inside each app.asar; and main.mjs
loaded Electron when run by plain Node (the smoke test printed two lines).
CI/CD. azure-pipelines.yml (§7): Validate on Windows, Linux and macOS; Package and the real update
test on each; Release (checks, SHA-256 sums, notes from this entry); on main only, Publish: tag in
Azure, mirror to GitHub, and a GitHub Release whose assets are verified before and after it is made
public (setup/github-release.mjs). The first run on main (488) stopped at its first Publish step,
before anything was tagged or published: PowerShell read "… is $head: bump …" as a scope-qualified
variable and refused to parse the script. It is ${head} now, and every pwsh block of the pipeline
was put through PowerShell's own parser. Publish runs only on main, so no branch run could have
shown it.
Also. The desktop Engines view refreshes the tab list when it opens, so a tab opened since no
longer shows as "Untitled" (seen in the README pictures). The MCP shim connects after initialize (an agent was
registered as "mcp-agent" in a race, seen as a flaky self-test on CI). .gitattributes makes every text
file LF: the extension suite compared file text and failed on a Windows checkout with autocrlf. The
unit runner's PASS counter had a control character where \b was meant, and counted nothing. The live
window.open test waits for the first paint before it clicks (a hosted CI machine received the click
before the button was hit-testable). On the hosted Ubuntu 24.04 machine a browser the daemon launched
aborted at start: without unprivileged user namespaces Chromium falls back to its setuid sandbox
helper, and that image's msedge-sandbox was not root-owned with mode 4755. The pipeline now restores
the helper as the package installs it (so G9's browsers run sandboxed there, as on a desktop), and
engine/launch.js turns that abort into the fix (sandboxHint), with G9_BROWSER_NO_SANDBOX=1 as an
explicit, Linux-only opt-out for containers (engine.test). The README is new, for people who use G9, in English and Persian
(README.fa.md), with pictures from a real run (setup/docs-screenshots.mjs, docs/assets/); the
technical reference moved to docs/REFERENCE.md. License: MIT (LICENSE).
Version 3.1.0 in package.json, extension/manifest.json, desktop/package.json and
desktop/package-lock.json.
SHA-256
5e658d0e782a382dc46d2d4473a03fae746fdd45f3c9e2cd43930f62e5db7a28 G9-3.1.0-mac-arm64.dmg
55b837dde63f89febf61d13496c0e5c24d96744fdf85122f6e1d370842e8df21 G9-3.1.0-mac-arm64.dmg.blockmap
2c8619b1ce7cef2498ee8682802b3cdcb6d9887ca3890511753476acdf0ce414 G9-3.1.0-mac-arm64.zip
73895d7969073c19f1be18ff1fa365c28afe0af9730ef13b138cd090d593f979 G9-3.1.0-mac-arm64.zip.blockmap
c61b242e2eefc36bb9a2b77eb40428ffd04155059d3ba1e4fbac264651ac1f83 G9-3.1.0-mac-x64.dmg
abd8e21365410e4fcc20738d2a3edf6f0cc93cef323d91d041213eac476589cd G9-3.1.0-mac-x64.dmg.blockmap
34c8e2badffa7bb1b8d3721a373856c0f7cb6290402ce6a281890831b8512ea1 G9-3.1.0-mac-x64.zip
2c66b6639595185ddabebe3939dc6f30b53bca689dfec23ea896f1a3590b89c3 G9-3.1.0-mac-x64.zip.blockmap
a6524868d112fd8c8c7276b7f7c304bc3555177705dd7f81c1b3d6aa83c2df6b G9-Setup-3.1.0.exe
4e959888a5a1b2df5ed76ea317d3adcfd1355a8a6024fdcfc6ed8de55675e2f7 G9-Setup-3.1.0.exe.blockmap
097ca22aad26970d602d4ab481de915657fe7fbe711f7f3548b5399b90fdee1a G9-x86_64.AppImage
4b37ca2c3dec1cd8b20...