Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions runtime-e2e/caller_name_audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ that it marshals onto the wire correctly (that's covered by the
equals that value verbatim.
2. Legacy `tool_type="mcp"` with **no** `caller_name` → the deprecated
fallback still resolves `policy_details.caller_name` to `"mcp"`.
3. **Neither** `caller_name` **nor** `tool_type` supplied → the platform's
default-fallback (getaxonflow/axonflow-enterprise#2903, folded into the
same #2953 merge) resolves `policy_details.caller_name` to `"unknown"` —
not the pre-#2903 default of `"claude_code"`. An unidentified caller must
never be silently attributed to a specific client.

The SDK's typed `AuditLogEntry` does not declare a `policy_details` field
(it's an internal JSONB blob), so the read side uses a raw `httpx` call
Expand All @@ -39,18 +44,17 @@ local-dev Enterprise stack in `axonflow-enterprise/main-tree/.env`). In
Community mode, tenant-audit reads are tenant-wide regardless, so the header
is a no-op there.

## Prerequisite: platform support is not yet on `main`
## Prerequisite: platform version

`caller_name` support (axonflow-enterprise#2953) is implemented but, as of
this writing, still an open PR on the `feat/2912-caller-name-tool-type-deprecation`
branch — not yet merged to `axonflow-enterprise` main. Against a stack built
from `axonflow-enterprise` main, this test will FAIL (the 45s poll of
`GET /api/v1/audit/tenant/{tenant_id}` times out waiting for
`policy_details.caller_name`, which the server doesn't write yet) — that's
not a bug in this test, it means the platform side isn't deployed on
whatever stack you're pointed at. Point your local `axonflow-enterprise`
checkout at that branch (or a later commit that includes it) before running
this test.
`caller_name` support (axonflow-enterprise#2953, which also folded in the
#2903 default-fallback fix) is merged to `axonflow-enterprise` main and
shipped in platform release **v9.11.0**. Point your stack at v9.11.0+ (or a
`main` checkout at/after commit `7a5984ec7`) before running this test.
Against an older platform, the third scenario (`unknown` default) and
possibly `policy_details.caller_name` itself will not resolve as expected,
and the 45s poll of `GET /api/v1/audit/tenant/{tenant_id}` will time out
waiting for a row that never lands with the expected shape — that's a
platform-version mismatch, not a bug in this test.

## Run

Expand All @@ -65,7 +69,8 @@ The orchestrator's `AuditLogger` batches writes (flush every 10s per
`platform/orchestrator/audit_logger.go`), so the test polls
`GET /api/v1/audit/tenant/{tenant_id}` for up to 45s before failing.

Exits non-zero if `caller_name` (or the `tool_type` fallback) does not reach
Exits non-zero if `caller_name`, the `tool_type` fallback, or the
neither-supplied `"unknown"` default does not reach
`policy_details.caller_name` on the real row.

## Companion unit coverage
Expand Down
38 changes: 36 additions & 2 deletions runtime-e2e/caller_name_audit/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@
2. Call it again with only the legacy `tool_type` set (no `caller_name`)
to prove the backward-compat fallback still resolves into
`policy_details.caller_name` on a real row.
3. The orchestrator's AuditLogger batches writes (flush every 10s), so
3. Call it a third time with NEITHER `caller_name` NOR `tool_type` set,
to prove the platform's default-fallback (getaxonflow/axonflow-
enterprise#2903, folded into the same #2953 merge) resolves to
`"unknown"` -- an unidentified caller must NOT be silently attributed
to a specific client. `"unknown"` replaced the pre-#2903 default of
`"claude_code"`.
4. The orchestrator's AuditLogger batches writes (flush every 10s), so
poll `GET /api/v1/audit/tenant/{tenant_id}` (the same route
`client.get_audit_logs_by_tenant` hits) until each row lands.
4. The SDK's typed `AuditLogEntry` does not surface `policy_details`
5. The SDK's typed `AuditLogEntry` does not surface `policy_details`
(it's an internal JSONB blob), so this step reads the raw JSON body
directly -- the only way to observe `policy_details.caller_name`
from outside the platform -- and asserts it equals what we sent.
Expand Down Expand Up @@ -77,6 +83,8 @@
WORKFLOW_CALLER_NAME = f"e2e-caller-name-wf-{_RUN_ID}"
WORKFLOW_TOOL_TYPE_FALLBACK = f"e2e-tool-type-fallback-wf-{_RUN_ID}"
WANT_TOOL_TYPE_FALLBACK = "mcp"
WORKFLOW_NEITHER_SUPPLIED = f"e2e-neither-supplied-wf-{_RUN_ID}"
WANT_DEFAULT_CALLER_NAME = "unknown" # #2903: was "claude_code" pre-fix

# Batch writer flushes every 10s (platform/orchestrator/audit_logger.go);
# give it generous headroom under CI/local load.
Expand Down Expand Up @@ -168,6 +176,19 @@ async def main() -> None:
f"SDK audit_tool_call (tool_type fallback) -> audit_id={resp2.audit_id} status={resp2.status}"
)

# 3. Neither caller_name nor tool_type supplied -- the platform's
# default-fallback (#2903) must resolve to "unknown", never silently
# attribute an unidentified caller to a specific client.
resp3 = await client.audit_tool_call(
AuditToolCallRequest(
tool_name="e2eNeitherSuppliedTool",
workflow_id=WORKFLOW_NEITHER_SUPPLIED,
)
)
print(
f"SDK audit_tool_call (neither supplied) -> audit_id={resp3.audit_id} status={resp3.status}"
)

policy_details_1 = _fetch_policy_details(WORKFLOW_CALLER_NAME)
got_caller_name = policy_details_1.get("caller_name")
if got_caller_name != WANT_CALLER_NAME:
Expand Down Expand Up @@ -195,6 +216,19 @@ async def main() -> None:
)
print(f"Wire policy_details (tool_type fallback case): {policy_details_2}")

policy_details_3 = _fetch_policy_details(WORKFLOW_NEITHER_SUPPLIED)
got_default = policy_details_3.get("caller_name")
if got_default != WANT_DEFAULT_CALLER_NAME:
_fail(
f"default fallback: policy_details.caller_name = {got_default!r}, want "
f"{WANT_DEFAULT_CALLER_NAME!r} (#2903) (full policy_details: {policy_details_3})"
)
print(
f"PASS: neither caller_name nor tool_type supplied -> "
f"policy_details.caller_name = {got_default!r} (#2903 default, not 'claude_code')"
)
print(f"Wire policy_details (neither-supplied case): {policy_details_3}")

print("ALL PASS: caller_name (#2912) verified end-to-end through the real SDK + platform")


Expand Down
Loading