You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/04 14:13:32 UTC

(maka) branch main updated: feat(runtime-host): expose extensible resource snapshots (#4774)

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new b3a8c44a52 feat(runtime-host): expose extensible resource snapshots (#4774)
b3a8c44a52 is described below

commit b3a8c44a529feb0480a14f1445746bf145585b17
Author: Wang <m4...@gmail.com>
AuthorDate: Fri Sep 4 22:13:27 2026 +0800

    feat(runtime-host): expose extensible resource snapshots (#4774)
    
    Generated-by: OpenAI Codex
---
 .github/workflows/windows-recovery.yml             |   1 +
 .../resources/licenses/npm/THIRD_PARTY_NOTICES.txt |  29 ++
 .../main/__tests__/runtime-host-management.test.ts |   2 +
 apps/desktop/src/main/runtime-host-boot.ts         |   1 +
 apps/desktop/src/main/runtime-host-management.ts   |  14 +-
 apps/desktop/src/preload/bridge-contract.d.ts      |   4 +
 apps/desktop/src/preload/preload.ts                |   3 +
 .../features/runtime-host-management/index.ts      |   1 +
 .../features/runtime-host-management/ports.ts      |   7 +
 .../ui/runtime-host-resource-dialog.tsx            | 358 +++++++++++++++++++
 .../create-runtime-host-management-services.ts     |   7 +
 .../settings/runtime-host-management-dialog.tsx    |   8 +-
 .../src/renderer/styles/settings/runtime-host.css  |   9 +
 docs/astryx-surface-file-inventory.md              |   3 +-
 docs/astryx-surface-file-inventory.paths           |   1 +
 package-lock.json                                  |  27 ++
 packages/cli/THIRD_PARTY_NOTICES.txt               |  29 ++
 packages/runtime-host/package.json                 |   1 +
 .../access-credential-grant-migration.test.ts      |  18 +-
 .../src/__tests__/connection-session.test.ts       |   6 +-
 .../src/__tests__/host-resources.test.ts           | 180 ++++++++++
 .../src/__tests__/operation-dispatcher.test.ts     |   1 +
 .../runtime-host/src/protocol/host-resources.ts    | 378 +++++++++++++++++++++
 packages/runtime-host/src/protocol/index.ts        |   5 +-
 packages/runtime-host/src/protocol/operations.ts   |   4 +
 .../src/server/access-credential-store.ts          |   5 +
 packages/runtime-host/src/server/host-kernel.ts    |   6 +
 .../src/server/host-resource-collector.ts          | 277 +++++++++++++++
 .../src/server/host-resource-probe-main.ts         |  47 +++
 .../runtime-host/src/server/host-resource-probe.ts |  99 ++++++
 .../src/server/operation-dispatcher.ts             |   2 +
 31 files changed, 1524 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/windows-recovery.yml b/.github/workflows/windows-recovery.yml
index 5ffd068e0f..aa7598430b 100644
--- a/.github/workflows/windows-recovery.yml
+++ b/.github/workflows/windows-recovery.yml
@@ -91,6 +91,7 @@ on:
       - 'packages/runtime-host/src/protocol/host-status.ts'
       - 'packages/runtime-host/src/protocol/skill-catalog.ts'
       - 'packages/runtime-host/src/server/access-credential-store.ts'
+      - 'packages/runtime-host/src/server/host-resource-probe.ts'
       - 'packages/runtime-host/src/server/skill-catalog-repository.ts'
       - 'packages/runtime-host/src/server/skill-catalog-transaction.ts'
       - 'packages/runtime/src/__tests__/runtime-continuation-crash.test.ts'
diff --git a/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt b/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt
index 565ca48843..ff0943aa45 100644
--- a/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt
+++ b/apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt
@@ -13620,6 +13620,35 @@ SOFTWARE.
 
 ================================================================================
 
+Package: systeminformation@5.33.8
+Declared license: MIT
+Selected license: MIT
+Repository: git+https://github.com/sebhildebrandt/systeminformation.git
+
+--- LICENSE ---
+The MIT License (MIT)
+
+Copyright (c) 2014-2026 Sebastian Hildebrandt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================================
+
 Package: tiny-typed-emitter@2.1.0
 Declared license: MIT
 Selected license: MIT
diff --git a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
index be45c0213a..a72a25fd0c 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
@@ -736,6 +736,7 @@ test('publishes update progress and waits for the managed profile to reconnect',
       assert.fail('published update must not inspect the development target'),
     resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
     currentHostEpoch: () => 'host-before-update',
+    liveHost: () => undefined,
     awaitUpdatedConnection: async (...args) => {
       connectionCompletions.push(args);
       if (failConnection) throw new Error('authentication required');
@@ -1502,6 +1503,7 @@ function unusedUpdateDependencies() {
       assert.fail('published update must not inspect the development target'),
     resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
     currentHostEpoch: () => undefined,
+    liveHost: () => undefined,
     awaitUpdatedConnection: async () => undefined,
     sendProgress: () => undefined,
   };
diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts
index 20cbacff8d..7caee1cf13 100644
--- a/apps/desktop/src/main/runtime-host-boot.ts
+++ b/apps/desktop/src/main/runtime-host-boot.ts
@@ -704,6 +704,7 @@ const runtimeHostManagement = createDesktopRuntimeHostManagement({
   resolveUpdatePackage: runtimeHostSetupPackage.resolve,
   currentHostEpoch: (profileId) =>
     runtimeHostManager?.current(profileId)?.candidate?.client.hostEpoch,
+  liveHost: (profileId) => runtimeHostManager?.current(profileId)?.candidate?.client,
   awaitUpdatedConnection: async (
     profileId,
     expectedHostId,
diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts
index dfd8ae7803..a7fa881c88 100644
--- a/apps/desktop/src/main/runtime-host-management.ts
+++ b/apps/desktop/src/main/runtime-host-management.ts
@@ -18,7 +18,10 @@
  */
 
 import type { IpcMain } from 'electron';
-import { decodeRuntimeHostOwnerConnectionCode } from '@maka/runtime-host/client';
+import {
+  decodeRuntimeHostOwnerConnectionCode,
+  type RuntimeHostConnection,
+} from '@maka/runtime-host/client';
 import {
   RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY,
   RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY,
@@ -138,6 +141,7 @@ export function createDesktopRuntimeHostManagement(input: {
     | DesktopRuntimeHostSetupPackage
     | Promise<DesktopRuntimeHostSetupPackage>;
   readonly currentHostEpoch: (profileId: string) => string | undefined;
+  readonly liveHost: (profileId: string) => Pick<RuntimeHostConnection, 'request'> | undefined;
   readonly awaitUpdatedConnection: (
     profileId: string,
     expectedHostId: string,
@@ -1015,6 +1019,11 @@ export function createDesktopRuntimeHostManagement(input: {
     );
   };
 
+  const getResources = (profileIdValue: unknown) => {
+    const host = input.liveHost(requireProfileId(profileIdValue));
+    return host?.request('host.resources.query', {}, 15_000);
+  };
+
   const channels = {
     run: 'runtime-host-management:run',
     update: 'runtime-host-management:update',
@@ -1028,6 +1037,7 @@ export function createDesktopRuntimeHostManagement(input: {
     reconcileUpdate: 'runtime-host-management:reconcile-update',
     getDirectPeer: 'runtime-host-management:get-direct-peer',
     configureDirectPeer: 'runtime-host-management:configure-direct-peer',
+    getResources: 'runtime-host-management:get-resources',
   } as const;
   input.ipcMain.handle(
     channels.run,
@@ -1078,6 +1088,8 @@ export function createDesktopRuntimeHostManagement(input: {
     reconcileUpdate(profileId));
   input.ipcMain.handle(channels.getDirectPeer, (_event, profileId: unknown) =>
     getDirectPeer(profileId));
+  input.ipcMain.handle(channels.getResources, (_event, profileId: unknown) =>
+    getResources(profileId));
   input.ipcMain.handle(
     channels.configureDirectPeer,
     (
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 117a5f6827..70b940e6b6 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -596,6 +596,9 @@ export interface DesktopRuntimeHostManagementProgress {
     | import('@maka/runtime-host/operator').RuntimeHostServiceUpdatePhase;
 }
 
+export type DesktopRuntimeHostResources =
+  import('@maka/runtime-host/protocol').HostResourcesResult;
+
 export interface DesktopRuntimeHostDirectPeerSnapshot {
   readonly state: 'unsupported' | 'not_configured' | 'disabled' | 'enabled';
   readonly peerId?: string;
@@ -877,6 +880,7 @@ export interface MakaBridge {
       policy: import('@maka/runtime-host/operator').RuntimeHostManagedUpdatePolicy,
     ): Promise<DesktopRuntimeHostUpdatePolicySnapshot>;
     reconcileUpdate(profileId: string): Promise<DesktopRuntimeHostUpdateReconciliationResponse>;
+    getResources(profileId: string): Promise<DesktopRuntimeHostResources | undefined>;
     getDirectPeer(profileId: string): Promise<DesktopRuntimeHostDirectPeerSnapshot>;
     configureDirectPeer(
       profileId: string,
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 83c8b6645f..bcb2babbf9 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -1686,6 +1686,9 @@ const makaBridge = {
     reconcileUpdate(profileId: string) {
       return ipcRenderer.invoke('runtime-host-management:reconcile-update', profileId);
     },
+    getResources(profileId: string) {
+      return ipcRenderer.invoke('runtime-host-management:get-resources', profileId);
+    },
     getDirectPeer(profileId: string) {
       return ipcRenderer.invoke('runtime-host-management:get-direct-peer', profileId);
     },
diff --git a/apps/desktop/src/renderer/features/runtime-host-management/index.ts b/apps/desktop/src/renderer/features/runtime-host-management/index.ts
index 7b16b77ad4..2e7cd2244e 100644
--- a/apps/desktop/src/renderer/features/runtime-host-management/index.ts
+++ b/apps/desktop/src/renderer/features/runtime-host-management/index.ts
@@ -25,6 +25,7 @@ export { PeerMeshPeerIdButton } from './ui/peer-mesh-peer-id-button.js';
 export { RuntimeHostAddComputerMenu } from './ui/runtime-host-add-computer-menu.js';
 export { RuntimeHostConnectionCodeButton } from './ui/runtime-host-connection-code-button.js';
 export { RuntimeHostConnectionCodeDialog } from './ui/runtime-host-connection-code-dialog.js';
+export { RuntimeHostResourceDialog } from './ui/runtime-host-resource-dialog.js';
 export {
   RuntimeHostPairingRecoveryButton,
   RuntimeHostProfileMoreMenu,
diff --git a/apps/desktop/src/renderer/features/runtime-host-management/ports.ts b/apps/desktop/src/renderer/features/runtime-host-management/ports.ts
index 606d21dd91..fd121577a0 100644
--- a/apps/desktop/src/renderer/features/runtime-host-management/ports.ts
+++ b/apps/desktop/src/renderer/features/runtime-host-management/ports.ts
@@ -20,6 +20,7 @@
 import type { RuntimeHostPeerMeshManagementAction } from '@maka/runtime-host/operator';
 import type { RuntimeHostWebRtcStunPolicy } from '@maka/runtime-host/operator';
 import type {
+  HostResourcesResult,
   PeerMeshInvitationResult,
   PeerMeshQueryResult,
 } from '@maka/runtime-host/protocol';
@@ -103,9 +104,15 @@ export interface RuntimeHostConnectionCodeServices {
   writeClipboardText(value: string): Promise<void>;
 }
 
+export interface RuntimeHostResourceServices {
+  query(profileId: string): Promise<HostResourcesResult | undefined>;
+  schedule(callback: () => void, delayMs: number): () => void;
+}
+
 export interface RuntimeHostManagementServices {
   readonly peerMesh: PeerMeshServices;
   readonly profilePairing: RuntimeHostProfilePairingServices;
   readonly connectionCodes: RuntimeHostConnectionCodeServices;
+  readonly resources: RuntimeHostResourceServices;
   readonly supportsWsl: boolean;
 }
diff --git a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-resource-dialog.tsx b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-resource-dialog.tsx
new file mode 100644
index 0000000000..79d48ab8b0
--- /dev/null
+++ b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-resource-dialog.tsx
@@ -0,0 +1,358 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useState } from 'react';
+import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
+import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
+import {
+  HOST_RESOURCE_KINDS,
+  availableHostResource,
+  deriveHostResourceUtilization,
+  type HostResourcesResult,
+} from '@maka/runtime-host/protocol';
+import type { UiLocale } from '@maka/core/ui-locale';
+import { Button, Spinner, Text, formatBytes, useUiLocale } from '@maka/ui';
+import { useRuntimeHostManagementServices } from '../services-context.js';
+
+const POLL_INTERVAL_MILLISECONDS = 2_000;
+
+type HostResourceState =
+  | { readonly kind: 'loading' }
+  | { readonly kind: 'unavailable' }
+  | {
+      readonly kind: 'ready';
+      readonly current: HostResourcesResult;
+      readonly previous?: HostResourcesResult;
+    };
+
+export function RuntimeHostResourceDialog(props: {
+  readonly profileId: string;
+  readonly hostName: string;
+}) {
+  const [open, setOpen] = useState(false);
+  const copy = hostResourceCopy(useUiLocale());
+  return (
+    <>
+      <div className="settingsRuntimeHostUpdatePolicyActions">
+        <Button
+          variant="secondary"
+          size="sm"
+          label={copy.open}
+          onClick={() => setOpen(true)}
+        />
+      </div>
+      {open ? (
+        <Dialog
+          isOpen
+          onOpenChange={setOpen}
+          purpose="form"
+          width={640}
+          maxHeight="calc(100dvh - 64px)"
+        >
+          <Layout
+            header={(
+              <DialogHeader
+                title={copy.title}
+                subtitle={props.hostName}
+                onOpenChange={setOpen}
+              />
+            )}
+            content={(
+              <LayoutContent padding={4}>
+                <RuntimeHostResourceFacts profileId={props.profileId} copy={copy} />
+              </LayoutContent>
+            )}
+            footer={(
+              <LayoutFooter hasDivider>
+                <div className="settingsRuntimeHostManagementActions">
+                  <Button variant="secondary" label={copy.done} onClick={() => setOpen(false)} />
+                </div>
+              </LayoutFooter>
+            )}
+          />
+        </Dialog>
+      ) : null}
+    </>
+  );
+}
+
+function RuntimeHostResourceFacts(props: {
+  readonly profileId: string;
+  readonly copy: ReturnType<typeof hostResourceCopy>;
+}) {
+  const services = useRuntimeHostManagementServices().resources;
+  const [state, setState] = useState<HostResourceState>({ kind: 'loading' });
+
+  useEffect(() => {
+    let disposed = false;
+    let cancelSchedule: (() => void) | undefined;
+    const poll = async () => {
+      try {
+        const snapshot = await services.query(props.profileId);
+        if (disposed) return;
+        setState((current) =>
+          snapshot
+            ? {
+                kind: 'ready',
+                current: snapshot,
+                ...(current.kind === 'ready' ? { previous: current.current } : {}),
+              }
+            : { kind: 'unavailable' },
+        );
+      } catch {
+        if (!disposed) setState({ kind: 'unavailable' });
+      } finally {
+        if (!disposed) cancelSchedule = services.schedule(poll, POLL_INTERVAL_MILLISECONDS);
+      }
+    };
+    void poll();
+    return () => {
+      disposed = true;
+      cancelSchedule?.();
+    };
+  }, [props.profileId, services]);
+
+  if (state.kind !== 'ready') {
+    return (
+      <section className="settingsRuntimeHostResources">
+        {state.kind === 'loading' ? (
+          <div className="settingsRuntimeHostSetupProgress" role="status">
+            <Spinner size="sm" />
+            <Text type="supporting">{props.copy.measuring}</Text>
+          </div>
+        ) : (
+          <Text type="supporting" color="secondary">{props.copy.unavailable}</Text>
+        )}
+      </section>
+    );
+  }
+
+  const cpu = availableHostResource(state.current, HOST_RESOURCE_KINDS.cpu);
+  const memory = availableHostResource(state.current, HOST_RESOURCE_KINDS.memory);
+  const graphics = availableHostResource(state.current, HOST_RESOURCE_KINDS.graphics);
+  const network = availableHostResource(state.current, HOST_RESOURCE_KINDS.network);
+  const storage = availableHostResource(state.current, HOST_RESOURCE_KINDS.storage);
+  const utilization = deriveHostResourceUtilization(state.current, state.previous);
+  const effectiveMemory = memory?.capacity.effectiveTotalBytes;
+  const usedMemory = memory
+    ? Math.max(0, memory.capacity.effectiveTotalBytes - memory.observation.availableBytes)
+    : undefined;
+  const graphicsSummary = !graphics || graphics.detection === 'unknown'
+    ? props.copy.graphicsUnknown
+    : graphics.detection === 'not_detected'
+      ? props.copy.noGraphicsAdapter
+      : graphics.devices
+          .map((device) => {
+            const name = [device.vendor, device.model].filter(Boolean).join(' ');
+            const graphicsMemory = device.memory.kind === 'dedicated'
+              ? formatBytes(device.memory.bytes)
+              : device.memory.kind === 'shared'
+                ? props.copy.sharedMemory
+                : props.copy.memoryUnknown;
+            const load = device.utilizationPercent === undefined
+              ? undefined
+              : formatPercent(device.utilizationPercent);
+            return [name, graphicsMemory, load].filter(Boolean).join(' · ');
+          })
+          .join('\n');
+  const storageSummary = storage?.volumes.length
+    ? [...storage.volumes]
+        .sort(
+          (left, right) => storageMountPriority(left.mount) - storageMountPriority(right.mount),
+        )
+        .slice(0, 8)
+        .map((volume) =>
+          props.copy.storageVolume(
+            volume.mount,
+            formatBytes(volume.availableBytes),
+            formatBytes(volume.totalBytes),
+            volume.filesystem,
+          ),
+        )
+        .join('\n')
+    : props.copy.unavailable;
+
+  return (
+    <section className="settingsRuntimeHostResources">
+      <dl className="settingsRuntimeHostManagementFacts">
+        <ResourceFact
+          label={props.copy.cpu}
+          value={cpu
+            ? `${cpu.capacity.model} · ${props.copy.logicalProcessors(
+                cpu.capacity.logicalProcessors,
+                cpu.capacity.availableParallelism,
+              )}`
+            : props.copy.unavailable}
+          wide
+        />
+        <ResourceFact
+          label={props.copy.cpuUsage}
+          value={!cpu
+            ? props.copy.unavailable
+            : utilization.cpuPercent === undefined
+              ? props.copy.measuring
+              : formatPercent(utilization.cpuPercent)}
+        />
+        <ResourceFact
+          label={props.copy.memory}
+          value={usedMemory === undefined || effectiveMemory === undefined
+            ? props.copy.unavailable
+            : `${formatBytes(usedMemory)} / ${formatBytes(effectiveMemory)} · ${formatPercent(
+                utilization.memoryPercent ?? 0,
+              )}`}
+        />
+        <ResourceFact label={props.copy.graphics} value={graphicsSummary} wide />
+        <ResourceFact label={props.copy.storage} value={storageSummary} wide />
+        <ResourceFact
+          label={props.copy.network}
+          value={
+            !network
+              ? props.copy.unavailable
+              : utilization.receivedBytesPerSecond === undefined ||
+            utilization.transmittedBytesPerSecond === undefined
+                ? props.copy.measuring
+                : props.copy.networkRate(
+                    network.observation.interfaceName,
+                    formatBytes(utilization.receivedBytesPerSecond),
+                    formatBytes(utilization.transmittedBytesPerSecond),
+                  )
+          }
+        />
+      </dl>
+    </section>
+  );
+}
+
+function ResourceFact(props: {
+  readonly label: string;
+  readonly value: string;
+  readonly wide?: boolean;
+}) {
+  return (
+    <div className={props.wide ? 'settingsRuntimeHostManagementFactWide' : undefined}>
+      <dt>{props.label}</dt>
+      <dd>{props.value}</dd>
+    </div>
+  );
+}
+
+function storageMountPriority(mount: string): number {
+  if (mount === '/' || /^[A-Za-z]:\\?$/u.test(mount)) return 0;
+  if (/^\/mnt\/[A-Za-z](?:\/|$)/u.test(mount)) return 1;
+  return 2;
+}
+
+function formatPercent(value: number): string {
+  return `${value.toFixed(value >= 10 ? 0 : 1)}%`;
+}
+
+const HOST_RESOURCE_COPY = {
+  'zh-CN': {
+    title: '主机资源',
+    open: '查看主机资源',
+    done: '完成',
+    unavailable: 'Host 连接后即可读取资源信息',
+    cpu: 'CPU',
+    cpuUsage: 'CPU 使用率',
+    memory: '内存',
+    graphics: '显示适配器',
+    storage: '磁盘',
+    noGraphicsAdapter: '未检测到',
+    graphicsUnknown: '无法确认',
+    sharedMemory: '共享内存',
+    memoryUnknown: '显存未知',
+    network: '网络吞吐',
+    measuring: '正在采样…',
+    logicalProcessors: (count: number, available: number) =>
+      count === available
+        ? `${count} 个逻辑处理器`
+        : `${available} / ${count} 个逻辑处理器可用`,
+    networkRate: (interfaceName: string, received: string, transmitted: string) =>
+      `${interfaceName} · ↓ ${received}/s · ↑ ${transmitted}/s`,
+    storageVolume: (
+      mount: string,
+      available: string,
+      total: string,
+      filesystem: string | undefined,
+    ) => `${mount} · ${available} 可用 / ${total}${filesystem ? ` · ${filesystem}` : ''}`,
+  },
+  'zh-TW': {
+    title: '主機資源',
+    open: '檢視主機資源',
+    done: '完成',
+    unavailable: 'Host 連線後即可讀取資源資訊',
+    cpu: 'CPU',
+    cpuUsage: 'CPU 使用率',
+    memory: '記憶體',
+    graphics: '顯示卡',
+    storage: '儲存空間',
+    noGraphicsAdapter: '未偵測到',
+    graphicsUnknown: '無法判定',
+    sharedMemory: '共享記憶體',
+    memoryUnknown: '顯示記憶體未知',
+    network: '網路輸送量',
+    measuring: '正在測量…',
+    logicalProcessors: (count: number, available: number) =>
+      count === available
+        ? `${count} 個邏輯處理器`
+        : `${available} / ${count} 個邏輯處理器可用`,
+    networkRate: (interfaceName: string, received: string, transmitted: string) =>
+      `${interfaceName} · ↓ ${received}/s · ↑ ${transmitted}/s`,
+    storageVolume: (
+      mount: string,
+      available: string,
+      total: string,
+      filesystem: string | undefined,
+    ) => `${mount} · ${available} 可用 / ${total}${filesystem ? ` · ${filesystem}` : ''}`,
+  },
+  en: {
+    title: 'Host resources',
+    open: 'View Host resources',
+    done: 'Done',
+    unavailable: 'Resource information is available while the Host is connected',
+    cpu: 'CPU',
+    cpuUsage: 'CPU usage',
+    memory: 'Memory',
+    graphics: 'Graphics adapters',
+    storage: 'Storage',
+    noGraphicsAdapter: 'None detected',
+    graphicsUnknown: 'Unable to determine',
+    sharedMemory: 'Shared memory',
+    memoryUnknown: 'Memory unknown',
+    network: 'Network throughput',
+    measuring: 'Measuring…',
+    logicalProcessors: (count: number, available: number) =>
+      count === available
+        ? `${count} logical processors`
+        : `${available} of ${count} logical processors available`,
+    networkRate: (interfaceName: string, received: string, transmitted: string) =>
+      `${interfaceName} · ↓ ${received}/s · ↑ ${transmitted}/s`,
+    storageVolume: (
+      mount: string,
+      available: string,
+      total: string,
+      filesystem: string | undefined,
+    ) =>
+      `${mount} · ${available} available / ${total}${filesystem ? ` · ${filesystem}` : ''}`,
+  },
+} satisfies Record<UiLocale, unknown>;
+
+function hostResourceCopy(locale: UiLocale) {
+  return HOST_RESOURCE_COPY[locale];
+}
diff --git a/apps/desktop/src/renderer/platform/desktop/create-runtime-host-management-services.ts b/apps/desktop/src/renderer/platform/desktop/create-runtime-host-management-services.ts
index 78b96bde9e..df388ab234 100644
--- a/apps/desktop/src/renderer/platform/desktop/create-runtime-host-management-services.ts
+++ b/apps/desktop/src/renderer/platform/desktop/create-runtime-host-management-services.ts
@@ -39,6 +39,13 @@ export function createDesktopRuntimeHostManagementServices(
       readClipboardText: () => navigator.clipboard.readText(),
       writeClipboardText: (value) => navigator.clipboard.writeText(value),
     },
+    resources: {
+      query: (profileId) => bridge.runtimeHostManagement.getResources(profileId),
+      schedule: (callback, delayMs) => {
+        const timer = window.setTimeout(callback, delayMs);
+        return () => window.clearTimeout(timer);
+      },
+    },
     peerMesh: {
       getConnectivityPolicy: () => bridge.runtimeHostPeerMesh.getConnectivityPolicy(),
       setConnectivityPolicy: (policy) => bridge.runtimeHostPeerMesh.setConnectivityPolicy(policy),
diff --git a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
index 586eb442ad..fafbab3b46 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
@@ -58,7 +58,10 @@ import {
   RuntimeHostProjectDirectoryEditor,
   type ProjectDirectoryRootDraft,
 } from './runtime-host-project-directory-editor.js';
-import { RuntimeHostConnectionCodeButton } from '../features/runtime-host-management';
+import {
+  RuntimeHostConnectionCodeButton,
+  RuntimeHostResourceDialog,
+} from '../features/runtime-host-management';
 
 type RuntimeHostManagementConfirmation =
   | { readonly kind: 'uninstall'; readonly allowInterruptActiveTasks: boolean }
@@ -775,6 +778,9 @@ export function RuntimeHostManagementDialog(props: {
                       <Fact label={copy.stateRoot} value={service.stateRoot} wide />
                     ) : null}
                   </dl>
+                  {target ? (
+                    <RuntimeHostResourceDialog profileId={target.id} hostName={target.name} />
+                  ) : null}
                   {serviceInstalled && fullManagement && target?.directPeerManagement ? (
                     <section className="settingsRuntimeHostDirectPeer">
                       <div className="settingsRuntimeHostUpdatePolicyHeading">
diff --git a/apps/desktop/src/renderer/styles/settings/runtime-host.css b/apps/desktop/src/renderer/styles/settings/runtime-host.css
index d4fd7d7d05..81e318c123 100644
--- a/apps/desktop/src/renderer/styles/settings/runtime-host.css
+++ b/apps/desktop/src/renderer/styles/settings/runtime-host.css
@@ -121,6 +121,15 @@
   font: var(--maka-text-body);
 }
 
+.settingsRuntimeHostResources {
+  display: grid;
+  gap: var(--space-3);
+}
+
+.settingsRuntimeHostResources dd {
+  white-space: pre-line;
+}
+
 .settingsRuntimeHostManagementDirectoryRoots {
   display: grid;
   gap: var(--space-2);
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index 5b7e3fdb67..43be274009 100644
--- a/docs/astryx-surface-file-inventory.md
+++ b/docs/astryx-surface-file-inventory.md
@@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.5.2` (194 component exports).
 
 Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding.
 
-**Totals:** 247 files — blocker 0, reimplementation 0, polish 1, aligned 246.
+**Totals:** 248 files — blocker 0, reimplementation 0, polish 1, aligned 247.
 
 ## Exclusions (explicit)
 
@@ -58,6 +58,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
 | `apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-connection-code-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, FormLayout, IconButton, Layout, LayoutContent, LayoutFooter, TextArea, Tooltip | aligned — uses Astryx (Button, Dialog, DialogHeader, FormLayout, IconButton, Layout, LayoutContent, LayoutFooter) | aligned |
 | `apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx` | dialog-overlay | Badge, Banner, Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, MoreMenu, SegmentedControl, SegmentedControlItem, Selector, Switch, Text, TextArea, TextInput, Tooltip | aligned — uses Astryx (Badge, Banner, Button, Dialog, DialogHeader, HStack, Layout, LayoutContent) | aligned |
 | `apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-profile-pairing-actions.tsx` | other | Button, MoreMenu | aligned — uses Astryx (Button, MoreMenu) | aligned |
+| `apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-resource-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, Layout, LayoutContent, LayoutFooter, Spinner, Text | aligned — uses Astryx (Button, Dialog, DialogHeader, Layout, LayoutContent, LayoutFooter, Spinner, Text) | aligned |
 | `apps/desktop/src/renderer/features/session-collaboration/services-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/features/session-collaboration/turn-request-inbox-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx` | dialog-overlay | Badge, Banner, Button, Dialog, DialogHeader, FormLayout, HStack, Layout, LayoutContent, LayoutFooter, List, ListItem, TextArea, Tooltip | aligned — uses Astryx (Badge, Banner, Button, Dialog, DialogHeader, FormLayout, HStack, Layout) | aligned |
diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths
index 279c0dc561..d720817716 100644
--- a/docs/astryx-surface-file-inventory.paths
+++ b/docs/astryx-surface-file-inventory.paths
@@ -29,6 +29,7 @@ apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-conne
 apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-connection-code-dialog.tsx
 apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx
 apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-profile-pairing-actions.tsx
+apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-resource-dialog.tsx
 apps/desktop/src/renderer/features/session-collaboration/services-context.tsx
 apps/desktop/src/renderer/features/session-collaboration/turn-request-inbox-context.tsx
 apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx
diff --git a/package-lock.json b/package-lock.json
index 9e54dcfa20..321c4a5390 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15908,6 +15908,32 @@
         "url": "https://github.com/fb55/nth-check?sponsor=1"
       }
     },
+    "node_modules/systeminformation": {
+      "version": "5.33.8",
+      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.8.tgz",
+      "integrity": "sha512-v4F6OGYGh7wDvV68YmjOmZwGixV9A/GQ7d2b84t0UF4CaOy9jipNWIJDkHqYDYiTPuiojqlwVQd0hfUKOUN7tQ==",
+      "license": "MIT",
+      "os": [
+        "darwin",
+        "linux",
+        "win32",
+        "freebsd",
+        "openbsd",
+        "netbsd",
+        "sunos",
+        "android"
+      ],
+      "bin": {
+        "systeminformation": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "funding": {
+        "type": "Buy me a coffee",
+        "url": "https://www.buymeacoffee.com/systeminfo"
+      }
+    },
     "node_modules/tar": {
       "version": "7.5.22",
       "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
@@ -17229,6 +17255,7 @@
         "@maka/core": "0.1.0",
         "@maka/runtime": "0.1.0",
         "@maka/storage": "0.1.0",
+        "systeminformation": "^5.33.8",
         "ws": "^8.21.3",
         "yaml": "^2.9.0",
         "zod": "^4.5.4"
diff --git a/packages/cli/THIRD_PARTY_NOTICES.txt b/packages/cli/THIRD_PARTY_NOTICES.txt
index 062aa20310..de25d90b90 100644
--- a/packages/cli/THIRD_PARTY_NOTICES.txt
+++ b/packages/cli/THIRD_PARTY_NOTICES.txt
@@ -6969,6 +6969,35 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
 
 ================================================================================
 
+Package: systeminformation@5.33.8
+Declared license: MIT
+Selected license: MIT
+Repository: git+https://github.com/sebhildebrandt/systeminformation.git
+
+--- LICENSE ---
+The MIT License (MIT)
+
+Copyright (c) 2014-2026 Sebastian Hildebrandt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+================================================================================
+
 Package: turndown@7.2.4
 Declared license: MIT
 Selected license: MIT
diff --git a/packages/runtime-host/package.json b/packages/runtime-host/package.json
index dee2266520..862fbc33c1 100644
--- a/packages/runtime-host/package.json
+++ b/packages/runtime-host/package.json
@@ -32,6 +32,7 @@
     "@maka/core": "0.1.0",
     "@maka/runtime": "0.1.0",
     "@maka/storage": "0.1.0",
+    "systeminformation": "^5.33.8",
     "ws": "^8.21.3",
     "yaml": "^2.9.0",
     "zod": "^4.5.4"
diff --git a/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts b/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts
index 0f94946fce..aac8e62468 100644
--- a/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts
+++ b/packages/runtime-host/src/__tests__/access-credential-grant-migration.test.ts
@@ -55,10 +55,10 @@ function storedCredential(operationGrants: readonly string[]): Record<string, un
   };
 }
 
-test('a renamed operation carries its stored authority to the successor', async () => {
+test('renamed and split operations carry their stored authority to successors', async () => {
   const path = await writeAccessFile({
     schemaVersion: 3,
-    credentials: [storedCredential(['host.status', 'task.ledger.query'])],
+    credentials: [storedCredential(['host.status', 'host.diagnostics.query', 'task.ledger.query'])],
     sessionGrants: [],
     turnAccessRequests: [],
   });
@@ -66,8 +66,18 @@ test('a renamed operation carries its stored authority to the successor', async
   const file = await readAccessCredentialFile(path);
   const credential = file.credentials[0];
   assert.ok(credential);
-  assert.deepEqual(credential.grants, ['host.status', 'session.todo.query']);
-  assert.deepEqual(effectiveOperationGrants(credential), ['host.status', 'session.todo.query']);
+  assert.deepEqual(credential.grants, [
+    'host.status',
+    'host.diagnostics.query',
+    'host.resources.query',
+    'session.todo.query',
+  ]);
+  assert.deepEqual(effectiveOperationGrants(credential), [
+    'host.status',
+    'host.diagnostics.query',
+    'host.resources.query',
+    'session.todo.query',
+  ]);
   assert.deepEqual(unresolvedPersistedGrants(file), []);
 });
 
diff --git a/packages/runtime-host/src/__tests__/connection-session.test.ts b/packages/runtime-host/src/__tests__/connection-session.test.ts
index ff6b96769b..67a35a2b28 100644
--- a/packages/runtime-host/src/__tests__/connection-session.test.ts
+++ b/packages/runtime-host/src/__tests__/connection-session.test.ts
@@ -1552,7 +1552,7 @@ function statusResponse(requestId: string): ResponseFrame {
 
 const UNUSED_HOST_DIAGNOSTICS_HANDLER: Pick<
   OperationHandlerMap,
-  'host.diagnostics.query' | 'host.upgrade.prepare'
+  'host.diagnostics.query' | 'host.resources.query' | 'host.upgrade.prepare'
 > = {
   'host.diagnostics.query': async () => ({
     ok: false,
@@ -1562,6 +1562,10 @@ const UNUSED_HOST_DIAGNOSTICS_HANDLER: Pick<
     ok: false,
     error: { code: 'internal_failure', message: 'not used' },
   }),
+  'host.resources.query': async () => ({
+    ok: false,
+    error: { code: 'internal_failure', message: 'not used' },
+  }),
 };
 
 function largeFailureResponse(requestId: string): ResponseFrame {
diff --git a/packages/runtime-host/src/__tests__/host-resources.test.ts b/packages/runtime-host/src/__tests__/host-resources.test.ts
new file mode 100644
index 0000000000..d9d4324810
--- /dev/null
+++ b/packages/runtime-host/src/__tests__/host-resources.test.ts
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import {
+  HOST_RESOURCE_KINDS,
+  HOST_RESOURCE_OPERATION_SPECS,
+  availableHostResource,
+  deriveHostResourceUtilization,
+  type HostResourceEnvelope,
+  type HostResourceJsonObject,
+  type HostResourcesResult,
+} from '../protocol/host-resources.js';
+import {
+  createHostResourceCollector,
+  type HostResourceSystemInformation,
+} from '../server/host-resource-collector.js';
+
+test('resource snapshots preserve unknown kinds while validating known resources', async () => {
+  const provider: HostResourceSystemInformation = {
+    async graphics() {
+      return {
+        controllers: [
+          {
+            vendor: 'Example',
+            model: 'Accelerator',
+            bus: 'PCIe',
+            vram: 8_192,
+            vramDynamic: false,
+            utilizationGpu: 25,
+          },
+        ],
+        displays: [],
+      };
+    },
+    async networkStats() {
+      throw new Error('network probe unavailable');
+    },
+    async fsSize() {
+      return [
+        {
+          fs: '/dev/test',
+          type: 'ext4',
+          size: 10_000,
+          used: 4_000,
+          available: 6_000,
+          use: 40,
+          mount: '/',
+          rw: true,
+        },
+      ];
+    },
+  };
+  const collected = await createHostResourceCollector(provider).snapshot('host-epoch');
+  const unknown: HostResourceEnvelope = {
+    kind: 'example.vendor.npu',
+    schemaVersion: 7,
+    status: 'available',
+    payload: { capacity: { tops: 40 } },
+  };
+  const decoded = HOST_RESOURCE_OPERATION_SPECS['host.resources.query'].decodeOutput({
+    ...collected,
+    resources: [...collected.resources, unknown],
+  });
+
+  assert.deepEqual(decoded.resources.at(-1), unknown);
+  assert.deepEqual(
+    decoded.resources.find((resource) => resource.kind === HOST_RESOURCE_KINDS.network),
+    {
+      kind: HOST_RESOURCE_KINDS.network,
+      schemaVersion: 1,
+      status: 'unavailable',
+      reason: 'probe_failed',
+    },
+  );
+  assert.equal(
+    availableHostResource(decoded, HOST_RESOURCE_KINDS.graphics)?.devices[0]?.memory.kind,
+    'dedicated',
+  );
+  assert.deepEqual(availableHostResource(decoded, HOST_RESOURCE_KINDS.storage)?.volumes[0], {
+    mount: '/',
+    filesystem: 'ext4',
+    totalBytes: 10_000,
+    availableBytes: 6_000,
+  });
+  assert.throws(
+    () =>
+      HOST_RESOURCE_OPERATION_SPECS['host.resources.query'].decodeOutput({
+        ...collected,
+        resources: collected.resources.map((resource) =>
+          resource.kind === HOST_RESOURCE_KINDS.cpu
+            ? { ...resource, status: 'available', payload: {} }
+            : resource,
+        ),
+      }),
+    /Invalid maka\.system\.cpu resource/u,
+  );
+  assert.throws(
+    () =>
+      HOST_RESOURCE_OPERATION_SPECS['host.resources.query'].decodeOutput({
+        ...collected,
+        resources: [...collected.resources, collected.resources[0]],
+      }),
+    /Duplicate Runtime Host resource kind/u,
+  );
+});
+
+test('utilization derives rates from same-epoch cumulative observations', () => {
+  const previous = snapshot('host-epoch', 1_000, 100, 200, 1_000, 2_000);
+  const current = snapshot('host-epoch', 3_000, 300, 500, 5_000, 8_000);
+  assert.deepEqual(deriveHostResourceUtilization(current, previous), {
+    memoryPercent: 75,
+    cpuPercent: 66.66666666666666,
+    receivedBytesPerSecond: 2_000,
+    transmittedBytesPerSecond: 3_000,
+  });
+
+  const replacement = snapshot('replacement', 5_000, 500, 700, 9_000, 12_000);
+  assert.deepEqual(deriveHostResourceUtilization(replacement, current), {
+    memoryPercent: 75,
+  });
+});
+
+function snapshot(
+  hostEpoch: string,
+  monotonicTimeMilliseconds: number,
+  busyTimeMilliseconds: number,
+  totalTimeMilliseconds: number,
+  receivedBytes: number,
+  transmittedBytes: number,
+): HostResourcesResult {
+  return {
+    hostEpoch,
+    observedAt: '2026-09-04T00:00:00.000Z',
+    monotonicTimeMilliseconds,
+    resources: [
+      available(HOST_RESOURCE_KINDS.cpu, {
+        capacity: {
+          model: 'CPU',
+          logicalProcessors: 4,
+          availableParallelism: 2,
+        },
+        observation: { busyTimeMilliseconds, totalTimeMilliseconds },
+      }),
+      available(HOST_RESOURCE_KINDS.memory, {
+        capacity: { visibleTotalBytes: 2_000, effectiveTotalBytes: 1_000 },
+        observation: { availableBytes: 250 },
+      }),
+      available(HOST_RESOURCE_KINDS.network, {
+        observation: {
+          interfaceName: 'eth0',
+          monotonicTimeMilliseconds,
+          receivedBytes,
+          transmittedBytes,
+        },
+      }),
+    ],
+  };
+}
+
+function available(kind: string, payload: HostResourceJsonObject): HostResourceEnvelope {
+  return { kind, schemaVersion: 1, status: 'available', payload };
+}
diff --git a/packages/runtime-host/src/__tests__/operation-dispatcher.test.ts b/packages/runtime-host/src/__tests__/operation-dispatcher.test.ts
index 6271d9390f..0700049bef 100644
--- a/packages/runtime-host/src/__tests__/operation-dispatcher.test.ts
+++ b/packages/runtime-host/src/__tests__/operation-dispatcher.test.ts
@@ -191,6 +191,7 @@ function validHandlers(): OperationHandlerMap {
   return {
     'host.status': unavailable,
     'host.diagnostics.query': unavailable,
+    'host.resources.query': unavailable,
     'host.upgrade.prepare': unavailable,
     ...createUnavailableHostCoreOperationHandlers(),
     ...createUnavailableDomainOperationHandlers(),
diff --git a/packages/runtime-host/src/protocol/host-resources.ts b/packages/runtime-host/src/protocol/host-resources.ts
new file mode 100644
index 0000000000..1c271e58a0
--- /dev/null
+++ b/packages/runtime-host/src/protocol/host-resources.ts
@@ -0,0 +1,378 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { z } from 'zod';
+import { requireEncodedByteLimit, requireExactRecord } from './codec.js';
+import { invalidProtocolFrame } from './errors.js';
+import { defineOperation } from './operation-spec.js';
+
+export const HOST_RESOURCE_RESULT_MAX_BYTES = 128 * 1024;
+export const HOST_RESOURCE_MAX_ENTRIES = 64;
+
+export const HOST_RESOURCE_KINDS = {
+  cpu: 'maka.system.cpu',
+  memory: 'maka.system.memory',
+  graphics: 'maka.system.graphics',
+  network: 'maka.system.network',
+  storage: 'maka.system.storage',
+} as const;
+
+export type HostResourceJsonValue =
+  | null
+  | boolean
+  | number
+  | string
+  | readonly HostResourceJsonValue[]
+  | HostResourceJsonObject;
+
+export interface HostResourceJsonObject {
+  readonly [key: string]: HostResourceJsonValue;
+}
+
+export type HostResourceUnavailableReason = 'unsupported' | 'permission_denied' | 'probe_failed';
+
+export type HostResourceEnvelope =
+  | {
+      readonly kind: string;
+      readonly schemaVersion: number;
+      readonly status: 'available';
+      readonly payload: HostResourceJsonObject;
+    }
+  | {
+      readonly kind: string;
+      readonly schemaVersion: number;
+      readonly status: 'unavailable';
+      readonly reason: HostResourceUnavailableReason;
+    };
+
+const POSITIVE_COUNT = z.number().int().positive().safe();
+const COUNT = z.number().int().nonnegative().safe();
+const PERCENTAGE = z.number().min(0).max(100);
+const NON_EMPTY_TEXT = z.string().min(1).max(512);
+
+const CPU_RESOURCE_V1_SCHEMA = z
+  .strictObject({
+    capacity: z.strictObject({
+      model: NON_EMPTY_TEXT,
+      logicalProcessors: POSITIVE_COUNT,
+      availableParallelism: POSITIVE_COUNT,
+    }),
+    observation: z.strictObject({
+      busyTimeMilliseconds: COUNT,
+      totalTimeMilliseconds: COUNT,
+    }),
+  })
+  .refine(
+    (value) =>
+      value.capacity.availableParallelism <= value.capacity.logicalProcessors &&
+      value.observation.busyTimeMilliseconds <= value.observation.totalTimeMilliseconds,
+    'CPU resource counters exceed capacity',
+  );
+
+const MEMORY_RESOURCE_V1_SCHEMA = z
+  .strictObject({
+    capacity: z.strictObject({
+      visibleTotalBytes: POSITIVE_COUNT,
+      effectiveTotalBytes: POSITIVE_COUNT,
+    }),
+    observation: z.strictObject({ availableBytes: COUNT }),
+  })
+  .refine(
+    (value) =>
+      value.capacity.effectiveTotalBytes <= value.capacity.visibleTotalBytes &&
+      value.observation.availableBytes <= value.capacity.effectiveTotalBytes,
+    'memory resource counters exceed capacity',
+  );
+
+const GRAPHICS_RESOURCE_V1_SCHEMA = z
+  .strictObject({
+    detection: z.enum(['detected', 'not_detected', 'unknown']),
+    devices: z
+      .array(
+        z.strictObject({
+          id: z.string().min(1).max(128),
+          model: NON_EMPTY_TEXT,
+          vendor: z.string().min(1).max(256).optional(),
+          memory: z.discriminatedUnion('kind', [
+            z.strictObject({ kind: z.literal('dedicated'), bytes: POSITIVE_COUNT }),
+            z.strictObject({ kind: z.literal('shared') }),
+            z.strictObject({ kind: z.literal('unknown') }),
+          ]),
+          utilizationPercent: PERCENTAGE.optional(),
+        }),
+      )
+      .max(32),
+  })
+  .refine(
+    (value) => (value.detection === 'detected') === value.devices.length > 0,
+    'graphics adapter detection does not match its devices',
+  );
+
+const NETWORK_RESOURCE_V1_SCHEMA = z.strictObject({
+  observation: z.strictObject({
+    interfaceName: NON_EMPTY_TEXT,
+    monotonicTimeMilliseconds: z.number().nonnegative().finite(),
+    receivedBytes: COUNT,
+    transmittedBytes: COUNT,
+  }),
+});
+
+const STORAGE_RESOURCE_V1_SCHEMA = z.strictObject({
+  volumes: z
+    .array(
+      z
+        .strictObject({
+          mount: z
+            .string()
+            .min(1)
+            .max(2 * 1024),
+          filesystem: z.string().min(1).max(128).optional(),
+          totalBytes: POSITIVE_COUNT,
+          availableBytes: COUNT,
+        })
+        .refine(
+          (volume) => volume.availableBytes <= volume.totalBytes,
+          'storage availability exceeds capacity',
+        ),
+    )
+    .max(64),
+});
+
+export type HostCpuResourceV1 = z.infer<typeof CPU_RESOURCE_V1_SCHEMA>;
+export type HostMemoryResourceV1 = z.infer<typeof MEMORY_RESOURCE_V1_SCHEMA>;
+export type HostGraphicsResourceV1 = z.infer<typeof GRAPHICS_RESOURCE_V1_SCHEMA>;
+export type HostNetworkResourceV1 = z.infer<typeof NETWORK_RESOURCE_V1_SCHEMA>;
+export type HostStorageResourceV1 = z.infer<typeof STORAGE_RESOURCE_V1_SCHEMA>;
+
+export interface HostResourcePayloads {
+  readonly [HOST_RESOURCE_KINDS.cpu]: HostCpuResourceV1;
+  readonly [HOST_RESOURCE_KINDS.memory]: HostMemoryResourceV1;
+  readonly [HOST_RESOURCE_KINDS.graphics]: HostGraphicsResourceV1;
+  readonly [HOST_RESOURCE_KINDS.network]: HostNetworkResourceV1;
+  readonly [HOST_RESOURCE_KINDS.storage]: HostStorageResourceV1;
+}
+
+export type HostResourceKind = keyof HostResourcePayloads;
+
+export interface HostResourcesResult {
+  readonly hostEpoch: string;
+  readonly observedAt: string;
+  readonly monotonicTimeMilliseconds: number;
+  readonly resources: readonly HostResourceEnvelope[];
+}
+
+export type HostResourcesQueryInput = Record<string, never>;
+
+export const HOST_RESOURCE_OPERATION_SPECS = {
+  'host.resources.query': defineOperation({
+    mode: 'query',
+    availability: 'bootstrap',
+    errors: ['host_draining', 'internal_failure'] as const,
+    decodeInput: (value) => {
+      requireExactRecord(value, 'host.resources.query input', []);
+      return {};
+    },
+    decodeOutput: decodeHostResourcesResult,
+  }),
+} as const;
+
+export function availableHostResource<K extends HostResourceKind>(
+  snapshot: HostResourcesResult | undefined,
+  kind: K,
+): HostResourcePayloads[K] | undefined {
+  const resource = snapshot?.resources.find(
+    (candidate) =>
+      candidate.kind === kind && candidate.schemaVersion === 1 && candidate.status === 'available',
+  );
+  return resource?.status === 'available'
+    ? (resource.payload as HostResourcePayloads[K])
+    : undefined;
+}
+
+export interface HostResourceUtilization {
+  readonly cpuPercent?: number;
+  readonly memoryPercent?: number;
+  readonly receivedBytesPerSecond?: number;
+  readonly transmittedBytesPerSecond?: number;
+}
+
+export function deriveHostResourceUtilization(
+  current: HostResourcesResult,
+  previous?: HostResourcesResult,
+): HostResourceUtilization {
+  const memory = availableHostResource(current, HOST_RESOURCE_KINDS.memory);
+  const utilization: HostResourceUtilization = {
+    ...(memory
+      ? {
+          memoryPercent: percentage(
+            memory.capacity.effectiveTotalBytes - memory.observation.availableBytes,
+            memory.capacity.effectiveTotalBytes,
+          ),
+        }
+      : {}),
+  };
+  if (!previous || previous.hostEpoch !== current.hostEpoch) return utilization;
+  const elapsedMilliseconds =
+    current.monotonicTimeMilliseconds - previous.monotonicTimeMilliseconds;
+  if (elapsedMilliseconds <= 0) return utilization;
+
+  const cpu = availableHostResource(current, HOST_RESOURCE_KINDS.cpu);
+  const previousCpu = availableHostResource(previous, HOST_RESOURCE_KINDS.cpu);
+  if (cpu && previousCpu) {
+    const busyDelta =
+      cpu.observation.busyTimeMilliseconds - previousCpu.observation.busyTimeMilliseconds;
+    const totalDelta =
+      cpu.observation.totalTimeMilliseconds - previousCpu.observation.totalTimeMilliseconds;
+    if (busyDelta >= 0 && totalDelta > 0) {
+      Object.assign(utilization, { cpuPercent: percentage(busyDelta, totalDelta) });
+    }
+  }
+
+  const network = availableHostResource(current, HOST_RESOURCE_KINDS.network);
+  const previousNetwork = availableHostResource(previous, HOST_RESOURCE_KINDS.network);
+  if (
+    network &&
+    previousNetwork &&
+    network.observation.interfaceName === previousNetwork.observation.interfaceName
+  ) {
+    const networkElapsedMilliseconds =
+      network.observation.monotonicTimeMilliseconds -
+      previousNetwork.observation.monotonicTimeMilliseconds;
+    if (networkElapsedMilliseconds <= 0) return utilization;
+    const elapsedSeconds = networkElapsedMilliseconds / 1_000;
+    assignRate(
+      utilization,
+      'receivedBytesPerSecond',
+      network.observation.receivedBytes - previousNetwork.observation.receivedBytes,
+      elapsedSeconds,
+    );
+    assignRate(
+      utilization,
+      'transmittedBytesPerSecond',
+      network.observation.transmittedBytes - previousNetwork.observation.transmittedBytes,
+      elapsedSeconds,
+    );
+  }
+  return utilization;
+}
+
+const ENVELOPE_SCHEMA = z.discriminatedUnion('status', [
+  z.strictObject({
+    kind: z.string().min(1).max(128),
+    schemaVersion: POSITIVE_COUNT,
+    status: z.literal('available'),
+    payload: z.unknown(),
+  }),
+  z.strictObject({
+    kind: z.string().min(1).max(128),
+    schemaVersion: POSITIVE_COUNT,
+    status: z.literal('unavailable'),
+    reason: z.enum(['unsupported', 'permission_denied', 'probe_failed']),
+  }),
+]);
+
+const RESULT_SCHEMA = z.strictObject({
+  hostEpoch: z.string().min(1).max(128),
+  observedAt: z.iso.datetime(),
+  monotonicTimeMilliseconds: z.number().nonnegative().finite(),
+  resources: z.array(ENVELOPE_SCHEMA).max(HOST_RESOURCE_MAX_ENTRIES),
+});
+
+const KNOWN_PAYLOAD_SCHEMAS: Readonly<Record<HostResourceKind, z.ZodType>> = {
+  [HOST_RESOURCE_KINDS.cpu]: CPU_RESOURCE_V1_SCHEMA,
+  [HOST_RESOURCE_KINDS.memory]: MEMORY_RESOURCE_V1_SCHEMA,
+  [HOST_RESOURCE_KINDS.graphics]: GRAPHICS_RESOURCE_V1_SCHEMA,
+  [HOST_RESOURCE_KINDS.network]: NETWORK_RESOURCE_V1_SCHEMA,
+  [HOST_RESOURCE_KINDS.storage]: STORAGE_RESOURCE_V1_SCHEMA,
+};
+
+function decodeHostResourcesResult(value: unknown): HostResourcesResult {
+  requireEncodedByteLimit(value, 'host.resources.query result', HOST_RESOURCE_RESULT_MAX_BYTES);
+  const result = parse(RESULT_SCHEMA, value, 'Runtime Host resources');
+  if (new Set(result.resources.map((resource) => resource.kind)).size !== result.resources.length) {
+    throw invalidProtocolFrame('Duplicate Runtime Host resource kind');
+  }
+  return {
+    ...result,
+    resources: result.resources.map((resource): HostResourceEnvelope => {
+      if (resource.status === 'unavailable') return resource;
+      const payload = decodeJsonObject(resource.payload);
+      const schema =
+        resource.schemaVersion === 1
+          ? KNOWN_PAYLOAD_SCHEMAS[resource.kind as HostResourceKind]
+          : undefined;
+      return {
+        ...resource,
+        payload: schema
+          ? (parse(schema, payload, `${resource.kind} resource`) as HostResourceJsonObject)
+          : payload,
+      };
+    }),
+  };
+}
+
+function decodeJsonObject(value: unknown): HostResourceJsonObject {
+  if (!value || typeof value !== 'object' || Array.isArray(value)) {
+    throw invalidProtocolFrame('Invalid Runtime Host resource payload');
+  }
+  return decodeJsonValue(value, 0) as HostResourceJsonObject;
+}
+
+function decodeJsonValue(value: unknown, depth: number): HostResourceJsonValue {
+  if (depth > 8) throw invalidProtocolFrame('Runtime Host resource payload is too deeply nested');
+  if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;
+  if (typeof value === 'number' && Number.isFinite(value)) return value;
+  if (Array.isArray(value)) {
+    if (value.length > 256) throw invalidProtocolFrame('Invalid Runtime Host resource payload');
+    return value.map((child) => decodeJsonValue(child, depth + 1));
+  }
+  if (!value || typeof value !== 'object') {
+    throw invalidProtocolFrame('Invalid Runtime Host resource payload');
+  }
+  const entries = Object.entries(value);
+  if (entries.length > 128) throw invalidProtocolFrame('Invalid Runtime Host resource payload');
+  return Object.fromEntries(
+    entries.map(([key, child]) => {
+      if (!key || key.length > 128) {
+        throw invalidProtocolFrame('Invalid Runtime Host resource payload');
+      }
+      return [key, decodeJsonValue(child, depth + 1)];
+    }),
+  );
+}
+
+function parse<T>(schema: z.ZodType<T>, value: unknown, label: string): T {
+  const result = schema.safeParse(value);
+  if (!result.success) throw invalidProtocolFrame(`Invalid ${label}`);
+  return result.data;
+}
+
+function percentage(numerator: number, denominator: number): number {
+  if (denominator <= 0) return 0;
+  return Math.max(0, Math.min(100, (numerator / denominator) * 100));
+}
+
+function assignRate(
+  target: HostResourceUtilization,
+  key: 'receivedBytesPerSecond' | 'transmittedBytesPerSecond',
+  delta: number,
+  elapsedSeconds: number,
+): void {
+  if (delta >= 0) Object.assign(target, { [key]: delta / elapsedSeconds });
+}
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index 9fada32cd6..b58467c6ee 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -75,6 +75,7 @@ export * from './configuration-change.js';
 export * from './connection-catalog-change.js';
 export * from './goal.js';
 export * from './hosted-execution.js';
+export * from './host-resources.js';
 export * from './plan.js';
 export * from './peer-mesh.js';
 export * from './project-catalog.js';
@@ -100,7 +101,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
 export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
 // Increment when the same protocol version no longer guarantees safe Client-Host
 // interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 111 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 112 as const;
+// 112: Owners can query the Host execution environment through an extensible,
+// bounded resource-envelope contract. Older Hosts do not implement the query.
 // 111: Client Capability tool schemas may use draft-07 tuple additionalItems.
 // Older Hosts reject the keyword, so peers must agree before capabilities are admitted.
 // 110: Runtime Host is the sole schema-migration authority for its State Root.
diff --git a/packages/runtime-host/src/protocol/operations.ts b/packages/runtime-host/src/protocol/operations.ts
index 0d52b1445e..d8c1ba0f5f 100644
--- a/packages/runtime-host/src/protocol/operations.ts
+++ b/packages/runtime-host/src/protocol/operations.ts
@@ -31,6 +31,7 @@ import { EXTERNAL_SESSION_OPERATION_SPECS } from './external-session.js';
 import { CLIENT_CAPABILITY_OPERATION_SPECS } from './client-capability.js';
 import { invalidProtocolFrame } from './errors.js';
 import { HOST_BOOTSTRAP_OPERATION_SPECS } from './host-status.js';
+import { HOST_RESOURCE_OPERATION_SPECS } from './host-resources.js';
 import { HOSTED_EXECUTION_OPERATION_SPECS } from './hosted-execution.js';
 import { GOAL_OPERATION_SPECS } from './goal.js';
 import { INTERACTION_OPERATION_SPECS } from './interaction.js';
@@ -161,6 +162,7 @@ export * from './agent-graph.js';
 export * from './execution-inspect.js';
 export * from './client-capability.js';
 export * from './goal.js';
+export * from './host-resources.js';
 export * from './memory.js';
 export * from './network-proxy.js';
 export * from './oauth.js';
@@ -185,6 +187,7 @@ export * from './workspace.js';
 
 export const HOST_OPERATION_SPECS = composeOperationSpecMaps(
   HOST_BOOTSTRAP_OPERATION_SPECS,
+  HOST_RESOURCE_OPERATION_SPECS,
   PEER_MESH_OPERATION_SPECS,
   HOSTED_EXECUTION_OPERATION_SPECS,
   ACCESS_AUTHORITY_OPERATION_SPECS,
@@ -276,6 +279,7 @@ export const REMOTE_OWNER_OPERATION_GRANTS = Object.freeze([
   'goal.control',
   'goal.query',
   'host.diagnostics.query',
+  'host.resources.query',
   'host.status',
   'interaction.answer',
   'interaction.query',
diff --git a/packages/runtime-host/src/server/access-credential-store.ts b/packages/runtime-host/src/server/access-credential-store.ts
index 4532bf0bec..973e0f398a 100644
--- a/packages/runtime-host/src/server/access-credential-store.ts
+++ b/packages/runtime-host/src/server/access-credential-store.ts
@@ -70,6 +70,11 @@ const PERSISTED_GRANT_MIGRATIONS: ReadonlyMap<string, PersistedGrantMigration> =
     'session.turns.query',
     { kind: 'replace', successors: ['session.turns.query', 'session.turn_landmarks.query'] },
   ],
+  // Resource inventory is a dedicated facet of the existing Host diagnostics authority.
+  [
+    'host.diagnostics.query',
+    { kind: 'replace', successors: ['host.diagnostics.query', 'host.resources.query'] },
+  ],
   // TaskLedger became SessionTodo; the query carried its authority over.
   ['task.ledger.query', { kind: 'replace', successors: ['session.todo.query'] }],
   // Retired with the Claude subscription provider, whose client identity the
diff --git a/packages/runtime-host/src/server/host-kernel.ts b/packages/runtime-host/src/server/host-kernel.ts
index 9655398dda..cd6df6702b 100644
--- a/packages/runtime-host/src/server/host-kernel.ts
+++ b/packages/runtime-host/src/server/host-kernel.ts
@@ -94,6 +94,7 @@ import {
 import { HostResidencyRegistry } from './host-residency-registry.js';
 import type { PeerMeshNode } from '../peer-mesh/node.js';
 import { createPeerMeshOperationHandlers } from './peer-mesh-authority.js';
+import { createHostResourceCollector } from './host-resource-collector.js';
 
 const DEFAULT_IDLE_GRACE_MS = 30_000;
 const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
@@ -207,6 +208,7 @@ export class RuntimeHostKernel {
   >();
   readonly #operationDrainWaiters = new Set<() => void>();
   readonly #residencies = new HostResidencyRegistry();
+  readonly #resourceCollector = createHostResourceCollector();
   readonly #lifecycle: RuntimeHostLifecycle;
   readonly #handshakeTimeoutMs: number;
   readonly #shutdownGraceMs: number;
@@ -700,6 +702,10 @@ export class RuntimeHostKernel {
               .map((entry) => collapseHomePath(entry, homedir(), process.platform)),
           },
         }),
+        'host.resources.query': async () => ({
+          ok: true,
+          result: await this.#resourceCollector.snapshot(this.hostEpoch),
+        }),
         'host.upgrade.prepare': async (input) => {
           if (input.expectedHostEpoch !== this.hostEpoch) {
             return {
diff --git a/packages/runtime-host/src/server/host-resource-collector.ts b/packages/runtime-host/src/server/host-resource-collector.ts
new file mode 100644
index 0000000000..a8f39de30b
--- /dev/null
+++ b/packages/runtime-host/src/server/host-resource-collector.ts
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { existsSync } from 'node:fs';
+import { availableParallelism, cpus, totalmem } from 'node:os';
+import { performance } from 'node:perf_hooks';
+import {
+  HOST_RESOURCE_KINDS,
+  type HostCpuResourceV1,
+  type HostGraphicsResourceV1,
+  type HostMemoryResourceV1,
+  type HostNetworkResourceV1,
+  type HostResourceEnvelope,
+  type HostResourceJsonObject,
+  type HostResourcesResult,
+  type HostStorageResourceV1,
+} from '../protocol/host-resources.js';
+import {
+  createIsolatedHostResourceSystemInformation,
+  type HostResourceSystemInformation,
+} from './host-resource-probe.js';
+
+export type { HostResourceSystemInformation } from './host-resource-probe.js';
+
+const SLOW_RESOURCE_CACHE_MILLISECONDS = 10_000;
+// Leave process-tree termination and scheduling headroom before the Host's
+// 10-second forced-shutdown boundary.
+const RESOURCE_PROBE_TIMEOUT_MILLISECONDS = 7_000;
+
+export interface HostResourceCollector {
+  snapshot(hostEpoch: string): Promise<HostResourcesResult>;
+}
+
+export function createHostResourceCollector(
+  provider: HostResourceSystemInformation = createIsolatedHostResourceSystemInformation(
+    RESOURCE_PROBE_TIMEOUT_MILLISECONDS,
+  ),
+): HostResourceCollector {
+  const collectGraphics = createResourceProbe({
+    collect: () => probeGraphics(provider),
+    cacheMilliseconds: SLOW_RESOURCE_CACHE_MILLISECONDS,
+  });
+  const collectStorage = createResourceProbe({
+    collect: () => probeStorage(provider),
+    cacheMilliseconds: SLOW_RESOURCE_CACHE_MILLISECONDS,
+  });
+  const collectNetwork = createResourceProbe({
+    collect: () => probeNetwork(provider),
+  });
+
+  return {
+    async snapshot(hostEpoch): Promise<HostResourcesResult> {
+      const [graphics, network, storage] = await Promise.all([
+        collectGraphics(),
+        collectNetwork(),
+        collectStorage(),
+      ]);
+      const cpu = collectCpu();
+      const memory = collectMemory();
+      const monotonicTimeMilliseconds = Math.floor(performance.now());
+      return {
+        hostEpoch,
+        observedAt: new Date().toISOString(),
+        monotonicTimeMilliseconds,
+        resources: [cpu, memory, graphics, network, storage],
+      };
+    },
+  };
+}
+
+function createResourceProbe(input: {
+  readonly collect: () => Promise<HostResourceEnvelope>;
+  readonly cacheMilliseconds?: number;
+}): () => Promise<HostResourceEnvelope> {
+  let cached: { readonly expiresAt: number; readonly resource: HostResourceEnvelope } | undefined;
+  let inFlight: Promise<HostResourceEnvelope> | undefined;
+  return async () => {
+    const now = performance.now();
+    if (cached && cached.expiresAt > now) return cached.resource;
+    if (!inFlight) {
+      const probe = input.collect().then((resource) => {
+        if (input.cacheMilliseconds) {
+          cached = {
+            expiresAt: performance.now() + input.cacheMilliseconds,
+            resource,
+          };
+        }
+        return resource;
+      });
+      const tracked = probe.finally(() => {
+        if (inFlight === tracked) inFlight = undefined;
+      });
+      inFlight = tracked;
+    }
+    return inFlight;
+  };
+}
+
+function collectCpu(): HostResourceEnvelope {
+  const processors = cpus();
+  const totals = processors.reduce(
+    (sum, processor) => {
+      const busy =
+        processor.times.user + processor.times.nice + processor.times.sys + processor.times.irq;
+      return {
+        busy: sum.busy + busy,
+        total: sum.total + busy + processor.times.idle,
+      };
+    },
+    { busy: 0, total: 0 },
+  );
+  const payload: HostCpuResourceV1 = {
+    capacity: {
+      model: processors[0]?.model.trim() || 'Unknown CPU',
+      logicalProcessors: Math.max(1, processors.length),
+      availableParallelism: Math.max(1, availableParallelism()),
+    },
+    observation: {
+      busyTimeMilliseconds: Math.max(0, Math.floor(totals.busy)),
+      totalTimeMilliseconds: Math.max(0, Math.floor(totals.total)),
+    },
+  };
+  return availableResource(HOST_RESOURCE_KINDS.cpu, payload);
+}
+
+function collectMemory(): HostResourceEnvelope {
+  const visibleTotalBytes = totalmem();
+  const constrainedBytes = process.constrainedMemory();
+  const effectiveTotalBytes =
+    constrainedBytes > 0 ? Math.min(visibleTotalBytes, constrainedBytes) : visibleTotalBytes;
+  const availableBytes = Math.min(effectiveTotalBytes, process.availableMemory());
+  const payload: HostMemoryResourceV1 = {
+    capacity: {
+      visibleTotalBytes,
+      effectiveTotalBytes,
+    },
+    observation: {
+      availableBytes: Math.max(0, availableBytes),
+    },
+  };
+  return availableResource(HOST_RESOURCE_KINDS.memory, payload);
+}
+
+async function probeGraphics(
+  provider: HostResourceSystemInformation,
+): Promise<HostResourceEnvelope> {
+  try {
+    const graphics = await provider.graphics();
+    const devices = graphics.controllers.slice(0, 32).map((controller, index) => {
+      const memoryMegabytes = finitePositive(controller.memoryTotal)
+        ? controller.memoryTotal
+        : finitePositive(controller.vram)
+          ? controller.vram
+          : undefined;
+      return {
+        id: `gpu-${index}`,
+        model:
+          controller.model.trim() || controller.name?.trim() || `Graphics adapter ${index + 1}`,
+        ...(controller.vendor.trim() ? { vendor: controller.vendor.trim() } : {}),
+        memory: controller.vramDynamic
+          ? ({ kind: 'shared' } as const)
+          : memoryMegabytes === undefined
+            ? ({ kind: 'unknown' } as const)
+            : ({
+                kind: 'dedicated',
+                bytes: Math.floor(memoryMegabytes * 1024 * 1024),
+              } as const),
+        ...(finitePercentage(controller.utilizationGpu)
+          ? { utilizationPercent: controller.utilizationGpu }
+          : {}),
+      };
+    });
+    const payload: HostGraphicsResourceV1 = {
+      detection:
+        devices.length > 0
+          ? 'detected'
+          : process.platform === 'linux' && existsSync('/dev/dxg')
+            ? 'unknown'
+            : 'not_detected',
+      devices,
+    };
+    return availableResource(HOST_RESOURCE_KINDS.graphics, payload);
+  } catch {
+    const payload: HostGraphicsResourceV1 = { detection: 'unknown', devices: [] };
+    return availableResource(HOST_RESOURCE_KINDS.graphics, payload);
+  }
+}
+
+async function probeNetwork(
+  provider: HostResourceSystemInformation,
+): Promise<HostResourceEnvelope> {
+  try {
+    const { interfaceName, stats } = await provider.networkStats();
+    if (!interfaceName) {
+      return unavailableResource(HOST_RESOURCE_KINDS.network, 'unsupported');
+    }
+    const observed = stats[0];
+    if (
+      stats.length !== 1 ||
+      !observed ||
+      observed.iface.toLowerCase() !== interfaceName.toLowerCase() ||
+      observed.operstate !== 'up'
+    ) {
+      return unavailableResource(HOST_RESOURCE_KINDS.network, 'probe_failed');
+    }
+    const payload: HostNetworkResourceV1 = {
+      observation: {
+        interfaceName,
+        monotonicTimeMilliseconds: Math.floor(performance.now()),
+        receivedBytes: safeCounter(observed.rx_bytes),
+        transmittedBytes: safeCounter(observed.tx_bytes),
+      },
+    };
+    return availableResource(HOST_RESOURCE_KINDS.network, payload);
+  } catch {
+    return unavailableResource(HOST_RESOURCE_KINDS.network, 'probe_failed');
+  }
+}
+
+async function probeStorage(
+  provider: HostResourceSystemInformation,
+): Promise<HostResourceEnvelope> {
+  try {
+    const volumes = (await provider.fsSize())
+      .filter((volume) => finitePositive(volume.size) && volume.mount.trim())
+      .slice(0, 64)
+      .map((volume) => ({
+        mount: volume.mount.trim(),
+        ...(volume.type.trim() ? { filesystem: volume.type.trim() } : {}),
+        totalBytes: Math.floor(volume.size),
+        availableBytes: Math.max(0, Math.min(Math.floor(volume.available), volume.size)),
+      }));
+    const payload: HostStorageResourceV1 = { volumes };
+    return availableResource(HOST_RESOURCE_KINDS.storage, payload);
+  } catch {
+    return unavailableResource(HOST_RESOURCE_KINDS.storage, 'probe_failed');
+  }
+}
+
+function availableResource(kind: string, payload: HostResourceJsonObject): HostResourceEnvelope {
+  return { kind, schemaVersion: 1, status: 'available', payload };
+}
+
+function unavailableResource(
+  kind: string,
+  reason: 'unsupported' | 'permission_denied' | 'probe_failed',
+): HostResourceEnvelope {
+  return { kind, schemaVersion: 1, status: 'unavailable', reason };
+}
+
+function finitePositive(value: number | null | undefined): value is number {
+  return typeof value === 'number' && Number.isFinite(value) && value > 0;
+}
+
+function finitePercentage(value: number | undefined): value is number {
+  return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
+}
+
+function safeCounter(value: number): number {
+  return Number.isSafeInteger(value) && value >= 0 ? value : 0;
+}
diff --git a/packages/runtime-host/src/server/host-resource-probe-main.ts b/packages/runtime-host/src/server/host-resource-probe-main.ts
new file mode 100644
index 0000000000..d6421c7c17
--- /dev/null
+++ b/packages/runtime-host/src/server/host-resource-probe-main.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as systemInformation from 'systeminformation';
+
+async function main(): Promise<void> {
+  const [kind, extra] = process.argv.slice(2);
+  if (extra !== undefined) throw new Error('Unexpected Runtime Host resource probe argument');
+  const result =
+    kind === 'graphics'
+      ? await systemInformation.graphics()
+      : kind === 'storage'
+        ? await systemInformation.fsSize()
+        : kind === 'network'
+          ? await networkStats()
+          : undefined;
+  if (result === undefined) throw new Error('Invalid Runtime Host resource probe');
+  process.stdout.write(JSON.stringify(result));
+}
+
+async function networkStats() {
+  const interfaceName = await systemInformation.networkInterfaceDefault();
+  return {
+    interfaceName,
+    stats: interfaceName ? await systemInformation.networkStats(interfaceName) : [],
+  };
+}
+
+void main().catch(() => {
+  process.exitCode = 1;
+});
diff --git a/packages/runtime-host/src/server/host-resource-probe.ts b/packages/runtime-host/src/server/host-resource-probe.ts
new file mode 100644
index 0000000000..daf83052fc
--- /dev/null
+++ b/packages/runtime-host/src/server/host-resource-probe.ts
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { spawn } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { terminateChildProcessTree } from '@maka/runtime/process-tree-terminator';
+import type * as systemInformation from 'systeminformation';
+
+const PROBE_ENTRYPOINT = fileURLToPath(new URL('./host-resource-probe-main.js', import.meta.url));
+const PROBE_OUTPUT_MAX_BYTES = 2 * 1024 * 1024;
+
+export interface HostResourceSystemInformation {
+  graphics(): Promise<systemInformation.Systeminformation.GraphicsData>;
+  networkStats(): Promise<{
+    readonly interfaceName: string;
+    readonly stats: systemInformation.Systeminformation.NetworkStatsData[];
+  }>;
+  fsSize(): Promise<systemInformation.Systeminformation.FsSizeData[]>;
+}
+
+export function createIsolatedHostResourceSystemInformation(
+  timeoutMilliseconds: number,
+): HostResourceSystemInformation {
+  return {
+    graphics: () => invokeProbe('graphics', timeoutMilliseconds),
+    networkStats: () => invokeProbe('network', timeoutMilliseconds),
+    fsSize: () => invokeProbe('storage', timeoutMilliseconds),
+  };
+}
+
+function invokeProbe<T>(
+  kind: 'graphics' | 'network' | 'storage',
+  timeoutMilliseconds: number,
+): Promise<T> {
+  const child = spawn(process.execPath, [PROBE_ENTRYPOINT, kind], {
+    detached: process.platform !== 'win32',
+    stdio: ['ignore', 'pipe', 'ignore'],
+    windowsHide: true,
+  });
+  return new Promise<T>((resolve, reject) => {
+    const chunks: Buffer[] = [];
+    let bytes = 0;
+    let settled = false;
+    const timer = setTimeout(
+      () => terminate(new Error('Runtime Host resource probe timed out')),
+      timeoutMilliseconds,
+    );
+    const finish = (outcome: { readonly value: T } | { readonly error: Error }) => {
+      if (settled) return;
+      settled = true;
+      clearTimeout(timer);
+      if ('value' in outcome) resolve(outcome.value);
+      else reject(outcome.error);
+    };
+    const terminate = (error: Error) => {
+      if (settled) return;
+      settled = true;
+      clearTimeout(timer);
+      void terminateChildProcessTree(child, 'SIGKILL').finally(() => reject(error));
+    };
+    child.stdout.on('data', (chunk: Buffer) => {
+      bytes += chunk.byteLength;
+      if (bytes > PROBE_OUTPUT_MAX_BYTES) {
+        terminate(new Error('Runtime Host resource probe output is too large'));
+        return;
+      }
+      chunks.push(chunk);
+    });
+    child.once('error', (error) => finish({ error }));
+    child.once('close', (code) => {
+      if (settled) return;
+      if (code !== 0) {
+        finish({ error: new Error('Runtime Host resource probe failed') });
+        return;
+      }
+      try {
+        finish({ value: JSON.parse(Buffer.concat(chunks).toString('utf8')) as T });
+      } catch {
+        finish({ error: new Error('Runtime Host resource probe returned invalid output') });
+      }
+    });
+  });
+}
diff --git a/packages/runtime-host/src/server/operation-dispatcher.ts b/packages/runtime-host/src/server/operation-dispatcher.ts
index ff6f237f9f..cba2b5569d 100644
--- a/packages/runtime-host/src/server/operation-dispatcher.ts
+++ b/packages/runtime-host/src/server/operation-dispatcher.ts
@@ -33,6 +33,7 @@ import {
   type ResponseFrameFor,
 } from '../protocol/index.js';
 import { HOST_BOOTSTRAP_OPERATION_SPECS } from '../protocol/host-status.js';
+import { HOST_RESOURCE_OPERATION_SPECS } from '../protocol/host-resources.js';
 import { ACCESS_AUTHORITY_OPERATION_SPECS } from '../protocol/access-authority.js';
 import { SESSION_COLLABORATION_OPERATION_SPECS } from '../protocol/session-collaboration.js';
 import { PEER_MESH_OPERATION_SPECS } from '../protocol/peer-mesh.js';
@@ -109,6 +110,7 @@ export type OperationHandlerMap = {
  */
 const HOST_CORE_SPEC_OBJECTS = [
   HOST_BOOTSTRAP_OPERATION_SPECS,
+  HOST_RESOURCE_OPERATION_SPECS,
   ACCESS_AUTHORITY_OPERATION_SPECS,
   SESSION_COLLABORATION_OPERATION_SPECS,
   PEER_MESH_OPERATION_SPECS,


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org