slicc-browsers
The browser catalog, JSON schemas and conformance corpus shared by the two programs that find and launch browsers for SLICC:
slicc browsersandslicc launchin slicc-cli (Go,CGO_ENABLED=0);playwright-cli listandopenin slicc-cdp (Rust, also built forwasm32-wasip1).
They can't share code, so they share data. Three packages wrap the same files, at one version:
| package | what it gives you |
|---|---|
Go module github.com/ai-ecoverse/slicc-browsers (package sliccbrowsers) |
the files embedded, typed structs, and corpus helpers |
Rust crate slicc-browsers (lib slicc_browsers) |
the files as &str constants, serde types behind features |
npm package @ai-ecoverse/slicc-browsers |
the JSON files and the schemas |
| file | what it is |
|---|---|
catalog.json |
the browsers, Electron apps, ranking, discovery, seven flags and prefs, profile and platform paths |
schema/catalog.schema.json |
JSON Schema (2020-12) for catalog.json |
schema/browsers.schema.json |
JSON Schema for slicc browsers --json and playwright-cli list --json, schema 1 |
schema/corpus.schema.json |
JSON Schema for corpus.json |
corpus/<case>/case.json, corpus/<case>/expected.json |
the conformance cases |
corpus.json |
every case in one file, generated from corpus/ by npm run corpus |
Go
import sliccbrowsers "github.com/ai-ecoverse/slicc-browsers"
c := sliccbrowsers.Default()
b, ok := c.Browser("chrome")
Default() *Catalogparses the embeddedcatalog.jsononce and panics if it doesn't parse (CI makes sure it does).Load() (*Catalog, error)is the same without the panic. Treat the result as read-only.Parse([]byte)parses another copy and rejects aschemaother than 1.(*Catalog).Browser(id)and(*Catalog).Priority(id), the index inbrowsers(its length for an unknown id).CatalogJSON(),CatalogSchemaJSON(),BrowsersSchemaJSON()andCorpusJSON()return copies of the embedded files.Corpus() ([]Case, error)returns the cases. See Running the corpus for the helpers.
Rust
[]
= "1"
[]
= { = "1", = ["corpus"] }
- Always:
CATALOG,CATALOG_SCHEMA,BROWSERS_SCHEMAandCORPUS_SCHEMA, each the file's text viainclude_str!, andSCHEMA(1). No dependencies. - Feature
serde:Catalogand the other types,Catalog::parse(&str),catalog()(parsesCATALOG),Catalog::browserandCatalog::priority. It pulls inserdeandserde_json. - Feature
corpus(impliesserde):CORPUS, the text ofcorpus.json, and the modulecorpuswithcorpus(),Case,Layoutand the helpers below. Use it from[dev-dependencies]only; with resolver 2 it stays out of normal builds.
Every feature builds for wasm32-wasip1.
npm
import catalog from '@ai-ecoverse/slicc-browsers' with { type: 'json' }. The exports are . (catalog.json), ./catalog.json, ./corpus.json, ./schema/catalog.schema.json, ./schema/browsers.schema.json and ./schema/corpus.schema.json.
catalog.json
"schema": 1. A minor release only adds fields; a field that changes meaning or goes away needs schema 2 and a major release. Relative paths are arrays of path segments, joined with the platform's separator. Top-level fields:
minChromiumMajor(116): endpoints whose user agent'sChrome/<major>(discovery.chromeMajorPattern) is lower are left out, and so are installed browsers withchromiumVersion: truewhose version's leading number is lower. A version that doesn't start with a number is kept.browsers: every known Chromium browser, in rank priority order. Each has:id,name,channel(stable,beta,dev,canary,nightly,snapshot,testing),engine(chromium), andchromiumVersion: whether its version is the Chromium version (Chrome, Edge, Chromium) or its own (Brave, Opera, Vivaldi, Arc).macos:bundleId,app(the bundle's file name),executable(underContents/MacOS), anddataDir?, the default user-data dir under~/Library/Application Support.windows?:roots(names fromplatforms.windows.roots, searched in order) andexecutables(each tried under each root);dataRootanddataDir, the default user-data dir;startMenuInternet?, the browser's key names under theStartMenuInternetregistry keys.linux?:bins(names onPATH),desktopIds(.desktopfile names without the suffix),flatpak?(the app id),snap?(the snap name),snapDataDir?(the user-data dir under~/snap/<snap>/common),configDir(the user-data dir under the config dir, also under~/.var/app/<flatpak>/configfor a flatpak), anddebs(dpkg package names for the version).
electron:macosApps(known Electron apps by bundle id, with display names),debugBlocked(app names that block remote debugging; they're listed withdebugSupport: "disabled"and never launched), andidPrefix(electron:, followed by the slug of the name: lower case, runs of other characters as one-).rank:tiers, 1 to 5:sevenOpen(an endpoint with a seven tab),endpoint(any other endpoint),chrome(installedchromeId),chromium(other installed Chromium browsers),electron(installed Electron apps).discovery:activePortFile(DevToolsActivePort) andprobePorts(9222 to 9229, probed byslicconly);sourceRank: when two sources find the same host and port, the lower rank wins, and on a tie the first found;names: an endpoint's name when it isn't from a known browser's user-data dir:electronwhen the user agent containselectronUserAgent, elseproducts[<product>]for the product before the/in/json/version'sBrowser, else that product, elsefallback;process: the process scan, see The process source.
seven:originSchemeandoriginHostPattern: a seven origin ishttps://<host>with a host matching the pattern, except for theexcludedLabels(www);sitePattern([*.]sliccy.ai);switchesanddisabledFeatures: the seven flags, all--disable-featuresvalues merged into one switch;prefs: values merged into a profile'sDefault/Preferencesat thepathof keys;discardExceptions: where the site pattern and the extra origins' hosts go, as alistof strings or as achrome-time-map, a map from site to the time it was added (microseconds since 1601-01-01 UTC, as a decimal string);cleanExit: values that mark the last exit clean;formsays where a key comes from:six(SLICC six's keys) orchrome-154(what Chrome 154 reads).
profiles: SLICC profiles live in<config dir>/<root>/<name>, and a snap's in~/snap/<snap>/common/<snapRoot>/<name>.legacyRootis the old root, which SLICC six also uses, andsixProfilePatternmatches six's profile names; neither is ever scanned.namePatternis a valid profile name.launch:chromiumArgs(with{profile}for the profile dir), then the seven flags, thenheadlesswith--headless. Electron apps getelectron.portFlagwith the first free port fromelectron.minPortup that isn't inelectron.reservedPorts.platforms: the config dir per OS, the Windows roots, and the Windows registry keys. A location is the environment variableenvwhen it's set (on Linux only an absolute path counts), elseabsolute, elsehomeunder the home dir.registry: under each ofhives, every subkey of eachstartMenuInternetkey, with itsopenCommanddefault value, and the default value ofappPaths\<name>for each ofappPathNames.
The process source
The process scan finds browsers started with a debugging port by something else:
- Process list. On Unix it's
discovery.process.unix(ps -ax -o pid= -o command=): one process a line, the pid, whitespace, and the command line. On Windows it'sdiscovery.process.windows, which prints a JSON array (or one object) ofProcessIdandCommandLine; entries with no command line are skipped. - What's skipped: pid 0, the scanning process itself, and commands whose first token is a wrapper. The first token is the quoted string when the command starts with a quote. A wrapper is a token whose base name, without
.exe,.cmdor.bat, is one ofwrappers, case-insensitively. A command with no executable before its first-token is skipped too. - The port comes from the first
--remote-debugging-portfollowed by=or whitespace and then digits. A port above 65535 drops the process. - The dir is
--user-data-dirat the start of a token, followed by=or whitespace, then one token (or a quoted string). - Port 0 means the port is in the dir's
DevToolsActivePort, checked like any other. With no dir, or a dir under the legacy profile root (asliccsegment followed byprofiles, case-insensitively), the process is skipped. - Any other port is probed on
127.0.0.1. An endpoint from the process scan hassource: "process"anduserDataDirset to the dir when there is one. - Tests must fake the process list: never scan the real one in a test.
Output schema 1
schema/browsers.schema.json describes slicc browsers --json, which playwright-cli list --json also prints. slicc-cli's README is the authoritative spec for it; this schema is a copy and follows it. Fields can be added; source is one of devtools-active-port, flag, probe and process.
The corpus
Each case is a fake machine: a home tree, DevToolsActivePort files, fake /json/version and /json/list answers, a fake process list, --cdp URLs, and installed browsers, with the ranked list it must produce in expected.json. corpus.json holds every case as { "name", "case", "expected" }.
case.json:
description, andos?: the OSes the case runs on (darwin,linux,windows; default all).needs: what a program must support to run the case:user-data-dirs(DevToolsActivePortin browsers' default user-data dirs),seven-profiles(in the profile roots),flag(--cdpURLs),process,probe(port probes,slicconly),origins(extra seven origins),installed(ranking installed browsers). A program skips a case that needs something it doesn't do.env?: environment variables for discovery, which sees nothing else from the real environment.files?: file contents by path. Every path starts with a token.cdp?,origins?,probePorts?:--cdpURLs, extra seven origins, and the ports to probe.processes?:{ "pid", "command" }, or{ "self": true, "command" }for the scanning process, whose pid the harness fills in.http?: fake servers byhost:port.localhostand[::1]are127.0.0.1. A listed port accepts TCP connections./json/versionanswers 200 withversion,/json/list(and/json) withlist. A missing body, or any other path, answers 404. An unlisted port refuses connections.installed?: installed browsers in the shape of output schema 1'sinstalled, ranked along with the endpoints.
Tokens. {home} (the fake home), {config} (the config dir), {sevenProfiles} (the profile root) and {userData:<id>} (the browser's first default user-data dir on this OS) stand for directories. In files, env, cdp, process commands and expected.json, a token and the path characters after it (up to whitespace, ", ' or ,) become a path on the OS under test, with \ on Windows. The token values come from catalog.json, with the case's env.
Never 9222 to 9229. A developer's own Chrome listens there. The corpus schema refuses those ports, npm run validate refuses them anywhere in the corpus, and the fakes never serve them.
Running the corpus
A harness, per case it runs:
- makes a temporary home and writes each of
files; - points discovery at that home and the case's
envonly, so no real user-data dir, profile or process is ever read; - feeds the fake process list (Unix:
PSOutput, Windows:CIMOutput) and routes every HTTP request and TCP dial to the fakes, so discovery never touches the network; - discovers endpoints, ranks them with
installed, and compares the JSON withexpected.jsonafter token expansion, as parsed JSON.
Go (slicc-cli does this in test/integration/corpus_test.go):
cases, _ := sliccbrowsers.Corpus()
for _, c := range cases
Rust, with the corpus feature:
use ;
let cat = catalog;
let os = match OS ;
for case in corpus.cases
Layout also gives Config, SevenProfiles, UserDataDirs(id), Token(name) and Expand(text) (Rust: config, seven_profiles, user_data_dirs, token, expand). Their results for a few homes are pinned in test/integration/layout.json, which the Go and Rust tests both check.
Development
Go 1.26, Rust stable with wasm32-wasip1, Node ≥ 24.
npm run corpusregeneratescorpus.jsonfromcorpus/; the pre-commit hook and CI fail when it's stale.npm run validatecheckscatalog.jsonandcorpus.jsonagainst the schemas, plus what a schema can't say: unique ids, bundle ids and names, known Windows roots, rising tiers, compiling patterns, known tokens, tier order andsevenOpenin eachexpected.json, and no port from 9222 to 9229.npm run lint: the slicc lint tools (Biome, no comments, no unit tests in git,AGENTS.md),gofmt,go mod tidy -diff,go vet, golangci-lint,cargo fmt --checkand clippy.npm test:validate, the Go tests intest/integration/, andcargo test --all-features.npm run build: the Go module, the crate (also forwasm32-wasip1), andnpm pack --dry-run.
Changing a table in slicc-cli/internal/browser means changing catalog.json here, releasing, and bumping the dependency; slicc-cli has no tables of its own. A new behaviour gets a corpus case.
Releases
semantic-release cuts one version from main for all three packages (.github/workflows/release.yml):
- The tag
vX.Y.Zand a GitHub release are the Go module's release.scripts/major-guard.mjsfails the release when the major doesn't fit the module path ingo.mod(no suffix allows v0 and v1,/vNonly vN). The workflow then asksproxy.golang.orgfor the new version andpkg.go.devto fetch it; both only warn. - npm: with the repository variable
SLICC_BROWSERS_NPM_PUBLISHset totrue, the package is stamped with the version and published through npm trusted publishing (OIDC, with provenance). A version that is already on npm is skipped. - crates.io: with the repository variable
SLICC_BROWSERS_CRATES_PUBLISHset totrue,Cargo.tomlis stamped with the version andcargo publishruns with a short-lived token from crates.io trusted publishing (rust-lang/crates-io-auth-action). A version that is already on crates.io is skipped.
A run started by hand (gh workflow run release.yml --ref main) on a commit that already has its tag publishes what's missing without a new release.
Go module versions. A pushed tag is the release: proxy.golang.org caches it and sum.golang.org records its hash for good. Never move or delete a pushed tag; retract a bad version in go.mod and release anew. A BREAKING CHANGE needs a /vN module path first.
First publish (by a maintainer with 2FA):
- npm:
npx fledgling add --new @ai-ecoverse/slicc-browsersclaims the name with a0.0.0placeholder and trustsai-ecoverse/slicc-browsers'release.yml. Then setSLICC_BROWSERS_NPM_PUBLISHtotrue. - crates.io: trusted publishing needs the crate to exist. Publish it once by hand from the latest tag:
git checkout vX.Y.Z && node scripts/set-crate-version.mjs X.Y.Z && cargo publish --allow-dirty, logged in withcargo login. Then, on crates.io, add a trusted publisher toslicc-browsers: GitHub, ownerai-ecoverse, repositoryslicc-browsers, workflowrelease.yml, no environment. Then setSLICC_BROWSERS_CRATES_PUBLISHtotrue.