6 min read

Agent action ticket references that survive review

Agent action ticket references connect approved change intent to API and SSH execution, giving reviewers evidence they can verify during incidents.

Agent action ticket references that survive review

An agent that can call a production API or open an SSH session needs more than a record that it acted. Reviewers need to know why that action was allowed at that moment. A change ticket reference gives them a path from an observed command or API call back to an approved intent.

That path only works when the reference becomes part of the action evidence before execution. Pasting a ticket number into a comment after an incident has begun is paperwork, not control. Teams that confuse the two eventually discover that every risky action has a ticket attached and none of the tickets explains it.

A ticket reference connects intent to execution

A change ticket answers a different question from an audit event. The ticket says what someone requested, why they requested it, what systems may be affected, and who accepted the risk. The event says what the agent actually attempted against a particular target, under a particular identity, and what happened.

Keep those records separate, then join them with a stable reference. Do not put the entire ticket body into every event. Ticket text changes, often contains confidential material, and makes event search worse. Store the canonical ticket identifier plus a small snapshot of the authorization facts that matter at dispatch time.

For a production database migration, that snapshot might include the ticket reference, its revision or update timestamp, the approved maintenance window, and the change owner. For an API call that disables an account, it might include the reference, the requested account identifier, and the person who accepted the request. The action record still needs its own request details and result.

This distinction catches a familiar failure. A team opens CHG-418 for a planned cache configuration change. An agent later uses CHG-418 while deleting a storage bucket because the engineer told it to "clean up resources related to the change." The ticket exists. The agent action has a reference. Yet the reference does not justify the target or operation. A reviewer needs enough structured context to see that mismatch without reading a chat transcript.

The OWASP Logging Cheat Sheet recommends recording the when, where, who, and what of security-relevant events. That guidance applies cleanly here, but agent actions add a fifth part: the declared authorization context. A ticket reference alone is too thin; a full ticket copy is too much. Record the reference and the narrow facts used to decide that the action matched it.

Sensitive work needs a written boundary

Require a ticket reference for actions whose consequences are hard to reverse, hard to discover, or expensive to explain later. Do not make agents ask for tickets before every harmless read. People will route around a rule that interrupts routine investigation, and the meaningful actions will disappear into exceptions.

Start by naming action classes rather than trying to predict every dangerous command. Most teams should include actions that:

  • modify production infrastructure, application configuration, or deployed code
  • create, revoke, or alter user and service access
  • export, delete, or move regulated or customer data
  • change payment, billing, notification, or public-facing behavior
  • use an emergency path or a credential with broad authority

An SSH connection itself may be sensitive in one environment and ordinary in another. The classification should follow what the connection can do and where it lands. A read-only production diagnostic session might need a session record but no ticket. An SSH command that changes firewall rules needs a reference even if the command is only one line.

Do not label a category "high risk" and stop there. Define the observable test at the control point. For example: any request using a production credential that sends a non-GET HTTP method requires a reference; any SSH invocation against the production host group requires one unless the command matches a documented diagnostic allowlist. The exact rules vary, but the test must be something a program and a reviewer can apply consistently.

There is a popular recommendation to require a ticket for every action because it sounds disciplined. It usually produces a pile of blanket tickets, copied references, and approvals nobody reads. Use the ticket requirement where it creates a decision point. Preserve complete logs for everything else, because a missing ticket should not become a missing event.

Capture the reference before the request leaves

The enforcement point must reject a sensitive request that lacks a valid reference before it sends the HTTP call or starts the SSH command. A later log enrichment job cannot repair that gap. Once an external system has accepted a request, your local record may be delayed, altered, or absent during the exact failure investigators need to reconstruct.

The request flow should have a clear order:

  1. The agent proposes an action with target, operation, and ticket reference.
  2. The control point checks that the reference has the required shape and obtains the ticket facts it needs.
  3. A human approval, if required, covers the proposed action and the reference together.
  4. The control point writes an intent event, dispatches the action, then writes the result event.

Writing an intent event matters. Suppose a network timeout occurs after an API provider receives DELETE /v1/projects/acme-prod. If you only log successful responses, the action journal falsely suggests that nothing occurred. An intent record tells the reviewer that the system attempted the request. A result of unknown is not a defect in the audit trail. It is the honest result until someone checks the destination system.

The same principle applies to SSH. Record the target host identity, the command or an approved command digest, the ticket reference, and the execution start before launching the process. Record the exit status, captured output policy, and completion time afterward. If the process loses its connection, retain that outcome instead of converting it into a clean failure.

Do not allow a client to submit a mutable free-text field called change_note and call the job done. A structured ticket_ref field permits validation, reporting, and reconciliation. Free text gives an agent a place to hide a plausible number in a paragraph.

A ticket number must match the requested scope

A valid-looking reference does not prove the action fits the approved work. The control point should compare the ticket's known scope with the request when that information is available, and it should force a human decision when it is not.

At minimum, validate that the ticket exists, has not been canceled, and is in a state your organization accepts for execution. Many teams also require a current change window and an approved owner. Those checks stop the lazy misuse of an old ticket, but they do not detect target drift.

Target drift occurs when the ticket describes one service, account, environment, or region while the request affects another. The best defense is structured scope in the ticket system itself. If a ticket has machine-readable fields for environment, service, repository, account, or maintenance window, compare them to the request attributes. Avoid trying to infer scope from prose. Natural-language descriptions are useful for humans, but a parser will approve nonsense with impressive confidence.

When a ticket only contains prose, present the action and ticket reference together for approval. The human should see the destination and verb in plain terms: "Apply configuration update to production service billing-api under CHG-418." Do not show only "Approve agent action under CHG-418." That wording hides the exact decision the person is being asked to make.

Do not build a sprawling rules language just to handle every ticket nuance. Start with a small set of comparisons you can explain during an incident: environment, target identifier, requested time, and requester or owner. Send uncertain cases to explicit approval. A narrow check that fails closed is better than a clever interpretation layer nobody can audit.

Use an event contract reviewers can query

Verify the audit chain offline
sp audit verify checks Sallyport's encrypted hash-chained log offline, without a key.

An action event needs stable fields, not a narrative assembled from terminal output. This example is a generic JSON record for the action attempt. It deliberately separates the declared reference from the facts observed during execution.

{
  "event_id": "act_01J8M7FQ6F2Y3K9D",
  "event_type": "action.intent",
  "occurred_at": "2025-03-08T14:32:11Z",
  "agent_run_id": "run_7e9d2",
  "actor": {
    "agent_process": "release-agent",
    "human_requester": "ops-204"
  },
  "ticket": {
    "system": "changes",
    "reference": "CHG-418",
    "observed_state": "approved",
    "observed_at": "2025-03-08T14:31:58Z",
    "scope_digest": "sha256:4ea4..."
  },
  "action": {
    "channel": "http",
    "operation": "PATCH",
    "target": "prod/billing-api/config",
    "request_digest": "sha256:35b9...",
    "idempotency_id": "chg-418-billing-01"
  },
  "decision": {
    "reference_required": true,
    "authorized_by": "ops-204",
    "decision_at": "2025-03-08T14:32:07Z"
  }
}

The completion event reuses event_id as a parent reference or uses a separate attempt_id. It records the HTTP status, SSH exit code, provider request ID when available, and an outcome such as succeeded, failed, or unknown. Do not place raw authorization headers, session cookies, request bodies with secrets, or SSH private material in this record.

A digest earns its place when you can retain proof of the exact payload without making sensitive content searchable by every log reader. Canonicalize the data before hashing. Define field ordering, encoding, and redaction rules; otherwise two equivalent requests produce different digests and the comparison becomes theater.

You can catch missing references in newline-delimited JSON before events reach long-term storage. This jq check returns a nonzero exit status for a sensitive action with no reference:

jq -e '
  select(.event_type == "action.intent")
  | select(.decision.reference_required == true)
  | select((.ticket.reference // "") | length == 0)
  | error("sensitive action has no ticket reference")
' actions.ndjson

Run a complementary query that finds references with no completion event. A ticket link is useful only when the journal shows whether the requested execution happened.

Do not let the agent mint its own evidence

An agent may propose a ticket reference, but it must not get to declare that the reference is approved and in scope. That is the same mistake as asking a process to attest that its own credentials are appropriate.

Give the agent one of two paths. In the first, a human supplies a reference when assigning work, and the agent receives a short-lived work context containing that reference and permitted target scope. In the second, the agent asks a trusted ticket lookup service for a ticket by reference, and the control point independently checks the response before dispatch. The agent sees only the facts it needs to formulate the request.

A signed work context can look like this conceptually:

{
  "ticket_ref": "CHG-418",
  "allowed_targets": ["prod/billing-api/config"],
  "allowed_operations": ["PATCH"],
  "expires_at": "2025-03-08T15:00:00Z",
  "issued_for_run": "run_7e9d2"
}

The issuer signs the serialized context. The control point verifies the signature, expiration, target, operation, and run identity. The agent cannot extend the expiry or add a second target without invalidating the signature. If your ticket system cannot issue signed contexts, keep the same logic server side and record the lookup response facts in the event.

Bind the context to a particular agent run. Without that binding, a compromised process can copy an approved reference from one task into another. Also record the code identity or process identity that made the call when your environment can provide it. A human name in a ticket and an anonymous local process in a log do not establish a trustworthy chain.

Retries and batches need individual evidence

Pair tickets with call evidence
Use your change record for intent and Sallyport's Activity journal for each executed call.

One ticket can authorize a bounded change window, but one ticket should never collapse many actions into one vague audit entry. Reviewers need to distinguish a planned sequence from repeated failure, partial completion, or an agent wandering beyond scope.

Give each attempt its own action ID. Attach the shared ticket reference to every attempt, and group related attempts with a run ID or change execution ID. Record an idempotency identifier when the destination supports it. An idempotency identifier helps determine whether a retry created a second change, but it does not replace an attempt record.

Consider an agent that updates ten service configurations. The first seven calls return success, the eighth times out, and the agent retries it twice. A useful journal says exactly that: ten intended targets, seven confirmed results, one unknown result, and two retries. A single ticket comment saying "configuration update completed" conceals the only service that may need manual inspection.

For batches, require the ticket to name a bounded population or an attached manifest. Store a digest of that manifest with the run. If the agent discovers an eleventh target after the change begins, stop and request a new scope decision. Treating discovery as permission is how maintenance work turns into an unreviewed migration.

Emergency work deserves a reference too, even if the reference is created after the first protective action. Record an emergency marker, the reason, the approving person, and the time the normal ticket was opened. Do not quietly reuse a routine ticket because opening an incident record feels slow. Emergencies need more evidence, not less.

Reconcile tickets and actions in both directions

Route production APIs through Sallyport
Let Sallyport inject HTTP credentials instead of exposing bearer, basic, or custom-header secrets to agents.

A weekly report that lists tickets mentioned in logs is not enough. You need two separate tests. First, find every sensitive action without a valid ticket reference. Second, find every completed or approved ticket that claims execution but has no matching action evidence.

The first test finds bypasses. The second finds false completion notes, manual work outside the agent path, and integration failures. Neither report proves wrongdoing. Both tell an operator where to ask a precise question while the context is still available.

Use a stable join rule. If the change system has multiple projects, store both the system name and reference. If references can be reused after archival, include an immutable ticket record ID or a snapshot revision. If a ticket can be edited after execution, preserve the state and scope digest observed when the action was authorized. Otherwise a later edit can make old execution evidence appear approved when it was not.

Review exceptions as first-class records. An exception should say who accepted it, why normal linkage failed, what action occurred, and when the exception expires. A spreadsheet of informal exceptions becomes a second, weaker change system within a few months.

Sallyport's documented Activity journal records individual calls, but teams should keep ticket linkage in their own change records or a companion immutable event until a documented ticket-reference field exists. Do not claim that a free-text note has the same evidentiary value as a field captured and validated before dispatch.

Preserve evidence without exposing ticket contents

Ticket systems often contain customer names, incident details, architecture notes, and access information. Action logs often have wider readership during operations and review. Store the reference and authorization snapshot, then let authorized reviewers open the ticket system when they need its full text.

Redact request data before computing a searchable representation, and preserve an access-controlled original only if investigation requirements justify it. A digest can confirm that a retained payload has not changed, but it cannot help a reviewer understand a payload they cannot retrieve. Decide deliberately which system holds the protected original and who may retrieve it.

Keep the ticket reference even after the ticket system deletes or archives the source record. The reference gives investigators a starting point, while the captured state, target, decision, and result keep the action record intelligible on its own. If retention rules require deletion, record that fact rather than leaving a broken link that looks like an error.

A simple first control is enough to expose the gaps: reject sensitive actions without a structured reference, write the intent before dispatch, and preserve the result after dispatch. Once that works, add scope comparison and reconciliation. The ticket number should make a reviewer faster and more certain, not give an agent a decorative string to attach to risky work.

FAQ

Is a change ticket reference the same as approval?

A ticket ID records the claimed reason for the action. Authorization records who or what was allowed to perform it. Keep both records because an approved process can still cite an unrelated, stale, or fraudulent ticket.

Which agent actions should require a ticket number?

Require references for actions that alter production state, move money or data, change access, rotate credentials, create public exposure, or bypass a normal deployment path. Read-only calls usually do not need a ticket unless the data itself is sensitive.

When should an agent attach a ticket reference to an action?

Ask for the ticket reference before the action leaves the control point, then bind it to the immutable action record. Adding a reference later only proves that someone edited a story after the event.

Is a ticket ID alone enough to approve sensitive work?

No. Ticket numbers are easy to copy and often remain visible after work closes. Validate that the ticket exists, matches the target, is in an acceptable state, and names a requester or owner who can approve the action.

Should the ticket contain the exact command an agent ran?

No. A ticket can describe a broad change, while an action event needs the exact endpoint, command, target, actor, result, and time. The ticket explains intent; the event proves execution.

How can an AI agent obtain a ticket reference safely?

Use a short-lived, signed work context or a server-side lookup that returns the reference and permitted scope. Do not let the agent invent a string and treat it as proof that the change system approved anything.

How should ticket references work for retries?

Treat a retry as a new attempt with a pointer to the original attempt. The records may share a ticket reference, but each needs its own action ID, timestamp, result, and idempotency marker.

Can one ticket cover a batch of agent actions?

Use the parent change ticket for the approved window and require a separate reference for work outside it. If the window spans many actions, record an action sequence and preserve each result rather than creating one vague completion note.

What happens if the ticket is already closed?

A closed ticket can still explain a completed action, but it should not normally authorize a new one. Your control point should reject closed or canceled references unless an explicit emergency process permits an exception and records who accepted it.

Where should reviewers look, in the ticket or the audit log?

Use the ticket system as the index for intent and the action journal as the evidence of what happened. Reconcile them in both directions: every sensitive event needs a reference, and every completed ticket needs execution evidence or an explicit reason why none exists.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov