1"use strict";(globalThis.webpackChunkcontrolzero_docs||=[]).push([[5621],{63677(e,n,r){r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>i,metadata:()=>t,toc:()=>a});const t=JSON.parse('{"id":"enterprise/enrollment-and-fleet","title":"Enrollment and Fleet Management","description":"Roll out Control Zero across an entire engineering org with enrollment tokens, fleet view, coverage tracking, and the cooperative MCP guard.","source":"@site/docs/enterprise/enrollment-and-fleet.md","sourceDirName":"enterprise","slug":"/enterprise/enrollment-and-fleet","permalink":"/docs/enterprise/enrollment-and-fleet","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{"title":"Enrollment and Fleet Management","description":"Roll out Control Zero across an entire engineering org with enrollment tokens, fleet view, coverage tracking, and the cooperative MCP guard."}}');var s=r(74848),l=r(28453);const i={title:"Enrollment and Fleet Management",description:"Roll out Control Zero across an entire engineering org with enrollment tokens, fleet view, coverage tracking, and the cooperative MCP guard."},o="Enrollment and Fleet Management",d={},a=[{value:"Architecture in 30 seconds",id:"architecture-in-30-seconds",level:2},{value:"Step 1: Generate an enrollment token",id:"step-1-generate-an-enrollment-token",level:2},{value:"Step 2: Run <code>controlzero enroll</code> on each machine",id:"step-2-run-controlzero-enroll-on-each-machine",level:2},{value:"Python",id:"python",level:3},{value:"Node.js",id:"nodejs",level:3},{value:"Verify enrollment worked",id:"verify-enrollment-worked",level:3},{value:"Step 3: Check the fleet view",id:"step-3-check-the-fleet-view",level:2},{value:"Step 4: Track coverage gaps",id:"step-4-track-coverage-gaps",level:2},{value:"Notify",id:"notify",level:3},{value:"Exempt",id:"exempt",level:3},{value:"Escalate",id:"escalate",level:3},{value:"Custom DLP rules",id:"custom-dlp-rules",level:2},{value:"Create a rule",id:"create-a-rule",level:3},{value:"Test before committing",id:"test-before-committing",level:3},{value:"Lifecycle: draft \u2192 live",id:"lifecycle-draft--live",level:3},{value:"Rollback",id:"rollback",level:3},{value:"Cooperative guard via MCP",id:"cooperative-guard-via-mcp",level:2},{value:"Operational defaults",id:"operational-defaults",level:2},{value:"Troubleshooting",id:"troubleshooting",level:2},{value:""enroll failed: HTTP 503: FEATURE_DISABLED"",id:"enroll-failed-http-503-feature_disabled",level:3},{value:""enroll failed: HTTP 401: INVALID_TOKEN"",id:"enroll-failed-http-401-invalid_token",level:3},{value:""enroll failed: HTTP 401: TOKEN_EXHAUSTED"",id:"enroll-failed-http-401-token_exhausted",level:3},{value:""enroll failed: HTTP 403: IP_NOT_ALLOWED"",id:"enroll-failed-http-403-ip_not_allowed",level:3},{value:"Heartbeat returns 401 INVALID_SIGNATURE",id:"heartbeat-returns-401-invalid_signature",level:3},{value:"Heartbeat returns 401 CLOCK_SKEW",id:"heartbeat-returns-401-clock_skew",level:3},{value:"Machine appears in /governance/fleet but <code>last_seen</code> doesn't update",id:"machine-appears-in-governancefleet-but-last_seen-doesnt-update",level:3},{value:"Production verification",id:"production-verification",level:3}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",hr:"hr",li:"li",mermaid:"mermaid",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,l.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"enrollment-and-fleet-management",children:"Enrollment and Fleet Management"})}),"\n",(0,s.jsxs)(n.p,{children:["This guide is for ",(0,s.jsx)(n.strong,{children:"org admins"})," who need to roll out Control Zero across\nan engineering team and keep it healthy. It covers the four pieces an\nadmin actually touches:"]}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Enrollment tokens"})," \u2014 generate single-use tokens that developers\nexchange for a signed identity on first run."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Fleet view"})," \u2014 see every enrolled machine with online / stale /\noffline status. ",(0,s.jsx)(n.strong,{children:"Coming soon: the fleet dashboard route is not yet\nreachable."})," Enrollment tokens, coverage tracking and custom DLP rules\n(items 1, 3 and 4) are unaffected and work today."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Coverage tracking"})," \u2014 find users in the org who have not enrolled\nyet, send reminders, exempt contractors, escalate persistent gaps."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Custom DLP rules"})," \u2014 write block / mask / detect rules for your\norg's specific sensitive content patterns."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["If you are a ",(0,s.jsx)(n.strong,{children:"developer"})," who just received an enrollment token, jump\nstraight to ",(0,s.jsxs)(n.a,{href:"#step-2-run-controlzero-enroll-on-each-machine",children:["Step 2: Run ",(0,s.jsx)(n.code,{children:"controlzero enroll"})]}),"."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"architecture-in-30-seconds",children:"Architecture in 30 seconds"}),"\n",(0,s.jsx)(n.mermaid,{value:'flowchart TD\n subgraph Backend["Control Zero Backend"]\n EP1["/api/orgs/{id}/enrollment-tokens"]\n EP2["/api/orgs/{id}/dlp/rules"]\n EP3["/api/orgs/{id}/fleet"]\n EP4["/api/orgs/{id}/coverage"]\n end\n\n Admin["Admin via Dashboard"]\n Dev["Developer Machine"]\n\n Admin --\x3e Backend\n Dev --\x3e Backend\n\n subgraph DevEndpoints["Developer Machine Endpoints"]\n E1["/api/enroll (1x)"]\n E2["/api/heartbeat (5min)"]\n E3["/api/policy (5min)"]\n E4["/api/audit (batch)"]\n end\n\n Backend --\x3e DevEndpoints\n\n E1 -. "single-use token + pubkey" .-> Backend\n E2 -. "signed every poll" .-> Backend\n E3 -. "conditional pull (304s)" .-> Backend\n E4 -. "tool decisions stream up" .-> Backend'}),"\n",(0,s.jsxs)(n.p,{children:["The developer machine ",(0,s.jsx)(n.strong,{children:"never"})," sends a static bearer token after\nenrollment. Every request after ",(0,s.jsx)(n.code,{children:"enroll"})," is signed with a fresh\nkeypair the SDK generated locally on first run. The private key\nlives at ",(0,s.jsx)(n.code,{children:"~/.controlzero/machine.key"})," (mode 0600) and is never\ntransmitted."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"step-1-generate-an-enrollment-token",children:"Step 1: Generate an enrollment token"}),"\n",(0,s.jsxs)(n.p,{children:["Open the dashboard and go to ",(0,s.jsx)(n.strong,{children:"Settings \u2192 Enrollment"})," (or directly:\n",(0,s.jsx)(n.code,{children:"/settings/enrollment"}),")."]}),"\n",(0,s.jsxs)(n.p,{children:["You will see a 3-step setup card. Click ",(0,s.jsx)(n.strong,{children:"Generate token"})," in step 2."]}),"\n",(0,s.jsx)(n.p,{children:"A yellow banner appears with the raw token value:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"cz_enroll_a3f1c2d8e7b9f4a5...\n"})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Copy it now."})," This is the only time you will see the raw value;\nthe server only stores ",(0,s.jsx)(n.code,{children:"sha256(token)"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"The token defaults to:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Expiry"}),": 24 hours"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Max uses"}),": 1 (single-use)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"IP allowlist"}),": none"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"To make a multi-use token, call the API directly:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:'curl -X POST https://api.controlzero.ai/api/orgs/$ORG_ID/enrollment-tokens \\\n -H "Cookie: cz_session=<your dashboard session cookie>" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "ttl_hours": 168,\n "max_uses": 50,\n "ip_allowlist": ["10.0.0.0/8", "192.168.1.0/24"]\n }\'\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Multi-use tokens ",(0,s.jsx)(n.strong,{children:"require"})," an IP allowlist. The server rejects\nmulti-use without one to limit blast radius if a token leaks."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsxs)(n.h2,{id:"step-2-run-controlzero-enroll-on-each-machine",children:["Step 2: Run ",(0,s.jsx)(n.code,{children:"controlzero enroll"})," on each machine"]}),"\n",(0,s.jsx)(n.p,{children:"The developer pastes the token into one command. Pick the language\nthat matches their workflow:"}),"\n",(0,s.jsx)(n.h3,{id:"python",children:"Python"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"pip install c
1ontrolzero\ncontrolzero enroll --token cz_enroll_a3f1c2d8e7b9f4a5...\n"})}),"\n",(0,s.jsx)(n.h3,{id:"nodejs",children:"Node.js"}),"\n",(0,s.jsxs)(n.p,{children:["Configure the Control Zero registry once: add ",(0,s.jsx)(n.code,{children:"@controlzero:registry=https://npm.controlzero.ai"})," to your ",(0,s.jsx)(n.code,{children:".npmrc"})," (or run ",(0,s.jsx)(n.code,{children:"npm config set @controlzero:registry https://npm.controlzero.ai"}),"). It applies to npm install and npx for the whole @controlzero scope."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install -g @controlzero/sdk\ncontrolzero enroll --token cz_enroll_a3f1c2d8e7b9f4a5...\n"})}),"\n",(0,s.jsx)(n.p,{children:"The command:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Generates a fresh signing keypair locally."}),"\n",(0,s.jsx)(n.li,{children:"Computes a stable machine fingerprint (hostname + OS + arch)."}),"\n",(0,s.jsxs)(n.li,{children:["POSTs the token + the machine fingerprint + the public key to\n",(0,s.jsx)(n.code,{children:"/api/enroll"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Receives back a server-assigned ",(0,s.jsx)(n.code,{children:"machine_id"}),", ",(0,s.jsx)(n.code,{children:"org_id"}),", the\norg's ",(0,s.jsx)(n.strong,{children:"signing public key"}),", and the org's ",(0,s.jsx)(n.strong,{children:"tamper behavior"}),"\nsetting."]}),"\n",(0,s.jsxs)(n.li,{children:["Persists state to ",(0,s.jsx)(n.code,{children:"~/.controlzero/enrollment.json"})," and the\nprivate key to ",(0,s.jsx)(n.code,{children:"~/.controlzero/machine.key"})," (mode 0600)."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Org-level signing key provisioning."})," The org's cryptographic\nsigning keypair is lazily initialized on the first enrollment\nrequest for that org. If no signing key exists yet, the backend\ngenerates one, stores the private half securely, and returns the\npublic half to the enrolling machine. All subsequent enrollments\nfor the same org receive the same public key. The SDK uses this\nkey to verify the signature on every policy bundle it pulls."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Configurable tamper behavior."})," The enrollment response includes\nthe org's ",(0,s.jsx)(n.code,{children:"tamper_behavior"})," setting, which controls how the SDK\nreacts when a policy bundle fails signature verification. Admins\nconfigure this in the dashboard under ",(0,s.jsx)(n.strong,{children:"Settings \u2192 Security"})," or\nin policy YAML. See\n",(0,s.jsx)(n.a,{href:"/docs/enterprise/tamper-detection",children:"Tamper Detection and Enforcement"})," for\nthe four available modes and recovery procedures."]}),"\n",(0,s.jsxs)(n.p,{children:["If you re-run ",(0,s.jsx)(n.code,{children:"controlzero enroll"})," on the same machine with a\ndifferent token, the backend deduplicates by ",(0,s.jsx)(n.code,{children:"(org_id, fingerprint)"}),"\nand returns the ",(0,s.jsx)(n.strong,{children:"same"})," ",(0,s.jsx)(n.code,{children:"machine_id"}),". This is by design: MDM\nthundering herds (same script pushed to every laptop on a schedule)\ndon't create duplicate fleet entries."]}),"\n",(0,s.jsx)(n.h3,{id:"verify-enrollment-worked",children:"Verify enrollment worked"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"controlzero heartbeat\n"})}),"\n",(0,s.jsx)(n.p,{children:"Output:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"Heartbeat OK.\n server_time : 2026-04-08T15:30:42Z\n policy_version : 12\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If this prints anything else (or errors), see ",(0,s.jsx)(n.a,{href:"#troubleshooting",children:"Troubleshooting"}),"."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"step-3-check-the-fleet-view",children:"Step 3: Check the fleet view"}),"\n",(0,s.jsxs)(n.p,{children:["Open the dashboard and go to ",(0,s.jsx)(n.strong,{children:"Governance \u2192 Fleet"})," (or directly:\n",(0,s.jsx)(n.code,{children:"/governance/fleet"}),")."]}),"\n",(0,s.jsx)(n.p,{children:"Each row shows:"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Field"}),(0,s.jsx)(n.th,{children:"Meaning"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Hostname"}),(0,s.jsxs)(n.td,{children:["Whatever ",(0,s.jsx)(n.code,{children:"os.hostname()"})," returned on the machine"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"User"}),(0,s.jsx)(n.td,{children:"The email captured at enrollment time (if present)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"OS"}),(0,s.jsx)(n.td,{children:"Platform + release string"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Status"}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"online"})," (last_seen under 2 min) / ",(0,s.jsx)(n.code,{children:"stale"})," (under 1 h) / ",(0,s.jsx)(n.code,{children:"offline"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Last seen"}),(0,s.jsx)(n.td,{children:"Relative time of the most recent heartbeat"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Decisions (24h)"}),(0,s.jsx)(n.td,{children:"Tool calls evaluated in the last 24 hours"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["The list is paginated (100 per page, max 500 via ",(0,s.jsx)(n.code,{children:"?limit=500"}),").\nSort order is ",(0,s.jsx)(n.code,{children:"last_seen DESC"})," so the freshest machines float\nto the top."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Status thresholds"})," are derived in real time from ",(0,s.jsx)(n.code,{children:"last_seen"}),",\nnot stored. A machine flips from ",(0,s.jsx)(n.code,{children:"online"})," to ",(0,s.jsx)(n.code,{children:"stale"})," 2 minutes\nafter its last heartbeat with no extra writes."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"step-4-track-coverage-gaps",children:"Step 4: Track coverage gaps"}),"\n",(0,s.jsxs)(n.p,{children:['A "coverage gap" is an org member who has ',(0,s.jsx)(n.strong,{children:"not"}
1)," enrolled a machine.\nOpen ",(0,s.jsx)(n.strong,{children:"Governance \u2192 Coverage"})," (",(0,s.jsx)(n.code,{children:"/governance/coverage"}),")."]}),"\n",(0,s.jsx)(n.p,{children:"Each gap row shows the user, their role, and three action buttons:"}),"\n",(0,s.jsx)(n.h3,{id:"notify",children:"Notify"}),"\n",(0,s.jsxs)(n.p,{children:["Sends a reminder (the actual notification channel is wired in your\norg's notification settings \u2014 email, Slack, etc). Increments the\n",(0,s.jsx)(n.code,{children:"reminder_count"})," on the user's ",(0,s.jsx)(n.code,{children:"coverage_user_state"})," row so you\ncan see persistent non-compliance."]}),"\n",(0,s.jsx)(n.h3,{id:"exempt",children:"Exempt"}),"\n",(0,s.jsx)(n.p,{children:"Removes the user from the coverage list with a required reason.\nCommon cases:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Contractor with no workstation"})," \u2014 they don't have a laptop to\nenroll."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"PM / non-technical role"})," \u2014 they don't run AI tools."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Maternity / leave"})," \u2014 temporary exemption with an ",(0,s.jsx)(n.code,{children:"exempt_until"}),"\ndate."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["The exemption is recorded in the audit log with the reason and the\nadmin who set it. Pass an ",(0,s.jsx)(n.code,{children:"exempt_until"})," ISO timestamp to make it\nauto-expire; otherwise it's permanent until manually revoked."]}),"\n",(0,s.jsx)(n.h3,{id:"escalate",children:"Escalate"}),"\n",(0,s.jsxs)(n.p,{children:['Flags the user as "persistent non-compliance" but ',(0,s.jsx)(n.strong,{children:"keeps them on\nthe coverage list"}),". Use this for users who have ignored multiple\nreminders. The dashboard shows escalated users with a red badge so\nthey stand out at a glance."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"custom-dlp-rules",children:"Custom DLP rules"}),"\n",(0,s.jsxs)(n.p,{children:["DLP rules detect or block sensitive content in tool calls and LLM prompts. A\nmask rule replaces matches only through Python SDK hooks on Claude Code and\nGemini CLI; other actively enforcing surfaces deny the call, while Kiro IDE's\n1.0.x hooks are observe-only and do not enforce the rule. Open ",(0,s.jsx)(n.strong,{children:"Governance \u2192\nDLP Rules"})," (",(0,s.jsx)(n.code,{children:"/governance/dlp-rules"}),")."]}),"\n",(0,s.jsx)(n.h3,{id:"create-a-rule",children:"Create a rule"}),"\n",(0,s.jsxs)(n.p,{children:["Click ",(0,s.jsx)(n.strong,{children:"New rule"}),". Fill in:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Name"}),": human-readable label, shown in audit logs."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Pattern"}),": an RE2 regex. PCRE features (lookaheads, backrefs)\nfail at compile time and the form rejects them inline."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Category"}),": ",(0,s.jsx)(n.code,{children:"pii"})," / ",(0,s.jsx)(n.code,{children:"secret"})," / ",(0,s.jsx)(n.code,{children:"ip"})," / ",(0,s.jsx)(n.code,{children:"financial"})," / ",(0,s.jsx)(n.code,{children:"compliance"})," / ",(0,s.jsx)(n.code,{children:"custom"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Action"}),": ",(0,s.jsx)(n.code,{children:"detect"})," (log only) / ",(0,s.jsx)(n.code,{children:"mask"})," (replace matches through Python SDK hooks on Claude Code and Gemini CLI; deny the call on other actively enforcing surfaces; Kiro IDE's 1.0.x hooks are observe-only and do not enforce the rule) / ",(0,s.jsx)(n.code,{children:"block"})," (deny the call)."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Scopes"}),": which surfaces enforce this rule. Pick any combination of\n",(0,s.jsx)(n.code,{children:"sdk"}),", ",(0,s.jsx)(n.code,{children:"gateway"}),", ",(0,s.jsx)(n.code,{children:"browser_ext"}),", ",(0,s.jsx)(n.code,{children:"scout"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"test-before-committing",children:"Test before committing"}),"\n",(0,s.jsxs)(n.p,{children:["The form has a built-in ",(0,s.jsx)(n.strong,{children:"Test pattern"})," affordance. Type a sample\ninput (e.g., ",(0,s.jsx)(n.code,{children:"customer SSN is 123-45-6789"}),") and hit ",(0,s.jsx)(n.strong,{children:"Test"}),". The\nbackend compiles the pattern and reports:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"compiles: true / false"})," \u2014 if false, the error message points to\nthe exact column."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"matched: true / false"})," \u2014 whether the sample triggered."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"matches: [...]"})," \u2014 up to 10 matched substrings."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Iterate until you're happy, then ",(0,s.jsx)(n.strong,{children:"Create as draft"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"lifecycle-draft--live",children:"Lifecycle: draft \u2192 live"}),"\n",(0,s.jsxs)(n.p,{children:["A new rule lands in ",(0,s.jsx)(n.code,{children:"draft"}),". Click ",(0,s.jsx)(n.strong,{children:"Publish"})," to transition it to\n",(0,s.jsx)(n.code,{children:"live"}),". Publishing bumps the org's monotonic ",(0,s.jsx)(n.code,{children:"policy_version"})," so\nthe next SDK ",(0,s.jsx)(n.code,{children:"/api/policy"})," poll picks up the change."]}),"\n",(0,s.jsx)(n.p,{children:"For high-impact rules you can use the two-pers
1on approval flow:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Admin A: ",(0,s.jsx)(n.code,{children:"POST /dlp/rules/{id}/request-approval"})]}),"\n",(0,s.jsxs)(n.li,{children:["Admin B (different user): ",(0,s.jsx)(n.code,{children:"POST /dlp/rules/{id}/approve"})]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Self-approval is rejected with ",(0,s.jsx)(n.code,{children:"403 SAME_ACTOR"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"rollback",children:"Rollback"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"POST /dlp/rules/{id}/rollback"})," with a ",(0,s.jsx)(n.code,{children:"target_version"})," in the body\nrestores any prior version's pattern + scopes + action. The result\nlands in ",(0,s.jsx)(n.code,{children:"draft"})," (not ",(0,s.jsx)(n.code,{children:"live"}),") so you have to explicitly re-publish.\nThis avoids accidentally rolling a regression straight into prod."]}),"\n",(0,s.jsxs)(n.p,{children:["The version history is append-only: ",(0,s.jsx)(n.code,{children:"GET /dlp/rules/{id}/versions"}),"\nreturns every prior state with the ",(0,s.jsx)(n.code,{children:"edited_by"})," admin and ",(0,s.jsx)(n.code,{children:"edit_reason"}),"."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"cooperative-guard-via-mcp",children:"Cooperative guard via MCP"}),"\n",(0,s.jsxs)(n.p,{children:["If your developers run AI coding clients (Claude Desktop, Cursor,\nCline) that already speak MCP, install the Control Zero MCP server\nto give those clients a way to ",(0,s.jsx)(n.strong,{children:"ask"})," before running a tool:"]}),"\n",(0,s.jsxs)(n.p,{children:["If you have not already pointed the ",(0,s.jsx)(n.code,{children:"@controlzero"})," scope at the Control Zero registry, do it once -- these packages are ",(0,s.jsx)(n.strong,{children:"not"})," on npmjs.org and a bare install will 404:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm config set @controlzero:registry https://npm.controlzero.ai\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install -g @controlzero/mcp-server\n"})}),"\n",(0,s.jsx)(n.p,{children:"Add to your client's MCP config (Claude Desktop example):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "mcpServers": {\n "controlzero": {\n "command": "controlzero-mcp",\n "env": {\n "CONTROLZERO_API_KEY": "cz_live_xxxxxxxx"\n }\n }\n }\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The client can now call ",(0,s.jsx)(n.code,{children:"check"}),' -- "is it OK if I run ',(0,s.jsx)(n.code,{children:"bash"}),' with these\nargs?" -- which returns ',(0,s.jsx)(n.code,{children:"allow"})," / ",(0,s.jsx)(n.code,{children:"warn"})," / ",(0,s.jsx)(n.code,{children:"deny"}),", the matching rules, and\nwhether every rule for the surface was evaluated. See\n",(0,s.jsx)(n.a,{href:"/docs/integrations/mcp-guard",children:"MCP Cooperative Guard"}),". Use\n",(0,s.jsx)(n.code,{children:"@controlzero/mcp-server"})," 2.0.1 or later."]}),"\n",(0,s.jsxs)(n.p,{children:["This is ",(0,s.jsx)(n.strong,{children:"cooperative"}),", not enforcement. The client could ignore\nthe result. Pair the MCP guard with the SDK guard call (hard\nenforcement) for full coverage."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"operational-defaults",children:"Operational defaults"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Setting"}),(0,s.jsx)(n.th,{children:"Default"}),(0,s.jsx)(n.th,{children:"Override"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Enrollment token TTL"}),(0,s.jsx)(n.td,{children:"24 hours"}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"ttl_hours"})," in create body, max 168 (7 days)"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Enrollment token max_uses"}),(0,s.jsx)(n.td,{children:"1"}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"max_uses"})," in create body, multi-use REQUIRES ",(0,s.jsx)(n.code,{children:"ip_allowlist"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Heartbeat interval"}),(0,s.jsx)(n.td,{children:"5 minutes"}),(0,s.jsx)(n.td,{children:"SDK config"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Policy poll interval"}),(0,s.jsx)(n.td,{children:"5 minutes"}),(0,s.jsx)(n.td,{children:"SDK config (uses ETag, ~99% are 304s)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Online status threshold"}),(0,s.jsx)(n.td,{children:"2 minutes"}),(0,s.jsx)(n.td,{children:"hardcoded in fleet derivation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Stale status threshold"}),(0,s.jsx)(n.td,{children:"1 hour"}),(0,s.jsx)(n.td,{children:"hardcoded in fleet derivation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Audit batch size"}),(0,s.jsx)(n.td,{children:"500 entries"}),(0,s.jsx)(n.td,{children:"enforced server-side, SDK batches client-side"})]})]})]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,s.jsx)(n.h3,{id:"enroll-failed-http-503-feature_disabled",children:'"enroll failed: HTTP 503: FEATURE_DISABLED"'}),"\n",(0,s.jsx)(n.p,{children:"Enrollment is not enabled for your organization. On Control Zero Cloud,\ncontact support to have enrollment turned on for your org."}),"\n",(0,s.jsx)(n.h3,{id:"enroll-failed-http-401-invalid_token",children:'"enroll failed: HTTP 401: INVALID_TOKEN"'}),"\n",(0,s.jsx)(n.p,{children:"The token is wrong, expired, revoked, or already used (single-use).\nGenerate a fresh one from the dashboard."}),"\n",(0,s.jsx)(n.h3,{id:"enroll-failed-http-401-token_exhausted",children:'"enroll failed: HTTP 401: TOKEN_EXHAUSTED"'}),"\n",(0,s.jsxs)(n.p,{children:["A multi-use token hit its ",(0,s.jsx)(n.code,{children:"max_uses"})," cap. Generate a new one with a\nhigher cap, or rotate to single-use tokens distributed via MDM."]}),"\n",(0,s.jsx)(n.h3,{id:"enroll-failed-http-403-ip_not_allowed",children:'"enroll failed: HTTP 403: IP_NOT_ALLOWED"'}),"\n",(0,s.jsxs)(n.p,{children:["The token has an ",(0,s.jsx)(n.code,{children:"ip_allowlist"})," and the calling machine's source IP\nis not in any of the listed CIDR blocks. Either add the developer's\noffice subnet to the allowlist or distribute single-use tokens that\ndon't have IP restrictions."]}),"\n",(0,s.jsx)(n.h3,{id:"heartbeat-returns-401-invalid_signature",children:"Heartbeat returns 401 INVALID_SIGNATURE"}),"\n",(0,s.jsx)(n.p,{children:"The local private key was deleted, corrupted, or doesn't match the\npublic key registered at enrollment time. The fix is to re-enroll\nwith a fresh token:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"rm -rf ~/.controlzero/\ncontrolzero enroll --token FRESH_TOKEN_HERE\n"})}),"\n",(0,s.jsx)(n.h3,{id:"heartbeat-returns-401-clock_skew",children:"Heartbeat returns 401 CLOCK_SKEW"}),"\n",(0,s.jsxs)(n.p,{children:["The machine's system clock is more than 5 minutes off from the\nbackend's wall clock. Run ",(0,s.jsx)(n.code,{children:"ntpdate"})," (Linux) or check the System\nSettings clock sync (macOS / Windows)."]}),"\n",(0,s.jsxs)(n.h3,{id:"machine-appears-in-governancefleet-but-last_seen-doesnt-update",children:["Machine appears in /governance/fleet but ",(0,s.jsx)(n.code,{children:"last_seen"})," doesn't update"]}),"\n",(0,s.jsxs)(n.p,{children:["Check the SDK process is actually running and not crashed. Crashes and startup\nerrors go to the process's own stderr or service log (",(0,s.jsx)(n.code,{children:"journalctl -u controlzero"}),"\nunder systemd), not to the audit log. Use the audit log \u2014 ",(0,s.jsx)(n.code,{children:"./controlzero.log"})," in\nthe working directory by default, or ",(0,s.jsx)(n.code,{children:"~/.controlzero/audit.log"})," in hosted mode \u2014\nto confirm whether governed events are still being recorded. Restart the SDK; the\nnext heartbeat will populate ",(0,s.jsx)(n.code,{children:"last_seen"})," immediately."]}),"\n",(0,s.jsx)(n.h3,{id:"production-verification",children:"Production verification"}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"/api/health"})," and synthetic sign-up monitor are public:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:'curl -sf https://api.controlzero.ai/health\n# {"status":"healthy","service":"control-zero","version":"0.1.0",...}\n'})}),"\n",(0,s.jsx)(n.p,{children:"The synthetic monitor runs every 5 minutes and exercises the full\nsign-up + enrollment + heartbeat path against production. If it\ngoes red, the GitHub Actions dashboard shows the failing step\nwithin 5 minutes."})]})}function h(e={}){const{wrapper:n}={...(0,l.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},28453(e,n,r){r.d(n,{R:()=>i,x:()=>o});var t=r(96540);const s={},l=t.createContext(s);function i(e){const n=t.useContext(l);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),t.createElement(l.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.