inspecting-agents · git:20260922.b59c194 · 2026-09-22 · sha256 480edead4a3a55f0

inspecting-agents git:20260922.b59c194A

Immutable. This exact content never changes and is served at /api/v1/blob/480edead4a3a55f0.

---
name: inspecting-agents
description: Answer operational questions about an organization's Turnkey agents with tk list and get commands, explicit pagination, and jq: what is pending and for how long, who voted, which users carry a tag, which keys each agent holds and when they expire, which secrets carry which properties, which policies mention a tag, who minted a key. Use for audits and "what is the state of X" questions; not for changing anything.
---

# Inspecting agents

Result: a read-only report that answers each question with the command that
produced it and the field the answer came from. Nothing is created, voted
on, or deleted.

There are no server-side filters in this checkout. Every question below is a
full traversal of one or more lists plus a local `jq` selection, so each
answer states its provenance: the command, the page or pages read, and the
field.

## Reference

- [resources](../../docs/resources.md): `user`, `user tag`, `api-key`, and `policy` list records.
- [activities](../../docs/activities.md): `activity list` pages and `activity get`.
- [secrets](../../docs/secrets.md): `secret list` metadata and paging.

## Rules

- Read only. Approving, rejecting, or waiting on what you find belongs to
  [monitoring-activities](../monitoring-activities/SKILL.md).
- Page to the end before answering "none". `activity list` and
  `secret list` page with `--limit` and `--cursor`; a page is the last one
  when `data.nextCursor` is `null`. `user list`, `user tag list`,
  `policy list`, and `api-key list` return everything in one response.
- Resolve tag names once, with `user tag list`, and match by `tagId`
  everywhere else. `userTags[]` on a user and the text of a policy carry
  ids, not names.
- `expiresAt` on an API key is a Unix-millisecond string, or `null` for a
  key that never expires. On the session route the anchor is the only
  permanent key an agent should hold; on the long-lived route the one
  runtime key is permanent.
- Who minted a key is a join, never a guess: the key's
  `credential.publicKey` against the `publicKey` inside an activity's
  `intent` (`createApiKeysIntentV2.apiKeys[]` or
  `createUsersIntentV4.users[].apiKeys[]`). When no activity in the pages
  you traversed matches, the answer is "unknown". Do not infer a minter from
  voters on other activities.
- A textual search of policy `consensus` and `condition` for a tag id is a
  candidate list, not a parsed dependency graph. Read each candidate before
  claiming it governs the tag.

## Instructions

Inputs: any profile in the organization (`admin` below), and the tag names
or ids the question is about. Every command below prints one JSON record;
`jq` selects from it.

1. **Resolve tag ids.**

   <!-- example: inspecting.tags -->
   ```sh
   tk --profile admin --message-format json user tag list | jq -r '.data.userTags[] | "\(.tagId) \(.tagName)"'
   AGENT_TAG=$(tk --profile admin --message-format json user tag list | jq -r '.data.userTags[] | select(.tagName == "agent") | .tagId')
   ```

   `data.userTags[]` carries `tagId`, `tagName`, and `userIds`. Steps 2 and
   7 use `$AGENT_TAG`. A name that matches nothing leaves the variable
   empty; stop and report that, do not fall back to a substring match.

2. **Which users carry a tag.**

   <!-- example: inspecting.tagged-users -->
   ```sh
   tk --profile admin --message-format json user list | jq --arg tag "$AGENT_TAG" '.data.users[] | select(.userTags | any(. == $tag)) | {userId, userName}'
   ```

   `data.users[].userTags[]` is the list of tag ids on each user. The list
   is complete in one response.

3. **Which keys an agent holds and when they expire.**

   <!-- example: inspecting.keys -->
   ```sh
   tk --profile admin --message-format json api-key list --user-id AGENT_USER_ID | jq '.data.apiKeys[] | {apiKeyId, apiKeyName, publicKey: .credential.publicKey, expiresAt}'
   tk --profile admin --message-format json api-key list --user-id AGENT_USER_ID | jq -r '.data.apiKeys[] | select(.expiresAt == null) | .apiKeyId'
   ```

   The second command lists the permanent keys. A session-route agent
   should show exactly one, its anchor; more than one permanent key on such
   an agent is a finding. Run step 3 once per user id from step 2.

4. **What is pending and for how long.**

   <!-- example: inspecting.pending -->
   ```sh
   tk --profile admin --message-format json activity list --limit 50 | jq --arg now "$(date +%s)" '.data.items[] | select(.status == "ACTIVITY_STATUS_CONSENSUS_NEEDED") | {id, type, ageSeconds: (($now | tonumber) - (.createdAt.seconds | tonumber))}'
   ```

   `data.items[]` is one page, newest first; `createdAt.seconds` is a
   Unix-second string. Continue with the cursor until it prints `null`:

   <!-- example: inspecting.next-page -->
   ```sh
   NEXT_ACTIVITY_ID=$(tk --profile admin --message-format json activity list --limit 50 | jq -r '.data.nextCursor')
   tk --profile admin --message-format json activity list --limit 50 --cursor $NEXT_ACTIVITY_ID | jq -r '.data.nextCursor'
   ```

   Apply the step 4 filter to every page and concatenate. A pending
   activity appears on exactly one page.

5. **Who voted on an activity.**

   <!-- example: inspecting.votes -->
   ```sh
   tk --profile admin --message-format json activity get ACTIVITY_ID | jq '.data.activity.votes[] | {userId, selection}'
   ```

   `selection` is `VOTE_SELECTION_APPROVED` or `VOTE_SELECTION_REJECTED`.
   The submitter's own approval is the first vote; the missing approver is
   whoever the policy consensus names and this list does not.

6. **Which secrets carry which properties.**

   <!-- example: inspecting.secrets -->
   ```sh
   tk --profile admin --message-format json secret list --limit 100 | jq '.data.secrets[] | {name, properties: (.staticProperties | map("\(.key)=\(.value)"))}'
   tk --profile admin --message-format json secret list --limit 100 --cursor SECRET_ID | jq -r '.data.nextCursor'
   ```

   `staticProperties[]` is `key` and `value`; values of the secrets
   themselves are never listed. Page with `--cursor` as in step 4.

7. **Which policies mention a tag.**

   <!-- example: inspecting.policies -->
   ```sh
   tk --profile admin --message-format json policy list | jq --arg tag "$AGENT_TAG" '.data.policies[] | select((.consensus // "") + (.condition // "") | contains($tag)) | {policyId, policyName, effect}'
   ```

   These are candidates. Read each one's `consensus` and `condition` to say
   whether the tag is an approver, a target, or both.

8. **Who minted a key.** Take the key's `credential.publicKey` from step 3
   into `$KEY_PUBLIC_KEY`, then join it against each page of activities:

   <!-- example: inspecting.minted-by -->
   ```sh
   tk --profile admin --message-format json activity list --limit 50 | jq --arg pk "$KEY_PUBLIC_KEY" '.data.items[] | select(any(.intent.createApiKeysIntentV2.apiKeys[]?, .intent.createUsersIntentV4.users[]?.apiKeys[]?; .publicKey == $pk)) | {id, type, status, minted: .createdAt.seconds, voters: [.votes[].userId]}'
   ```

   Page as in step 4. One match gives the activity and its voters, the
   first of which submitted it. No match across every page is "unknown".
   Empty output on the first page alone proves nothing.

9. **Hand off.** One line per question: the answer, the command, the pages
   read (`1 of 1`, `3 of 3`), and the field it came from. Pending ids,
   permanent-key findings, and "unknown" minters are listed explicitly.
   Stop.

## Verified by

| Examples | Test |
|---|---|
| inspecting.tags, inspecting.tagged-users, inspecting.keys, inspecting.pending, inspecting.next-page, inspecting.votes, inspecting.secrets, inspecting.policies, inspecting.minted-by | users::inspecting_agents_jq_recipes_answer_live_records |

## Troubleshooting

- `jq: command not found`: install `jq`; every recipe here selects from the
  JSON record with it. Nothing in `tk` depends on it.
- `user get` or `api-key list` fails with `not_found`: the user id belongs
  to another organization or was mistyped. Check `--profile` and the id
  from step 2.
- Step 1 leaves `$AGENT_TAG` empty: the tag name is different in this
  organization. Read the full `user tag list` output and pick the id by
  hand; do not guess from a similar name.
- A page has zero items but the previous page had a cursor: the previous
  page was exactly `--limit` long. That empty page is the end.
- `activity list` shows nothing pending but a command reported `pending`:
  the vote may have landed since. `activity get ACTIVITY_ID` shows the
  current status of that one activity.
- Step 8 finds no match: widen the traversal to every page before saying
  "unknown"; an activity older than the pages read is not evidence of
  anything.

## Related Skills

- [monitoring-activities](../monitoring-activities/SKILL.md): approve, reject,
  or wait on what step 4 found.
- [managing-identities](../managing-identities/SKILL.md): rotate or revoke a
  key that step 3 flagged.
- [provisioning-session-agent](../provisioning-session-agent/SKILL.md): the
  expiring keys and anchor that step 3 reads.
- [managing-policies](../managing-policies/SKILL.md): read or change the
  policies step 7 listed.