{
"class_uid": 1007, "category_uid": 1, "type_uid": 100701, "activity_id": 1,
"severity_id": 1, "status_id": 0, "message": "tool call bash",
"time": 1786881335332,
"metadata": {
"product": { "name": "dsh-ocsf-forwarder", "vendor_name": "dsh-security-plugins", "version": "0.1.0" },
"version": "1.9.0", "profiles": ["ai_operation", "cloud", "osint", "record_integrity"],
"log_provider": "deepseek-harness", "log_name": "session",
"uid": "01JB0SESSION:7", "correlation_uid": "01JB0SESSION:call_9f2",
"sequence": 7, "logged_time": 1786823920155, "original_time": "1786881335332",
"tenant_uid": "platform-eng", "labels": ["prod"], "tags": [{ "name": "owner", "value": "soc" }]
},
"cloud": { "provider": "Other" }, "osint": [],
"ai_agent": { "name": "deepseek-harness", "type_id": 1, "instance_uid": "01JB0SESSION" },
"actor": { "process": { "pid": 4242, "name": "dsh" }, "user": { "name": "agent", "type_id": 1 } },
"device": {
"type_id": 0, "hostname": "app-01.example.test",
"uid": "0c6f1f1a-9c1e-4f0a-9a63-6a1a6c5f1b2e",
"os": { "name": "linux", "type_id": 0 }
},
"process": { "name": "curl", "cmd_line": "hmac-sha256:d7df26fddfd3af030679709c66165379" },
"attestation_list": [{
"uid": "7a1f0c5e-6b2d-4c8a-9f31-2d5b8e0a1c74:41",
"chain_uid": "7a1f0c5e-6b2d-4c8a-9f31-2d5b8e0a1c74",
"prev_event": { "uid": "01JB0SESSION:6", "type_uid": 600302, "fingerprint": { "value": "9d1c…", "algorithm_id": 3, "encoding_id": 1 } },
"fingerprint": { "value": "4b77…", "algorithm_id": 3, "encoding_id": 1 }
}],
"observables": [{ "name": "process.cmd_line", "type_id": 8, "value": "hmac-sha256:d7df26…" }],
"unmapped": {
"dsh": {
"v": 1, "session_id": "01JB0SESSION", "event_type": "tool/call", "seq": 7,
"replayed": false, "cwd": "/srv/app", "tool": "bash", "tool_class": "process-launch",
"arguments": [{ "key": "command", "class": "command", "length": 53, "digest": "hmac-sha256:d7df26…" }],
"turn": 1, "step": 0, "call_id": "call_9f2", "phase": "invoke"
}
}
}
The model called bash with curl -s https://api.example.test/v1/x?token=sk-live-1. The record
says a process was launched, that its executable was curl, how long the command was, and gives a
digest that joins it to every other occurrence of the same command — and discloses neither the URL
nor the token.
attestation_list is the OCSF record_integrity profile: the record’s own SHA-256 fingerprint and
the fingerprint of the record before it in the spool. Tamper-evidence gives the
canonicalisation a reader recomputes it from, and what it is and is not evidence of.
Every OCSF class is additionalProperties: false, so the extension attributes live under
unmapped, which is the base event’s own slot for exactly this. cloud and osint are stubs — a
host agent has no cloud deployment and no open-source intelligence — and metadata.profiles
declares both, because an attribute whose profile is undeclared fails validation just as an
undefined one does.
The same rule decides where a class-owned attribute may appear. src_endpoint is only on API
Activity, Authorize Session and HTTP Activity records; the top-level user object is only on
Authorize Session, which is the one class this plugin emits that defines it. The account behind
every other record is actor.user, which every class does define. The conformance suite checks
each class against its own OCSF definition rather than the union of all seven, because the union
accepts exactly the stampings additionalProperties: false rejects.
All 44 session event types this build knows (packages/core/session/src/known-event-types.ts in
the harness, catalogued in its docs/persistence-catalog.md).
type_uid = class_uid * 100 + activity_id.
Tool events are classified by tool name first — see Tool classification — which is why they list several classes.
| # | Session event | OCSF class (class_uid) |
activity_id |
Status / notes |
|---|---|---|---|---|
| 1 | agent/inbox/spliced |
API Activity (6003) | 3 Update | Counts only; inserted message text digested. |
| 2 | agent-preset/selected |
Application Lifecycle (6002) | 8 Update | Composition change; preset id. |
| 3 | approval/asked |
Authorize Session (3003) | 1 Assign Privileges | status_id: 0 (pending). privileges: [tool:<name>], unmapped.dsh.approval_id. The prompt reason quotes the command being approved, so it is digested. |
| 4 | approval/decided |
Authorize Session (3003) | 1 Assign Privileges | status_id: 1 for allowed-once, else 2. duration + unmapped.dsh.approval_latency_ms from the paired ask. |
| 5 | approval/policy |
Authorize Session (3003) | 1 Assign Privileges | Session policy switch (ask/never). |
| 6 | assistant/chunk |
— | — | Dropped. Token-level stream deltas: highest-volume type in the log and pure content. The assembled assistant/message is byte-complete. Re-enable per deployment via includeEventTypes. |
| 7 | assistant/message |
API Activity (6003) | 2 Read | Model completion. message_context.ai_role_id: 2, token counts from usage. Text digested in the SOC lane. |
| 8 | command/run |
API Activity (6003) | 1 Create | Slash command; api.operation = command:<name>, args digested. Correlates to 9 by commandId. |
| 9 | command/done |
API Activity (6003) | 3 Update | status_id from kind; duration from the paired command/run. |
| 10 | compaction/start |
API Activity (6003) | 3 Update | Holds the compaction lock. |
| 11 | compaction/end |
API Activity (6003) | 3 Update | status_id: 2 when error present; duration from the pair. |
| 12 | compaction/prune |
API Activity (6003) | 4 Delete | History removal — kept deliberately: shadowed seq range and token count are a tamper-relevant signal. The payload carries no compactionId, so the record is correlated by the range it replaced. |
| 13 | compaction/summary |
API Activity (6003) | 3 Update | Model-written replacement for history. Summary text digested in the SOC lane; ai_model from the event’s own provider/model. |
| 14 | feedback/record |
API Activity (6003) | 1 Create | Metadata only, and dropped by default (dropEventTypes): free-text human remark about the session, no security value, high privacy cost. |
| 15 | goal/change |
API Activity (6003) | 3 Update | Goal text digested. |
| 16 | hook/invoked |
Process Activity (1007) | 1 Launch | A hook is a subprocess. process.name = hook point, unmapped.dsh.handler_id, unmapped.dsh.dialect. |
| 17 | hook/result |
Process Activity (1007) | 2 Terminate | status_id from decision, reduced to the protocol’s approve/allow/block/deny/ask with anything else recorded as other plus a digest; process.exit_code; duration = durationMs. |
| 18 | llm/retry |
API Activity (6003) | 2 Read | status_id: 2 (the attempt that failed), status_detail = failure code. |
| 19 | llm/retry-started |
API Activity (6003) | 2 Read | status_id: 0; the wait completed and the next attempt starts. |
| 20 | permission/preset |
Authorize Session (3003) | 1 Assign Privileges | privileges: [preset:<name>]. |
| 21 | plan/mode |
API Activity (6003) | 3 Update | Plan mode on/off. |
| 22 | request/context |
Application Lifecycle (6002) | 8 Update | Provider/model route change. Folds into ai_model for every later record in the session. |
| 23 | request/header |
Application Lifecycle (6002) | 8 Update | Capability-set change. Tool names and count, model config, and a digest of the system prompt. Never the prompt text or tool schemas in the SOC lane. |
| 24 | sandbox/mode |
Authorize Session (3003) | 1 Assign Privileges | Confinement change; privileges: [sandbox:<mode>]. High value. |
| 25 | schedule/change |
Scheduled Job Activity (1006) | 1 Create / 2 Update / 3 Delete / 99 Other | From operation (create / dispatch / delete); job: { name, uid } from schedule.id on a create, id otherwise. |
| 26 | session/end-seed |
— | — | Dropped. Internal construction marker; its meaning is carried by the seed-replay boundary record. |
| 27 | session/title |
API Activity (6003) | 3 Update | Dropped by default: a model-written summary of the user’s prompt — user content by another name. |
| 28 | session/title-llm-request |
API Activity (6003) | 2 Read | Dropped by default: carries prompt text. |
| 29 | step/start |
API Activity (6003) | 2 Read | Opens one model call plus its tool executions; status_id: 0. |
| 30 | step/end |
API Activity (6003) | 2 Read | status_id: 1, duration from the paired step/start. |
| 31 | subagent/descriptor |
Application Lifecycle (6002) | 3 Start | This session is the child. mode and provider only: the payload names no session id, so no delegation is invented. |
| 32 | todo/write |
API Activity (6003) | 3 Update | Dropped by default: UI state made of user/model task text. Item count only if re-enabled. |
| 33 | tool/call |
by tool name: 1007 / 1001 / 4002 / 6003 | by tool name | status_id: 0 (in flight). metadata.correlation_uid = <session>:<callId>. |
| 34 | tool/result |
by tool name (same class as its call) | by tool name | status_id from content[0].isError; start_time/end_time/duration from the correlated call. |
| 35 | tool/code-dispatch-start |
by inner tool name | by tool name | Sub-call inside run_code; unmapped.dsh.parent_call_id. |
| 36 | tool/code-dispatch |
by inner tool name | by tool name | Sub-call settlement; status_id from isError. |
| 37 | tool-workflow/run-start |
Application Lifecycle (6002) | 3 Start | |
| 38 | tool-workflow/run-end |
Application Lifecycle (6002) | 4 Stop | status_id from stopReason. |
| 39 | tool-workflow/agent-start |
Application Lifecycle (6002) | 3 Start | Member agent. The one event that names a child: delegation = { uid: childId, parent_uid: <this session> }. |
| 40 | tool-workflow/agent-end |
Application Lifecycle (6002) | 4 Stop | status_id from outcome. |
| 41 | turn/start |
API Activity (6003) | 1 Create | The unit of agent work; status_id: 0. |
| 42 | turn/end |
API Activity (6003) | 1 Create | TurnEndReason is the outcome discriminant; duration from the paired turn/start. A provider failure contributes its code and a digest of its message. |
| 43 | user/message |
API Activity (6003) | 1 Create | message_context.ai_role_id: 1; unmapped.dsh.message_source distinguishes a human prompt from an injected context. Text digested in the SOC lane. |
| 44 | web/deepseek-search-llm-request |
API Activity (6003) | 2 Read | Auxiliary search request; ai_operation with api.service.name = 'deepseek-search'. |
Unknown (out-of-repo, plugin-merged) event types fall through to API Activity 6003 / activity
99 Other with metadata only. SessionEventMap is merge-extensible, so the mapper’s switch ends
in a documented default, never assertNever.
tool/call, tool/result, and the two tool/code-dispatch* events are classified by tool name:
| Tools | Class | activity_id |
Extra objects |
|---|---|---|---|
bash, pwsh, run_code, terminal_open, terminal_send |
Process Activity (1007) | 1 Launch | process.cmd_line (per commandLine policy), process.name, actor.process = the harness process |
terminal_close, terminal_signal, job_kill |
Process Activity (1007) | 2 Terminate | |
read, read_image, glob, grep |
File System Activity (1001) | 2 Read | file: { name, path, type_id } from file_path/path. A grep pattern is not a path and never fills one. |
write |
File System Activity (1001) | 1 Create | |
edit, str_replace_editor |
File System Activity (1001) | 3 Update | |
web_fetch, web_search |
HTTP Activity (4002) | 3 Get | http_request: { http_method, url } under the url policy; satisfies the class’s at_least_one: [http_request, http_response] |
| everything else | API Activity (6003) | 2 Read | api.operation = tool:<name> |
The table is a Record<string, ToolClass> constant plus a documented default. It is not
configurable: misclassifying bash as an API call on a deployment’s say-so would break every
process-based detection downstream. Deployments extend coverage for their own tools through
toolClasses (additive only — a config entry may add an unknown tool name, never reclassify a
known one).
A tool call contributes at most one observable, carrying the same value the record’s own object carries and typed for what that value actually is:
name |
type_id |
Value |
|---|---|---|
process.cmd_line |
8 Hash, or 13 Command Line under commandLine: full |
The command under the commandLine policy — a keyed digest by default. The type follows the policy, so a digest is never presented to a SIEM as a command line. |
file.path |
45 File Path |
The file_path/path argument itself, emitted verbatim: a path is the security signal, not a secret. Never the argument record it was read from. |
http_request.url.url_string |
6 URL |
The URL under the url policy — scheme and host by default, so the query string that carries reset and API tokens is gone before the record exists. |
A call whose arguments name no subject — a grep with only a pattern, a web_fetch whose URL does
not parse, any API-class tool — contributes none.
observables[] is the one place a redacted value and its raw source sit one line apart in the
mapper, so it is covered by an invariant rather than by a test per call site: a sentinel secret is
placed in every text-bearing session-event payload field, a full forwarder run is driven over them,
and the serialized SOC-lane records are searched for all of them at once.