install-kube-agents
Install, set up, or deploy kube-agents (the Kubernetes Agentic Harness) and its Platform Agent onto a GKE cluster in a GCP project, interactively or non-interactively. Use when asked to install, bootstrap, or onboard kube-agents, or to plan one with a dry run.
- License 许可
- Apache-2.0
- 最近更新
- 2026-11-12
- Stars
- ★ 126
- Forks
- 43
01 前言 Front Matter
- name
- install-kube-agents
- description
- Install, set up, or deploy kube-agents (the Kubernetes Agentic Harness) and its Platform Agent onto a GKE cluster in a GCP project, interactively or non-interactively. Use when asked to install, bootstrap, or onboard kube-agents, or to plan one with a dry run.
03 SKILL.md 正文
install-kube-agents Skill
Install kube-agents for an operator: detect their environment and interview them for the choices
that shape the install, preview it with --dry-run, and apply only after they confirm. Follow
Install workflow.
What install.sh actually does
It is a front-end, not a second provisioner. It loads install.env (the install's
hand-authored configuration), collects anything still missing, generates
terraform/examples/full-install/terraform.tfvars from the result, and then runs the composition's
lifecycle.sh apply — the Terraform root in
terraform/examples/full-install/ owns every
GCP resource and installs the Helm chart (charts/kube-agents) that owns every Kubernetes one.
The chart installs the operator and the PlatformAgent into the kubeagents-system namespace
(DEFAULT_NAMESPACE; --agent-namespace overrides it).
Terraform state goes to a GCS bucket (<project>-kube-agents-tfstate, versioned, prefix
kube-agents/<cluster>), so uninstall.sh and upgrade.sh can find the install from a fresh
clone. The installer sources
scripts/installer/installer_common.sh before its
first prompt, so its defaults and accepted values are the ones defined there; that file is where a
default changes.
Order of operations: resolve the image/source ref → check CLI prerequisites (including
terraform, which it offers to install; make is not needed) → put the repository on disk and
verify it against that ref → load install.env → interview for what is missing → generate
terraform.tfvars → refuse a service account another install in the project owns
(check_service_account_ownership, before the summary and the dry-run exit) → run
lifecycle.sh apply. The source check happens before the interview, so a bad ref fails in
seconds rather than after a dozen answers. Some steps stay gcloud calls outside the apply — before
it, CMEK, the Workload Identity pool and NetworkPolicy enforcement on a pre-existing cluster; after
it, the managed-OTel scope on a cluster it created — and the GitHub App PEM import runs through
the Minty CLI so the key never enters Terraform state. Re-running the installer (or its --menu
Day-2 panel's Save & Apply) reconciles every change through one terraform apply.
Install workflow
Run the three stages in order. Stages 1 and 2 create and change nothing in the project or cluster;
nothing is applied until the operator confirms in stage 3. Pass --non-interactive to every
install.sh call: a tool call has no terminal for the installer's /dev/tty prompts, so the
interview happens in chat instead.
After every install.sh call, read the exit code before /tmp/kube-agents-install-report.json.
Several exits (the service-account ownership check, input validation, a missing tool under
--dry-run, the GitHub App PEM import) write no report and leave an earlier run's in place.
Resolve the latest stable release tag from GitHub Releases
(e.g. 0.4.0; a GitLab install needs 0.9.0 or later) first, and substitute it for <RELEASE_VERSION> in every command you run or hand
over; never leave the placeholder unrendered.
Stage 1: Detect and interview
Read the environment; change nothing:
gcloud config get-value project
gcloud config get-value compute/region
gcloud auth application-default print-access-token >/dev/null && echo "ADC: ok"
gcloud container clusters list --project <PROJECT_ID>
gcloud storage ls gs://<PROJECT_ID>-kube-agents-tfstate/kube-agents/
No ADC: ok means no Application Default Credentials. Stage 3 cannot run without them unless one
of the google provider's credential variables is set — GOOGLE_OAUTH_ACCESS_TOKEN,
GOOGLE_CREDENTIALS, GOOGLE_CLOUD_KEYFILE_JSON or GCLOUD_KEYFILE_JSON, which Terraform reads
before ADC (stage 2 says why). With none of them set, have the operator run
gcloud auth application-default login now.
A prefix in that bucket means an install already exists in the project, and so does an
install.env where the installer looks for one: $KUBE_AGENTS_INSTALL_ENV, beside install.sh,
the current directory, or $HOME/kube-agents/ (where the curl | bash form clones). Ask whether
the operator wants upgrade-kube-agents instead. A re-run reads
that install.env; scripts/installer/README.md owns the
precedence between it, flags and defaults.
Propose the detected value or the default for each decision below, and have the operator confirm or change it. Never ask for a secret in chat: the installer reads each one from the environment it runs in, where the operator sets it.
- Project and region —
--gcp-project-id,--gcp-region. Defaults: the activegcloudproject, and itscompute/regionor, when that is unset,DEFAULT_REGION. - Cluster — create one (
--gke-cluster-name, defaultDEFAULT_CLUSTER_NAME; Autopilot unless--gke-cluster-mode=standardor a zonal region) or install onto one from the list. An existing cluster can need changes; stage 2 lists them. - Model provider —
--model-provider.vertex_aiauthenticates through Workload Identity and needs no API key;gemini,anthropicandopenaireadGEMINI_API_KEY,ANTHROPIC_API_KEYorOPENAI_API_KEY, andgeminialso finds a key stored in Secret Manager. The default isDEFAULT_MODEL_PROVIDER; pass the flag for any other choice.openai_compatiblepoints the gateway at the operator's own OpenAI-compatible server and has no defaults: ask for the served model name (--model-default-name), the base URL with/v1(--openai-compatible-api-base), and the server pod's port for an in-cluster Service (--openai-compatible-target-port) or the CIDRs of a private address (--openai-compatible-egress-cidrs). The key (OPENAI_COMPATIBLE_API_KEY) and a CA Secret for a private CA are optional.sap(SAP AI Core) and any other LiteLLM provider slug read their credentials from a Secret the operator creates in the agent's namespace and names with--litellm-env-secret(never ask for the service key itself); a slug outside the menu also needs--model-default-name. The same flag names the Secret holding anopenai_compatiblegateway's OAuth2 client credentials. - How the operator reaches the agent — a terminal when no chat flag is passed (see
Handing over a chat-less install),
--enable-google-chat, or--enable-slack(tokens inSLACK_BOT_TOKENandSLACK_APP_TOKEN).--enable-web-consoleadds a browser chat page for chat-less installs, reached by port-forward (Web console).--enable-hermes-dashboard=trueadds the Hermes Web UI, which a port-forward cannot reach under gVisor; the PlatformAgent reference gives its access path. - Where the GitOps repository lives, only if the operator wants GitOps pull requests. Ask
which forge holds it. On GitHub, pass
--gitops-org,--gitops-repoand--github-app-id; see GitOps Repository & GitHub Token Minter Configuration. On GitLab (gitlab.com or a self-managed instance), pass--gitops-forge=gitlab,--gitops-repoand, for a self-managed instance,--gitops-host; see GitOps Repository on GitLab. Never combine the GitHub App flags with--gitops-forge=gitlab: the installer refuses them. On a GitHub Enterprise Server, pass the GitHub flags and--gitops-host(and--gitops-ca-filefor a private CA); see GitHub Enterprise Server. - Whether scale-up failures get investigated —
--enable-stockout-investigator(defaultfalse; it also turns on the Pub/Sub adapter). Always offer it. On, the cluster autoscaler'snoScaleUplogs for the install's own cluster reach the Platform Agent, which diagnoses the Pending workload (quota, ComputeClass fallbacks, capacity advice) and opens a remediation pull request. Off, an unschedulable pod is still triaged from itsFailedSchedulingevents, but nothing reads the autoscaler's verdicts or proposes a fix. It adds a Log Router sink, a Pub/Sub topic and subscription, and grants (publisher for the sink, subscriber and viewer for the agent); settingENABLE_STOCKOUT_INVESTIGATOR=falseininstall.envremoves them on the next run. A pull request needs the GitOps repository from decision 5, and a chat post needs a home channel (--google-chat-home-channelor--slack-home-channel): undernext, a proactive post with no home channel is refused and kept for a digest (or goes to Slack when both platforms are on); the gateway logs the refusal and thePlatformAgentcarriesHomeChannelUnset.
Leave --permission-set at read-only unless the operator asks for custom. The flags in
Adopting a cluster the operator already owns are
the operator's decision, never a default you fill in.
Stage 2: Dry run
Run the installer with the agreed flags and --dry-run. It validates the Terraform configuration
and, when Application Default Credentials exist, runs terraform plan against local state; it
creates nothing, the state bucket included:
curl -fsSL https://raw.githubusercontent.com/gke-labs/kube-agents/<RELEASE_VERSION>/install.sh | bash -s -- \
--dry-run \
--non-interactive \
--gcp-project-id="YOUR_GCP_PROJECT_ID" \
--gke-cluster-name="platform-agent-host" \
--gcp-region="us-central1" \
--model-provider="gemini" \
--permission-set="read-only"
A dry run regenerates terraform.tfvars, so back up a real deployment's copy first. It writes no
install.env: a dry run provisions nothing, so it has no install to record, and an existing one is
never rewritten. It also installs no missing tool — a machine without any installer prerequisite
(terraform, helm, gh, gke-gcloud-auth-plugin and the rest) fails it with
Dry-run validation will not install missing tools, naming the tool; install it and re-run.
Go through the output with the operator:
DRY_RUN_SUCCESSin/tmp/kube-agents-install-report.jsondoes not clear an existing cluster. The dry run exits before the consent gates, so it never reportsREFUSED_*.- On an existing cluster, the
Existing Cluster Mutations (Adoption)summary and anyDry-run: skipping terraform plan because ...warning are the questions to put to the operator, as Adopting a cluster the operator already owns sets out. No Application Default Credentials; skipping the resource previewmeans no plan ran, and on an existing cluster the NetworkPolicy and node-pool checks were skipped too, so their warnings are missing. Have the operator rungcloud auth application-default loginand repeat stage 2. Do not go to stage 3 without ADC or one of the provider's credential variables (GOOGLE_OAUTH_ACCESS_TOKEN,GOOGLE_CREDENTIALS,GOOGLE_CLOUD_KEYFILE_JSON,GCLOUD_KEYFILE_JSON): the apply's Terraform needs one, the installer's own auth check tests neither, and on an existing cluster the Workload Identity, CMEK and NetworkPolicy changes run before Terraform starts — the install fails after making them. A credential variable does not bring the preview back, since the dry run's plan gate checks ADC alone.No ... API key was providedis a warning, not a failure: the install would finish with an agent that cannot call its model. Resolve the key before stage 3.
When the operator would rather run the apply themselves, use --generate-only
instead: it runs the consent gates and reports REFUSED_* itself.
Stage 3: Confirm and apply
When the operator chooses an adoption flag in stage 2, re-run stage 2 with it: the flag changes both
the adoption summary and whether terraform plan runs at all, so the first preview no longer shows
what will run. Summarize the project, the cluster (new or existing, and each change the run makes to
an existing one), the namespace, the model provider and how the operator reaches the agent. Wait
for an explicit go-ahead. Then run the last stage 2 command without --dry-run:
curl -fsSL https://raw.githubusercontent.com/gke-labs/kube-agents/<RELEASE_VERSION>/install.sh | bash -s -- \
--non-interactive \
--gcp-project-id="YOUR_GCP_PROJECT_ID" \
--gke-cluster-name="platform-agent-host" \
--gcp-region="us-central1" \
--model-provider="gemini" \
--permission-set="read-only"
- Non-zero exit: the install did not finish. Relay the error the installer printed. A
REFUSED_*status leaves the cluster unchanged — relay the options and stop. SUCCESS, orSUCCESS_PENDING_ROLLOUT(applied, but a deployment had not reported ready; relay thekubectl rollout statuscommand the installer printed): relay how to reach the agent — the terminal commands in Handing over a chat-less install (including thekubectl port-forwardcommand for the web console when--enable-web-consolewas enabled), which also fetch credentials, or the chat platform the operator enabled — then confirm the pods withkubectl get pods -n kubeagents-system. Relaynetwork_policy_enforcementwhen it is notenforced(Machine-Readable Results).
The kubectl commands in this skill assume the default namespace; under --agent-namespace,
substitute its value.
Local sources and pre-confirmed runs
The same invocation runs from the official release bundle (recommended) or a checkout pinned to the
release tag; add --dry-run for stage 2 exactly as above. Run an apply without stages 1 and 2 only
when the operator has already confirmed these exact flags, or in CI.
curl -fsSL https://github.com/gke-labs/kube-agents/releases/download/<RELEASE_VERSION>/kube-agents-<RELEASE_VERSION>.tar.gz | tar -xz
cd kube-agents-<RELEASE_VERSION>
./install.sh --non-interactive \
--gcp-project-id="YOUR_GCP_PROJECT_ID" \
--gke-cluster-name="platform-agent-host" \
--gcp-region="us-central1" \
--model-provider="gemini" \
--permission-set="read-only"
If a Git checkout is required, clone pinned to the release tag:
git clone --branch <RELEASE_VERSION> https://github.com/gke-labs/kube-agents.git
cd kube-agents
./install.sh --non-interactive \
--gcp-project-id="YOUR_GCP_PROJECT_ID" \
--gke-cluster-name="platform-agent-host" \
--gcp-region="us-central1" \
--model-provider="gemini" \
--permission-set="read-only"
Do not clone main to deploy an official release: manifests and CRD schemas on main evolve continuously and diverge from released container images. Running install scripts against a mismatched checkout will fail verify_local_source_ref to prevent deploying incompatible manifests.
Generate-Only Mode
To generate configuration files (install.env and terraform.tfvars), run pre-apply validation checks, and hand off the apply to the operator without creating or mutating cloud resources, use --generate-only:
curl -fsSL https://raw.githubusercontent.com/gke-labs/kube-agents/<RELEASE_VERSION>/install.sh | bash -s -- \
--generate-only \
--non-interactive \
--gcp-project-id="YOUR_GCP_PROJECT_ID" \
--gke-cluster-name="platform-agent-host" \
--gcp-region="us-central1"
In --generate-only mode, the installer:
- Writes
install.env(if absent) andterraform/examples/full-install/terraform.tfvars. One refusal lives inside this step rather than after it: if the cluster already exists, nothing stated a memory mode (no--memory=, noMEMORYininstall.env), and the run cannot ask the cluster whether it is running the Hindsight memory store, the generator stops instead of defaulting to a value whose apply would delete the store's database. That applies under--generate-onlyand--dry-runas well, because both write the same realterraform.tfvarsand--generate-onlyexists to hand it tolifecycle.sh apply. It exits 1 having written noterraform.tfvarsat all — the opposite of the step-2 refusals below. Pass--memory=hindsight|file|off, recordMEMORYininstall.env, or restore access to the cluster. - Runs the same pre-apply validation checks a real run does: the GitOps organization check, the service-account ownership check, the scope container preflight when a folder or organisation is declared, which on this route, and on the interactive
ganswer, only warns (the apply it hands off may run as another identity; a run that applies,-yor the interactiveY, refuses, exit 1 with no report status like the ownership check, when the credentials Terraform applies with cannot set IAM policy on a container or an enforced organisation policy forbids the Cloud Asset API), the scope export sink preflight when the drift ingress is on andSCOPE_PROJECTSlists a project the drift sinks will cover, which warns and refuses on the same routes when those credentials cannot create a Log Router sink (logging.sinks.create) in one of them (a project whose sink the install's Terraform state already holds warns instead when the credentials can read it), and the existing-cluster node-pool and NetworkPolicy consent gates, but not the live-scope check, which needs the install's kubeconfig context and runs only on a run that will apply; the handoff says so. The interactiveganswer is given after the pre-flight summary, by which point that check has run too, so the two routes differ in that one check. A cluster needing--migrate-node-pools, or one enforcing no NetworkPolicy that was given neither--enable-network-policynor--accept-no-network-policy(or that combines--enable-web-consolewith--accept-no-network-policy), is refused (REFUSED_MISSING_NODE_POOL_MIGRATION,REFUSED_MISSING_NETWORK_POLICY,REFUSED_WEB_CONSOLE_WITHOUT_NETWORK_POLICY), as is one that cannot be described (FAILED_PREFLIGHT_CLUSTER_UNREADABLE). AREFUSED_*status is a question for the operator, not a flag to add: see Adopting a cluster the operator already owns. Step 1 has already written both files by then, so a refusal exits 1 leavinginstall.envandterraform.tfvarson disk — unvalidated, and with no handoff printed. Do not read the presence ofterraform.tfvarsas success; read the report status. - Prints a checklist of out-of-Terraform prerequisites (CMEK database encryption, Workload Identity, NetworkPolicy, GitHub App PEM import, OTel scope, and, with Google Chat or Slack on, the chat setup checklist commands to run after the apply) and the
lifecycle.sh applycommand with remote state variables (KUBE_AGENTS_STATE_BUCKETandKUBE_AGENTS_STATE_PREFIX). - Exits 0 with status
GENERATE_ONLY_SUCCESSin/tmp/kube-agents-install-report.json, or exits 1 with theREFUSED_*/FAILED_PREFLIGHT_*status from step 2.
On a GitLab install, step 2 skips the GitOps organization check: there is no GitHub organization.
Generate-only never creates the Secrets (the token Secret, and the CA Secret for a private CA). On a
first install it writes the GitLab keys into install.env. Over an existing install.env it does
not change them: have the operator set GITOPS_FORGE, GITOPS_HOST, GITOPS_REPO,
GITLAB_TOKEN_SECRET and, for a private CA, GITOPS_CA_SECRET there before
lifecycle.sh apply. Otherwise the next upgrade.sh removes the GitLab forge. After
lifecycle.sh apply, the person who runs the install creates the Secrets by hand (Terraform only
names them), as
Dry run and generate-only
shows.
The interactive wizard also offers the same choice by answering g at the final confirmation step.
Adopting a cluster the operator already owns
Installing onto a cluster somebody else made can require changing that cluster, and modifying a
cluster the operator already owns is never a decision you make on your own. The pre-flight
summary (Existing Cluster Mutations (Adoption), also printed by --dry-run) lists every change
the run would make; on an agent-driven run there is no interactive prompt, so the flags are the
consent and you must obtain it before passing one. Present each pending change with its cost and ask:
- Workload Identity pool — enabled on the control plane without a flag when missing. Non-revertible. Say so before running a real install.
- CMEK database encryption — enabled without a flag when missing: a Cloud KMS key ring and key
(permanent), the Cloud KMS API, and a control-plane update.
ALLOW_UNENCRYPTED_SECRETS=trueskips it. Say so before running a real install. - Legacy node pools (
REFUSED_MISSING_NODE_POOL_MIGRATION) — two answers:--migrate-node-poolsrecreates every node on those pools and restarts the workloads on them, kube-agents' or not; or stop. There is no install without Workload Identity. - No NetworkPolicy enforcement (
REFUSED_MISSING_NETWORK_POLICY) — three answers (or when--enable-web-consoleis enabled,--enable-network-policy,--enable-web-console=falsepaired with--accept-no-network-policy, or stop, since the web console requires NetworkPolicy enforcement and cannot be combined with--accept-no-network-policy), and you present the applicable ones:--enable-network-policyenables the legacy Calico addon: a control-plane update that may recreate node pools and restart workloads unrelated to kube-agents.--accept-no-network-policyinstalls without enforcement (unavailable when--enable-web-consoleis enabled unless--enable-web-console=falseis also passed to turn the web console off). The cluster is not modified. Every NetworkPolicy kube-agents ships is inert: the agent pod's egress confinement, the shell sandbox's deny-all (the sandbox is where model-authored commands run), and the LiteLLM, minter and Hindsight policies. The confinement lost is kube-agents' own, not the operator's workloads'; "we trust the workloads in this cluster" does not answer it. Recorded in the report and on thePlatformAgent.- Stop. The cluster is unchanged.
- Web console without NetworkPolicy enforcement (
REFUSED_WEB_CONSOLE_WITHOUT_NETWORK_POLICY) — emitted when--enable-web-console(WEB_CONSOLE_ENABLED=true) is combined with--accept-no-network-policy(ACCEPT_NO_NETWORK_POLICY=true) against a cluster that still enforces no NetworkPolicy. Present the operator's choices: enable NetworkPolicy enforcement (--enable-network-policyand removeACCEPT_NO_NETWORK_POLICY=truefrominstall.env), disable the web console (--enable-web-console=false/WEB_CONSOLE_ENABLED=false), or stop. - gVisor node pool — on an existing Standard cluster
--enable-gvisor=true(the default) adds a billablegvisor-poolnode pool. Say so;--enable-gvisor=falseruns the agent unsandboxed instead.
A refusal is not a failure to route around: a REFUSED_* status with an unchanged cluster is the
installer doing its job. Report it, relay the options above, and pass a flag only when the operator
has chosen. When they choose --accept-no-network-policy and an install.env already exists, tell
them to add ACCEPT_NO_NETWORK_POLICY=true to it (the installer prints the same instruction): the
next upgrade.sh regenerates from that file and is refused without the key. When they later
confine the cluster, tell them to remove the key again; the installer warns while it lingers.
A GitHub token minter that this install did not make (one from make deploy-github, by hand, or
from another Helm release) also stops a run, with exit 1 and no REFUSED_* status, in two cases.
The run names the GitOps repository with keys that do not match the PlatformAgent (it prints the
lines to set), or the run deploys the chart's own minter over the other one. In the second case,
ask the operator whether to remove the other minter or to keep it with GITHUB_APP_ID empty. See
"When an upgrade is refused" in the upgrade page.
Source verification
Before provisioning, the installer requires the checkout holding the Terraform configuration and
chart to be at the same commit as --image-tag and to have no uncommitted changes — the install
sources and the container image must come from one revision. A dirty or mismatched checkout aborts with instructions.
--allow-unverified-source (or ALLOW_UNVERIFIED_SOURCE=true) downgrades that to a warning; use it
when iterating on the installer itself, not for a deployment you intend to keep. --dry-run is
lenient already.
GCP IAM permission sets
--permission-set chooses which GCP IAM role bundle the composition grants the agent's GSA (its
permission_set variable; custom becomes a project_roles list). It does not affect
Kubernetes RBAC, which is read-only in every set, and it does not gate the GitOps pull-request
path, which works in every set. See the site's
security and IAM reference.
| Set | Grants |
|---|---|
read-only |
Auditing and observability: read roles, no write to infrastructure. Default. |
custom |
Exactly the roles passed in --custom-roles; no built-in bundle. |
Machine-Readable Results
Upon completion, install.sh generates a machine-readable JSON status report at /tmp/kube-agents-install-report.json:
{
"status": "SUCCESS",
"dry_run": false,
"generate_only": false,
"non_interactive": true,
"project_id": "YOUR_GCP_PROJECT_ID",
"cluster_name": "platform-agent-host",
"timestamp": "2026-08-05T03:35:00Z"
}
The full report also carries gvisor_enabled, memory_mode, and network_policy_enforcement. A
report written before the run decided them says so: gvisor_enabled is null before the interview,
and the other two are empty, rather than restating a default the run never applied. For
network_policy_enforcement that covers a run that failed early, a --dry-run (it never reaches the
cluster step; only with --accept-no-network-policy against a cluster that enforces nothing does it
report absent-accepted), and a --generate-only run under --enable-network-policy, which has
not enabled anything yet.
network_policy_enforcement is enforced (Dataplane V2 or Calico, or a cluster this run created),
enabled-by-install (this run turned Calico on, under --enable-network-policy), or
absent-accepted (installed without enforcement, under --accept-no-network-policy); the last is
also stamped onto the PlatformAgent as kubeagents.x-k8s.io/network-policy-enforcement, so it
outlives the report. Relay it to the operator in either of the last two cases.
Handing over a chat-less install
Chat is opt-in and both platforms default off, and a non-interactive run with no
--enable-google-chat or --enable-slack selects "None" — so the common agent-driven install
has no chat and the operator has no way to reach the agent unless you give them one. Under
spec.mode: next the PlatformAgent carries the NoChatPlatform condition, which says where the
findings go instead. The installer prints the way in at the chat-configuration step and again when it finishes, under
--- [Talking to the Agent from a Terminal] ---, with the cluster, region, project and namespace
already substituted. Relay those two commands verbatim (plus the Web Console kubectl port-forward
command and http://localhost:8080 URL when --enable-web-console was enabled):
gcloud container clusters get-credentials <CLUSTER_NAME> --location <REGION> --project <PROJECT_ID> --dns-endpoint
kubectl exec -it deployment/platform-agent-gateway -n kubeagents-system -c platform-agent -- hermes -p platform
- Take
--dns-endpointfrom what the installer printed rather than adding it:gcloudrejects it on a cluster publishing no externally reachable DNS endpoint, and the installer omits it there. -p platformreaches the Platform Agent. A barehermesreaches the Planning Agent front door, which is where a chat message would have landed.kubectl port-forwardto the agent pod is not an alternative: the agent is sandboxed under gVisor by default and the forward cannot see into the sandbox.kubectl execenters it. (When--enable-web-consoleis enabled, the separate web console pod does not run under gVisor and is reached via thekubectl port-forward svc/kube-agents-web-console -n kubeagents-system 8080:8080command the installer prints.)- Say what the operator does not get until they enable a platform: scheduled reports and
alert-driven triage reach chat only; meanwhile the audit reports stay in the report store on the
data volume and the triage runs on the board (the
NoChatPlatformcondition says so), and the first-run inventory report waits a day for a human connecting over chat — read it meanwhile withkubectl exec platform-agent-shell-0 -n kubeagents-system -c shell -- cat /opt/data/INVENTORY.md(the report is on the shell sandbox pod, not the agent pod); a day after the sweep it is filed as an issue labelledagent:onboardingin the GitOps repository when that is on GitHub (else in the install's one managed GitHub repository), and a report that could not be filed is retired a week after the sweep (INSTALL.md, "What to Expect After Installation").
GitOps Repository & GitHub Token Minter Configuration
When deploying kube-agents with GitOps pull-request workflows enabled, the Platform Agent creates pull requests against an infrastructure-as-code repository via the GitHub Token Minter (minty). The minter runs in-cluster and signs short-lived GitHub App installation tokens using a private key securely stored in Google Cloud KMS. (For GitLab or self-managed Gitea GitOps repositories, which use a Kubernetes Secret token instead of the GitHub App token minter, see GitLab as the GitOps forge and Gitea as the GitOps forge.)
Prerequisites
The GitHub App and its permissions, the private key, the Cloud KMS signing key and its default names, and the Go toolchain the import needs are documented once, in Token minter. Read it before running either path below. Restating those values here is how the two copies drift apart, and this file already defers the same way for flag defaults.
One input decides whether the minter can work at all, so it is worth stating where the command is: --gitops-org must be a GitHub organization. Minty resolves App installations at /orgs/{org}/installation, which returns 404 for a personal account, and install.sh refuses one rather than deploying a minter that can never mint a token.
The commands below are stage 2 dry runs showing the GitOps flags. Replace the placeholders and example values with the operator's values from stage 1, review the output as stage 2 describes, and apply through stage 3 once the operator confirms — Path 1 creates a Cloud KMS key ring and key, which cannot be deleted.
Deployment Path 1: Automated Import via install.sh
In this path, install.sh automatically creates the Cloud KMS keyring/key (if missing) and imports the GitHub App private key using the Minty CLI before Terraform applies:
./install.sh --dry-run --non-interactive \
--gcp-project-id="YOUR_GCP_PROJECT_ID" \
--gke-cluster-name="platform-agent-host" \
--gcp-region="us-central1" \
--model-provider="gemini" \
--gitops-org="YOUR_GITHUB_ORG" \
--gitops-repo="YOUR_GITOPS_REPO" \
--github-app-id="YOUR_GITHUB_APP_ID" \
--github-pem-path="/path/to/app-private-key.pem"
Delete the .pem once the stage 3 apply succeeds. Cloud KMS keys cannot be destroyed, and a later run or upgrade finds the ENABLED version and skips the import.
Deployment Path 2: Pre-Provisioned / Ahead-Of-Time (AOT) Key
For CI/CD pipelines and anywhere runners must not handle raw private keys, the key is imported ahead of time — the procedure, and the keyring and key names install.sh expects, are in Token minter. Once the key holds an ENABLED version, invoke install.sh without --github-pem-path:
./install.sh --dry-run --non-interactive \
--gcp-project-id="YOUR_GCP_PROJECT_ID" \
--gke-cluster-name="platform-agent-host" \
--gcp-region="us-central1" \
--model-provider="gemini" \
--gitops-org="YOUR_GITHUB_ORG" \
--gitops-repo="YOUR_GITOPS_REPO" \
--github-app-id="YOUR_GITHUB_APP_ID" \
--kms-keyring="YOUR_KEY_RING" \
--kms-key="YOUR_KEY"
Pass both KMS flags with the names the operator pre-provisioned. A pre-provisioned key carries no owner labels, and the installer adopts one only when both names are given explicitly.
GitHub Enterprise Server
If the operator's GitOps repository is on a GitHub Enterprise Server (a GitHub instance at a host they run, not github.com), install.sh, upgrade.sh and uninstall.sh install, upgrade and remove it, with --gitops-host and, for a private CA, --gitops-ca-file. Follow GitHub Enterprise Server in INSTALL.md with them. Its steps and their order:
- Decide the cluster. A new cluster goes into the project's
defaultVPC network (the installer has no network setting). If the instance is reachable only from another VPC or a Shared VPC, the operator creates the cluster in that VPC, and you install onto it with--gke-cluster-name. - On an existing cluster, check the network path now (DNS and HTTPS on port 443) with the two
kubectl runcommands in INSTALL.md. On a new cluster, run them right after the install. A private instance needs a route, firewall rules and, for a private DNS zone, a Cloud DNS forwarding zone; those are the operator's to set up. - The operator creates the GitHub App on the instance (not on github.com), with the same permissions as on github.com and no webhook, installed on the organization, the GitOps repository and each context repository, and downloads its private key. The repository must exist and belong to an organization.
- Run
install.sh --dry-runwith--gitops-host=<host>,--gitops-org,--gitops-repo(the name, not a URL),--github-app-id,--github-pem-pathand, only for a private CA,--gitops-ca-file=<regular PEM file>. Show the operator the summary. Then run it without--dry-run. The installer imports the App's key into Cloud KMS before the apply, writes the CA into the Secretgithub-forge-caafter the apply, and restarts the token minter so that it reads the CA. - Check the minter's
SOURCE_SYSTEM_API_BASE_URLand the gitops-state ConfigMap'sgithub_hostwith the commands in INSTALL.md.
Rules for the agent:
--gitops-hostis a lowercase hostname with no scheme, path or port. Do not give it for github.com, except to switch an install back to github.com. The installer refuses a host undergithub.com,githubusercontent.com,ghe.comor another domain where GitHub runs its own services, andgitlab.com.- Do not give
--gitops-ca-filewhen a public CA signed the certificate. Do not use/dev/stdinor a process substitution for it. - The organization check asks the instance with the CA and sends no credential. On an instance in private mode it warns that it cannot verify the organization and goes on.
- To upgrade, run
upgrade.sh. To remove, rununinstall.sh. Both read the forge frominstall.env(GITOPS_FORGE=github,GITOPS_HOST,GITOPS_CA_SECRET). - A switch between github.com and an instance needs the App on the new host and a new
--kms-key, becauseinstall.shimports a key only into a key with noENABLEDversion. The installer refuses a change of host that keeps the recorded App or key. INSTALL.md, "Switch between github.com and a GitHub Enterprise Server", gives the command.--gitops-host=github.comswitches back. After a switch,platform-agent-gitops-statestill lists the old host's repositories (the operator never removes an entry). Remove them as INSTALL.md says in the same section; after a switch back to github.com this is required, because an entry on the instance holds the minter's policy pruning (Degraded,MinterPruningHeld). - With
--generate-only, the operator creates the CA Secret and restarts the minter afterlifecycle.sh apply; INSTALL.md gives the commands. Over an existinginstall.env, generate-only does not record the forge keys: have the operator setGITOPS_FORGE=github,GITOPS_HOSTand, for a private CA,GITOPS_CA_SECRETthere beforelifecycle.sh apply, or the nextupgrade.shrenders github.com. - When
install.envrecordsGITOPS_FORGE=gitlab, also pass--gitops-forge=githubto switch to an instance.
Ask the operator for: the instance's hostname (port 443 only), the organization and repository on it, the App ID and the path of its private key, the CA file if a private CA signed the certificate, whether the cluster exists, and which VPC reaches the instance. An install serves one GitHub host, so this install has no github.com repositories. If the operator does not want install.sh, INSTALL.md also has the Terraform + Helm method.
GitOps Repository on GitLab
A GitLab install has no GitHub App and no token minter. The credential broker holds a GitLab access
token in a Kubernetes Secret, and the agent and its shell sandbox never get it.
GitLab as the GitOps forge has the
procedure, the network, DNS and certificate rules, a reachability test, the connection check,
token expiry and rotation, and the install.env keys. Read it before the dry run.
Rules for the agent:
- Use release 0.9.0 or later. An earlier release has no
--gitops-forgeflag. - The operator creates the GitOps project first. The installer does not create it.
--gitops-repois the project's full path with its nested groups (group/subgroup/project), or itshttps://URL on that host. Put the host in--gitops-host, not in the path. Leave--gitops-hostout for gitlab.com.- The broker uses the token only for the declared repositories. Another project in the
same group gets no write token.
inspect-repositoryreads a public project with no token, and a private one only when it is a context repository and a read-only token is stored. A project outside those groups is refused. Tell the operator to use the token with the smallest reach. That is a project access token on the GitOps project, or a group access token on the project's own group. Forplatform/infra/gitops, that group isplatform/infra, notplatform. Tell the operator to record the token's expiry date, and to check the token before the install, as the GitLab page shows. - Do not ask the operator to paste the token. Do not put the token in a flag value, a variable or
the transcript. Use one of these:
- Do not give
--gitlab-token-file. The install completes and prints the command that creates the Secret, and the operator runs it. Until then, GitLab calls answerFORGE_CREDENTIAL_UNAVAILABLE. No restart is necessary afterwards. - The operator gives a file path or a password-manager command, which you pass as
--gitlab-token-file=<(…). The installer reads the whole source and removes all whitespace, newlines inside it too. The source must hold only the token. For apassentry with more lines, use<(pass show gitlab/agent-token | head -n1). The operator unlocksgpg-agentbefore the run. Do not use/dev/stdinundercurl … | bash, because standard input is the script there.
- Do not give
- Only when the operator wants the agent to read a private GitLab context repository, also set
GITLAB_READ_TOKEN_FILEto a source that holds a read-only token (Reporter role,read_repositoryscope only), with the same rules as the token source above. It must not be the same source as--gitlab-token-file. Use a release whoseinstall.env.examplelistsGITLAB_READ_TOKEN_FILE; an older release ignores it. The GitLab page's "Private context repositories" section has the setup. - Only when a private CA signed the instance's certificate, add
--gitops-ca-fileto the dry run and to the stage 3 run. Do not give it for gitlab.com or for a public CA. Ask the operator for the CA certificates (public; never a private key) as a regular PEM file: the root CA, and the intermediate CAs if the server does not send them. Do not use/dev/stdinor a process substitution for the CA. The installer refuses the flag without a self-managed--gitops-host. Use a release whoseinstall.sh --helplists--gitops-ca-file. - The dry run checks that the token source is readable, and stops if a regular token file is
empty. It stores no token, and
install.shnever connects to GitLab. Only the stage 3 run reads the token into the Secret, after the apply. - On a cluster that already exists, have the operator run the network-path test from the GitLab
page before stage 3, in the
defaultnamespace:kubeagents-systemdoes not exist yet. On a new cluster, run it after stage 3, inkubeagents-system. After stage 3, run the connection check from the same page.
The command below is a stage 2 dry run. Apply through stage 3 once the operator confirms:
./install.sh --dry-run --non-interactive \
--gcp-project-id="YOUR_GCP_PROJECT_ID" \
--gke-cluster-name="platform-agent-host" \
--gcp-region="us-central1" \
--model-provider="gemini" \
--gitops-forge=gitlab \
--gitops-host="gitlab.example.com" \
--gitops-repo="platform/infra/gitops" \
--gitlab-token-file=<(pass show gitlab/agent-token | head -n1)
Pass the same --gitlab-token-file source, and the same GITLAB_READ_TOKEN_FILE if you set one, to
the stage 3 run. The dry run only checks them.
Supported Command-Line Flags
Defaults marked "installer_common.sh" reach the installer through
scripts/installer/installer_common.sh; the values themselves are listed in
install.defaults.env at the repository root, not here. Run ./install.sh --help for the authoritative list.
- Over an existing
install.env, do not pass a chat flag (--enable-slack,--enable-google-chat,--slack-*,--google-chat-allowed-users,--google-chat-home-channel,--google-chat-mode,--chat-topic-name) to change a key the file sets: it is refused. Have the operator edit the key, then re-run without the flag. A key the file lacks is recorded with the value the run applies; Slack tokens never are.scripts/installer/README.mdhas the rule.
| Flag | Description | Default |
|---|---|---|
-y, --non-interactive |
Run without blocking on /dev/tty prompts |
false |
--dry-run |
Output plan and terraform.tfvars without creating resources |
false |
--generate-only |
Write install.env and terraform.tfvars, run the pre-apply checks, print the operator handoff, and exit without creating or mutating resources. Mutually exclusive with --dry-run |
false |
--menu, --config |
Launch the Day-2 control panel instead of installing | false |
--gcp-project-id=ID |
Target GCP Project ID | Active gcloud project |
--gcp-region=REGION |
Target GCP Region | installer_common.sh DEFAULT_REGION |
--gke-cluster-name=NAME |
GKE Cluster Name | installer_common.sh DEFAULT_CLUSTER_NAME |
--gke-cluster-mode=MODE |
Shape of a cluster this run creates: autopilot | standard. Autopilot is regional: unset at a zonal --gcp-region builds standard, explicit autopilot there is an error. No bearing on an existing cluster, whose live shape the generator probes |
autopilot |
--agent-namespace=NAME |
Kubernetes namespace the agent and operator are installed into | installer_common.sh DEFAULT_NAMESPACE |
--mode=MODE |
The PlatformAgent's spec.mode: next (first-install default; a cluster already running kube-agents keeps its mode) | today (rollback; a mirrored first install's default). Recorded as PLATFORM_AGENT_MODE when install.env sets none; later, switch by editing that key, since a disagreeing --mode is refused |
installer_common.sh DEFAULT_FIRST_INSTALL_PLATFORM_AGENT_MODE |
--image-tag=TAG |
Validated immutable release tag or full commit SHA (developer/CI only) | Developer and CI/CD testing only; end users must use official release installations. Default: inferred from baked release, bundle, or local HEAD |
--registry-prefix=PATH |
Registry path (no URL scheme) for the first-party images this project builds | installer_common.sh DEFAULT_REGISTRY_PREFIX |
--third-party-registry-prefix=PATH |
Registry path holding the mirrored third-party images (cert-manager, LiteLLM, fluent-bit, token minter, Hindsight). Not implied by --registry-prefix |
unset — upstream registries |
--allow-unverified-source |
Provision from a dirty or mismatched checkout | false |
--model-provider=NAME |
gemini | vertex_ai | anthropic | openai | openai_compatible | sap | any other LiteLLM provider slug |
installer_common.sh DEFAULT_MODEL_PROVIDER |
--vertex-location=LOCATION |
Vertex AI serving location, a region or global. The global endpoint gives no in-region ML processing guarantee |
installer_common.sh DEFAULT_VERTEX_LOCATION |
--gemini-api-key=KEY |
Gemini API key | Looked up in Secret Manager |
--openai-api-key=KEY |
OpenAI API key | unset |
--anthropic-api-key=KEY |
Anthropic API key | unset |
--model-default-name=NAME |
Model for model-default; required with openai_compatible (the name the server serves) and with a provider slug outside the menu |
unset |
--litellm-env-secret=NAME |
Secret in the agent's namespace the gateway reads whole as environment variables; the operator creates it (SAP AI Core service key, another provider's variables, or an openai_compatible gateway's OAuth2 client credentials) |
unset |
--openai-compatible-api-base=URL |
openai_compatible: the server's OpenAI-compatible base URL, including /v1 |
unset |
--openai-compatible-target-port=PORT |
openai_compatible: the server pod's port, required when the URL names an in-cluster Service |
unset |
--openai-compatible-egress-cidrs=LIST |
openai_compatible: private CIDRs the server is reached at, when the URL is not an in-cluster Service (comma-separated) |
unset |
--openai-compatible-ca-secret-name=NAME |
openai_compatible: Secret (key ca.crt) holding the CA bundle of a server behind a private CA |
unset |
--openai-compatible-api-key=KEY |
openai_compatible: optional API key the server checks |
unset |
--permission-set=SET |
Agent GCP IAM set: read-only | custom |
read-only |
--custom-roles=ROLES |
Roles for --permission-set=custom (space- or comma-separated) |
unset |
--scope-projects=IDS |
Projects beyond the install's whose GKE clusters get a Cluster Agent (space- or comma-separated); the agent's service account gets the read roles in each. Recorded as SCOPE_PROJECTS; a flag on a re-run applies for that run and warns; the preflight checks the credentials Terraform applies with can create a sink in each |
unset (the install's project alone) |
--scope-folders=IDS |
Numeric GCP folder IDs: every project beneath, at any depth; the read roles plus roles/cloudasset.viewer bound on the folder; the Cloud Asset API enabled. Recorded as SCOPE_FOLDERS; the preflight checks the credentials Terraform applies with can set IAM policy on each |
unset (no container) |
--scope-organizations=IDS |
Numeric GCP organisation IDs, bound the same way. Wide; prefer folders. Recorded as SCOPE_ORGANIZATIONS |
unset (no container) |
--scope-shared-vpc-hosts=IDS |
Shared VPC host project IDs: every attached service project, resolved when Terraform plans (as the identity it applies with) and granted the read roles, with roles/compute.viewer in the host for the lookup; a lookup that identity cannot make fails the plan before anything is applied. Recorded as SCOPE_SHARED_VPC_HOSTS |
unset (no selector) |
--scope-metrics-scopes=IDS |
Metrics Scope scoping-project IDs: every project the scope monitors, resolved and granted the same way, the scoping project too for the lookup; a monitored project the planning identity cannot name is left out by naming its number in --scope-exclude-projects. Recorded as SCOPE_METRICS_SCOPES |
unset (no selector) |
--scope-max-projects=N |
The most projects the reconcile lists per run, the management project included (spec.scope.maxProjects, 1 to 5000); a project past it reads over-cap. Recorded as SCOPE_MAX_PROJECTS |
unset (the CRD's 100) |
--scope-exclude-projects=IDS |
Project IDs or shell-style globs (*-sandbox) to leave unmanaged |
unset |
--scope-exclude-clusters=TRIPLES |
Clusters to leave unmanaged and out of fleet answers and the per-cluster audits, each as project/location/cluster |
unset |
--scoped-sa-pool-enabled[=BOOL] |
Arm the scoped service account pool: one reader service account per project the plan lists in scope, created in the install's project; the credential broker selects from them and refuses a cluster in a project with none. Members hold no IAM grant yet, so leave it off. Recorded as SCOPED_SA_POOL_ENABLED |
false |
--scoped-sa-pool-max-accounts=N |
The most pool accounts the plan may create in the install's project, set from its free service-account quota (a whole number of at least 1); a pool past it is refused at plan. Recorded as SCOPED_SA_POOL_MAX_ACCOUNTS |
unset (the module's 100) |
--gitops-org=ORG |
GitHub organization for the GitOps IaC repository. Not used with --gitops-forge=gitlab |
unset |
--gitops-repo=REPO |
GitOps IaC repository name. With --gitops-forge=gitlab, the project's full path (group/subgroup/project) or its https:// URL on the GitLab host |
gke-fleet-iac |
--gitops-forge=FORGE |
Forge that holds the GitOps repository: github or gitlab (gitlab.com or a self-managed instance). A GitLab install has no GitHub App and no token minter |
github |
--gitops-host=HOST |
The forge's hostname, with no scheme, port or path. With github: a GitHub Enterprise Server host; a host that GitHub operates and gitlab.com are refused, and github.com switches back to github.com. With gitlab: a self-managed GitLab host; a GitHub hostname is refused |
github.com or gitlab.com |
--gitlab-token-file=PATH |
File, /dev/stdin (not under curl … | bash) or <(command) that holds the GitLab access token (scopes api and write_repository). A regular file is checked before the apply. Read into the Secret after the apply and never recorded. Without it, a non-interactive run prints the command that creates the Secret |
unset |
--gitlab-token-secret=NAME |
Kubernetes Secret that holds the GitLab token | gitlab-forge-token |
--gitops-ca-file=PATH |
Regular PEM file with the CA certificates of a self-managed GitLab or a GitHub Enterprise Server behind a private CA. Stored in Secret gitlab-forge-ca or github-forge-ca, and trusted by the credential broker for that host only. Needs a self-managed --gitops-host. On a GitHub Enterprise Server the token minter trusts it too, and the installer restarts the minter |
unset |
--github-app-id=ID |
Numeric GitHub App ID for the token minter | unset |
--github-pem-path=PATH |
Path to downloaded GitHub App private key (.pem) for initial Cloud KMS import |
unset |
--kms-keyring=NAME |
Cloud KMS Key Ring name for the token minter key | <cluster>-minter-keyring (new install) |
--kms-key=NAME |
Cloud KMS CryptoKey name for the token minter signing key | <cluster>-minter-key (new install) |
--enable-google-chat |
Enable the Google Chat integration | false |
--google-chat-allowed-users=EMAILS |
Comma-separated chat users allowed to reach the agent; empty allows everyone | unset |
--enable-slack[=true|false] |
Enable the Slack socket-mode relay (non-interactive runs require --slack-bot-token and --slack-app-token) |
false |
--slack-bot-token=TOKENS |
Comma-separated Slack bot tokens (xoxb-...), one per workspace the agent serves |
unset |
--slack-app-token=TOKEN |
Slack socket-mode app-level token (xapp-...) |
unset |
--slack-allowed-users=USERS |
Comma-separated Slack member IDs (U0123ABCD, not emails) allowed to reach the agent; empty allows everyone |
unset |
--slack-home-channel=CHANNEL |
Slack channel ID for unsolicited alerts/messages (e.g. C01234567) |
unset |
--slack-home-channel-name=NAME |
Display name of that channel (e.g. #gke-alerts) |
unset |
--google-chat-home-channel=SPACE |
Google Chat space (spaces/<id>) for unsolicited alerts and reports; under next a proactive post with no home channel is refused and kept for a digest, or goes to Slack when Slack is on too (HomeChannelUnset) |
unset |
--enable-stockout-investigator |
Investigate cluster-autoscaler scale-up failures and open remediation pull requests; also enables the Pub/Sub adapter. Recorded as ENABLE_STOCKOUT_INVESTIGATOR on a new install.env; later, turn it off in install.env |
false |
--enable-pubsub-platform |
Enable the Pub/Sub platform adapter on its own; --enable-stockout-investigator already implies it |
false |
--enable-gvisor=true|false |
Enable GKE Sandbox (gVisor) runtime isolation | true |
--enable-hermes-dashboard=true|false |
Enable the Hermes Web UI on port 9119 | false |
--enable-web-console=true|false |
Deploy the web console, a browser page for the agent reached by kubectl port-forward: chat threads, event triage and scheduled checks, and usage. Requires NetworkPolicy enforcement (cannot be combined with --accept-no-network-policy) |
false |
--enable-gke-backup-plan=true|false |
Provision a GKE Backup Plan for the cluster | false |
--migrate-node-pools |
Authorize migrating legacy GCE metadata server node pools to GKE_METADATA (recreates those nodes, restarts their workloads). Without it such a cluster is refused unchanged. The cluster's owner decides; never pass it unasked |
false |
--enable-network-policy |
Authorize enabling the legacy Calico NetworkPolicy addon on an existing Standard cluster without Dataplane V2 (may recreate nodes). One of two answers (or the only answer when --enable-web-console is enabled); with neither the cluster is refused unchanged. The owner decides; never pass it unasked |
false |
--accept-no-network-policy |
The other answer (unavailable when --enable-web-console is enabled): install without modifying the cluster. Every NetworkPolicy kube-agents ships is then inert, the agent sandbox's included; recorded in the report and on the PlatformAgent. The owner decides; never pass it unasked |
false |
--memory=MODE |
Long-term agent memory engine: file | hindsight | off |
file |
-h, --help, -? |
Output CLI usage banner and parameter details | N/A |
04 同仓库更多 Skills
gce-compute-fleet-audit
Audits standalone GCE virtual machines, Managed Instance Groups (MIGs), serial console boot failures, sole-tenant node group headroom, and orphaned disk snapshots.
fleet-audit
Publish the findings of an autonomous fleet audit as one continuously-rewritten issue per audit stream per install on the GitOps repository's forge, and propose fixes as narrow remediation pull requests (merge requests on GitLab).
gke-rightsizing-checks
Five required checks, and the arithmetic, before any proposal to lower a workload's CPU or memory requests or limits, or to change a node pool's machine type or size, and before any claim of capacity freed. Read-only. For "lower the requests of X", "right-size this deployment", "can this pool use a smaller machine type", and any answer that claims nodes, CPU, memory or a percentage freed.
review-chat-ux
Reviews a diff that changes what users read in Slack or Google Chat against the chat UX rules, and reports each violation with file:line and the rule it breaks.
github-issue-resolver
Autonomously poll, triage, investigate, and resolve unaddressed open issues on our target repository (on GitHub or GitLab) strictly within authorized scope.
review-docs-drift
Reviews a pull request for documentation drift — finds which docs the change should have updated, verifies doc claims against source, and checks the docs map and AGENTS.md themselves for staleness.