> ## Documentation Index
> Fetch the complete documentation index at: https://namespace.so/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Runner Controls

export const KeepTabPosition = () => {
  useEffect(() => {
    const stateKey = "__namespaceKeepTabPosition";
    const releaseState = state => {
      state.instances -= 1;
      if (state.instances > 0) return;
      state.removeListeners();
      if (window[stateKey] === state) delete window[stateKey];
    };
    const existingState = window[stateKey];
    if (existingState) {
      existingState.instances += 1;
      return () => releaseState(existingState);
    }
    const maxSettleTime = 250;
    const requiredStableFrames = 3;
    const positionTolerance = 0.5;
    let animationFrame;
    let observer;
    let settleDeadline;
    let stableFrames = 0;
    let tabListToKeep;
    let tabListTop;
    const findTab = event => {
      if (!(event.target instanceof Element)) return null;
      return event.target.closest(".tab-container [role='tab']");
    };
    const findScrollContainer = element => {
      for (let parent = element.parentElement; parent; parent = parent.parentElement) {
        const {overflowY} = getComputedStyle(parent);
        if ((overflowY === "auto" || overflowY === "scroll") && parent.scrollHeight > parent.clientHeight) {
          return parent;
        }
      }
      return null;
    };
    const stopKeepingPosition = () => {
      if (animationFrame) cancelAnimationFrame(animationFrame);
      animationFrame = undefined;
      observer?.disconnect();
      tabListToKeep = undefined;
    };
    const restoreTabListPosition = () => {
      if (!tabListToKeep?.isConnected) return 0;
      const offset = tabListToKeep.getBoundingClientRect().top - tabListTop;
      if (Math.abs(offset) < positionTolerance) return offset;
      const scrollOptions = {
        top: offset,
        behavior: "instant"
      };
      const scrollContainer = findScrollContainer(tabListToKeep);
      if (scrollContainer) {
        scrollContainer.scrollBy(scrollOptions);
      } else {
        window.scrollBy(scrollOptions);
      }
      return offset;
    };
    const settlePosition = () => {
      const offset = restoreTabListPosition();
      stableFrames = Math.abs(offset) < positionTolerance ? stableFrames + 1 : 0;
      if (stableFrames >= requiredStableFrames || performance.now() >= settleDeadline) {
        stopKeepingPosition();
        return;
      }
      animationFrame = requestAnimationFrame(settlePosition);
    };
    observer = new MutationObserver(() => {
      stableFrames = 0;
      restoreTabListPosition();
    });
    const keepTabListInPlace = tab => {
      stopKeepingPosition();
      tabListToKeep = tab.closest("[role='tablist']");
      if (!tabListToKeep) return;
      tabListTop = tabListToKeep.getBoundingClientRect().top;
      settleDeadline = performance.now() + maxSettleTime;
      stableFrames = 0;
      observer.observe(document.getElementById("content") ?? document.documentElement, {
        attributes: true,
        attributeFilter: ["aria-selected", "class"],
        childList: true,
        subtree: true
      });
      animationFrame = requestAnimationFrame(settlePosition);
    };
    const selectWithoutNavigation = event => {
      if (!event.isTrusted) return;
      const tab = findTab(event);
      if (!tab) return;
      keepTabListInPlace(tab);
      event.preventDefault();
      event.stopPropagation();
      tab.click();
      restoreTabListPosition();
    };
    const selectWithKeyboard = event => {
      if (!event.isTrusted) return;
      const tab = findTab(event);
      const tabList = tab?.closest("[role='tablist']");
      if (!tab || !tabList) return;
      const tabs = Array.from(tabList.querySelectorAll(":scope > [role='tab']"));
      const currentIndex = tabs.indexOf(tab);
      let nextIndex;
      switch (event.key) {
        case "ArrowLeft":
          nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
          break;
        case "ArrowRight":
          nextIndex = (currentIndex + 1) % tabs.length;
          break;
        case "Home":
          nextIndex = 0;
          break;
        case "End":
          nextIndex = tabs.length - 1;
          break;
        case "Enter":
        case " ":
          nextIndex = currentIndex;
          break;
        default:
          return;
      }
      keepTabListInPlace(tab);
      event.preventDefault();
      event.stopPropagation();
      tabs[nextIndex]?.click();
      tabs[nextIndex]?.focus({
        preventScroll: true
      });
      restoreTabListPosition();
    };
    document.addEventListener("click", selectWithoutNavigation, true);
    document.addEventListener("keydown", selectWithKeyboard, true);
    const state = {
      instances: 1,
      removeListeners: () => {
        stopKeepingPosition();
        document.removeEventListener("click", selectWithoutNavigation, true);
        document.removeEventListener("keydown", selectWithKeyboard, true);
      }
    };
    window[stateKey] = state;
    return () => releaseState(state);
  }, []);
  return null;
};

export const CenteredImage = ({src, alt, width, caption, className}) => {
  const [basePath, setBasePath] = useState("");
  useEffect(() => {
    const path = window.location.pathname;
    setBasePath(path === "/docs" || path.startsWith("/docs/") ? "/docs" : "");
  }, []);
  return <Frame caption={caption} className={className} style={{
    maxWidth: width,
    marginInline: "auto"
  }}>
			<OptimizedImage src={`${basePath}${src}`} alt={alt} />
		</Frame>;
};

<KeepTabPosition />

Runner Controls are settings for specific, situational needs: job scheduling, security, resource limits, networking, and container access.

Namespace lets you configure these as additional feature settings beyond your base runner configuration.
You can pass these settings to a runner, for example `container.privileged`, `github.run-id`, or `tailscale.spec`.
How you pass them depends on whether you use runner labels or a runner profile.

<Tabs>
  <Tab title="With runner profiles">
    The `namespace-features:` label cannot be used with profiles. Instead,
    append one or more `;key=value` pairs directly to the profile name:

    ```yaml theme={null}
    runs-on: namespace-profile-my-profile;container.privileged=true;container.host-pid-namespace=true
    ```
  </Tab>

  <Tab title="With runner labels">
    Pass the settings as a separate `namespace-features:` label (multiple
    values are separated by `;`), and append the
    `-with-features` suffix to your machine label:

    ```yaml theme={null}
    runs-on:
      - nscloud-ubuntu-22.04-amd64-4x8-with-features
      - namespace-features:container.privileged=true;container.host-pid-namespace=true
    ```

    The `-with-features` suffix matters because GitHub assigns a job to any
    runner whose labels are a superset of the job's labels. Keeping the suffix on the machine label prevents
    jobs that don't request these features from being scheduled onto a feature-enabled runner.
  </Tab>
</Tabs>

Check each page below to see how to enable Runner Controls.

<CardGroup cols={2}>
  <Card title="Job ordering & priority" href="/docs/solutions/github-actions/runner-controls/job-ordering">
    Control the order jobs are picked up with deterministic run-id assignment and numeric priority.
  </Card>

  <Card title="Privileged workflows" href="/docs/solutions/github-actions/runner-controls/privileged-workflows">
    Run jobs privileged or in the host PID namespace for sysctl, bubblewrap, and Nix.
  </Card>

  <Card title="Swap space" href="/docs/solutions/github-actions/runner-controls/swap">
    Add a swap file so jobs survive short memory spikes without a larger shape.
  </Card>

  <Card title="Tailscale" href="/docs/solutions/github-actions/runner-controls/tailscale">
    Connect a runner to your tailnet with the `tailscale.spec` feature.
  </Card>

  <Card title="Access levels" href="/docs/solutions/github-actions/runner-controls/access-levels">
    Restrict which Namespace APIs a runner can reach with Permissive, Limited, or Restricted tiers.
  </Card>

  <Card title="Running jobs in containers" href="/docs/solutions/github-actions/runner-controls/containerized-jobs">
    Reach Namespace resources, caches, git mirrors, and the local builder from a container job.
  </Card>
</CardGroup>

## Runner environment

### Systemd

Namespace Runner images by default do NOT use Systemd.
For some users this might require small changes to their workflows, e.g. replacing
`sudo systemctl start ...`
with
`sudo service start ...`.

If your workflows rely on systemd, an image using systemd is available [upon request](mailto:support@namespace.so).

<a id="macos-features" />

<a id="bleeding-edge-images" />

<a id="bleeding-edge-macos" />

### macOS bleeding-edge images

Namespace team continuously makes changes to macOS runner images to keep the software up-to-date and
add new Xcode versions as soon as Apple releases them.
[More info on image updates](/docs/architecture/compute/macos#macos-image-updates).

To avoid regressions upcoming images go through multiple release stages before production release.
This means that new Xcode versions become available to most customers with a short delay.
However, it is possible to take advantage of new Xcode versions early. You can enroll your
runners into using bleeding-edge macOS images.

**Note**: Images in the bleeding-edge channel have not passed the full set of validation checks and may contain regressions.
We are happy to hear feedback from early users of these images via Namespace support channels.
But enrolling should be done without expectation of perfect stability.

<Tabs>
  <Tab title="With runner profiles">
    Visit the [runner profile editor](https://cloud.namespace.so/workspace/actions/profiles) and select the **Use bleeding-edge images** checkbox.

    <CenteredImage width={500} alt="runner profile macOS bleeding edge checkbox" src="/docs/images/github-actions/macos-bleeding-edge.webp" />
  </Tab>

  <Tab title="With runner labels">
    Add `namespace-features:macos.channel=preview` to the list of runner labels:

    ```yaml {3} theme={null}
    runs-on:
      - nscloud-macos-sequoia-arm64-6x14-with-features
      - namespace-features:macos.channel=preview
    ```
  </Tab>
</Tabs>

## Hands-on support

Need help configuring your runners? Our team is here to assist:

* **Technical support**: Reach out to [support@namespace.so](mailto:support@namespace.so) to talk to one of our engineers.
* **Community**: Join our community [Discord](https://discord.gg/DqMzDFR6Hc) to learn about tips and best practices.
