Skip to content
Merged
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
537 changes: 537 additions & 0 deletions .github/openclaw/qualification/patches/008-namespace.patch

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion .github/openclaw/qualify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ for arm in baseline candidate; do
for patch in "$inputs/patches/001-proxy.patch" "$inputs"/patches/00[3-7]-*.patch; do
git apply --exclude=CHANGELOG.md --exclude='docs/*' --exclude='test/js/node/worker_threads/*' "$patch"
done
git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/008-namespace.patch"
cp "$inputs/worker-resource-limits.test.ts" test/js/node/worker_threads/openclaw-resource-limits.test.ts
git diff --check
git diff --binary HEAD > "$QUALIFICATION_DIR/bun-adapters.patch"
Expand Down Expand Up @@ -58,6 +59,7 @@ done
python3 "$inputs/compare-fork-ci.py" "$QUALIFICATION_DIR/baseline" "$QUALIFICATION_DIR/candidate" --output "$QUALIFICATION_DIR/faithfulness.json"
candidate="$QUALIFICATION_DIR/bun/build/qualify-candidate/bun"
unset BUN_WEBKIT_ARTIFACT_MANIFEST BUN_BUILD_CACHE_DIR BUN_BUILD_PREFETCH_DIR
"$candidate" test test/js/bun/resolve/esModule.test.ts test/js/bun/namespace-prototype-pollution.test.ts test/js/bun/resolve/require-esm-evaluating-cycle.test.ts test/js/bun/import-attributes/import-attributes.test.ts test/js/node/module/require-extensions.test.ts > "$QUALIFICATION_DIR/namespace.log" 2>&1
"$candidate" "$inputs/segmenter.js" > "$QUALIFICATION_DIR/segmenter.log" 2>&1
"$candidate" test test/js/node/vm/vm.test.ts -t 'a Proxy in the prototype chain' > "$QUALIFICATION_DIR/proxy.log" 2>&1
"$candidate" test test/js/node/worker_threads/openclaw-resource-limits.test.ts -t 'resourceLimits' > "$QUALIFICATION_DIR/resource-limits.log" 2>&1
Expand All @@ -73,7 +75,7 @@ assert faith['selected_files']==44 and faith['result_files']==46
for name in ['als-plugin','als-native']:
rows=json.loads((root/(name+'.json')).read_text())['results']
assert len(rows)==14 and all(r['exitCode']==0 for r in rows)
for name,minimum in [('proxy',4),('resource-limits',14)]:
for name,minimum in [('proxy',4),('resource-limits',14),('namespace',41)]:
text=(root/(name+'.log')).read_text()
passes=re.search(r'(\d+) pass',text)
assert passes and int(passes[1])>=minimum, name+' missing passing cases'
Expand Down
17 changes: 17 additions & 0 deletions .github/openclaw/verify-jsc-results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
"""The stress runner's results file, not its exit status, is authoritative."""
import json
import sys
from pathlib import Path

root = Path(sys.argv[1])
lines = (root / "results").read_text().splitlines()
if not lines:
raise SystemExit("JSC produced no test results")
namespace = [line for line in lines if "module-namespace-esmodule-marker.js" in line]
if not namespace:
raise SystemExit("The namespace marker regression did not execute")
failures = [line for line in lines if not line.endswith(": PASS")]
if failures:
raise SystemExit("JSC failures:\n" + "\n".join(failures))
print(json.dumps({"tests": len(lines), "marker_variants": len(namespace), "passed": True}))
84 changes: 84 additions & 0 deletions .github/workflows/openclaw-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: OpenClaw engine checks
on:
pull_request:
branches: [openclaw/main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: openclaw-engine-checks-${{ github.ref }}
cancel-in-progress: true
jobs:
linux-native:
if: github.repository == 'openclaw/WebKit'
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 150
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 16
persist-credentials: false
sparse-checkout-cone-mode: false
sparse-checkout: |
/*
!/LayoutTests/
!/JSTests/*
!/PerformanceTests/
!/Websites/
!/ManualTests/
!/WebDriverTests/
/JSTests/stress/
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '26.3.0'
- name: Verify pipeline integrity and runner support
run: |
python3 .github/openclaw/test_release.py
bash .github/openclaw/preflight.sh
- name: Build native Linux engine with the unchanged lane
env:
BUILDX_METADATA_PROVENANCE: max
run: python3 .github/openclaw/artifacts.py build --lane bun-webkit-linux-amd64 --output "$RUNNER_TEMP/linux-artifact"
- name: Install pinned Bun and JSC test tools
run: |
curl -fsSL --retry 5 https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
echo 'deb https://apt.llvm.org/noble/ llvm-toolchain-noble-23 main' | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends cmake ninja-build nasm ccache golang-go clang-23 lld-23 llvm-23 unzip ruby ruby-highline libjson-pp-perl
rustup toolchain install nightly-2026-09-15 --profile minimal --component rust-src
echo 'RUSTUP_TOOLCHAIN=nightly-2026-09-15' >> "$GITHUB_ENV"
echo '/usr/lib/llvm-23/bin' >> "$GITHUB_PATH"
curl -fSL --retry 5 https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64.zip -o "$RUNNER_TEMP/bun.zip"
unzip -q "$RUNNER_TEMP/bun.zip" -d "$RUNNER_TEMP/bootstrap"
echo "$RUNNER_TEMP/bootstrap/bun-linux-x64" >> "$GITHUB_PATH"
- name: Exercise module namespace engine regressions
run: |
mkdir "$RUNNER_TEMP/jsc-artifact"
tar -xzf "$RUNNER_TEMP/linux-artifact/bun-webkit-linux-amd64.tar.gz" -C "$RUNNER_TEMP/jsc-artifact"
# $vm is a JavaScript identifier.
# shellcheck disable=SC2016
"$RUNNER_TEMP/jsc-artifact/bun-webkit/bin/jsc" --useDollarVM=1 -e 'if (typeof $vm.moduleNamespaceForRequire !== "function") throw new Error("namespace test hook is missing");'
Tools/Scripts/run-jsc-stress-tests JSTests/stress --jsc "$RUNNER_TEMP/jsc-artifact/bun-webkit/bin/jsc" --child-processes 16 --filter 'module-namespace|module-loader-security-thenable-namespace' --output-dir "$RUNNER_TEMP/jsc-results" --no-retry
python3 .github/openclaw/verify-jsc-results.py "$RUNNER_TEMP/jsc-results"
- name: Qualify the paired Bun adapter and fork selection
env:
ARTIFACT_DIR: ${{ runner.temp }}/linux-artifact
QUALIFICATION_DIR: ${{ runner.temp }}/qualification
run: bash .github/openclaw/qualify.sh
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: always()
with:
name: linux-native-proof
path: |
${{ runner.temp }}/qualification/*.json
${{ runner.temp }}/qualification/*.log
${{ runner.temp }}/qualification/*.sha256
${{ runner.temp }}/qualification/*.patch
${{ runner.temp }}/qualification/baseline/*.json
${{ runner.temp }}/qualification/baseline/*.log
${{ runner.temp }}/qualification/candidate/*.json
${{ runner.temp }}/qualification/candidate/*.log
${{ runner.temp }}/jsc-results/results
${{ runner.temp }}/jsc-results/*.log
if-no-files-found: error
retention-days: 7
52 changes: 52 additions & 0 deletions JSTests/stress/module-namespace-esmodule-marker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//@ requireOptions("--useDollarVM=1")

function check(value, message) {
if (!value)
throw new Error(message);
}

function checkMarker(namespace, value) {
check(Object.getPrototypeOf(namespace) === null, "null prototype");
check(!Object.isExtensible(namespace), "non-extensible namespace");
check("__esModule" in namespace, "marker in namespace");
check(Object.prototype.hasOwnProperty.call(namespace, "__esModule"), "own marker");
const descriptor = Object.getOwnPropertyDescriptor(namespace, "__esModule");
check(descriptor.value === value && descriptor.writable && descriptor.enumerable && !descriptor.configurable, "marker descriptor");
check(!Reflect.set(namespace, "__esModule", value), "set fails even for the same value");
check(!Reflect.deleteProperty(namespace, "__esModule"), "delete fails");
check(Reflect.defineProperty(namespace, "__esModule", { value }), "same-value define succeeds");
for (const descriptor of [{ value: {} }, { writable: false }, { enumerable: false }, { configurable: true }, { get() {} }])
check(!Reflect.defineProperty(namespace, "__esModule", descriptor), "invalid definition fails");
}

async function test() {
const namespace = await import("./resources/namespace-esmodule-default.js");
check(!("__esModule" in namespace), "ordinary namespace has no marker");
check(!Object.prototype.hasOwnProperty.call(namespace, "__esModule"), "no own marker");
check(Object.getOwnPropertyDescriptor(namespace, "__esModule") === undefined, "no marker descriptor");
check(!Reflect.set(namespace, "__esModule", true), "cannot create marker with set");
check(!Reflect.defineProperty(namespace, "__esModule", { value: true }), "cannot create marker with defineProperty");
// Only Bun embedders request the require(esm) facade.
if (typeof $vm.moduleNamespaceForRequire !== "function")
return;
const required = $vm.moduleNamespaceForRequire(namespace);
check(required !== namespace, "separate namespace");
check($vm.moduleNamespaceForRequire(namespace) !== required, "fresh facade for a new require-cache entry");
check($vm.moduleNamespaceForRequire(required) === required, "already marked facade");
check(!("__esModule" in namespace), "import remains unmarked");
checkMarker(required, true);
check(Reflect.ownKeys(required).map(String).join() === "Alpha,__esModule,_a,bump,default,value,zzz,Symbol(Symbol.toStringTag)", "sorted own keys");
check(Object.getOwnPropertySymbols(required).length === 1, "marker is not a symbol");
namespace.bump();
check(required.value === 2 && namespace.value === 2, "live bindings");
gc();
check(required.value === 2, "facade survives collection");
const explicit = await import("./resources/namespace-esmodule-explicit.js");
check($vm.moduleNamespaceForRequire(explicit) === explicit, "explicit undefined marker retained");
checkMarker(explicit, undefined);
const noDefault = await import("./resources/namespace-esmodule-no-default.js");
check($vm.moduleNamespaceForRequire(noDefault) === noDefault, "no facade without default");
check(!("__esModule" in noDefault), "no marker without default");
}

test().catch($vm.abort);
4 changes: 4 additions & 0 deletions JSTests/stress/resources/namespace-esmodule-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default { d: 1 };
export const Alpha = 1, _a = 2, zzz = 3;
export let value = 1;
export function bump() { value++; }
2 changes: 2 additions & 0 deletions JSTests/stress/resources/namespace-esmodule-explicit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const __esModule = undefined;
export default 1;
1 change: 1 addition & 0 deletions JSTests/stress/resources/namespace-esmodule-no-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 1;
6 changes: 6 additions & 0 deletions OPENCLAW.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,9 @@ use an old Bun executable with headers/libraries from another engine ABI.
- Preserve module-loader async context through resolution and evaluation.
- Add opt-in per-VM worker managed-heap and stack budgets.
- Add checksum-pinned, immutable OpenClaw artifact publication with a Linux regression gate.

## Namespace interop qualification

Pull requests run the read-only Linux engine check. It builds the unchanged native lane, verifies the JSC namespace stress results, and builds Bun with the matching qualification adapters before running the fork selection and namespace regressions. It has no publication job.

The namespace adapter in `.github/openclaw/qualification/patches/008-namespace.patch` accompanies the engine facade API. Keep it in sync with the matching Bun integration until that integration becomes the qualification baseline. The native engine change and Bun consumer must be rebuilt together; a new engine release alone does not remove Bun's old namespace prototype.
54 changes: 48 additions & 6 deletions Source/JavaScriptCore/runtime/JSModuleNamespaceObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,9 @@ bool JSModuleNamespaceObject::getOwnPropertySlotCommon(JSGlobalObject* globalObj
auto iterator = m_exports.find(propertyName.uid());
if (iterator == m_exports.end()) {
#if USE(BUN_JSC_ADDITIONS)
if (propertyName == vm.propertyNames->__esModule) {
return Base::getOwnPropertySlot(this, globalObject, propertyName, slot);
if (m_hasESModuleMarker && propertyName == vm.propertyNames->__esModule) {
slot.setValue(this, static_cast<unsigned>(PropertyAttribute::DontDelete), jsBoolean(true));
return true;
}
#endif
return false;
Expand Down Expand Up @@ -267,8 +268,6 @@ bool JSModuleNamespaceObject::put(JSCell* cell, JSGlobalObject* globalObject, Pr
auto* thisObject = uncheckedDowncast<JSModuleNamespaceObject>(cell);
if (thisObject->m_isOverridingValue) {
return true;
} else if (propertyName == vm.propertyNames->__esModule && !thisObject->m_exports.contains(propertyName.uid())) [[unlikely]] {
RELEASE_AND_RETURN(scope, Base::put(thisObject, globalObject, propertyName, value, slot));
}
#endif

Expand Down Expand Up @@ -302,6 +301,10 @@ bool JSModuleNamespaceObject::deleteProperty(JSCell* cell, JSGlobalObject* globa
RETURN_IF_EXCEPTION(scope, false);
}

#if USE(BUN_JSC_ADDITIONS)
if (thisObject->m_hasESModuleMarker && propertyName == vm.propertyNames->__esModule)
return false;
#endif
return !thisObject->m_exports.contains(propertyName.uid());
}

Expand All @@ -328,7 +331,18 @@ void JSModuleNamespaceObject::getOwnPropertyNames(JSObject* cell, JSGlobalObject
thisObject->ensureDeferredNamespaceEvaluation(globalObject);
RETURN_IF_EXCEPTION(scope, void());
}
#if USE(BUN_JSC_ADDITIONS)
const Identifier& esModuleIdent = vm.propertyNames->__esModule;
bool needsESModuleMarker = thisObject->m_hasESModuleMarker
&& propertyNames.includeStringProperties();
#endif
for (const auto& name : thisObject->m_exports.keys()) {
#if USE(BUN_JSC_ADDITIONS)
if (needsESModuleMarker && WTF::codePointCompareLessThan(StringView(esModuleIdent.impl()), StringView(name.get()))) {
propertyNames.add(esModuleIdent);
needsESModuleMarker = false;
}
#endif
if (mode == DontEnumPropertiesMode::Exclude) {
// Perform [[GetOwnProperty]] to throw ReferenceError if binding is uninitialized.
PropertySlot slot(cell, PropertySlot::InternalMethodType::GetOwnProperty);
Expand All @@ -339,6 +353,10 @@ void JSModuleNamespaceObject::getOwnPropertyNames(JSObject* cell, JSGlobalObject
}
propertyNames.add(name);
}
#if USE(BUN_JSC_ADDITIONS)
if (needsESModuleMarker)
propertyNames.add(esModuleIdent);
#endif
if (propertyNames.includeSymbolProperties()) {
scope.release();
thisObject->getOwnNonIndexPropertyNames(globalObject, propertyNames, mode);
Expand Down Expand Up @@ -366,8 +384,6 @@ bool JSModuleNamespaceObject::defineOwnProperty(JSObject* cell, JSGlobalObject*
#if USE(BUN_JSC_ADDITIONS)
if (thisObject->m_isOverridingValue) {
return true;
} else if (propertyName == vm.propertyNames->__esModule && !thisObject->m_exports.contains(propertyName.uid())) {
RELEASE_AND_RETURN(scope, Base::defineOwnProperty(thisObject, globalObject, propertyName, descriptor, shouldThrow));
}
#endif

Expand Down Expand Up @@ -423,6 +439,32 @@ bool JSModuleNamespaceObject::defineOwnProperty(JSObject* cell, JSGlobalObject*

#if USE(BUN_JSC_ADDITIONS)

JSModuleNamespaceObject* JSModuleNamespaceObject::createNamespaceForRequire(JSGlobalObject* globalObject)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
if (m_isDeferred) [[unlikely]] {
ensureDeferredNamespaceEvaluation(globalObject);
RETURN_IF_EXCEPTION(scope, nullptr);
}
if (m_hasESModuleMarker || m_exports.contains(vm.propertyNames->__esModule.impl()) || !m_exports.contains(vm.propertyNames->defaultKeyword.impl()))
return this;

// Node's require(esm) facade adds a marker without changing the imported namespace.
// Copy resolutions, not values, so re-exports and uninitialized/live bindings stay live.
Vector<std::pair<Identifier, AbstractModuleRecord::Resolution>> resolutions;
resolutions.reserveInitialCapacity(m_exports.size());
for (const auto& entry : m_exports) {
resolutions.append({ Identifier::fromUid(vm, entry.key.get()), {
AbstractModuleRecord::Resolution::Type::Resolved, entry.value.moduleRecord.get(), entry.value.localName,
} });
}
auto* result = create(globalObject, globalObject->moduleNamespaceObjectStructure(), m_moduleRecord.get(), WTF::move(resolutions));
RETURN_IF_EXCEPTION(scope, nullptr);
result->m_hasESModuleMarker = true;
return result;
}

void JSModuleNamespaceObject::overrideExports(JSGlobalObject* globalObject, const WTF::Function<bool(JSGlobalObject* globalObject, const Identifier& exportName, JSC::JSValue& result)>& iter)
{
VM& vm = globalObject->vm();
Expand Down
6 changes: 2 additions & 4 deletions Source/JavaScriptCore/runtime/JSModuleNamespaceObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class JSModuleNamespaceObject final : public JSNonFinalObject {
DECLARE_EXPORT_INFO;

#if USE(BUN_JSC_ADDITIONS)
JS_EXPORT_PRIVATE JSModuleNamespaceObject* createNamespaceForRequire(JSGlobalObject*);
bool overrideExportValue(JSGlobalObject*, PropertyName, JSValue);
void overrideExports(JSGlobalObject* globalObject, const WTF::Function<bool(JSGlobalObject* globalObject, const Identifier& exportName, JSC::JSValue &result)>& iter);
#endif
Expand All @@ -74,10 +75,6 @@ class JSModuleNamespaceObject final : public JSNonFinalObject {

AbstractModuleRecord* moduleRecord() LIFETIME_BOUND { return m_moduleRecord.get(); }

#if USE(BUN_JSC_ADDITIONS)
WTF::TriState m_hasESModuleMarker = WTF::TriState::Indeterminate;
#endif

private:
JS_EXPORT_PRIVATE JSModuleNamespaceObject(VM&, Structure*, AbstractModuleRecord*, Vector<std::pair<Identifier, AbstractModuleRecord::Resolution>>&&, bool isDeferred);
JS_EXPORT_PRIVATE void finishCreation(JSGlobalObject*, bool shouldPreventExtensions);
Expand All @@ -97,6 +94,7 @@ class JSModuleNamespaceObject final : public JSNonFinalObject {
const bool m_isDeferred;
#if USE(BUN_JSC_ADDITIONS)
bool m_isOverridingValue = false;
bool m_hasESModuleMarker = false;
#endif

friend size_t cellSize(JSCell*);
Expand Down
13 changes: 13 additions & 0 deletions Source/JavaScriptCore/tools/JSDollarVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4287,6 +4287,18 @@ JSC_DEFINE_HOST_FUNCTION(functionGlobalObjectForObject, (JSGlobalObject*, CallFr
}

#if USE(BUN_JSC_ADDITIONS)
JSC_DEFINE_HOST_FUNCTION(functionModuleNamespaceForRequire, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
DollarVMAssertScope assertScope;
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
auto* namespaceObject = dynamicDowncast<JSModuleNamespaceObject>(callFrame->argument(0));
if (!namespaceObject)
return throwVMTypeError(globalObject, scope, "expected a module namespace"_s);
auto* result = namespaceObject->createNamespaceForRequire(globalObject);
RETURN_IF_EXCEPTION(scope, { });
return JSValue::encode(result);
}

// The embedder's AsyncLocalStorage frame slot (JSGlobalObject::m_asyncContextData
// field 0), which promise reactions and async continuations capture and restore.
JSC_DEFINE_HOST_FUNCTION(functionAsyncContext, (JSGlobalObject* globalObject, CallFrame*))
Expand Down Expand Up @@ -6023,6 +6035,7 @@ void JSDollarVM::finishCreation(VM& vm)
addFunction(vm, alwaysAllow, "decommittedMarkedBlockPagePoison"_s, functionDecommittedMarkedBlockPagePoison, 0);
#if USE(BUN_JSC_ADDITIONS)
addFunction(vm, alwaysAllow, "evacuateAuxiliaryBlocks"_s, functionEvacuateAuxiliaryBlocks, 2);
addFunction(vm, allowIfNotFuzz, "moduleNamespaceForRequire"_s, functionModuleNamespaceForRequire, 1);
#endif

addFunction(vm, allowIfNotFuzz, "globalObjectCount"_s, functionGlobalObjectCount, 0);
Expand Down
1 change: 1 addition & 0 deletions jsc.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Fix `Intl.Segmenter` containing lookups around surrogate pairs so segment boundaries and word status agree with iteration.
- Expose the require(esm) `__esModule` marker as an own enumerable namespace property while preserving unmarked imports and live bindings. Adapts [oven-sh/WebKit#279](https://github.com/oven-sh/WebKit/pull/279). Thanks @robobun!

This changelog covers updates to JavaScriptCore in Bun's WebKit fork, from `9a2cc42ae1bf` to `7bc2f97e2835`.

Expand Down