oap session

Inspect AgentSession resources

oap session approve <session> [<requestID>]

Publish an approval decision envelope to NATS as if the named approver had clicked Approve (or --deny) in the channel. Drives any pending approval on the session — tool-call, info-leakage, or content-inspection. Useful for end-to-end testing without a Slack workspace.

The decision still flows through channelsd's normal pipeline:

  • approver canonical-id is derived from --approver-email
  • SpiceDB LookupSubjectIncludes validates the approver against the request's ApproverSubject set
  • on approve: SpiceDB grant tuple is written; runner re-Checks against the grant and proceeds
  • on deny / not_authorized: runner sees the denied decision

requestID may be omitted when the session has exactly one pending approval (of any kind); the command picks that one and prints its kind. If multiple approvals are pending, requestID is required (the show output above lists each one with its kind).

Flags:

--approver-email string  Email of the approver — must match the SpiceDB approver subject. When omitted, the CLI's cached/logged-in identity is used. Pass an explicit email as an unverified override for scripting (use `oap identity canonical-id <email>` to preview the canonical form).
--deny                   Publish a deny decision instead of approve
--force                  Publish even if --approver-email is not in the request's required approver set (skips the client-side pre-check; channelsd's server-side check still applies)
--timeout duration       How long to wait for the Applied envelope when --wait is true (default "30s")
--wait                   Subscribe to the outbound Applied envelope and surface the channelsd-side outcome before returning (default "true")

oap session capabilities <session>

Show every permission class this AgentSession's tools can reach.

Each row is a handle and the severity of reaching it:

readonly perm:read:github_repo readwrite perm:write:tracker_issue external tool:apply_workspace

The severity is the MAX across every tool reaching that handle — a class one tool reads and another writes is a write class, because the reach is what matters, not the mildest route to it.

This is a REPORT, not a grant. It says what the tools could reach if authorized; whether any particular call is allowed is decided at dispatch by the per-tool check, the plan gate, and the approval flow. A session may hold far less than this list.

The surface is published by the runner at session start, so a session that has not started yet — or one whose runner predates this field — shows nothing.

Flags:

--tools  also show which tools reach each handle (the answer to "why can it do this?")

oap session capture <session>

Read a finished session's durable records and write a steelthread bundle: a transcript, the fixture manifests to boot it, the SpiceDB relationships its authorization depended on, and a golden authorization trace.

Mechanical throughout — no model is invoked, and every emitted field is derived from a record or left empty. Exits non-zero when the capture cannot be made faithfully, rather than writing a bundle that would replay differently than it recorded.

Every byte that would be written is scanned before anything is: once against the live credential values the referenced Secrets hold, and once for anything SHAPED like a credential — a private key, a JWT, a provider token, a kubeconfig client key. A hit refuses the capture, and no flag turns that off.

--redact removes identifiers a scan cannot recognize and a heuristic must not guess at: a customer, a partner, an internal hostname. Replacements run over the final bytes BEFORE the scans, so a redaction can remove a secret but can never suppress a finding about one.

Write the original ALONE (--redact acme-corp) and a stand-in is generated for you: the same byte length, derived from the original so re-capturing the session produces the same token, unique across the rules, and spelled so a reader of the committed fixture can see it is a placeholder.

Name your own (--redact acme-corp=COMPANY-A) and it must be the SAME BYTE LENGTH as what it replaces. That is enforced, not advised. The bundle records values DERIVED from the redacted text — an artifact's size, a content length — which the replay recomputes from the redacted bytes; a shorter token moved one real capture's artifact size from 5291 to 5276 and the replay diverged at the artifact, many turns from the rule that caused it.

Give it the same SHAPE too — same case, same character set. A redacted value still goes through whatever validated it the first time, and an upper-case token standing in for a lowercase slug makes the replayed call fail its own constraint. A generated stand-in is lowercase alphanumeric for that reason.

--elide-skill is the structural counterpart for a third-party skill repo, which a textual rule cannot rewrite: the authority appears in a SkillSource's repoURL, in each Skill's canonicalName and provenance, in the object names, and in the controller owner-ref that binds them — and desynchronising any one of those fails the skill's provenance gate. This moves them together and drops the skill content that came from that repo.

It REFUSES unless every AgentClass link to the elided source targets the sandbox. A sandbox-targeted skill is staged to disk and reaches neither the system prompt nor load_skill, so the run depended only on its existence; an agent-targeted one's description IS part of the recorded prompt, and emptying it would replay a different prompt with nothing to notice.

The stand-in authority is held to the SAME BYTE LENGTH as the one it replaces, exactly as --redact is and for the same reason: the rewrite puts that string into every canonical name, every repoURL and every skill ref the fixture emits, and a length change desynchronises any recorded value derived from a byte count. Write the authority ALONE (--elide-skill github.com/someorg/somerepo) and a same-length one is generated — counting an exact byte total out of a plausible repo locator by hand is how an operator ends up picking a shorter one.

The skill CONTENT it drops is deliberately not padded back to its original size. Nothing derives a count from it: a sandbox-targeted skill's body and description reach neither the prompt nor load_skill, which is the same fact the target refusal above rests on.

Flags:

--description string       Bundle description
--elide-skill stringArray  Rewrite a skill repo authority to a stand-in and drop that source's skill content, as old=new (github.com/someorg/somerepo=github.com/exampleorg/examplerepo, and the replacement must be the same byte length), or as the authority alone to have a same-length one generated. Refused for any skill the AgentClass does not target at the sandbox. Repeatable
--name string              Bundle name (default: the session name)
--overwrite                Replace an existing bundle in --output (clears the directory first)
--redact stringArray       Replace every occurrence of a string in the emitted bytes, as old=new (the replacement must be the same byte length), or as the original alone to have a same-length stand-in generated. Repeatable
--redact-from string       File of redactions, one per line in either --redact form; # comments and blank lines are ignored
-o, --output string        Directory to write the bundle into (required)

oap session delete <name>

Delete an AgentSession

oap session grant <session> <subject>

Grant an additional participant on the named AgentSession.

<subject> may be either:

  • A subject-set expression: "<type>:<id>#<relation>" (e.g. "group:engineering#member")
  • A direct user reference: "user:<canonicalID>" (use 'oap identity canonical-id <email>' to derive)

Bypasses the permission_request flow — useful for programmatic provisioning.

oap session hold <session>

Creates a SessionHold that freezes the named AgentSession: the runner stops accepting new turns and a release card is published for a human to review the session's history and either release it or leave it held.

--reason is required — it is platform-authored text shown on that card, and a hold with no reason gives the reviewer nothing to act on.

There is no CLI release: release is a human decision on the card, gated to the session's owner.

Flags:

--reason string  Why this session is being held (required; shown on the release card)

oap session logs <name>

Print every memory turn (system / user / assistant / tool) recorded for the given AgentSession. By default, the command prints the full history and exits — pass --follow to keep streaming new turns until the session reaches a terminal phase (or Ctrl-C).

Flags:

--timeout duration  Client-side cap when --follow is set (default "30m0s")
-f, --follow        Keep streaming until the session is terminal or canceled

oap session operations <name>

Reconstructs the audit trail for an AgentSession by grouping its ToolCalls under the operation_id label they were stamped with at dispatch time. When a memory token is available, also fetches the session memory to recover the original new_operation description for each operation.

oap session participants <session>

List all users that currently have interact permission on the named AgentSession.

Calls SpiceDB's LookupSubjects API for agentsession#interact. Requires SPICEDB_ENDPOINT and SPICEDB_TOKEN to be set.

oap session revoke <session> <subject>

Revoke a participant from the named AgentSession.

<subject> may be either:

  • A subject-set expression: "<type>:<id>#<relation>" (e.g. "group:engineering#member")
  • A direct user reference: "user:<canonicalID>" (use 'oap identity canonical-id <email>' to derive)

Bypasses the permission_request flow — useful for programmatic provisioning.

oap session revoke-slot <session> <resourceType>:<id>#<permission>

Remove a single slot grant, so the session can no longer reach that instance.

The next authorization check denies — a slot grant is a SpiceDB tuple, and its removal is visible immediately to any later check. Work already dispatched is not clawed back; the revocation applies from the next call onward.

Standing is symmetric with APPROVAL, plus platform admin: whoever could have granted the instance can take it back. Pass --as to name the canonical identity the check runs against; without it the command refuses rather than acting unattributed.

Use 'oap session slots <session>' to see what is held. For a value slot the id is the hash the tool computes, which is what this command expects.

Flags:

--as string  canonical identity the revoke-standing check runs against (required)

oap session show <name>

Show status of an AgentSession

oap session slots <session>

List every instance this AgentSession currently holds through a slot grant.

Each row is "<resourceType>:<id>#<permission>" — the object, and the permission that grant confers. The permission is part of the identity, not decoration: grants are per-permission, so a read-scoped grant and a write-scoped grant on the same instance are different objects. For a VALUE slot (a URL, a path) the id is the hash the tool's own permission check computes, not the original value — the value goes to the tool, SpiceDB only ever sees the hash. That is by design, and it is why this listing exists: without it the held set is not discoverable at all.

Grants also expire on their own (bounded by the session's wall-clock lifetime cap), so an empty list may mean "expired" rather than "never granted".

oap session un-deny <session> <subject>

Lift a Deny on the named AgentSession.

<subject> is either a direct user reference — "user:<canonicalID>", from 'oap identity canonical-id <email>' — or a subject set such as "group:eng#member". Both are valid: the blocklist accepts every subject type owner and participant do, because a grant that can be made to a group must be revocable from that same group.

Denying subtracts from every permission the session grants — interact, approve, fork, and manage_scope — so lifting it restores whatever standing the subject had before. It does NOT grant standing they never had.