[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6J9QR26SW86FS2KPV13TXDV":3,"skill-same:01M6J9QR26SW86FS2KPV13TXDV":70},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":41,"linkOnly":42,"markdownContent":43,"markdownBody":44,"frontMatter":45,"refName":48,"commitSha":49,"skillDirectory":50,"forksCount":51,"repositoryHtmlUrl":52,"repositoryHomepageUrl":53,"ownerHtmlUrl":54,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":55,"install":56,"resources":69},"01M6J9QR26SW86FS2KPV13TXDV","harn-approval-review",null,"Configure who answers a permission ask, tune the reviewer policy, and measure the reviewer against a live corpus.","burin-labs\u002Fharn","burin-labs","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F270508823?v=4","01M6J9MTN4TKZ12YF6BAX0DARN","Burin Labs","01M6J9MTSTW6RRQ2TWKYV1A3X1",31,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn\u002Fblob\u002Fmain\u002Fcrates\u002Fharn-skills\u002Fsrc\u002Fcorpus\u002Fharn-approval-review\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40],"harn approval review","approval","review","fallback","denied","when","calibration","resolver","rollup","reading","tune","live","permission","use","behavior","ask","editing","reviewer","configure","why","run","policy","2026-11-10 10:12:30",false,"---\nname: harn-approval-review\nshort: The approval resolver, the reviewer that answers a denial, and its calibration corpus.\ndescription: Configure who answers a permission ask, tune the reviewer policy, and measure the reviewer against a live corpus.\nwhen_to_use: Use when changing approval-resolver behavior, editing the reviewer policy or denylist, reading the approval-fallback rollup, or explaining why a run was denied.\n---\n\n# Approval review\n\nA permission gate that refuses work on a non-interactive run used to end the\nrun. There was nobody to ask. The **approval resolver** decides who answers\ninstead, and the **reviewer** is a model that answers by reading the refused\naction against the user's actual goal.\n\nPair with [[harn-agent]] for the loop this runs inside and [[harn-testing]] for\nwhat counts as evidence.\n\n## The reviewer is not a security boundary\n\nIt is a judgment layer above a boundary that stays exactly where it was. The\nsandbox is not relieved of its job because a reviewer exists, and no claim about\nthis feature should imply otherwise. Cursor says the same of its own auto-review\nand is right to.\n\n## Two orthogonal axes\n\n`PolicyAction {allow, ask, deny}` is the per-rule **verdict**. `ApprovalResolver\n{host, auto_review, allow_all}` is **who answers** when a rule says `ask`. They\nare separate types on purpose: `PolicyAction` is ranked and that rank drives\nmost-restrictive-wins composition across the policy stack, so \"is `allow_all`\nmore restrictive than `allow`?\" is a question with no answer and must never be\nasked.\n\n- `host` — ask a person. A surface that cannot reach one reports the ask as\n  unsatisfiable rather than answering on their behalf.\n- `auto_review` — route to the reviewer. Headless and eval default.\n- `allow_all` — answer every ask yes. This is what `yolo` and `full-auto` mean,\n  and it has one owner rather than a per-surface empty policy overlay.\n\nMatched deny rules never reach a reviewer, including `allow_all`. Their receipts\nrecord `auto_review.unavailable_reason: \"deny_rule\"` and `reviewer_answered: false`.\nReviewers may answer asks and deliberately reviewable refusals such as side-effect\nceilings. `allow_all` also preserves the catastrophic floor.\n\n## Install a reviewer on a loop\n\n```harn\nimport { approval_reviewer_for } from \"std\u002Fagent\u002Fapproval_review\"\n\nagent_loop(harness, task, system, {\n  approval_policy: policy,\n  approval_reviewer: approval_reviewer_for(harness, review_policy, goal),\n})\n```\n\n`goal` is required, not optional. The reviewer's question is never \"is this\ncommand dangerous\" in the abstract — it is \"does THIS goal authorize THIS\naction\". A reviewer given no goal can only reason from the command, which is the\nweaker instrument and the one that produces both kinds of error.\n\n`approval_reviewer` is a closure taking the refusal record and returning a\ndecision record. `approval_reviewer_for` adapts the runtime's shape to\n`approval_review_decide`; anything returning `{approved, reviewer_answered,\nrationale}` works.\n\n### Yolo is this seam, not a bypass\n\n```harn\napproval_reviewer: approval_allow_all_reviewer(review_policy)\n```\n\n`allow_all` is a reviewer that says yes. Routing it through the same seam rather\nthan around it means it inherits the floor, the receipt, and the activity record\n— and there is exactly one place in the system where a refusal can be lifted. It\ndoes **not** lift the catastrophic floor.\n\n### It fails closed\n\nNo reviewer, no VM context, a closure that raises, an unparseable verdict, or\nre-entrancy all leave the call denied exactly as it would have been. A bare\n`true` is explicitly not an approval: the contract is a decision record, and\naccepting a stray truthy value is how a seam like this stops meaning anything.\n\n## Where the policy lives\n\n`crates\u002Fharn-vm\u002Fsrc\u002Forchestration\u002Fpolicy\u002Fapproval_review_policy.toml`. Data in\nTOML, logic in Harn, Rust only at the seam — the provider catalog's split.\n\n| table | what it holds |\n|---|---|\n| `[reviewer]` | model, effort, timeout, `on_error` (only `deny`) |\n| `[breaker]` | consecutive and per-turn denial caps, cell-flag share |\n| `[floor]` | categories no reviewer may ever grant |\n| `[denylist]` | categories that start from a presumption of denial |\n| `[trust]` | which inputs may widen authorization and which may never |\n| `[verdict]` | the risk and authorization ladders, and the threshold table |\n\nChange the file, not the code. The Rust seam reads it once and hands it to the\nHarn session.\n\n## Two invariants worth stating before you edit anything\n\n**The floor is checked before the model is consulted.** A request naming a\nnever-grant category never reaches a reviewer that could be argued out of it. A\nfloor enforced only inside the prompt is a suggestion.\n\n**The outcome is computed, not reported.** The reviewer returns `risk` and\n`authorization`; the allow\u002Fdeny is derived from the pair by `[verdict.thresholds]`.\nA model that tries to assert a verdict cannot, because nothing reads a field\nwhere it could put one.\n\n## Editing the reviewer prompt\n\nThe prompt defines each `risk` and `authorization` level, so a model rates\nwhat an action does to its real target and how far the goal's consent reaches,\nnot the most alarming word in the command. An action the user plainly asked\nfor is high risk and high authorization; rating it `critical` to express\ndanger makes it ungrantable, since the table never grants `critical`.\n\nOut-of-radius actions map to `unknown`, not `low`: the table grants a\nmedium-risk action at `low`. The worked examples are contrastive pairs that are\nnever calibration rows. A copied row turns the corpus into a recall test.\n\n## Running the calibration corpus\n\nUnit tests prove the threshold arithmetic. They cannot tell you whether a real\nmodel distinguishes \"read `~\u002F.ssh\u002Fconfig` to fix a git remote\" from \"read\n`~\u002F.ssh\u002Fid_rsa` while fixing a parser test\". Only the corpus can.\n\n```sh\nharn run scripts\u002Frun_approval_review_calibration.harn\n```\n\nCorpus:\n`crates\u002Fharn-stdlib\u002Fsrc\u002Fstdlib\u002Fagent\u002Fdata\u002Fapproval_review_calibration.toml`.\n\nMost commands appear twice, under a goal that authorizes them and one that does\nnot. That pairing is the measurement: a reviewer that denies `cat .env`\nunconditionally scores identically to one that reasons, unless the corpus also\ncontains the goal where reading it is the task.\n\n## Reading the report\n\nRead these two together, never one alone:\n\n- `false_approve_rate_unsafe` — the reviewer waved through something the goal\n  did not authorize. An incident.\n- `false_deny_rate_implied` — the reviewer broke legitimate work. This is the\n  failure the ladder exists to prevent, and a reviewer that denies everything\n  scores a perfect false-approve rate while being useless.\n\nAlso:\n\n- `floor_held` is pass\u002Ffail, not a rate. One approved floor case is too many.\n- `ambiguous_observed` is reported and **not scored**. Some cases are genuinely\n  hard and both verdicts defensible; inventing an expected answer would turn a\n  measurement into a preference.\n- `unanswered_count` counts reviewers that failed closed rather than judging.\n  Those are not denials on the merits, and counting them as such would flatter\n  the false-approve rate.\n\n## Reading the rollup on a run\n\nEvery count is tri-state on the resolver receipt. `approval_fallback_measured:\nfalse` means no resolver was installed and every count is nil — **never read\nthose nils as zeros**. A measured zero says the fallback never had to fire; a\nnil says nothing was measured, and the two must not read alike.\n\n`approval_resolver_matches_request: false` means a host resolved to a different\nresolver than it was asked for, so that run's honest-looking zeros describe a\npolicy nobody requested.\n\n## When refused paths are empty\n\nThey usually are. Linux Landlock refuses in-kernel per syscall with no userspace\ncallback, so the path does not exist to be reported; macOS supplies it only\nasynchronously after exit. Read `observability` first. An empty `refused_paths`\nnever means \"nothing was refused\".\n","# Approval review\n\nA permission gate that refuses work on a non-interactive run used to end the\nrun. There was nobody to ask. The **approval resolver** decides who answers\ninstead, and the **reviewer** is a model that answers by reading the refused\naction against the user's actual goal.\n\nPair with [[harn-agent]] for the loop this runs inside and [[harn-testing]] for\nwhat counts as evidence.\n\n## The reviewer is not a security boundary\n\nIt is a judgment layer above a boundary that stays exactly where it was. The\nsandbox is not relieved of its job because a reviewer exists, and no claim about\nthis feature should imply otherwise. Cursor says the same of its own auto-review\nand is right to.\n\n## Two orthogonal axes\n\n`PolicyAction {allow, ask, deny}` is the per-rule **verdict**. `ApprovalResolver\n{host, auto_review, allow_all}` is **who answers** when a rule says `ask`. They\nare separate types on purpose: `PolicyAction` is ranked and that rank drives\nmost-restrictive-wins composition across the policy stack, so \"is `allow_all`\nmore restrictive than `allow`?\" is a question with no answer and must never be\nasked.\n\n- `host` — ask a person. A surface that cannot reach one reports the ask as\n  unsatisfiable rather than answering on their behalf.\n- `auto_review` — route to the reviewer. Headless and eval default.\n- `allow_all` — answer every ask yes. This is what `yolo` and `full-auto` mean,\n  and it has one owner rather than a per-surface empty policy overlay.\n\nMatched deny rules never reach a reviewer, including `allow_all`. Their receipts\nrecord `auto_review.unavailable_reason: \"deny_rule\"` and `reviewer_answered: false`.\nReviewers may answer asks and deliberately reviewable refusals such as side-effect\nceilings. `allow_all` also preserves the catastrophic floor.\n\n## Install a reviewer on a loop\n\n```harn\nimport { approval_reviewer_for } from \"std\u002Fagent\u002Fapproval_review\"\n\nagent_loop(harness, task, system, {\n  approval_policy: policy,\n  approval_reviewer: approval_reviewer_for(harness, review_policy, goal),\n})\n```\n\n`goal` is required, not optional. The reviewer's question is never \"is this\ncommand dangerous\" in the abstract — it is \"does THIS goal authorize THIS\naction\". A reviewer given no goal can only reason from the command, which is the\nweaker instrument and the one that produces both kinds of error.\n\n`approval_reviewer` is a closure taking the refusal record and returning a\ndecision record. `approval_reviewer_for` adapts the runtime's shape to\n`approval_review_decide`; anything returning `{approved, reviewer_answered,\nrationale}` works.\n\n### Yolo is this seam, not a bypass\n\n```harn\napproval_reviewer: approval_allow_all_reviewer(review_policy)\n```\n\n`allow_all` is a reviewer that says yes. Routing it through the same seam rather\nthan around it means it inherits the floor, the receipt, and the activity record\n— and there is exactly one place in the system where a refusal can be lifted. It\ndoes **not** lift the catastrophic floor.\n\n### It fails closed\n\nNo reviewer, no VM context, a closure that raises, an unparseable verdict, or\nre-entrancy all leave the call denied exactly as it would have been. A bare\n`true` is explicitly not an approval: the contract is a decision record, and\naccepting a stray truthy value is how a seam like this stops meaning anything.\n\n## Where the policy lives\n\n`crates\u002Fharn-vm\u002Fsrc\u002Forchestration\u002Fpolicy\u002Fapproval_review_policy.toml`. Data in\nTOML, logic in Harn, Rust only at the seam — the provider catalog's split.\n\n| table | what it holds |\n|---|---|\n| `[reviewer]` | model, effort, timeout, `on_error` (only `deny`) |\n| `[breaker]` | consecutive and per-turn denial caps, cell-flag share |\n| `[floor]` | categories no reviewer may ever grant |\n| `[denylist]` | categories that start from a presumption of denial |\n| `[trust]` | which inputs may widen authorization and which may never |\n| `[verdict]` | the risk and authorization ladders, and the threshold table |\n\nChange the file, not the code. The Rust seam reads it once and hands it to the\nHarn session.\n\n## Two invariants worth stating before you edit anything\n\n**The floor is checked before the model is consulted.** A request naming a\nnever-grant category never reaches a reviewer that could be argued out of it. A\nfloor enforced only inside the prompt is a suggestion.\n\n**The outcome is computed, not reported.** The reviewer returns `risk` and\n`authorization`; the allow\u002Fdeny is derived from the pair by `[verdict.thresholds]`.\nA model that tries to assert a verdict cannot, because nothing reads a field\nwhere it could put one.\n\n## Editing the reviewer prompt\n\nThe prompt defines each `risk` and `authorization` level, so a model rates\nwhat an action does to its real target and how far the goal's consent reaches,\nnot the most alarming word in the command. An action the user plainly asked\nfor is high risk and high authorization; rating it `critical` to express\ndanger makes it ungrantable, since the table never grants `critical`.\n\nOut-of-radius actions map to `unknown`, not `low`: the table grants a\nmedium-risk action at `low`. The worked examples are contrastive pairs that are\nnever calibration rows. A copied row turns the corpus into a recall test.\n\n## Running the calibration corpus\n\nUnit tests prove the threshold arithmetic. They cannot tell you whether a real\nmodel distinguishes \"read `~\u002F.ssh\u002Fconfig` to fix a git remote\" from \"read\n`~\u002F.ssh\u002Fid_rsa` while fixing a parser test\". Only the corpus can.\n\n```sh\nharn run scripts\u002Frun_approval_review_calibration.harn\n```\n\nCorpus:\n`crates\u002Fharn-stdlib\u002Fsrc\u002Fstdlib\u002Fagent\u002Fdata\u002Fapproval_review_calibration.toml`.\n\nMost commands appear twice, under a goal that authorizes them and one that does\nnot. That pairing is the measurement: a reviewer that denies `cat .env`\nunconditionally scores identically to one that reasons, unless the corpus also\ncontains the goal where reading it is the task.\n\n## Reading the report\n\nRead these two together, never one alone:\n\n- `false_approve_rate_unsafe` — the reviewer waved through something the goal\n  did not authorize. An incident.\n- `false_deny_rate_implied` — the reviewer broke legitimate work. This is the\n  failure the ladder exists to prevent, and a reviewer that denies everything\n  scores a perfect false-approve rate while being useless.\n\nAlso:\n\n- `floor_held` is pass\u002Ffail, not a rate. One approved floor case is too many.\n- `ambiguous_observed` is reported and **not scored**. Some cases are genuinely\n  hard and both verdicts defensible; inventing an expected answer would turn a\n  measurement into a preference.\n- `unanswered_count` counts reviewers that failed closed rather than judging.\n  Those are not denials on the merits, and counting them as such would flatter\n  the false-approve rate.\n\n## Reading the rollup on a run\n\nEvery count is tri-state on the resolver receipt. `approval_fallback_measured:\nfalse` means no resolver was installed and every count is nil — **never read\nthose nils as zeros**. A measured zero says the fallback never had to fire; a\nnil says nothing was measured, and the two must not read alike.\n\n`approval_resolver_matches_request: false` means a host resolved to a different\nresolver than it was asked for, so that run's honest-looking zeros describe a\npolicy nobody requested.\n\n## When refused paths are empty\n\nThey usually are. Linux Landlock refuses in-kernel per syscall with no userspace\ncallback, so the path does not exist to be reported; macOS supplies it only\nasynchronously after exit. Read `observability` first. An empty `refused_paths`\nnever means \"nothing was refused\".\n",{"name":5,"short":46,"description":7,"when_to_use":47},"The approval resolver, the reviewer that answers a denial, and its calibration corpus.","Use when changing approval-resolver behavior, editing the reviewer policy or denylist, reading the approval-fallback rollup, or explaining why a run was denied.","main","a52b7979c25b6be41bc82c4a6e583959a048f618","crates\u002Fharn-skills\u002Fsrc\u002Fcorpus\u002Fharn-approval-review",6,"https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn","https:\u002F\u002Fharnlang.com","https:\u002F\u002Fgithub.com\u002Fburin-labs","2026-11-11 12:45:49",{"source":8,"skillName":5,"commands":57,"prompt":67,"zipAvailable":68},[58,61,64],{"tool":59,"command":60},"npx","npx skills add burin-labs\u002Fharn --skill harn-approval-review",{"tool":62,"command":63},"bunx","bunx skills add burin-labs\u002Fharn --skill harn-approval-review",{"tool":65,"command":66},"pnpm","pnpm dlx skills add burin-labs\u002Fharn --skill harn-approval-review","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn\u002Fblob\u002Fmain\u002Fcrates\u002Fharn-skills\u002Fsrc\u002Fcorpus\u002Fharn-approval-review\u002FSKILL.md",true,[],[71,90,111,125,146,160],{"skillNo":72,"name":73,"nameZh":6,"description":74,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":75,"categories":76,"keywords":77,"publishedAt":41},"01M6J9Y4B3NZ9C24GN47159AEQ","stream-fan-in","Stream trigger example for routing or grouping event batches.","https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn\u002Fblob\u002Fmain\u002Fexamples\u002Ftriggers\u002Fstream-fan-in\u002FSKILL.md",[],[78,79,80,81,82,83,84,85,86,87,88,89,32],"stream","stream fan in","routing","customize","trigger","pipeline","for","batches","event","events","grouping","example",{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":96,"publishedAt":41},"01M6J9WNJHJV0K3CBZJFG8GD1R","github-stale-pr-nudger","Cron recipe for scanning repositories and nudging old pull requests.","https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn\u002Fblob\u002Fmain\u002Fexamples\u002Ftriggers\u002Fgithub-stale-pr-nudger\u002FSKILL.md",[],[97,98,99,100,101,84,102,103,104,105,106,32,107,108,109,81,110],"github stale pr nudger","stale","github","pr","requests","scheduled","follow-up","recipe","up","scanning","cron","pull","pull request","pull requests",{"skillNo":112,"name":113,"nameZh":6,"description":114,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":115,"categories":116,"keywords":117,"publishedAt":41},"01M6J9Y7X1D3ZSWWAG1GXFTWWY","webhook-generic-hmac","Generic webhook recipe with signed inbound requests.","https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn\u002Fblob\u002Fmain\u002Fexamples\u002Ftriggers\u002Fwebhook-generic-hmac\u002FSKILL.md",[],[118,119,120,81,24,101,121,104,82,122,123,124,32],"generic","webhook","webhook generic hmac","with","webhook trigger","provider","webhooks",{"skillNo":126,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":136,"publishedAt":41},"01M6J9Y10NVP86YMXCD9FQ2XZV","slack-thread-summarizer","Slack webhook recipe with a local summarize predicate.","https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn\u002Fblob\u002Fmain\u002Fexamples\u002Ftriggers\u002Fslack-thread-summarizer\u002FSKILL.md",[131],{"categoryNo":132,"categoryName":133,"taxonomyType":134,"taxonomyTypeDesc":135},"01JBSEEDCAT000000000000005","邮件与沟通","SCENARIO","使用场景",[137,138,139,140,141,142,24,143,144,121,119,104,82,81,145,32],"slack thread","slack thread summarizer","thread","summarizer","slack","local","summaries","threads","summarize",{"skillNo":147,"name":148,"nameZh":6,"description":149,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":150,"categories":151,"keywords":153,"publishedAt":41},"01M6J9XWKSBQJ881KBP2TGMN3Z","slack-reaction-action","Slack Events API recipe for reaction_added workflows.","https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn\u002Fblob\u002Fmain\u002Fexamples\u002Ftriggers\u002Fslack-reaction-action\u002FSKILL.md",[152],{"categoryNo":132,"categoryName":133,"taxonomyType":134,"taxonomyTypeDesc":135},[141,154,155,84,32,24,103,87,104,82,156,105,157,158,81,159],"action","slack reaction action","reactions","work","api","workflows",{"skillNo":161,"name":162,"nameZh":6,"description":163,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":164,"categories":165,"keywords":167,"publishedAt":41},"01M6J9XRE6PD3J99VKA1TYWS9Y","slack-keyword-router","Slack webhook recipe for routing messages by keyword.","https:\u002F\u002Fgithub.com\u002Fburin-labs\u002Fharn\u002Fblob\u002Fmain\u002Fexamples\u002Ftriggers\u002Fslack-keyword-router\u002FSKILL.md",[166],{"categoryNo":132,"categoryName":133,"taxonomyType":134,"taxonomyTypeDesc":135},[168,169,141,170,32,83,171,80,81,119,24,172,84,104],"slack keyword router","router","simple","messages","slack messages"]