Limit a role to some objects
Pin the repositories ghapp reaches, then give each role the repositories it covers, so an agent with write tools writes only where one of its roles allows.
- Who
- You, as the admin of the server, and an agent that drafts the change
- Where
- A terminal with strazactl, the console, your midPoint and the agent's machine
- Profile
- Standalone and enterprise
In short
On ghapp, a role with pr_create could open pull requests in every repository the App reaches. You pin ghapp’s repositories in its manifest, store a listing credential so Straza lists them, and give each role the repositories it covers. The gateway then refuses a call whose repo no role of the caller covers.
On this page
A role on an MCP server names the tools its holders may call. On ghapp, a GitHub App server that reaches every repository its App is installed on, a role with pr_create could then open pull requests in all of them. This page pins ghapp’s repositories in Straza and gives each role the repositories it covers, so an agent that holds a write role writes only where that role allows. You end with a role that writes in two repositories, a role that reads in every repository, an agent in midPoint that holds both, and a refused call on a repository the write role does not cover.
You write no declaration by hand, because ghapp marks its own tools. Each of its 14 repository tools, such as repo_get, pr_create and pr_merge, names its repository in one required argument, repo, as owner/name. With git transport on, git_fetch and git_push name theirs the same way. Its listing tool, repo_list, returns every repository the App reaches with GitHub’s repository id and the custom properties you ask for, here team. That tool reaches every repository at once, so only a role on every repository may hold it, and ghapp has no tool that touches no repository.
Before you start
- ghapp running where strazad can reach it, started with
--httpon that address, with its App id and private key. The App on this page is installed on the organizationghapp-straza-test, whose repositories carry the custom propertyteam. Start ghapp withGHAPP_MCP_ATTRIBUTES=team, so its listing reports that property. - Two random values of your own for ghapp.
GHAPP_MCP_GATEWAY_SECRETis the server’s own secret, which Straza sends on every tool call.GHAPP_MCP_LISTING_TOKENopensrepo_listalone, and Straza lists the repositories with it. - A Straza global admin to add ghapp. In the console, from the pin to the roles, ghapp’s own admin may do every Straza step. For the strazactl steps, an admin API token in
STRAZA_API_TOKENthat holdsapps:read,apps:write,drafts:readanddrafts:write, minted in the console as Mint an admin API token shows. Reading the audit log needs a global admin or an admin role whose areas includeaudit:read, as Delegated admin sets up. - midPoint connected to Straza as midPoint sets it up, an admin API token with the scope
scim:read, and a midPoint admin login for the assignment. Any SCIM client reads the samecoversvalues. - An AI agent that calls through Straza’s gateway, such as
joe-java-developer-agentof the demo stack.
Let an agent draft the change
An agent can write both drafts this page needs, and you publish them. A coding agent such as Claude Code with the Straza skill holds no Straza admin credential, so you run the strazactl reads it asks for and hand it their output. It proposes a draft through the tool straza__draft_submit, which the gateway lists only to a session that holds the role straza-draft-config, as Propose and publish a change shows. No agent can publish, and the listing credential never passes through one, because you type it at a hidden prompt.
Tell the agent which repositories each team works in. It then works in this order:
- It adds the block that
strazactl apps objects ghappprints to the manifest thatstrazactl apps export ghappprints, from the output you hand it, and submits the file throughstraza__draft_submitas the first draft.straza__draft_statusreads the server’s check. - You publish the first draft and store the listing credential, as the steps below show. Straza then lists ghapp’s repositories.
- It writes one role file per team from the list that
strazactl apps objects ghappnow prints, with the repositories you named, and submits the files throughstraza__draft_submitas the second draft. - You publish the second draft and give the roles to agents in your IGA tool.
Two drafts are needed because a role can name a repository only once Straza has listed it. A role on every repository needs no list, so it may ride in the first draft. An agent’s draft waits under Drafts like any other, and you publish it in the console. The steps below make the same change by hand. Run their commands in your own terminal, on a token that no agent can read, never in a terminal an agent shares.
Add ghapp
ghapp is a remote server that reads a bearer token. Straza sends ghapp’s
GHAPP_MCP_GATEWAY_SECRETas the server’s own secret on every tool call. The addresshttp://ghapp:3003/mcpis ghapp’s name and port on the demo stack’s Compose network, so put the address strazad reaches ghapp at in its place.- MCP servers
- Add MCP server
On Server, type
ghapp, pick A server you run, over HTTP and enter ghapp’s address. The Credential step comes next: pick One shared secret for this server, type the value ofGHAPP_MCP_GATEWAY_SECRET, and leave the way it is sent onA bearer token in Authorization. Press Save and publish on Review. Add a server walks each step of the wizard.cat > ghapp.yaml <<'EOF' apiVersion: straza.dev/v1beta1 kind: App metadata: name: ghapp namespace: ai.straza description: GitHub through one GitHub App, a token per call for one repo server: name: ai.straza/ghapp description: Repos, pull requests and issues on GitHub. Each call gets a GitHub token for the one repo it names. repository: url: https://github.com/strazahq/ghapp-mcp-server source: github version: "0.0.1" straza: runtime: kind: remote remote: url: http://ghapp:3003/mcp credential: kind: static inject: as: header name: Authorization template: "Bearer {{secret}}" exposure: tools: ["*"] limits: rps: 10 EOF strazactl spec validate app -f ghapp.yaml strazactl apps install -f ghapp.yaml strazactl apps secret set ghappghapp.yaml: app OK installed ghapp (remote runtime). Health: degraded, requires a credential and none is stored. Set one with strazactl apps secret set ghapp, which asks for the value at a hidden prompt. secret set for the MCP server ghapp: the server's own secret, fingerprint ae0fThe install says that ghapp needs its secret, and
strazactl apps secret set ghappasks for it at a hidden prompt. Type the value ofGHAPP_MCP_GATEWAY_SECRETthere.Pin ghapp’s repositories
Pinning copies ghapp’s marks into its manifest, and Straza decides with that copy only. A mark that ghapp changes later therefore waits for a person to publish it.
strazactl apps objects ghapp strazactl apps objects draft ghappghapp pins no object type. Its tool marks declare this block: straza: objects: type: repo list: tool: repo_list attributes: - name: team setBy: org tools: branch_list: argument: repo file_get: argument: repo issue_comment: argument: repo issue_create: argument: repo issue_get: argument: repo issue_list: argument: repo pr_comment: argument: repo pr_create: argument: repo pr_get: argument: repo pr_list: argument: repo pr_merge: argument: repo repo_get: argument: repo repo_list: all search_code: argument: repo search_issues: argument: repo Pin it with strazactl apps objects draft ghapp, then publish the draft. Created draft 39 with 1 document. Nothing changes until a person publishes it. ~ App/ghapp warning App/ghapp ghapp has no listing credential, so Straza cannot list its repos, and only roles on every repo can use its repo tools until it has one. Add one with strazactl apps secret set ghapp --listing after you publish. Publish draft 39 in the console under Drafts: http://localhost:8420/console/drafts/39On a server that pins nothing, the first command prints the
straza.objectsblock that ghapp’s marks declare and ends with the command that pins it. The second opens a draft of ghapp’s manifest with that block, notedPins the repos that ghapp declares in its tool marks.strazactl runs on an admin API token, and a token never publishes, so a person publishes the draft in the console under Drafts, as the console form of this step shows.The console has no button that pins a server, so the draft comes from
strazactl apps objects draft ghappor from an agent. You review and publish it here:- Drafts
- Waiting
Select the draft. Under What changes, ghapp’s card shows the row
Pinned type, which readsnonenow andrepo (repos), listed by repo_list. 14 tools name a repo, 1 reaches every repo.after publishing. Press Publish…, then Publish.The pinned block reads as follows, written as you would keep it in a manifest file, such as one in the apps directory. Each repository tool names
repo,repo_listreaches every repository, andlist.attributeskeeps theteamproperty that ghapp reports. The pluralrepos, the word the console uses for the tab and the questions, is the type plus s, so the block leaves it out.straza: objects: type: repo list: tool: repo_list attributes: - {name: team, setBy: org} tools: branch_list: {argument: repo} file_get: {argument: repo} issue_comment: {argument: repo} issue_create: {argument: repo} issue_get: {argument: repo} issue_list: {argument: repo} pr_comment: {argument: repo} pr_create: {argument: repo} pr_get: {argument: repo} pr_list: {argument: repo} pr_merge: {argument: repo} repo_get: {argument: repo} repo_list: all search_code: {argument: repo} search_issues: {argument: repo}ghapp reports
setBy: orgforteamwhen only the organization’s owners may set the property, andobjectotherwise, including when it cannot read the property’s settings. WithoutGHAPP_MCP_ATTRIBUTES, the block has noattributesline. The attribute shows as a column on the Repos tab and in the strazactl table. A role still covers repositories by name or covers every one, and the role editor’s filter matches a repository’s name and title only.The draft’s check compares the block with ghapp’s live tools.
repo_listmust be marked read-only, andrepomust be a required string argument of every tool that names it. Because the block names every tool, nothing needs a decision. The check warnsghapp has no listing credential, so Straza cannot list its repos, and only roles on every repo can use its repo tools until it has one. Add one with strazactl apps secret set ghapp --listing after you publish.The warning does not stop the publish, and the next step clears it.When the server already has roles, the first pin gives each of them
covers: every, so no agent gains or loses a repository at that publish. For a role namedghapp-devs, the check saysPinning repos on ghapp keeps today's reach: ghapp-devs now covers every repo. Narrow it in this draft or later.Store the listing credential
Straza lists ghapp’s repositories with a credential of its own, the listing credential. It sends that credential on its own calls to
repo_listand on no other call, and it never falls back to the server’s own secret or a role’s. On its side, ghapp acceptsGHAPP_MCP_LISTING_TOKENonrepo_listalone and answers any other tool called with itThe listing token opens repo_list only. Call this tool through Straza.A leaked listing token therefore opens no write.CLI onlyThe console has no box for the listing credential, so this step runs in a terminal.
strazactl apps secret set ghapp --listinglisting credential set for the MCP server ghapp, fingerprint 5606Type the value of
GHAPP_MCP_LISTING_TOKENat the hidden prompt. The answer names the credential’s fingerprint, never its value, and Straza lists ghapp’s repositories within about 15 seconds. A listing credential can be stored only once the type is pinned, which is why this step follows the publish. Until one is stored,strazactl apps objects ghappand the Repos tab sayghapp has no listing credential, so Straza cannot list its repos. Add one with strazactl apps secret set ghapp --listing, then refresh with strazactl apps objects refresh ghapp.Check the list
- MCP servers
- ghapp
- Repos
The tab carries ghapp’s plural and appears once the type is pinned. Its strip reads
ghapp lists its repos with repo_list.and then the last listing with its count. Each repository is one row with the columns Repo, team, Covered by and In open drafts, and the help icon beside its name gives its id. Until a role covers it, a row readsno roleandNo agent reaches it.under Covered by. In open drafts names each open draft you may read that would add a role to the repository or drop one, such asDraft 41 adds ghapp-write-payments.strazactl apps objects ghappghapp lists its repos with repo_list. Last listing less than a minute ago: 3 repos. VALUE ID TITLE TEAM COVERED BY ghapp-straza-test/pay-api 1425832684 pay-api payments no role ghapp-straza-test/pay-web 1425833185 pay-web payments no role ghapp-straza-test/site 1425833478 site web no roleThe first line says how the last listing went, with its count of repositories. The table has the columns VALUE, ID, TITLE, TEAM and COVERED BY, and a repository reads
no roleuntil a role covers it.In the demo organization,
ghapp-straza-test/pay-apiandghapp-straza-test/pay-webcarry the teampayments, andghapp-straza-test/sitecarries the teamweb. A value in the list is the exact owner/name a call must carry, byte for byte, soghapp-straza-test/Pay-APIis another name thanghapp-straza-test/pay-api. Straza lists again every 10 minutes. After you create a repository,strazactl apps objects refresh ghappasks for a listing that one replica runs within 30 seconds, at most once a minute per server.Write the two roles
Each role names its tools and the repositories it covers.
ghapp-readholds the read tools on every repository, the ones the App reaches later included.ghapp-write-paymentsholds the write tools on the two repositories of the teampayments, by name, and a repository added later stays out until a person adds it. An agent that holds both reads everywhere and writes in those two repositories only.- Roles
- New role
- Application role
On
Kind, server and name, pick ghapp, typewrite-paymentsafter its name and press Next. On Access, pickOnly the tools you tickunder Which tools and tickissue_comment,issue_create,pr_comment,pr_createandpr_merge. Under the tools, On which repos saysghapp says 14 of its tools work on one repo. Pick which repos this role reaches with them.It offers two cards:Card Its line Every repo All, today’s count, thentoday, and every repo ghapp lists later.These repos Pick from ghapp's list. A repo added later stays out until you add it.Pick These repos and tick
ghapp-straza-test/pay-apiandghapp-straza-test/pay-webin the list, whichFilter reposnarrows. With These repos picked,repo_listcannot be ticked and readsrepo_list cannot be in a role on some repos: nothing keeps it to them. Make a role on every repo for it.The foldHow it is storedshows the access row as a role file holds it. Press Next, read Review, and press Save and publish.Make
ghapp-readthe same way, withbranch_list,file_get,issue_get,issue_list,pr_get,pr_list,repo_get,search_codeandsearch_issues, and Every repo.- Roles
- ghapp-write-payments
- Access
The column On which repos reads
2 repos: ghapp-straza-test/pay-api and ghapp-straza-test/pay-web. Onghapp-readit readsevery repo.A role on a pinned server is written as a role file, because
strazactl roles createtakes no covers line. Both files go into one draft:mkdir -p ghapp-roles cat > ghapp-roles/ghapp-read.yaml <<'EOF' apiVersion: straza.dev/v1beta1 kind: Role metadata: name: ghapp-read spec: kind: application description: Read tools of ghapp on every repo server: ghapp bindings: - app: ghapp tools: - branch_list - file_get - issue_get - issue_list - pr_get - pr_list - repo_get - search_code - search_issues covers: every EOF cat > ghapp-roles/ghapp-write-payments.yaml <<'EOF' apiVersion: straza.dev/v1beta1 kind: Role metadata: name: ghapp-write-payments spec: kind: application description: Write tools of ghapp on the payments repos server: ghapp bindings: - app: ghapp tools: - issue_comment - issue_create - pr_comment - pr_create - pr_merge covers: objects: - ghapp-straza-test/pay-api - ghapp-straza-test/pay-web EOF strazactl drafts check -f ghapp-roles/ strazactl drafts create -f ghapp-roles/ --note "Read on every repo, write on the payments repos"+ Role/ghapp-read + Role/ghapp-write-payments Checked against live state at 2026-10-19 17:37 UTC. info Role/ghapp-read Nobody holds ghapp-read yet, so it reaches nothing until someone is assigned it. info Role/ghapp-write-payments Nobody holds ghapp-write-payments yet, so it reaches nothing until someone is assigned it. Nothing was stored. The documents can be published: strazactl drafts create -f ghapp-roles/ Created draft 40 with 2 documents. Nothing changes until a person publishes it. + Role/ghapp-read + Role/ghapp-write-payments info Role/ghapp-read Nobody holds ghapp-read yet, so it reaches nothing until someone is assigned it. info Role/ghapp-write-payments Nobody holds ghapp-write-payments yet, so it reaches nothing until someone is assigned it. Publish draft 40 in the console under Drafts: http://localhost:8420/console/drafts/40drafts checkstores nothing, anddrafts createstores the draft. A person publishes it in the console under Drafts, because a token never publishes.The check refuses a role that nothing keeps to its repositories, and each refusal says what to change:
The role file The check says has no coversline on its binding to ghappghapp-read binds tools that work on repos. Say which repos it covers: every repo, or these repos by name. In the role file, write covers: every, or covers: {objects: [<repo>, ...]}.names ghapp-straza-test/Pay-API, which differs in caseghapp-straza-test/Pay-API is not in ghapp's repo list. Compare the name with ghapp's Repos tab or strazactl apps objects ghapp. If the repo is new, refresh the list with strazactl apps objects refresh ghapp, then try again.puts repo_listin a role on some reposrepo_list cannot be in a role on some repos: nothing keeps it to them. Make a role on every repo for it.writes covers: {every: true}Role ghapp-read does not read as a Role document: line 21: a covers mapping takes only the key objects, and this one carries every. Write covers: every for every object, or covers: {objects: [<name>, ...]} for some. Compare it with strazactl roles export ghapp-read and send the draft again.Straza stores each named repository with its id. A role therefore keeps covering a repository that GitHub renames, and a repository deleted and made again under the old name is not covered by the old line. ghapp’s own admin may write
covers: everyon a role ghapp owns while the settingadmin.serverAdminsCoverEveryis true, its default. Set it to false to leaveeveryto a global admin, and the Every repo card then readsOn this deployment only a global admin may make ghapp-read cover every repo, including the repos ghapp lists later. Name the repos it covers, or ask a global admin.Configuration lists the setting.Give the roles to an agent in midPoint
Your IGA tool sees what each role covers. Straza renders each role’s SCIM group with
covers, one value per access row and named repository. midPoint maps it to the role’s extension itemstrazaCoversand shows it in the column Covers of its Straza application roles list. Read the values a certifier sees:for role in ghapp-read ghapp-write-payments; do curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" -G http://localhost:8420/scim/v2/Groups \ --data-urlencode "filter=displayName eq \"$role\"" \ | jq -c '.Resources[0] | {displayName, covers: .["urn:straza:params:scim:schemas:extension:2.0:Group"].covers}' done{"displayName":"ghapp-read","covers":["ghapp:repo every"]} {"displayName":"ghapp-write-payments","covers":["ghapp:repo \"ghapp-straza-test/pay-api\"","ghapp:repo \"ghapp-straza-test/pay-web\""]}ADMIN_API_TOKENholds a token with the scopescim:read, such as the one midPoint mints.ghapp-readgivesghapp:repo every.ghapp-write-paymentsgives one value per repository,ghapp:repo "ghapp-straza-test/pay-api"andghapp:repo "ghapp-straza-test/pay-web", and the quotes keep a repository named every from passing for every repository. Like the rest of the group’s extension,coversis read-only, and a PATCH on it answers400withscimTypemutability.Assign the imported roles
AR:ghapp-readandAR:ghapp-write-paymentsto the agent in midPoint, directly or through an access package, as Assign an access package shows. midPoint writes one membership per role over SCIM. On the demo stack, this assigns both roles tojoe-java-developer-agentover midPoint’s REST interface:AGENT_OID=b6f9f2c8-0d5a-4e77-9a1e-9d3c0e5a1c4e for role in AR:ghapp-read AR:ghapp-write-payments; do ROLE_OID= for try in $(seq 1 90); do ROLE_OID=$(curl -s -u 'administrator:StrazaEval5ecr3t!' -H 'Content-Type: application/xml' \ -X POST --data-binary "<q:query xmlns:q=\"http://prism.evolveum.com/xml/ns/public/query-3\"><q:filter><q:equal><q:path>name</q:path><q:value>$role</q:value></q:equal></q:filter></q:query>" \ http://localhost:8087/midpoint/ws/rest/roles/search | grep -o 'oid="[0-9a-f-]\{36\}"' | head -1 | cut -d'"' -f2) [ -n "$ROLE_OID" ] && break sleep 2 done if [ -z "$ROLE_OID" ]; then echo "$role is not in midPoint after 180 seconds, so nothing was assigned. Check that midPoint's role LiveSync task runs, then run this again." break fi cat > assign.xml <<EOF <objectModification xmlns="http://midpoint.evolveum.com/xml/ns/public/common/api-types-3" xmlns:c="http://midpoint.evolveum.com/xml/ns/public/common/common-3" xmlns:t="http://prism.evolveum.com/xml/ns/public/types-3"> <itemDelta> <t:modificationType>add</t:modificationType> <t:path>c:assignment</t:path> <t:value><c:targetRef oid="$ROLE_OID" type="c:RoleType"/></t:value> </itemDelta> </objectModification> EOF curl -s -o /dev/null -w "$role HTTP %{http_code}\n" -u 'administrator:StrazaEval5ecr3t!' -H 'Content-Type: application/xml' \ -X PATCH --data-binary @assign.xml http://localhost:8087/midpoint/ws/rest/users/$AGENT_OID doneAR:ghapp-read HTTP 204 AR:ghapp-write-payments HTTP 204AGENT_OIDis the agent in your midPoint. midPoint imports a new Straza role at its next role LiveSync, which runs every 10 seconds on the demo stack, so the loop waits up to 180 seconds for each imported role to appear before it assigns it. The agent holds both roles from its next check-in.The column Covers shows each role’s
strazaCovers, the same values SCIM gave above. Read them from midPoint’s REST interface:for role in AR:ghapp-read AR:ghapp-write-payments; do curl -s -u 'administrator:StrazaEval5ecr3t!' -H 'Content-Type: application/xml' -H 'Accept: application/json' \ -X POST --data-binary "<q:query xmlns:q=\"http://prism.evolveum.com/xml/ns/public/query-3\"><q:filter><q:equal><q:path>name</q:path><q:value>$role</q:value></q:equal></q:filter></q:query>" \ http://localhost:8087/midpoint/ws/rest/roles/search \ | jq -c '.object.object[0] | {name, strazaCovers: [.extension.strazaCovers] | flatten | map(.["@value"])}' done{"name":"AR:ghapp-read","strazaCovers":["ghapp:repo every"]} {"name":"AR:ghapp-write-payments","strazaCovers":["ghapp:repo \"ghapp-straza-test/pay-api\"","ghapp:repo \"ghapp-straza-test/pay-web\""]}Read the refusal
Call a write tool on a repository the write role does not cover. From the agent’s machine, through ghapp’s own endpoint, this reads
ghapp-straza-test/pay-apiand then tries to open a pull request inghapp-straza-test/site:{ printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"docs-example","version":"1"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"repo_get","arguments":{"repo":"ghapp-straza-test/pay-api"}}}' \ '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"pr_create","arguments":{"repo":"ghapp-straza-test/site","title":"Bump the Go toolchain","head":"bump-go","base":"main"}}}'; sleep 5; } \ | straza mcp ghapp{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"extensions":{"io.modelcontextprotocol/ui":{}},"logging":{},"resources":{"listChanged":true},"tools":{"listChanged":true}},"protocolVersion":"2025-06-18","serverInfo":{"name":"straza","version":"v1.1.0-138-g8cd72d69"}}} {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Straza: pr_create on ghapp-straza-test/site was refused: none of your roles on ghapp covers that repo. Your roles cover pr_create on ghapp-straza-test/pay-api and ghapp-straza-test/pay-web. Check the repo's name, or ask an admin for a role that covers it."}],"isError":true}} {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"ghapp-straza-test/pay-api (id 1425832684): private, default branch \"main\"."}],"structuredContent":{"archived":false,"defaultBranch":"main","fullName":"ghapp-straza-test/pay-api","htmlUrl":"https://github.com/ghapp-straza-test/pay-api","id":"1425832684","name":"pay-api","owner":"ghapp-straza-test","ownerType":"Organization","private":true}}}The first call goes through, because
ghapp-readcovers every repository. The second never reaches ghapp. Its refusal comes back before the answer torepo_get, because it never leaves Straza. Straza answers it with a tool result marked as an error, and the agent readsStraza: pr_create on ghapp-straza-test/site was refused: none of your roles on ghapp covers that repo. Your roles cover pr_create on ghapp-straza-test/pay-api and ghapp-straza-test/pay-web. Check the repo's name, or ask an admin for a role that covers it.That sentence names only repositories the agent’s own roles cover, at most ten. A repository that no held role covers and one the list does not hold get the same answer, so an agent learns nothing about repositories it cannot use. Roles on every repository cover one the list does not hold yet, so a repository created a minute ago works at once through such a role. When a refused call names a repository the list does not hold, Straza asks for a listing, at most once a minute, and the new repository then shows in the list, where a person can add it to a role that names repositories. The check runs before any policy, so no policy rule can allow a call it refuses, and the refusal spends no approval.
- Audit
- deny
The newest row reads
ghapp.pr_create on ghapp-straza-test/siteunder What, followed by the call’s arguments, with the agent’s sentence under Reason. Select it. The sheet has a row named Repo with the value and its id in ghapp’s list.strazactl audit tail --app ghapp --type straza.audit.mcp --limit 3#382 [joe-java-developer-agent] {"data":{"app":"ghapp","count":14,"default":false,"effect":"allow","event":"tools.list","granted":false,"harness":"claude-code/mcp","reason":"catalog served","ruleId":"","session":"01a156a5-c19a-7b1f-af1e-a4df00283dde","setName":"","snapshot":"de711b9f89fd7ef39105fa6f30103ce06449f1969206f97ffa8b71564c5eace1","tool":"","toolName":"","user":"01a1547c-366e-7767-97eb-caa598255d94"},"id":"ea5ff55c-a242-4fbc-be4f-e7c477802997","source":"strazad","specversion":"1.0","time":"2026-10-20T00:11:06.076296916Z","type":"straza.audit.mcp"} #383 [joe-java-developer-agent] {"data":{"app":"ghapp","arguments":"{\"repo\":\"ghapp-straza-test/pay-api\"}","bindingId":"01a1553d-bc97-7c68-a755-4d26e5d08b2f","credentialId":"01a1553c-ae88-7e94-9afc-dc6cfc5d14be","credentialSource":"shared","default":true,"effect":"allow","event":"tool.pre","granted":true,"harness":"claude-code/mcp","object":{"id":"1425832684","reach":"argument","type":"repo","value":"ghapp-straza-test/pay-api"},"reason":"allowed by role access. No policy rule gates this tool.","role":"ghapp-read","ruleId":"","session":"01a156a5-c19a-7b1f-af1e-a4df00283dde","setName":"","snapshot":"de711b9f89fd7ef39105fa6f30103ce06449f1969206f97ffa8b71564c5eace1","tool":"mcp.call","toolName":"repo_get","user":"01a1547c-366e-7767-97eb-caa598255d94"},"id":"7443f5cc-7af8-493b-b12b-8a7a50c302a9","source":"strazad","specversion":"1.0","time":"2026-10-20T00:11:06.086979293Z","type":"straza.audit.mcp"} #384 [joe-java-developer-agent] {"data":{"app":"ghapp","arguments":"{\"repo\":\"ghapp-straza-test/site\",\"title\":\"Bump the Go toolchain\",\"head\":\"bump-go\",\"base\":\"main\"}","default":false,"effect":"deny","event":"tool.pre","granted":false,"harness":"claude-code/mcp","object":{"id":"1425833478","reach":"argument","type":"repo","value":"ghapp-straza-test/site"},"reason":"Straza: pr_create on ghapp-straza-test/site was refused: none of your roles on ghapp covers that repo. Your roles cover pr_create on ghapp-straza-test/pay-api and ghapp-straza-test/pay-web. Check the repo's name, or ask an admin for a role that covers it.","rolesChecked":["ghapp-write-payments"],"ruleId":"","session":"01a156a5-c19a-7b1f-af1e-a4df00283dde","setName":"","snapshot":"de711b9f89fd7ef39105fa6f30103ce06449f1969206f97ffa8b71564c5eace1","tool":"mcp.call","toolName":"pr_create","user":"01a1547c-366e-7767-97eb-caa598255d94"},"id":"fdadeb1e-3f0b-42f6-8159-a1392a6bf3c3","source":"strazad","specversion":"1.0","time":"2026-10-20T00:11:06.093335815Z","type":"straza.audit.mcp"}The deny record carries
object, with the typerepo, the reachargument, the valueghapp-straza-test/siteand its id in ghapp’s list,1425833478. It also carriesrolesCheckedwithghapp-write-payments, the held role that bindspr_create. It has noroleand nobindingId, because no role admitted the call.A covered call then meets your policies like any other call. An approval that a policy asks for on such a call holds for that repository alone, because Straza ties the approval to the repository’s type and value. The approval card names the repository only inside the call’s arguments, so with
approval.preview.enabledset to false the approver does not see it. Events lists every field of the record.
What ghapp promises
Straza checks the repository a call names, and ghapp keeps the call to that repository. Every marked server makes three promises that Straza cannot check: a call touches only the object its argument names, the value names exactly one object at the time of the call, and the server accepts calls only from the gateway. Object marks for server authors states them for a server’s author.
ghapp keeps them in four ways. It compares the name with GitHub’s own spelling byte for byte and never follows a rename redirect. When a role on some repositories admitted the call, Straza sends the repository’s id from its list as params._meta["ai.straza/object-id"], and ghapp refuses the call when the name now leads to another id, such as a repository deleted and made again. It then makes a GitHub token for that one repository with only the tool’s permissions. Its last guard is the caller: ghapp accepts the gateway secret, a token from a listed identity provider client whose only credential is Straza’s client assertion key, and the listing token on repo_list alone.
Straza checks repositories in its gateway only, and its harness hooks never run this check. An agent that reached ghapp some other way would meet no repository check, which is why the third promise matters. The security model draws that boundary.
Next
- Objects explains the model behind these steps.
- Object marks for server authors is the contract for a server that wants the same limits.
- Access per role previews which tools a role’s sessions see and puts a tool behind approval.
- midPoint hands the roles out through access packages and takes them away again.
Walked on v1.0.0-1561-g98d49634 on 2026-10-20, except the console steps, read from source, and the last line of each draft's output, written from the code.
The enterprise demo stack on Rocky Linux 10.1, with ghapp started beside it from its own deploy kit on a private GitHub App installed on the organization ghapp-straza-test. Every strazactl step but the two publishes ran on an admin API token, and the note strazactl then printed on stderr on a token is left out of the output. Both drafts were then published on the admin's own strazactl login, with y answered at the prompt. strazactl no longer signs in or publishes, so a person publishes each draft in the console, and the last line of each draft's output, which names that way, was written from the code on 2026-11-01 and not run. Both secrets were set from STRAZA_SECRET_VALUE, so no hidden prompt shows. The SCIM read ran on a token with scim:read alone. midPoint had imported both roles before the assignment loop ran, so it did not wait. The four refusals in the table were checked with strazactl drafts check on edited copies of the two role files. The agent's calls ran as joe-java-developer-agent through straza mcp ghapp in the demo stack's harness container, a Debian 12 machine with straza v1.1.0-138-g8cd72d69. The console steps were read from the console's source and not clicked. A fix then changed three sentences, so the second and fourth rows of the refusal table, the agent's calls and the audit records were walked again on 2026-10-20 at v1.0.0-1561-g98d49634. The other steps ran on 2026-10-19 at v1.0.0-1556-ge9afd09a.