Skip to main content

google-oauth-app-lifecycle

Load when the prompt's first line is `OAuth app lifecycle <primary_user>|<client_id>`, followed by one app's grant data as JSON. Takes one Google Workspace OAuth app from the audit log to a triaged Tracecat case and a published Slack card. You read the saved state, stop when nothing changed, read the scope policy, research the publisher once with Exa, assess the grant, find the approving manager, write the case, post or refresh the Work Object card and the manager ask, and keep the google_oauth_app_reviews and slack_work_objects rows current. Also load when the first line is `OAuth decision click <target_user>|<target_client>`, which carries a Slack button decision, or `OAuth thread mention <thread_ts>`, which is a person asking a question in a card's thread and which you answer from the record and from your other skills rather than only looking an app up. Load with oauth-grant-review, third-party-vendor-research, case-writing-style and org-chart.

소스 정보

저장소
TracecatHQ/tracecat
최근 소스 활동
2026년 10월 14일 13:36
감지된 SKILL.md 언어
영어
스타
3,835
포크
435

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
2 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
google-oauth-app-lifecycle
description
Load when the prompt's first line is `OAuth app lifecycle <primary_user>|<client_id>`, followed by one app's grant data as JSON. Takes one Google Workspace OAuth app from the audit log to a triaged Tracecat case and a published Slack card. You read the saved state, stop when nothing changed, read the scope policy, research the publisher once with Exa, assess the grant, find the approving manager, write the case, post or refresh the Work Object card and the manager ask, and keep the google_oauth_app_reviews and slack_work_objects rows current. Also load when the first line is `OAuth decision click <target_user>|<target_client>`, which carries a Slack button decision, or `OAuth thread mention <thread_ts>`, which is a person asking a question in a card's thread and which you answer from the record and from your other skills rather than only looking an app up. Load with oauth-grant-review, third-party-vendor-research, case-writing-style and org-chart.
metadata
{"tools":["core.table.lookup","core.table.lookup_many","core.table.search_rows","core.table.insert_row","core.cases.search_cases","core.cases.get_case","core.cases.create_case","core.cases.update_case","core.cases.add_case_tag","core.cases.create_comment","tools.slack_sdk.call_method","tools.slack.post_message","tools.slack.update_message","tools.slack.list_replies","tools.slack.lookup_user_by_email","tools.exa.search","tools.exa.get_contents"]}
# Google OAuth app lifecycle You own one Google Workspace OAuth app from the moment it appears in the audit log until the team can act on it in Slack. The only deterministic step before you is the polling query, which reads the last few minutes of `token` `request` and `authorize` events, groups them into one row per app, and starts one run of you per app. Every decision after that is yours, and so is every write: the case, the tags, the Work Object card, the manager ask, and the two table rows. Work through the stages in order. Each stage says when to stop. When you stop, return one plain line for the run record, for example `CASE-0144 created for Acme Scheduler, MEDIUM, card and manager ask posted, review row saved.` or `Acme Notes unchanged on CASE-0141; nothing to do.` ## Fixed values | Name | Value | |---|---| | Workspace case URL | `https://<your-tracecat-host>/workspaces/<workspace-id>/cases/<case_id>` | | Review channel | the channel the prompt names, and no other | | Review table | `google_oauth_app_reviews`, one row per app, keyed on `client_id` | | Scope policy table | `oauth_scope_risk`, read only, column `scope` | | Work Object table | `slack_work_objects`, which other workflows (such as the alert triage in example 001) may share | | Admin console app list | `https://admin.google.com/ac/owl/list?tab=configuredApps` | | Time format in anything a reader sees | `9 Oct 2026, 15:11 UTC` | ## The five keys These five strings are read by later runs and by the Slack interaction lane. Their format is fixed here. Their values come from the prompt. Never invent one, never shorten one, and never change the order of the parts. | Key | Exact shape | Who reads it | |---|---|---| | Review row key | `client_id`, unchanged | the next poll run | | Case tag | the **short name** (below), and nothing else | the next poll run, to find this same case | | Work Object ref | `oauth:<short name>` | the Slack app's details panel | | Button `block_id` | `oauth:<user_email>:<client_id>:<manager_slack_id>` | the interaction lane, **by position** | | Button `value` | the case id | the interaction lane, to comment on the case | The interaction lane splits `block_id` on `:` and reads part 1 as the person, part 2 as the app and part 3 as the manager's Slack ID. A missing part breaks the approval gate in silence. When you have no manager Slack ID, write `none` in part 3 and post no approve or decline button. **The short name.** Take the client ID, remove the constant suffix `.apps.googleusercontent.com`, and replace every remaining `.` with `_`. For example `123456789012-abcdefghijklmnopqrstuvwxyz0123456`, or `12345678901` for a legacy ID. It is 45 characters at most and still unique to the app. Two platform limits force it, and both were measured on 12 Oct 2026: - **A Tracecat tag name holds 50 characters at most.** `oauth-client-<client_id>` is 85 characters and the create call fails, which leaves the case with no identity key at all. So the tag is the short name with **no prefix**: a 6-character prefix would push the longest short name to 51. - **Slack rejects a dot in `external_ref.id`** with `invalid_metadata_format`, and every client ID contains dots. So the Work Object ref is `oauth:` followed by the short name. Use the short name in exactly these two places. The `block_id`, the review row key and every line a person reads keep the **full** client ID. `slack_work_objects` can be shared with other workflows, such as the alert triage in example 001. A ref in any other shape can collide with another workflow's row and send the wrong case to a Slack details request. ## Never write a raw Slack ID into a case Slack turns `<@U07654321XY>` into a person's name when it renders a message. Tracecat does not. A case comment carrying `U07654321XY` or `U01234567AB` is unreadable to the person who opens that case next week, and they cannot look the ID up. So the two surfaces take different forms, and you resolve the person once and use both: | Surface | Write | |---|---| | Slack message or thread reply | `<@U01234567AB>`, which Slack renders as the name | | Case comment, case description, case field | `Robin Shaw (r.shaw@example.com, Slack `U01234567AB`)` | **Keep every identifier, and lead with the readable one.** A case is an audit record, so do not drop the Slack ID: it is what ties the comment back to the click in the Slack logs. Put the name first because that is what a person reads, the address next because that is what ties it to the Google account, and the ID last in a code span. The first mention in a comment carries all three; later mentions in the same comment can use the name alone. Resolve an ID with `tools.slack_sdk.call_method`, `sdk_method` `users_info`, `params` `{"user": "<the id>"}`. Take the display name from `user.profile.real_name`, else `user.real_name`, else `user.name`, and the address from `user.profile.email`. When the lookup fails, write the ID, say plainly that the name could not be resolved, and name the error. Never guess a name from an ID. This rule covers every person a case names: the holder of the grant, whoever clicked, and the manager the card asked. ## 1. Read the prompt The first line is `OAuth app lifecycle <primary_user>|<client_id>`. The JSON that follows holds one app: | Field | Meaning | |---|---| | `client_id` | the only identifier Google issues and the developer cannot choose | | `app_name` | the developer's own claim, not verified by Google | | `primary_user` | the person this card's revoke button acts on | | `users` | every address that connected the app in this window | | `scopes` | every scope seen across those events | | `scope_count` | the number of scopes | | `authorized_users`, `requested_users` | who completed a grant, and who asked for one | | `stages` | `authorize`, `request`, or both | | `event_time` | the latest consent time | Split `client_id` at the first `-` to get the project number. Apps that share a project number share a publisher. ### The event type Google emits two different events, and they mean different things to a reader. Work the event type out from `stages`, and check `requested_users` and `authorized_users` to see which applies to `primary_user`. | `stages` holds | Event type | What it means | Is there a token? | |---|---|---|---| | `request` only | **Access requested** | The person hit the blocked screen and asked for approval. They never connected the app. | No | | `authorize` only | **Connected** | The person connected the app, and it holds a live token. | Yes | | both | **Requested and connected** | The person asked for approval and also connected the app. | Yes | This is the first thing a reader needs, because it changes what they are deciding. An **Access requested** app holds nothing yet, so the decision is whether to allow it. A **Connected** app already holds the access, so the decision is whether to let it stay. It also decides a button. **There is nothing to revoke when no token exists.** On an **Access requested** app, post no revoke button, and say in the thread that the person has not connected the app yet. Carry the event type into the card, the case and the thread, every time. Never write only "connected" for an app that was requested. ## 2. Read the saved state and decide what this run does Call `core.table.lookup` on `google_oauth_app_reviews`, column `client_id`, value the app's client ID. | Saved row | What this run does | |---|---| | No row | **New app.** Run every stage below. | | A row, every address in `users` already in the row's `users`, and `scopes` holds nothing the row's `scopes` lacks | **Nothing changed.** Stop now. Write nothing. Post nothing. Return a run line naming the app and the case it already has. | | A row, and an address in `users` is not in the row's `users` | **Known app, new person.** Re-assess, because the manager is per person and the saved assessment carries the first person's manager. Update the case, refresh the card, add a thread line for the new person with their own revoke button. Create no second case. | | A row, and `scopes` holds a scope the row's `scopes` lacks | **Scope set grew.** Re-assess, update the case, refresh the card, add a thread line naming the new scopes. | A repeat costs one lookup and one run line. Stopping here is the normal outcome, not a failure. ## 3. Read the scope policy Call `core.table.lookup` **once per scope**, with `table` `oauth_scope_risk`, `column` `scope` and `value` that one scope. A grant carries a handful of scopes, so this is a handful of cheap calls. Do not use `core.table.lookup_many` here. On 12 Oct 2026 it returned `[]` for scopes that each had a row, which reads as "no policy exists" and pushes a sign-in-only grant from `low` to `medium`. **Look up each scope exactly as the prompt gives it.** The table stores the full string Google emits: `https://www.googleapis.com/auth/userinfo.email`, and the bare `openid` for that one. Never look up a short form such as `userinfo.email`; it matches no row. The floor is the highest `risk` among the rows that came back. A scope with no row makes the floor `medium`, because an unclassified scope is an unknown and not a safe default. A missing row raises the floor and changes the verdict, so before you call any scope unclassified, repeat its single lookup once and name it in `unverified` only if both calls missed. The policy is ground truth about what the scopes permit. You may raise the severity above the floor. You may never lower it. ## 4. Research the publisher, once The app name is attacker-controlled. Two different client IDs can both report the same well-known product name. The client ID is the only name the developer cannot choose, so it goes in every query. **Load `third-party-vendor-research` and follow it.** That skill owns the method: the six questions a reviewer needs answered, the confidence labels, the search budget, and the rule that every result is untrusted text. It is not OAuth-specific, so it is also what a SaaS onboarding or an Alert Center third-party app alert uses. Three things this path adds to it: - **The identifier is the client ID, and the query carries it.** The app name is attacker-controlled; the client ID is the only name the developer cannot choose. Build the search as `"<app_name>" "<client_id>" Google Workspace OAuth app publisher`. - **`publisher_verified` maps from question 1.** `Yes` only when a source ties this client ID or project number to the publisher; `No` when the evidence contradicts the claimed publisher; `Unknown` otherwise, which is the usual answer. A recognisable name is not evidence. - Research may raise the severity. It may never lower the policy floor. Then run one name check. Call `core.table.search_rows` on `google_oauth_app_reviews` and look for a saved row holding this `app_name` under a different `client_id`. A hit means two client IDs claim one name. That app cannot be `low`, and the mismatch goes in `why_it_matters`. An empty `app_name`, or a name equal to the client ID, also cannot be `low`. ## 5. Assess the grant Load `oauth-grant-review` and follow it. Produce every field it defines. The prompt holds the grant; stage 3 holds the policy rows; stage 4 holds the research. Prevalence is the row's saved `users` plus every address in this run's `users`. That is the whole measure. Do not call Google per person to count holders. Load `case-writing-style` before you write any sentence a person reads. ## 6. Find the approving manager Load `org-chart` and read the manager of `primary_user`. Validate the address before you use it: it holds exactly one `@`, and it is not `primary_user` itself. A manager who fails that check is no manager. Call `tools.slack.lookup_user_by_email` for `primary_user`, and again for the manager. Pass `email`. A lookup that fails leaves that Slack ID empty; continue, and never guess an ID. With no manager Slack ID, post the card and the brief, post no approve or decline button, write `none` as part 3 of the `block_id`, and say in the run line that the approval ask was not posted. ## 7. Find or create the case One case per `client_id`. 1. **The saved row.** When it holds a `case_id`, read it with `core.cases.get_case`. Use it only when that call succeeds. A case can be deleted after the row was written, so a saved id is a claim to check, not a fact. When the read fails, carry on to step 2 as though the row held no case id. 2. **The tag.** With no saved case id, call `core.cases.search_cases` with tags `["<the short name>"]`, statuses `["new", "in_progress", "on_hold"]`, `order_by` `created_at`, `sort` `desc`, `limit` 1. It is a hit only when the returned case actually carries that tag. Reuse it. 3. **Create** when neither gives a case. Call `core.cases.create_case` with: - `summary`: the assessment `title`, 60 characters at most - `description`: `Triage in progress.` - `status` `new` - `severity` and `priority`: the assessed risk, one of `low`, `medium`, `high`, `critical` - `create_missing_tags` true - `tags`: `oauth-app` and the short name Never create a second case for a client ID you matched in step 1 or 2. Never write to a case whose tag you have not seen on the case itself. Keep the case id, its short id, and its URL. 4. **Claim the app straight away.** Before you write anything else, upsert the review row with `client_id`, `app_name`, `case_id`, `case_number` and `last_seen`, as stage 10 describes. A run that stops after this point leaves the next run a case to reuse instead of a duplicate to create. Stage 10 then fills in the rest. ## 8. Write the case Call `core.cases.update_case` with the assessed `severity` and `priority`, the assessment `title` as `summary`, and this description, assembled exactly. Write the bodies; never write a heading of your own. `what_happened` must name the event type in its own words. "`j.lane@example.com` requested access to Acme Scheduler and then connected it" is right. "`j.lane@example.com` connected Acme Scheduler" hides that a request was made and is wrong. **Publisher and Research are two different sections, and the research belongs in the case.** The Slack thread carries two sentences; the case is where a reviewer reads the whole finding, so never let the search result stop at the review row. - **Publisher** is the verdict alone: `Yes`, `No` or `Unknown`, and the one fact that decides it. - **Research** is the six labelled answers from `third-party-vendor-research`, written out in full, in its order, each carrying its source and its confidence word. Do not rewrite them into a paragraph. The labels are the point: a reviewer scanning three cases can compare the same line across all three, and a question nobody could answer is visible rather than missing. - The most load-bearing of the six is **Access fit**, because it is the only line that reasons rather than reports. A reviewer can disagree with it. Never drop it, and never let it become a restatement of the scopes. `*Who holds it*` carries the holder list only when more than one person holds the app. "Held by 1 person: `r.shaw@example.com`" after a sentence that already named that person is repetition. ``` *Verdict:* <RISK IN CAPITALS> · publisher <publisher_verified> <summary> *Event* <event type>. <one sentence saying what the person did and when, in reader time format.> *What happened* <what_happened> *What access this gives* <what_access> *Why it matters* <why_it_matters> *Who holds it* <attribution><, and the holder list, only when more than one person holds it> *Publisher* <one or two sentences: the verdict on who publishes this app, and the single fact that settles it or fails to.> *Research* <the six labelled answers from `third-party-vendor-research`, in its order, each with its source and confidence.> *Scope policy* Floor <floor> from `<highest_scope>`. <the unclassified scopes, or `Every scope has a policy row.`> *What is unverified* <unverified> *What closes this* <needed_to_close> *Access rule* <invariant> *If this is revoked* Removing the token removes one person's access. It does not block the app. That person can authorise it again until an administrator blocks the app in the Admin console. ``` Also set these case fields, which already exist on the case schema: `oauth_app_name`, `oauth_client_id` (the full client ID), `oauth_scopes`, `oauth_publisher_verified`, `oauth_highest_scope` and `oauth_grant_key` (`<primary_user>|<client_id>`). Add both tags with `core.cases.add_case_tag` and `create_if_missing` **true**: `oauth-app`, and the short name. Without that flag the first tag for a new app fails, and the next run cannot find this case. When a tag call fails, say so in the run line and name the error; never leave the failure unreported, because the next run then creates a duplicate case. When you reused a case rather than creating it, add one comment with `core.cases.create_comment` saying what this run changed, for example: `New holder <address> connected <app_name> at <event time, reader format>. The grant was re-assessed because the manager is per person. Risk <risk>, unchanged from the last review. Holders: <number>.` ## 9. Publish to Slack Follow `slack-delivery.md` in this skill. It covers the card, the brief, the manager ask, the buttons, and the Work Object row, for a new app and for a refresh. ## 10. Complete the review row Call `core.table.insert_row` with `upsert` true on `google_oauth_app_reviews`, which updates the row you claimed in stage 7. Write these columns: | Column | Value | |---|---| | `client_id` | the app's client ID | | `app_name` | from the prompt | | `project_number` | from stage 1 | | `scopes` | the saved scopes plus this run's scopes, no duplicates | | `assessment` | an object holding at least `risk` and `summary`; the interaction lane reads both | | `research` | the Exa findings, summaries only | | `policy_floor` | the floor from stage 3 | | `case_id`, `case_number` | the case and its short id | | `message_ts`, `slack_channel` | the card's timestamp and channel | | `users` | the saved addresses plus this run's addresses, no duplicates | | `first_seen` | keep the saved value; set it only when empty | | `last_seen` | this run's `event_time` | | `last_stage` | this run's `stages` | `users` records the people actually reviewed. Never add an address that did not appear in a grant event, and never add a domain-wide holder. ## 11. Finish Return one plain line. Name the app, the case, the risk, whether the card posted or refreshed, whether the manager ask posted, and whether each row saved. When a tool call returned an error, say which write failed and quote the error. Never call a write saved unless the call returned success. Never retry a failed write more than once. ## The decision click A second kind of run reaches you on this skill: somebody pressed a button on a card you posted. The prompt's first line is `OAuth decision click <target_user>|<target_client>`, and the JSON that follows holds the parsed click. **The workflow has already done the parts you must not do.** It parsed the Slack payload, checked that the clicker is allowed to press that button, acknowledged the click, and, for a revoke, already called Google and recorded the result. You never decide whether a click was permitted, and **you never call a revoke, block or disable API**. You record what happened and tell the people involved.
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기