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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
- uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9
with:
version: 0.12.6
- name: Check agent documentation bundle
run: uv run --no-project docs/package_agent_docs.py --check

- name: Prepare chart test dependencies
run: |
set -euo pipefail
Expand Down
16 changes: 16 additions & 0 deletions docs/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,19 @@ The footprint catalog is generated during each HTML build by rendering the track
The documentation workflow builds pull requests without publishing. After a change lands on `main`, it publishes the generated static site to GitHub Pages using the `github-pages` environment. A repository administrator must first enable **Settings → Pages → Build and deployment → Source: GitHub Actions**.

The intended URL is `https://nvidia.github.io/AI-Factory-Operations-Agent/`. Until Pages is enabled and a deployment succeeds, that URL will not serve the documentation. No registry or inference credentials are needed for the documentation build.

## Agent reference bundle

The Helm chart also includes the tracked product Markdown for the agent to read
inside its sandbox, alongside any Base Command Manager manuals. After editing
`README.md` or Markdown under `docs/`, refresh the bundle before packaging the chart:

```bash
uv run --no-project docs/package_agent_docs.py
```

CI checks that the bundle matches its sources. Startup stages it under the agent
workspace's `product-docs/`, with a deployment profile containing
module enablement and registered cluster names. Credentials are excluded from that
profile. The current tool catalog and live tool results remain authoritative for
capabilities and infrastructure state.
32 changes: 32 additions & 0 deletions docs/package_agent_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Bundle tracked Markdown for the agent workspace; --check verifies freshness."""
import gzip
import io
from pathlib import Path
import subprocess
import sys
import tarfile

root = Path(__file__).resolve().parents[1]
output = root / "helm/mosaic-stack/files/agent-documentation.tar.gz"
paths = subprocess.check_output(
["git", "ls-files", "-z", "README.md", "docs"], cwd=root
).decode().split("\0")
archive = io.BytesIO()
with tarfile.open(fileobj=archive, mode="w") as tar:
for name in sorted(name for name in paths if name.endswith(".md")):
content = (root / name).read_bytes()
entry = tarfile.TarInfo(name)
entry.size, entry.mode = len(content), 0o644
tar.addfile(entry, io.BytesIO(content))
compressed = io.BytesIO()
with gzip.GzipFile(fileobj=compressed, mode="wb", filename="", mtime=0) as stream:
stream.write(archive.getvalue())
bundle = compressed.getvalue()
if "--check" in sys.argv:
if not output.exists() or output.read_bytes() != bundle:
sys.exit("Agent documentation is stale: run uv run --no-project docs/package_agent_docs.py")
else:
output.write_bytes(bundle)
Binary file not shown.
2 changes: 2 additions & 0 deletions helm/mosaic-stack/files/openclaw-seed/workspace/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ For a cause investigation, establish which resources and path the workload actua

Local `exec` runs inside the per-session OpenShell sandbox in every access mode. Use it for sandbox-local commands, scripts, and data processing; OpenShell enforces the execution boundary. A local command rejection does not establish that other tools or integrations are unavailable.

Read bundled product docs and `deployment.json` from `product-docs/` in the sandbox workspace.

The bundled sandbox does not include BCM's `cmsh` CLI. Use the registered BCM tools to access BCM when that integration is enabled. If those tools are absent, report that BCM is not configured for this deployment. Changing the session access mode does not install commands or enable integrations.

`run_remote_ssh` executes on an installer-configured host outside the sandbox. In View mode, use only its supported read-only diagnostic argv. Remote commands outside that policy require Edit; do not rewrite a rejected command to bypass the policy.
Expand Down
15 changes: 15 additions & 0 deletions helm/mosaic-stack/templates/openclaw-seed-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,22 @@ metadata:
name: openclaw-seed
labels:
{{- include "mosaic-stack.labels" . | nindent 4 }}
binaryData:
agent-documentation.tar.gz: {{ required "Packaged agent documentation is missing" (.Files.Get "files/agent-documentation.tar.gz") | b64enc | quote }}
data:
agent-deployment.json: |-
{
"release": {{ .Release.Name | quote }},
"namespace": {{ .Release.Namespace | quote }},
"chartVersion": {{ .Chart.Version | quote }},
"modules": {
{{- range $name, $module := .Values.modules }}
{{ $name | quote }}: {{ $module.enabled }},
{{- end }}
"bcmConfigured": {{ eq (include "mosaic-stack.bcmEnabled" .) "true" }}
},
"kubernetesClusters": {{ keys (include "mosaic-stack.kubernetesClusters" . | fromJson) | sortAlpha | toJson }}
}
openclaw.json: |-
{{ tpl (.Files.Get "files/openclaw-seed/nemoclaw.json") . | indent 4 }}
gpu-networking.SKILL.md: |-
Expand Down
5 changes: 5 additions & 0 deletions helm/mosaic-stack/templates/openclaw.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ spec:
rm -rf /home/node/.openclaw/identity /home/node/.openclaw/devices
mkdir -p /home/node/.openclaw/workspace/skills/gpu-networking
cp /seed/gpu-networking.SKILL.md /home/node/.openclaw/workspace/skills/gpu-networking/SKILL.md
documentation=/home/node/.openclaw/workspace/product-docs
rm -rf "$documentation" /home/node/.openclaw/workspace/skills/agent-documentation
mkdir -p "$documentation"
tar -xzf /seed/agent-documentation.tar.gz -C "$documentation"
cp /seed/agent-deployment.json "$documentation/deployment.json"
cp /seed/AGENTS.md /home/node/.openclaw/workspace/AGENTS.md
cp /seed/HEARTBEAT.md /home/node/.openclaw/workspace/HEARTBEAT.md
cp /seed/IDENTITY.md /home/node/.openclaw/workspace/IDENTITY.md
Expand Down
40 changes: 38 additions & 2 deletions helm/mosaic-stack/tests/chart-render.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ test("preloads selected skill instructions from their authoritative files", () =
}
});

test("packaged GPU documentation installs locally with research and UI disabled", () => {
test("packaged product and GPU documentation installs locally with research, UI and BCM disabled", () => {
const directory = mkdtempSync(join(tmpdir(), "skill-offline-"));
try {
execFileSync("helm", ["package", chart, "--destination", directory]);
Expand All @@ -1097,7 +1097,17 @@ test("packaged GPU documentation installs locally with research and UI disabled"
const seed = join(directory, "seed");
const home = join(directory, "home");
execFileSync("mkdir", ["-p", seed, home]);
const legacySkill = join(home, "workspace/skills/agent-documentation");
execFileSync("mkdir", ["-p", legacySkill]);
writeFileSync(join(legacySkill, "SKILL.md"), "Previous documentation skill");
const seedSection = output.split("name: openclaw-seed\n")[1].split("\n---")[0];
const binaryData = seedSection.split("binaryData:\n")[1].split("\ndata:")[0];
for (const line of binaryData.trim().split("\n")) {
const [name, encoded] = line.trim().split(": ");
writeFileSync(join(seed, name), Buffer.from(JSON.parse(encoded), "base64"));
}
execFileSync("mkdir", ["-p", join(home, "workspace/docs")]);
writeFileSync(join(home, "workspace/docs/existing-manual.md"), "Existing manual");
for (const line of seedSection.split("\n")) {
if (line.startsWith(" ") && !line.startsWith(" ") && line.endsWith(": |-")) {
const key = line.trim().slice(0, -4);
Expand All @@ -1109,6 +1119,16 @@ test("packaged GPU documentation installs locally with research and UI disabled"
.map(line => line.slice(14)).join("\n")
.replaceAll("/home/node/.openclaw", home).replaceAll("/seed/", `${seed}/`);
execFileSync("sh", ["-eu", "-c", script]);
assert.equal(existsSync(legacySkill), false);
const documentation = join(home, "workspace/product-docs");
const root = join(chart, "../..");
const sources = execFileSync("git", ["ls-files", "-z", "README.md", "docs"], {cwd: root, encoding: "utf8"})
.split("\0").filter(name => name.endsWith(".md"));
for (const source of sources) {
assert.deepEqual(readFileSync(join(documentation, source)), readFileSync(join(root, source)));
}
assert.equal(JSON.parse(readFileSync(join(documentation, "deployment.json"), "utf8")).modules.kubernetes, false);
assert.equal(readFileSync(join(home, "workspace/docs/existing-manual.md"), "utf8"), "Existing manual");
const relative = "skills/gpu-networking";
const workspace = join(home, "workspace");
const references = join(workspace, relative, "references");
Expand Down Expand Up @@ -1155,7 +1175,6 @@ test("configures optional Loki access without exposing its credential to UI or s
assert.doesNotMatch(defaults, /name: MOSAIC_LOKI_AUTHORIZATION/);
});


test("external inference can trust a site CA with or without the compatibility proxy", () => {
for (const enabled of [true, false]) {
const output = render("--set", `llm.mode=external,llm.external.baseUrl=https://inference.example.com/v1,llm.external.model=test-model,llm.requestCompatibility.enabled=${enabled},llm.external.caCertificateSecret=site-ca,llm.external.caCertificateKey=root.pem`);
Expand Down Expand Up @@ -1186,3 +1205,20 @@ test("configures a tenant-scoped VictoriaMetrics backend and native Grafana data
assert.ok(render(...args, "--show-only", template).includes(endpoint));
}
});

test("deployment reference reflects module settings without exporting credentials", () => {
for (const enabled of [true, false]) {
const seed = render("--set", `modules.kubernetes.enabled=${enabled}`,
"--set-string", "observability.grafanaAuth.existingSecret=private-secret-ref",
"--set-string", "modules.research.secrets.nvidiaApiKey=private-test-value",
"--show-only", "templates/openclaw-seed-configmap.yaml");
const text = configMapBlock(seed, "agent-deployment.json");
const profile = JSON.parse(text);
assert.equal(profile.modules.kubernetes, enabled);
assert.equal(profile.namespace, "mosaic-test");
assert.deepEqual(profile.kubernetesClusters, ["local"]);
assert.equal(text.includes("private-secret-ref"), false);
assert.equal(text.includes("private-test-value"), false);
assert.ok(Buffer.byteLength(seed) < 1048576);
}
});
Loading