Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B2.4.mp4Permanent scope makes every injection a successful one, because the authority is always there when the attacker arrives.
Short-lived, purpose-bound grants issued per task and expiring with it.
Issue a scoped grant, use it, then replay it after expiry and after the task closed.
Standing authority means a successful injection always finds a live credential waiting. Just-in-time authority means the attacker has to arrive during the ninety seconds the grant exists, and be doing the one task it was scoped to.
bind_call() and a cnf claim. A token minted for get_booking(2) stops working against get_booking(3). It does not stop get_booking(2) returning Priya's booking to Dana — that is rows 1 and 4 of cybertravels/LABELS.md, and C2.3 is where it is found.Binding a delegated token to the exact call it was minted for, so a captured token cannot be replayed against different arguments — including a clear statement of what that does not close, which is whether the object belongs to the caller.
Permanent scope makes every injection a successful one, because the authority is already there when the attacker arrives.
Short-lived, purpose-bound grants issued per task and expiring with it.
Median grant lifetime, and the amount of standing scope removed.
Measure this
| what to measure | denominator or target |
|---|---|
| grant lifetime | median, in minutes |
| standing scope removed | count of scopes, against the baseline |
standing just-in-time +-----------------+ +---------------------+ | granted once | | granted per task | | lives forever | | expires in 90s | | any task | | this resource only | +-----------------+ +---------------------+ injection always finds injection has to arrive a live credential during the window, on that task
Mitigates: T3 Privilege Compromise · T2 Tool Misuse.
B2.3 narrows authority at the moment of delegation. This lesson removes it when the task is over.
Standing authority is the reason an injection is always worth attempting: the credential is there, permanently, waiting. Every successful B1.3 lands on a live grant. Just-in-time authority changes the arithmetic — the attacker has to arrive during a window that exists only while a specific task is running, and that is bound to a specific resource.
Three properties, and the third is the one usually skipped:
Short-lived. Minutes, not months. Theft has a deadline.
Purpose-bound. Scoped to this resource, not to the resource class. Not reports:write but reports:write on report 8812.
Revoked on completion. The grant ends when the task ends, not when the timer does. A task that finishes in ten seconds should not leave a fifteen-minute credential lying around, which is the difference between a TTL and an actual lifecycle.
The operational cost is real and worth stating plainly: something must issue these, and if that path breaks, work stops. That is the trade — a system that fails closed under a control outage, in exchange for a system that has no standing authority to steal.
What this control closes. Removes the standing grant an injection needs. The attacker must now arrive inside a window bound to one task and one resource.
Just-in-time authority is only worth building where standing authority exists today, and at CyberTravels that list is not the one in the design document — it is in the authorisation graph and in the OAuth scopes the credential provider stored when someone first connected the payments API. The procedure diffs what each identity holds against what its declared tools actually need, and flags every permanent grant. This is the file in this repository:
The skill — skills/attestation/entitlement-overprivilege-analyzer/SKILL.md
name: entitlement-overprivilege-analyzer
description: >-
Analyse entitlement and identity registries to find over-privileged access
held by a deployment's identities. Use to check granted entitlements
against what the declared tools actually need, to find over-broad OAuth
scopes on credential providers, or to find standing privilege.
allowed-tools: Bash, ReadControls: Controls 1 and 3 — over-privileged access
The IAM verifier measures cloud permissions against cloud usage. This skill measures application-level entitlements — relationship-graph authorisation, OAuth scopes on stored credential providers, and identity-registry relationships — against what the declared tool surface actually requires.
An agent can hold a minimal cloud role and an OAuth token with full mailbox access.
After the IAM baseline is verified, not instead of it. The IAM verifier asks whether the role is default-deny; this asks whether the entitlements actually granted exceed what the declared tools need. Reach for it when scopes were granted at integration time, when a credential provider holds OAuth scopes nobody has reviewed, or when looking for standing privilege.
Input — the fixture committed at the top of scripts/entitlement_overprivilege_analyzer.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.
Output — the opening lines of a real run:
the task's own write ok permitted
a different report REFUSED bound to report/8812
a different scope REFUSED scoped to reports:write
after the task completes REFUSED task closed
after the TTL expires REFUSED expired
An injection landing at 09:14 needs a task to be open, on the resource
it wants, holding the scope it wants. Standing authority required none
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"deployment_id": "str",
"identities": [
{"id": "str",
"granted": ["str"],
"required_by_tools": ["str"],
"excess": [{"grant": "str", "justification_gap": "str"}]}
],
"oauth_scope_excess": [{"provider": "str", "granted_scope": "str", "needed_scope": "str"}],
"standing_privilege": ["str"],
"verdict": "PASS|PARTIAL|FAIL"
}
Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons B2.4Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b2-4-just-in-time-authority:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B2.4 | type /b2-4-just-in-time-authority |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B2.4 | type /b2-4-just-in-time-authority in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B2.4 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B2.4 | type $b2-4-just-in-time-authority, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py B2.4Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at B2.4 --out work/cybertravels
python3 scripts/checkpoint.py --at B2.4 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
A grant bound to one scope, one resource and one task permits only the task's own write — refusing a different report, a different scope, any use after the task closes, and any use after the TTL expires. The skill also reports its shape. Note what its procedure insists on: the denominator is the capability set the tools require, not another set of grants — comparing grants against grants is how a review concludes that an over-privileged agent is normal — and a narrow-looking scope still counts as standing privilege if it never expires.
Take one standing grant an agent holds and work out what would break if it expired in two minutes. That list is the real cost of just-in-time, and it is usually shorter than expected.
Tools used
Skill source: skills/attestation/entitlement-overprivilege-analyzer