Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion images/chromium-headful/run-docker.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ mkdir -p "$HOST_RECORDINGS_DIR"
RUN_AS_ROOT="${RUN_AS_ROOT:-false}"

# Build Chromium flags file and mount
CHROMIUM_FLAGS_DEFAULT="--user-data-dir=/home/kernel/user-data --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=*"
CHROMIUM_FLAGS_DEFAULT="--user-data-dir=/home/kernel/user-data --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=* --enable-features=WebMCPTesting,DevToolsWebMCPSupport"
if [[ "$RUN_AS_ROOT" == "true" ]]; then
CHROMIUM_FLAGS_DEFAULT="$CHROMIUM_FLAGS_DEFAULT --no-sandbox --no-zygote"
fi
Expand Down
2 changes: 1 addition & 1 deletion images/chromium-headful/run-unikernel.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,7 @@ volume_name="${NAME}-flags"
# RUN_AS_ROOT defaults to true in unikernel (for now, until we figure it out)
RUN_AS_ROOT="${RUN_AS_ROOT:-true}"

chromium_flags_default="--user-data-dir=/home/kernel/user-data --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=*"
chromium_flags_default="--user-data-dir=/home/kernel/user-data --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=* --enable-features=WebMCPTesting,DevToolsWebMCPSupport"
if [[ "$RUN_AS_ROOT" == "true" ]]; then
chromium_flags_default="$chromium_flags_default --no-sandbox --no-zygote"
fi
Expand Down
3 changes: 3 additions & 0 deletions server/cmd/api/api/chromium.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -598,6 +598,9 @@ func (s *ApiService) mergeAndWriteChromiumFlags(ctx context.Context, newTokens [

// Merge existing flags with new flags using token-aware API
mergedTokens := chromiumflags.MergeFlags(existingTokens, newTokens)
// Fold kernel-namespaced disable tokens into the plain Chromium switch so
// /chromium/flags only ever holds switches Chromium understands.
mergedTokens = chromiumflags.TranslateKernelDisableFeatures(mergedTokens)

if err := writeChromiumFlags(mergedTokens); err != nil {
log.Error("failed to write flags", "error", err)
Expand Down
1 change: 1 addition & 0 deletions server/cmd/chromium-launcher/main.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,7 @@ func main() {
os.Exit(1)
}
final:=chromiumflags.MergeFlagsWithRuntimeTokens(baseFlags, runtimeTokens)
final=chromiumflags.TranslateKernelDisableFeatures(final)
final=withDefaultPrivateNetworkBypass(final)

// Diagnostics for parity with previous scripts
Expand Down
5 changes: 5 additions & 0 deletions server/cmd/wrapper/chromium.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,11 @@ func applyHeadlessDefaultFlags() {
"--disable-renderer-backgrounding",
"--disable-search-engine-choice-screen",
"--disable-software-rasterizer",
// WebMCP (navigator.modelContext + CDP WebMCP domain, Chromium 151+).
// DevToolsWebMCPSupport is default-on upstream; listing it keeps the
// CDP domain stable against finch flips. Sessions can opt out per
// instance via --disable-features=WebMCPTesting,DevToolsWebMCPSupport.
"--enable-features=WebMCPTesting,DevToolsWebMCPSupport",
"--enable-use-zoom-for-dsf=false",
"--export-tagged-pdf",
"--force-color-profile=srgb",
Expand Down
90 changes: 75 additions & 15 deletions server/lib/chromiumflags/chromiumflags.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,10 +36,11 @@ func appendCSVInto(dst *[]string, csv string) {
}
}

// parseTokenStream extracts extension-related flags and collects non-extension flags.
// It returns the list of non-extension tokens and, via references, fills the buckets for
// --load-extension, --disable-extensions-except and a possible --disable-extensions token for that stream.
func parseTokenStream(tokens []string, load, except *[]string, disableAll *string) (nonExt []string) {
// parseTokenStream extracts extension-related and feature-list flags and collects the rest.
// It returns the leftover tokens and, via references, fills the buckets for
// --load-extension, --disable-extensions-except, --enable-features, --disable-features
// and a possible --disable-extensions token for that stream.
func parseTokenStream(tokens []string, load, except, enableFeatures, disableFeatures *[]string, disableAll *string) (nonExt []string) {
for _, tok := range tokens {
switch {
case strings.HasPrefix(tok, "--load-extension="):
Expand All@@ -48,6 +49,12 @@ func parseTokenStream(tokens []string, load, except *[]string, disableAll *strin
case strings.HasPrefix(tok, "--disable-extensions-except="):
val := strings.TrimPrefix(tok, "--disable-extensions-except=")
appendCSVInto(except, val)
case strings.HasPrefix(tok, "--enable-features="):
val := strings.TrimPrefix(tok, "--enable-features=")
appendCSVInto(enableFeatures, val)
case strings.HasPrefix(tok, "--disable-features="):
val := strings.TrimPrefix(tok, "--disable-features=")
appendCSVInto(disableFeatures, val)
case tok == "--disable-extensions":
*disableAll = tok
default:
Expand DownExpand Up@@ -128,29 +135,52 @@ func ReadOptionalFlagFile(path string) ([]string, error) {
// extensions_enabled() returns true, which is false when --disable-extensions-except is used.
// Any paths from --disable-extensions-except are merged into --load-extension instead.
//
// Non-extension flags from both base and runtime are combined with deduplication (first occurrence preserved).
// Feature-list flags (--enable-features / --disable-features) from both sources are also
// merged into single comma-separated tokens. Chromium keeps only one value per switch, so
// emitting duplicates would silently drop all but the last list. When a feature appears in
// both lists Chromium disables it — disable wins over enable.
//
// Non-feature, non-extension flags from both base and runtime are combined with deduplication
// (first occurrence preserved).
func MergeFlags(baseTokens, runtimeTokens []string) []string {
// Buckets
var (
baseNonExt []string // Non-extension related flags contained in base
runtimeNonExt []string // Non-extension related flags contained in runtime
baseLoad []string // --load-extension flags contained in base
baseExcept []string // --disable-extensions-except flags for base (parsed but not re-emitted)
rtLoad []string // --load-extension flags contained in runtime
rtExcept []string // --disable-extensions-except flags contained in runtime (parsed but not re-emitted)
baseDisableAll string // --disable-extensions flag contained in base
rtDisableAll string // --disable-extensions flag contained in runtime
baseNonExt []string // Non-extension related flags contained in base
runtimeNonExt []string // Non-extension related flags contained in runtime
baseLoad []string // --load-extension flags contained in base
baseExcept []string // --disable-extensions-except flags for base (parsed but not re-emitted)
baseEnableFeat []string // --enable-features values contained in base
baseDisableFeat []string // --disable-features values contained in base
rtLoad []string // --load-extension flags contained in runtime
rtExcept []string // --disable-extensions-except flags contained in runtime (parsed but not re-emitted)
rtEnableFeat []string // --enable-features values contained in runtime
rtDisableFeat []string // --disable-features values contained in runtime
baseDisableAll string // --disable-extensions flag contained in base
rtDisableAll string // --disable-extensions flag contained in runtime
)

baseNonExt = parseTokenStream(baseTokens, &baseLoad, &baseExcept, &baseDisableAll)
runtimeNonExt = parseTokenStream(runtimeTokens, &rtLoad, &rtExcept, &rtDisableAll)
baseNonExt = parseTokenStream(baseTokens, &baseLoad, &baseExcept, &baseEnableFeat, &baseDisableFeat, &baseDisableAll)
runtimeNonExt = parseTokenStream(runtimeTokens, &rtLoad, &rtExcept, &rtEnableFeat, &rtDisableFeat, &rtDisableAll)

// Merge extension lists - include paths from --disable-extensions-except in load paths
// since we no longer emit --disable-extensions-except
mergedLoad := union(baseLoad, rtLoad)
mergedLoad = union(mergedLoad, baseExcept)
mergedLoad = union(mergedLoad, rtExcept)

// Merge feature lists - a feature listed in both enable and disable stays in both;
// Chromium resolves that conflict as disabled.
mergedEnableFeat := union(baseEnableFeat, rtEnableFeat)
mergedDisableFeat := union(baseDisableFeat, rtDisableFeat)

var featureFlags []string
if len(mergedEnableFeat) > 0 {
featureFlags = append(featureFlags, "--enable-features="+strings.Join(mergedEnableFeat, ","))
}
if len(mergedDisableFeat) > 0 {
featureFlags = append(featureFlags, "--disable-features="+strings.Join(mergedDisableFeat, ","))
}

// Construct final extension-related flags respecting override semantics:
// 1) If runtime specifies --disable-extensions, it overrides everything extension related
// 2) Else if base specifies --disable-extensions and runtime does NOT specify any --load-extension, keep base disable
Expand All@@ -169,6 +199,7 @@ func MergeFlags(baseTokens, runtimeTokens []string) []string {

// Combine and dedupe (preserving first occurrence)
combined := append(append([]string{}, baseNonExt...), runtimeNonExt...)
combined = append(combined, featureFlags...)
combined = append(combined, extFlags...)
seen := make(map[string]struct{}, len(combined))
final := make([]string, 0, len(combined))
Expand DownExpand Up@@ -243,3 +274,32 @@ func WriteFlagFile(path string, tokens []string) error {
data = append(data, '\n')
return os.WriteFile(path, data, 0o644)
}

// KernelDisableFeaturesPrefix is a kernel-namespaced switch for per-session feature
// disabling. Chromium ignores unknown switches, so on images that predate translation
// the token is inert instead of last-win clobbering boot-time --disable-features lists.
const KernelDisableFeaturesPrefix = "--kernel-disable-features="

// TranslateKernelDisableFeatures folds any --kernel-disable-features tokens into the
// single --disable-features token and returns the result. Values are unioned with any
// existing disable list; ordering of unrelated tokens is preserved. Idempotent.
func TranslateKernelDisableFeatures(tokens []string) []string {
var kernelVals, disableVals []string
rest := make([]string, 0, len(tokens))
for _, tok := range tokens {
switch {
case strings.HasPrefix(tok, KernelDisableFeaturesPrefix):
appendCSVInto(&kernelVals, strings.TrimPrefix(tok, KernelDisableFeaturesPrefix))
case strings.HasPrefix(tok, "--disable-features="):
appendCSVInto(&disableVals, strings.TrimPrefix(tok, "--disable-features="))
default:
rest = append(rest, tok)
}
}
if len(kernelVals) == 0 {
return tokens
}
merged := union(disableVals, kernelVals)
rest = append(rest, "--disable-features="+strings.Join(merged, ","))
return rest
}
108 changes: 92 additions & 16 deletions server/lib/chromiumflags/chromiumflags_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,12 +41,16 @@ func TestAppendCSVInto(t *testing.T) {

func TestParseTokenStream_BaseAndRuntime(t *testing.T) {
var (
baseLoad []string
baseExcept []string
rtLoad []string
rtExcept []string
baseDisable string
rtDisable string
baseLoad []string
baseExcept []string
rtLoad []string
rtExcept []string
baseDisable string
rtDisable string
baseEnableFeat []string
baseDisFeat []string
rtEnableFeat []string
rtDisFeat []string
)

baseTokens := []string{
Expand All@@ -62,8 +66,8 @@ func TestParseTokenStream_BaseAndRuntime(t *testing.T) {
"--foo",
}

baseNonExt := parseTokenStream(baseTokens, &baseLoad, &baseExcept, &baseDisable)
runtimeNonExt := parseTokenStream(runtimeTokens, &rtLoad, &rtExcept, &rtDisable)
baseNonExt := parseTokenStream(baseTokens, &baseLoad, &baseExcept, &baseEnableFeat, &baseDisFeat, &baseDisable)
runtimeNonExt := parseTokenStream(runtimeTokens, &rtLoad, &rtExcept, &rtEnableFeat, &rtDisFeat, &rtDisable)

if !reflect.DeepEqual(baseLoad, []string{"/e1", "/e2"}) {
t.Fatalf("base load-extension parsed incorrectly: %#v", baseLoad)
Expand DownExpand Up@@ -127,16 +131,20 @@ func TestOverrideSemantics_DisableRuntime_Wins(t *testing.T) {
runtimeTokens := parseFlags(runtimeFlags)

var (
baseLoad []string
baseExcept []string
rtLoad []string
rtExcept []string
baseDisable string
runtimeDisable string
baseLoad []string
baseExcept []string
rtLoad []string
rtExcept []string
baseEnableFeat []string
baseDisableFeat []string
rtEnableFeat []string
rtDisableFeat []string
baseDisable string
runtimeDisable string
)

_ = parseTokenStream(baseTokens, &baseLoad, &baseExcept, &baseDisable)
_ = parseTokenStream(runtimeTokens, &rtLoad, &rtExcept, &runtimeDisable)
_ = parseTokenStream(baseTokens, &baseLoad, &baseExcept, &baseEnableFeat, &baseDisableFeat, &baseDisable)
_ = parseTokenStream(runtimeTokens, &rtLoad, &rtExcept, &rtEnableFeat, &rtDisableFeat, &runtimeDisable)

var extFlags []string
if runtimeDisable != "" {
Expand DownExpand Up@@ -287,6 +295,30 @@ func TestMergeFlags(t *testing.T) {
runtimeFlags: []string{"--bar", "--load-extension=/e2", "--disable-extensions-except=/x2"},
want: []string{"--foo", "--bar", "--load-extension=/e1,/e2,/x1,/x2"},
},
{
name: "merge enable-features from base and runtime",
baseFlags: []string{"--enable-features=WebMCPTesting"},
runtimeFlags: []string{"--enable-features=Foo"},
want: []string{"--enable-features=WebMCPTesting,Foo"},
},
{
name: "merge disable-features across sources",
baseFlags: []string{"--disable-features=A,B"},
runtimeFlags: []string{"--disable-features=B,C"},
want: []string{"--disable-features=A,B,C"},
},
{
name: "runtime can opt out of a base-enabled feature via disable list",
baseFlags: []string{"--enable-features=WebMCPTesting,DevToolsWebMCPSupport"},
runtimeFlags: []string{"--disable-features=WebMCPTesting"},
want: []string{"--enable-features=WebMCPTesting,DevToolsWebMCPSupport", "--disable-features=WebMCPTesting"},
},
{
name: "enable and disable features coexist across sources",
baseFlags: []string{"--enable-features=A", "--disable-features=B"},
runtimeFlags: []string{"--enable-features=C", "--disable-features=D"},
want: []string{"--enable-features=A,C", "--disable-features=B,D"},
},
}

for _, tt := range tests {
Expand All@@ -298,3 +330,47 @@ func TestMergeFlags(t *testing.T) {
})
}
}

func TestTranslateKernelDisableFeatures(t *testing.T) {
tests := []struct {
name string
tokens []string
want []string
}{
{
name: "no kernel tokens is a no-op",
tokens: []string{"--foo", "--disable-features=A,B"},
want: nil,
},
{
name: "kernel token folds into existing disable list",
tokens: []string{"--foo", "--disable-features=A,B", "--kernel-disable-features=WebMCPTesting"},
want: []string{"--foo", "--disable-features=A,B,WebMCPTesting"},
},
{
name: "kernel token creates a disable list when none exists",
tokens: []string{"--foo", "--kernel-disable-features=WebMCPTesting,DevToolsWebMCPSupport"},
want: []string{"--foo", "--disable-features=WebMCPTesting,DevToolsWebMCPSupport"},
},
{
name: "multiple kernel tokens and duplicates are unioned",
tokens: []string{"--kernel-disable-features=A", "--bar", "--kernel-disable-features=B,A"},
want: []string{"--bar", "--disable-features=A,B"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := TranslateKernelDisableFeatures(tt.tokens)
if tt.want == nil {
if !reflect.DeepEqual(got, tt.tokens) {
t.Fatalf("expected unchanged input:\n got: %#v\nwant: %#v", got, tt.tokens)
}
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("TranslateKernelDisableFeatures() mismatch:\n got: %#v\nwant: %#v", got, tt.want)
}
})
}
}
Loading