PageSourceSearch

https://docs.controlzero.ai/assets/js/3c0c2cfd.bfc696e0.js

js controlzero.ai collected 2026-10-05 13:56:06 UTC 16,057 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkcontrolzero_docs||=[]).push([[8549],{52436(e,r,s){s.r(r),s.d(r,{assets:()=>o,contentTitle:()=>a,default:()=>h,frontMatter:()=>l,metadata:()=>n,toc:()=>d});const n=JSON.parse('{"id":"concepts/secrets-hitl","title":"Secrets approvals","description":"The deny fires. A rule tagged escalateondeny: true denies exactly as","source":"@site/docs/concepts/secrets-hitl.md","sourceDirName":"concepts","slug":"/concepts/secrets-hitl","permalink":"/docs/concepts/secrets-hitl","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":22,"frontMatter":{"sidebar_position":22,"title":"Secrets approvals"},"sidebar":"docsSidebar","previous":{"title":"Multi-user keys","permalink":"/docs/concepts/multi-user-keys"},"next":{"title":"Pricing","permalink":"/docs/concepts/pricing"}}');var t=s(74848),i=s(28453);const l={sidebar_position:22,title:"Secrets approvals"},a="Approvals on secret reads",o={},d=[{value:"Canonical actions",id:"canonical-actions",level:2},{value:"SDK API",id:"sdk-api",level:2},{value:"Hybrid gate (defense in depth)",id:"hybrid-gate-defense-in-depth",level:2},{value:"Value redaction (three layers)",id:"value-redaction-three-layers",level:2},{value:"Approval drawer for secrets",id:"approval-drawer-for-secrets",level:2},{value:"Default scope: user-scoped (opposite of tool-call default)",id:"default-scope-user-scoped-opposite-of-tool-call-default",level:2}
1,{value:"Rotation invalidates grants",id:"rotation-invalidates-grants",level:2},{value:"When NOT to require approval on a secret",id:"when-not-to-require-approval-on-a-secret",level:2},{value:"See also",id:"see-also",level:2}];function c(e){const r={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",mdxAdmonitionTitle:"mdxAdmonitionTitle",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components},{MaturityBadge:s}=r;return s||function(e,r){throw new Error("Expected "+(r?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("MaturityBadge",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(r.header,{children:(0,t.jsx)(r.h1,{id:"approvals-on-secret-reads",children:"Approvals on secret reads"})}),"\n",(0,t.jsxs)(r.admonition,{type:"warning",children:[(0,t.jsxs)(r.mdxAdmonitionTitle,{children:[(0,t.jsx)(r.code,{children:"escalate_on_deny"})," does not raise an approval request"]}),(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.strong,{children:"The deny fires."})," A rule tagged ",(0,t.jsx)(r.code,{children:"escalate_on_deny: true"})," denies exactly as\nwritten -- the matched rule's own ",(0,t.jsx)(r.code,{children:"policy_id"}),", ",(0,t.jsx)(r.code,{children:'effect: "deny"'}),",\n",(0,t.jsx)(r.code,{children:'reason_code: "RULE_MATCH"'}),". You are protected."]}),(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.strong,{children:"The escalation does not."})," The tag is accepted by the policy schema and\ncarried into the policy bundle, and no enforcer acts on it: no approval request\nis raised, no approver is notified, and ",(0,t.jsx)(r.code,{children:"decision.requires_approval"})," stays\n",(0,t.jsx)(r.code,{children:"false"}),". Code that branches on ",(0,t.jsx)(r.code,{children:"decision.requires_approval"})," in order to act on\nthis tag therefore never runs. (A different mechanism, LLM function policies'\n",(0,t.jsx)(r.code,{children:"require_approval"}),", does set that field -- ",(0,t.jsx)(r.code,{children:"escalate_on_deny"})," is not wired to\nit.) Tracking: ",(0,t.jsx)(r.a,{href:"https://github.com/maruthiprithivi/control_zero/issues/2391",children:"#2391"}),"."]}),(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.strong,{children:"To request approval today, call it explicitly."}),"\n",(0,t.jsx)(r.code,{children:"client.request_approval(decision)"})," posts a real approval request. Nothing\nabout the tag calls it for you. See ",(0,t.jsx)(r.a,{href:"/docs/sdk/hitl-callback",children:"Approval callback"}),".\nPer ",(0,t.jsx)(r.a,{href:"https://github.com/maruthiprithivi/control_zero/issues/2363",children:"#2363"})," the\napprover-facing ",(0,t.jsx)(r.code,{children:"/approvals"})," pages are gated in production, so requests are\nresolved through the API."]}),(0,t.jsxs)(r.p,{children:["This applies to the published Python SDK (",(0,t.jsx)(r.code,{children:"controlzero"})," 1.13.14) and the\npublished Node SDK (",(0,t.jsx)(r.code,{children:"@controlzero/sdk"})," 1.13.6) alike."]})]}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.strong,{children:"Supported modes:"})," ",(0,t.jsx)("span",{class:"mode-pill hosted",children:"Hosted"})," ",(0,t.jsx)("span",{class:"mode-pill hybrid",children:"Hybrid"})," ",(0,t.jsx)("span",{class:"mode-pill local",children:"Local"}),"\n",(0,t.jsx)(r.strong,{children:"Available in:"})," ",(0,t.jsx)("span",{class:"tier-pill free",children:"Free"})," ",(0,t.jsx)("span",{class:"tier-pill solo",children:"Solo"})," ",(0,t.jsx)("span",{class:"tier-pill teams",children:"Teams"})," (free for all tiers)\n",(0,t.jsx)(r.strong,{children:"Status:"})," ",(0,t.jsx)(s,{tier:"BETA"}),"\n",(0,t.jsx)(r.strong,{children:"SDK:"})," 1.6.0+"]}),"\n",(0,t.jsxs)(r.admonition,{title:"Availability",type:"info",children:[(0,t.jsxs)(r.p,{children:["Secret approvals share the approval (HITL) feature. The request path works on\nevery deployment, including the hosted (SaaS) plan. The feature is in ",(0,t.jsx)(r.strong,{children:"BETA"}),"\nand is ",(0,t.jsx)(r.strong,{children:"off by default"})," -- an administrator turns it on per scope."]}),(0,t.jsxs)(r.p,{children:["The approver-facing pages are not reachable yet. The approvals inbox and request\ndetail routes redirect to the dashboard in every shipped deployment, so an\napprover cannot release a held secret from the UI today;
1 the request runs to its\ndeadline and the SDK raises ",(0,t.jsx)(r.code,{children:"HITLTimeoutError"})," with a synthesized deny. The\nsecret is not returned, so the read fails closed. See\n",(0,t.jsx)(r.a,{href:"../guides/approvals",children:"Set up approvals"})," for the toggle and the end-to-end flow."]})]}),"\n",(0,t.jsxs)(r.p,{children:["Secrets are higher-stakes than tool calls. A leaked ",(0,t.jsx)(r.code,{children:"OPENAI_API_KEY"})," costs more than a single bad ",(0,t.jsx)(r.code,{children:"rm"}),". Approvals on secret reads give admins a checkpoint: when an agent fetches a credential, the admin can approve, deny, or scope the grant to a single user."]}),"\n",(0,t.jsx)(r.h2,{id:"canonical-actions",children:"Canonical actions"}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Action"}),(0,t.jsx)(r.th,{children:"Default policy"}),(0,t.jsx)(r.th,{children:"What it does"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"Secrets:read <name>"})}),(0,t.jsx)(r.td,{children:"Allow if explicitly granted"}),(0,t.jsx)(r.td,{children:"Agent fetches a credential value"})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"Secrets:list"})}),(0,t.jsx)(r.td,{children:"Allow if explicitly granted"}),(0,t.jsx)(r.td,{children:"Agent enumerates secret names (no values)"})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"Secrets:write <name>"})}),(0,t.jsx)(r.td,{children:"Hard-deny in starter policy"}),(0,t.jsx)(r.td,{children:"Admin operation; usually not for agents"})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"Secrets:delete <name>"})}),(0,t.jsx)(r.td,{children:"Hard-deny in starter policy"}),(0,t.jsx)(r.td,{children:"Admin operation; usually not for agents"})]})]})]}),"\n",(0,t.jsx)(r.p,{children:"Typical project policy:"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-yaml",children:"version: '1'\nrules:\n  - allow: Secrets:list\n  - deny: Secrets:read\n    escalate_on_deny: true\n    reason: 'credential reads need approval'\n  - deny: Secrets:write\n  - deny: Secrets:delete\n"})}),"\n",(0,t.jsxs)(r.p,{children:["The ",(0,t.jsx)(r.code,{children:"escalate_on_deny: true"})," tag records your INTENT that this deny is one a\nhuman should review. It does not raise the request -- see the warning above.\nYour code raises it, by calling ",(0,t.jsx)(r.code,{children:"client.request_approval(decision)"})," on the\nreturned deny."]}),"\n",(0,t.jsx)(r.h2,{id:"sdk-api",children:"SDK API"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-python",children:'# Python (1.6.0+)\nsecret = client.get_secret("OPENAI_API_KEY")\nsecret = await client.get_secret_async("OPENAI_API_KEY")\n'})}),"\n",(0,t.jsxs)(r.p,{children:["Internally ",(0,t.jsx)(r.code,{children:"get_secret(name)"}),":"]}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsxs)(r.li,{children:["SDK calls ",(0,t.jsx)(r.code,{children:'client.guard("Secrets", method="read", ...)'}),", which is what\nmatches the ",(0,t.jsx)(r.code,{children:"Secrets:read"})," rule above. (",(0,t.jsx)(r.code,{children:"guard()"})," has no ",(0,t.jsx)(r.code,{children:"resource"}),"\nparameter, and the colon form ",(0,t.jsx)(r.code,{children:'guard("Secrets:read")'})," does not match that\nrule -- it falls through to the fail-closed default.)"]}),"\n",(0,t.jsx)(r.li,{children:"If allow, fetch from secrets backend; return value."}),"\n",(0,t.jsxs)(r.li,{children:["If deny, and your code treats this rule as one a human should review, call\n",(0,t.jsx)(r.code,{children:"client.request_approval(decision)"}),". On approve, fetch and return. There is\nno flag on the decision that marks it approval-eligible for you --\n",(0,t.jsx)(r.code,{children:"decision.requires_approval"})," stays ",(0,t.jsx)(r.code,{children:"false"}),"."]}),"\n",(0,t.jsxs)(r.li,{children:["If deny (final), raise ",(0,t.jsx)(r.code,{children:"PolicyDeniedError"}),"."]}),"\n"]}),"\n",(0,t.jsx)(r.h2,{id:"hybrid-gate-defense-in-depth",children:"Hybrid gate (defense in depth)"}),"\n",(0,t.jsx)(r.p,{children:"Two layers gate every secret read:"}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"SDK gate (fast path):"})," ",(0,t.jsx)(r.code,{children:"get_secret()"})," consults the policy engine before touching the secrets backend."]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"Secrets backend gate (backstop):"})," the secrets backend independently consults the policy engine on every read, catching non-SDK callers (curl, MCP server, browser ext)."]}),"\n"]}),"\n",(0,t.jsx)(r.p,{children:"A malicious SDK that bypasses its own gate still fails the backend check."}),"\n",(0,t.jsx)(r.h2,{id:"value-redaction-three-layers",children:"Value redaction (three layers)"}),"\n",(0,t.jsx)(r.p,{children:"The secret value NEVER appears in:"}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"Audit logs:"})," only ",(0,t.jsx)(r.code,{children:"secret_name"})," + ",(0,t.jsx)(r.code,{children:"secret_value_sha256"})," (64-char hex). DB CHECK constraint at the table layer rejects any row that violates this."]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"Telemetry:"})," PostHog events for ",(0,t.jsx)(r.code,{children:"Secrets:*"})," actions are dropped entirely; no payload field passes a value-shape regex."]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"Errors + stack traces:"})," explicit redaction in every exception path."]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"Approver UI:"})," drawer shows ",(0,t.jsx)(r.code,{children:"value: <hidden, sha256: ab12...cd34>"}),". Never the value itself."]}),"\n"]}),"\n",(0,t.jsxs)(r.p,{children:["If any layer attempts to log a value-shaped string for a ",(0,t.jsx)(r.code,{children:"Secrets:*"}
1)," action, the SDK raises ",(0,t.jsx)(r.code,{children:"E1709 SecretValueLeakInPayload"})," and the request fails closed."]}),"\n",(0,t.jsx)(r.h2,{id:"approval-drawer-for-secrets",children:"Approval drawer for secrets"}),"\n",(0,t.jsx)(r.p,{children:"Same drawer pattern as tool-call approvals, with three differences:"}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:'Red-bordered "SECRET ACCESS" banner'})," at the top."]}),"\n",(0,t.jsxs)(r.li,{children:["Secret name visible; value sha256 fingerprint visible; ",(0,t.jsx)(r.strong,{children:"value never visible"}),"."]}),"\n",(0,t.jsx)(r.li,{children:'"Last N reads of this secret" inline list for anomaly detection.'}),"\n"]}),"\n",(0,t.jsx)(r.p,{children:"The approver verifies they're approving the right secret by matching the sha256 against the expected value, without ever seeing the value itself."}),"\n",(0,t.jsx)(r.h2,{id:"default-scope-user-scoped-opposite-of-tool-call-default",children:"Default scope: user-scoped (opposite of tool-call default)"}),"\n",(0,t.jsxs)(r.p,{children:["Tool-call ",(0,t.jsx)(r.code,{children:"Approve forever"})," defaults to project-wide. Secret ",(0,t.jsx)(r.code,{children:"Approve forever"})," defaults to ",(0,t.jsx)(r.strong,{children:"user-scoped"})," (",(0,t.jsx)(r.code,{children:"condition.user IN [requestor_email]"}),")."]}),"\n",(0,t.jsxs)(r.p,{children:["Reasoning: a long-lived secret rule for the whole project undermines the vault. Admin must explicitly remove the ",(0,t.jsx)(r.code,{children:"condition.user"}),' clause via "Edit before applying" to widen.']}),"\n",(0,t.jsx)(r.h2,{id:"rotation-invalidates-grants",children:"Rotation invali
1dates grants"}),"\n",(0,t.jsxs)(r.p,{children:["When the secret value is rotated, a trigger on ",(0,t.jsx)(r.code,{children:"secrets.updated_at"})," automatically sets ",(0,t.jsx)(r.code,{children:"revoked_at = NOW()"})," on all ",(0,t.jsx)(r.code,{children:"hitl_grants"})," rows for that ",(0,t.jsx)(r.code,{children:"secret_name"}),". Reasoning: a grant was issued against a specific value; rotation invalidates it; next fetch needs a new approval."]}),"\n",(0,t.jsx)(r.h2,{id:"when-not-to-require-approval-on-a-secret",children:"When NOT to require approval on a secret"}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"Public config"})," (e.g. ",(0,t.jsx)(r.code,{children:"PUBLIC_ANALYTICS_KEY"}),"). Approval friction with no benefit."]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"Build-time-only credentials"})," read once at boot. Approval would block boot."]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.strong,{children:"Mass-fetch helpers"})," that pull dozens of secrets in a loop. Per-fetch approval would be unworkable."]}),"\n"]}),"\n",(0,t.jsx)(r.p,{children:"Tag the secrets you'd lose sleep over if they leaked. Leave routine config un-tagged."}),"\n",(0,t.jsx)(r.h2,{id:"see-also",children:"See also"}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.a,{href:"./hitl-approval",children:"Approval Workflow"}),". The parent concept"]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.a,{href:"./multi-user-keys",children:"Multi-user keys"}),". Identity for shared API keys"]}),"\n",(0,t.jsx)(r.li,{children:(0,t.jsx)(r.a,{href:"../errors/E1709-secret-leak-in-payload",children:"E1709 secret value leak"})}),"\n",(0,t.jsx)(r.li,{children:(0,t.jsx)(r.a,{href:"../errors/E1710-secret-approval-required",children:"E1710 secret approval required"})}),"\n",(0,t.jsx)(r.li,{children:(0,t.jsx)(r.a,{href:"../errors/E1711-secret-not-found",children:"E1711 secret not found"})}),"\n"]})]})}function h(e={}){const{wrapper:r}={...(0,i.R)(),...e.components};return r?(0,t.jsx)(r,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},28453(e,r,s){s.d(r,{R:()=>l,x:()=>a});var n=s(96540);const t={},i=n.createContext(t);function l(e){const r=n.useContext(i);return n.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function a(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:l(e.components),n.createElement(i.Provider,{value:r},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.