diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a12ad02..9cf2eb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/docs/building.md b/docs/building.md index de4d66a..5ef1d46 100644 --- a/docs/building.md +++ b/docs/building.md @@ -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. diff --git a/docs/package_agent_docs.py b/docs/package_agent_docs.py new file mode 100644 index 0000000..571d2a9 --- /dev/null +++ b/docs/package_agent_docs.py @@ -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) diff --git a/helm/mosaic-stack/files/agent-documentation.tar.gz b/helm/mosaic-stack/files/agent-documentation.tar.gz new file mode 100644 index 0000000..4edf4b8 Binary files /dev/null and b/helm/mosaic-stack/files/agent-documentation.tar.gz differ diff --git a/helm/mosaic-stack/files/openclaw-seed/workspace/AGENTS.md b/helm/mosaic-stack/files/openclaw-seed/workspace/AGENTS.md index 882084f..10951bf 100644 --- a/helm/mosaic-stack/files/openclaw-seed/workspace/AGENTS.md +++ b/helm/mosaic-stack/files/openclaw-seed/workspace/AGENTS.md @@ -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. diff --git a/helm/mosaic-stack/templates/openclaw-seed-configmap.yaml b/helm/mosaic-stack/templates/openclaw-seed-configmap.yaml index 1894397..5d64d61 100644 --- a/helm/mosaic-stack/templates/openclaw-seed-configmap.yaml +++ b/helm/mosaic-stack/templates/openclaw-seed-configmap.yaml @@ -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: |- diff --git a/helm/mosaic-stack/templates/openclaw.yaml b/helm/mosaic-stack/templates/openclaw.yaml index efeaa96..ddf835d 100644 --- a/helm/mosaic-stack/templates/openclaw.yaml +++ b/helm/mosaic-stack/templates/openclaw.yaml @@ -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 diff --git a/helm/mosaic-stack/tests/chart-render.test.mjs b/helm/mosaic-stack/tests/chart-render.test.mjs index 999031f..d342e03 100644 --- a/helm/mosaic-stack/tests/chart-render.test.mjs +++ b/helm/mosaic-stack/tests/chart-render.test.mjs @@ -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]); @@ -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); @@ -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"); @@ -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`); @@ -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); + } +});