slicc-browsers
The browser catalog, JSON schemas and conformance corpus shared by the two programs that find and launch browsers for SLICC:
slicc browsers and slicc launch in slicc-cli (Go, CGO_ENABLED=0);
playwright-cli list and open in slicc-cdp (Rust, also built for wasm32-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() *Catalog parses the embedded catalog.json once 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 a schema other than 1.
(*Catalog).Browser(id) and (*Catalog).Priority(id), the index in browsers (its length for an unknown id).
CatalogJSON(), CatalogSchemaJSON(), BrowsersSchemaJSON() and CorpusJSON() return copies of the embedded files.
Corpus() ([]Case, error) returns the cases. See Running the corpus for the helpers.
Rust
[dependencies]
slicc-browsers = "1"
[dev-dependencies]
slicc-browsers = { version = "1", features = ["corpus"] }
- Always:
CATALOG, CATALOG_SCHEMA, BROWSERS_SCHEMA and CORPUS_SCHEMA, each the file's text via include_str!, and SCHEMA (1). No dependencies.
- Feature
serde: Catalog and the other types, Catalog::parse(&str), catalog() (parses CATALOG), Catalog::browser and Catalog::priority. It pulls in serde and serde_json.
- Feature
corpus (implies serde): CORPUS, the text of corpus.json, and the module corpus with corpus(), Case, Layout and 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's Chrome/<major> (discovery.chromeMajorPattern) is lower are left out, and so are installed browsers with chromiumVersion: true whose 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), and chromiumVersion: 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 (under Contents/MacOS), and dataDir?, the default user-data dir under ~/Library/Application Support.
windows?: roots (names from platforms.windows.roots, searched in order) and executables (each tried under each root); dataRoot and dataDir, the default user-data dir; startMenuInternet?, the browser's key names under the StartMenuInternet registry keys.
linux?: bins (names on PATH), desktopIds (.desktop file 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>/config for a flatpak), and debs (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 with debugSupport: "disabled" and never launched), and idPrefix (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 (installed chromeId), chromium (other installed Chromium browsers), electron (installed Electron apps).
discovery:
activePortFile (DevToolsActivePort) and probePorts (9222 to 9229, probed by slicc only);
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: electron when the user agent contains electronUserAgent, else products[<product>] for the product before the / in /json/version's Browser, else that product, else fallback;
process: the process scan, see The process source.
seven:
originScheme and originHostPattern: a seven origin is https://<host> with a host matching the pattern, except for the excludedLabels (www);
sitePattern ([*.]sliccy.ai);
switches and disabledFeatures: the seven flags, all --disable-features values merged into one switch;
prefs: values merged into a profile's Default/Preferences at the path of keys;
discardExceptions: where the site pattern and the extra origins' hosts go, as a list of strings or as a chrome-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;
form says where a key comes from: six (SLICC six's keys) or chrome-154 (what Chrome 154 reads).
profiles: SLICC profiles live in <config dir>/<root>/<name>, and a snap's in ~/snap/<snap>/common/<snapRoot>/<name>. legacyRoot is the old root, which SLICC six also uses, and sixProfilePattern matches six's profile names; neither is ever scanned. namePattern is a valid profile name.
launch: chromiumArgs (with {profile} for the profile dir), then the seven flags, then headless with --headless. Electron apps get electron.portFlag with the first free port from electron.minPort up that isn't in electron.reservedPorts.
platforms: the config dir per OS, the Windows roots, and the Windows registry keys. A location is the environment variable env when it's set (on Linux only an absolute path counts), else absolute, else home under the home dir. registry: under each of hives, every subkey of each startMenuInternet key, with its openCommand default value, and the default value of appPaths\<name> for each of appPathNames.
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's discovery.process.windows, which prints a JSON array (or one object) of ProcessId and CommandLine; 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, .cmd or .bat, is one of wrappers, case-insensitively. A command with no executable before its first - token is skipped too.
- The port comes from the first
--remote-debugging-port followed by = or whitespace and then digits. A port above 65535 drops the process.
- The dir is
--user-data-dir at 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 (a slicc segment followed by profiles, case-insensitively), the process is skipped.
- Any other port is probed on
127.0.0.1. An endpoint from the process scan has source: "process" and userDataDir set 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, and os?: the OSes the case runs on (darwin, linux, windows; default all).
needs: what a program must support to run the case: user-data-dirs (DevToolsActivePort in browsers' default user-data dirs), seven-profiles (in the profile roots), flag (--cdp URLs), process, probe (port probes, slicc only), 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?: --cdp URLs, 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 by host:port. localhost and [::1] are 127.0.0.1. A listed port accepts TCP connections. /json/version answers 200 with version, /json/list (and /json) with list. A missing body, or any other path, answers 404. An unlisted port refuses connections.
installed?: installed browsers in the shape of output schema 1's installed, 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
env only, 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 with expected.json after token expansion, as parsed JSON.
Go (slicc-cli does this in test/integration/corpus_test.go):
cases, _ := sliccbrowsers.Corpus()
for _, c := range cases {
if !c.Runs(runtime.GOOS, sliccbrowsers.NeedFlag, sliccbrowsers.NeedProcess /* … */) {
continue
}
l := sliccbrowsers.Layout{OS: runtime.GOOS, Home: t.TempDir(), Getenv: func(k string) string { return c.Input.Env[k] }}
// write c.Files(l); use c.CDP(l), c.PSOutput(l, os.Getpid()) or c.CIMOutput(l, os.Getpid()),
// c.Transport() as the http.RoundTripper and c.DialContext for TCP; compare with c.ExpectedJSON(l)
}
Rust, with the corpus feature:
use slicc_browsers::corpus::{corpus, Layout, NEED_PROCESS, NEED_PROFILES};
let cat = slicc_browsers::catalog();
let os = match std::env::consts::OS { "macos" => "darwin", other => other };
for case in corpus().cases {
if !case.runs(os, &[NEED_PROFILES, NEED_PROCESS]) {
continue;
}
let home = tempdir.path().to_str().unwrap();
let env = |k: &str| case.input.env.get(k).cloned();
let layout = Layout { os, home, env: &env, catalog: &cat };
// write case.files(&layout); case.ps_output(&layout, std::process::id()) or case.cim_output(…);
// answer HTTP with case.http_get(host_port, path) and TCP with case.server(host_port).is_some();
// compare with case.expected(&layout)
}
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 corpus regenerates corpus.json from corpus/; the pre-commit hook and CI fail when it's stale.
npm run validate checks catalog.json and corpus.json against 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 and sevenOpen in each expected.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 --check and clippy.
npm test: validate, the Go tests in test/integration/, and cargo test --all-features.
npm run build: the Go module, the crate (also for wasm32-wasip1), and npm 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.Z and a GitHub release are the Go module's release. scripts/major-guard.mjs fails the release when the major doesn't fit the module path in go.mod (no suffix allows v0 and v1, /vN only vN). The workflow then asks proxy.golang.org for the new version and pkg.go.dev to fetch it; both only warn.
- npm: with the repository variable
SLICC_BROWSERS_NPM_PUBLISH set to true, 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_PUBLISH set to true, Cargo.toml is stamped with the version and cargo publish runs 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-browsers claims the name with a 0.0.0 placeholder and trusts ai-ecoverse/slicc-browsers' release.yml. Then set SLICC_BROWSERS_NPM_PUBLISH to true.
- 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 with cargo login. Then, on crates.io, add a trusted publisher to slicc-browsers: GitHub, owner ai-ecoverse, repository slicc-browsers, workflow release.yml, no environment. Then set SLICC_BROWSERS_CRATES_PUBLISH to true.