STRAZAdocs
Markdown

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 --http on that address, with its App id and private key. The App on this page is installed on the organization ghapp-straza-test, whose repositories carry the custom property team. Start ghapp with GHAPP_MCP_ATTRIBUTES=team, so its listing reports that property.
  • Two random values of your own for ghapp. GHAPP_MCP_GATEWAY_SECRET is the server’s own secret, which Straza sends on every tool call. GHAPP_MCP_LISTING_TOKEN opens repo_list alone, 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_TOKEN that holds apps:read, apps:write, drafts:read and drafts: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 include audit: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 same covers values.
  • An AI agent that calls through Straza’s gateway, such as joe-java-developer-agent of 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:

  1. It adds the block that strazactl apps objects ghapp prints to the manifest that strazactl apps export ghapp prints, from the output you hand it, and submits the file through straza__draft_submit as the first draft. straza__draft_status reads the server’s check.
  2. You publish the first draft and store the listing credential, as the steps below show. Straza then lists ghapp’s repositories.
  3. It writes one role file per team from the list that strazactl apps objects ghapp now prints, with the repositories you named, and submits the files through straza__draft_submit as the second draft.
  4. 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.

  1. Add ghapp

    ghapp is a remote server that reads a bearer token. Straza sends ghapp’s GHAPP_MCP_GATEWAY_SECRET as the server’s own secret on every tool call. The address http://ghapp:3003/mcp is ghapp’s name and port on the demo stack’s Compose network, so put the address strazad reaches ghapp at in its place.

    1. MCP servers
    2. 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 of GHAPP_MCP_GATEWAY_SECRET, and leave the way it is sent on A bearer token in Authorization. Press Save and publish on Review. Add a server walks each step of the wizard.

    Your terminal administration
    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 ghapp
    ghapp.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 ae0f

    The install says that ghapp needs its secret, and strazactl apps secret set ghapp asks for it at a hidden prompt. Type the value of GHAPP_MCP_GATEWAY_SECRET there.

  2. 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.

    Your terminal administration
    strazactl apps objects ghapp
    strazactl apps objects draft ghapp
    ghapp 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/39

    On a server that pins nothing, the first command prints the straza.objects block 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, noted Pins 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 ghapp or from an agent. You review and publish it here:

    1. Drafts
    2. Waiting

    Select the draft. Under What changes, ghapp’s card shows the row Pinned type, which reads none now and repo (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_list reaches every repository, and list.attributes keeps the team property that ghapp reports. The plural repos, 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: org for team when only the organization’s owners may set the property, and object otherwise, including when it cannot read the property’s settings. Without GHAPP_MCP_ATTRIBUTES, the block has no attributes line. 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_list must be marked read-only, and repo must be a required string argument of every tool that names it. Because the block names every tool, nothing needs a decision. The check warns 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. 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 named ghapp-devs, the check says Pinning repos on ghapp keeps today's reach: ghapp-devs now covers every repo. Narrow it in this draft or later.

  3. 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_list and on no other call, and it never falls back to the server’s own secret or a role’s. On its side, ghapp accepts GHAPP_MCP_LISTING_TOKEN on repo_list alone and answers any other tool called with it The listing token opens repo_list only. Call this tool through Straza. A leaked listing token therefore opens no write.

    CLI only

    The console has no box for the listing credential, so this step runs in a terminal.

    Your terminal administration
    strazactl apps secret set ghapp --listing
    listing credential set for the MCP server ghapp, fingerprint 5606

    Type the value of GHAPP_MCP_LISTING_TOKEN at 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 ghapp and the Repos tab say ghapp 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.

  4. Check the list

    1. MCP servers
    2. ghapp
    3. 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 reads no role and No 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 as Draft 41 adds ghapp-write-payments.

    Your terminal administration
    strazactl apps objects ghapp
    ghapp 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 role

    The 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 role until a role covers it.

    In the demo organization, ghapp-straza-test/pay-api and ghapp-straza-test/pay-web carry the team payments, and ghapp-straza-test/site carries the team web. A value in the list is the exact owner/name a call must carry, byte for byte, so ghapp-straza-test/Pay-API is another name than ghapp-straza-test/pay-api. Straza lists again every 10 minutes. After you create a repository, strazactl apps objects refresh ghapp asks for a listing that one replica runs within 30 seconds, at most once a minute per server.

  5. Write the two roles

    Each role names its tools and the repositories it covers. ghapp-read holds the read tools on every repository, the ones the App reaches later included. ghapp-write-payments holds the write tools on the two repositories of the team payments, 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.

    1. Roles
    2. New role
    3. Application role

    On Kind, server and name, pick ghapp, type write-payments after its name and press Next. On Access, pick Only the tools you tick under Which tools and tick issue_comment, issue_create, pr_comment, pr_create and pr_merge. Under the tools, On which repos says ghapp 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, then today, 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-api and ghapp-straza-test/pay-web in the list, which Filter repos narrows. With These repos picked, repo_list cannot be ticked and reads repo_list cannot be in a role on some repos: nothing keeps it to them. Make a role on every repo for it. The fold How it is stored shows the access row as a role file holds it. Press Next, read Review, and press Save and publish.

    Make ghapp-read the same way, with branch_list, file_get, issue_get, issue_list, pr_get, pr_list, repo_get, search_code and search_issues, and Every repo.

    1. Roles
    2. ghapp-write-payments
    3. Access

    The column On which repos reads 2 repos: ghapp-straza-test/pay-api and ghapp-straza-test/pay-web. On ghapp-read it reads every repo.

    A role on a pinned server is written as a role file, because strazactl roles create takes no covers line. Both files go into one draft:

    Your terminal administration
    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/40

    drafts check stores nothing, and drafts create stores 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 covers line on its binding to ghapp ghapp-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 case ghapp-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_list in a role on some repos repo_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: every on a role ghapp owns while the setting admin.serverAdminsCoverEvery is true, its default. Set it to false to leave every to a global admin, and the Every repo card then reads On 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.

  6. 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 item strazaCovers and shows it in the column Covers of its Straza application roles list. Read the values a certifier sees:

    Terminal SCIM, as midPoint reads it
    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_TOKEN holds a token with the scope scim:read, such as the one midPoint mints. ghapp-read gives ghapp:repo every. ghapp-write-payments gives one value per repository, ghapp:repo "ghapp-straza-test/pay-api" and ghapp: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, covers is read-only, and a PATCH on it answers 400 with scimType mutability.

    Assign the imported roles AR:ghapp-read and AR:ghapp-write-payments to 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 to joe-java-developer-agent over midPoint’s REST interface:

    Terminal midPoint REST, as the midPoint admin
    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
    done
    AR:ghapp-read HTTP 204
    AR:ghapp-write-payments HTTP 204

    AGENT_OID is 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:

    Terminal midPoint REST, as the midPoint admin
    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\""]}
  7. 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-api and then tries to open a pull request in ghapp-straza-test/site:

    Agent's machine client
    { 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-read covers every repository. The second never reaches ghapp. Its refusal comes back before the answer to repo_get, because it never leaves Straza. Straza answers it with a tool result marked as an error, and the agent reads 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.

    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.

    1. Audit
    2. deny

    The newest row reads ghapp.pr_create on ghapp-straza-test/site under 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.

    Your terminal administration
    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 type repo, the reach argument, the value ghapp-straza-test/site and its id in ghapp’s list, 1425833478. It also carries rolesChecked with ghapp-write-payments, the held role that binds pr_create. It has no role and no bindingId, 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.enabled set 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.

Search documentation

Search page titles, commands, and article text.

↑ ↓ Choose resultEnter OpenEsc Close