-
Notifications
You must be signed in to change notification settings - Fork 9
Skill tool schemas: parse the platform Input format correctly; validate property keys at registration #362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
895ef55
4318b23
0e76aa7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import ( | |
| "os" | ||
| "sync" | ||
| "time" | ||
| "unicode/utf8" | ||
|
|
||
| "go.opentelemetry.io/otel/trace" | ||
| ) | ||
|
|
@@ -100,6 +101,11 @@ const ( | |
| // cancelled mid-flight; carries partial usage counts captured up to | ||
| // the cancellation point. See issue #87 / FWS-3. | ||
| AuditLLMCallCancelled = "llm_call_cancelled" | ||
| // AuditLLMCallFailed records a provider/gateway-rejected or errored LLM | ||
| // call (#361). Without it a failing call is invisible to the audit | ||
| // stream — an agent whose every task 400s at the provider showed | ||
| // nothing but healthy pre-failure llm_call rows (field-hit 2026-07-22). | ||
| AuditLLMCallFailed = "llm_call_failed" | ||
|
|
||
| // Credential events (governance R9). Emitted per BeforeToolExec | ||
| // when a JIT credential is materialized for a tool call, and again | ||
|
|
@@ -963,6 +969,19 @@ type LLMCallAuditArgs struct { | |
| // Used for streaming calls aborted mid-flight; partial usage counts are | ||
| // still carried. | ||
| Cancelled bool | ||
| // Failed flips the emitted event to llm_call_failed (#361): the provider | ||
| // or gateway errored/rejected the call. ErrorText carries the bounded | ||
| // error detail (e.g. an input_schema validation message) into | ||
| // fields.error so the failure reason reaches the audit stream, not just | ||
| // pod logs. Failed takes precedence over Cancelled. | ||
| // | ||
| // Privacy: fields.error is NOT subject to the payload-capture toggle | ||
| // (an operator who disabled capture still needs failure reasons), so it | ||
| // is ALWAYS secret-scrubbed (RedactSecrets) and capped at 512B — a | ||
| // provider that echoes a request fragment in an error body can't leak a | ||
| // credential into the stream (review #362). | ||
| Failed bool | ||
| ErrorText string | ||
| // Fields carries optional extra metadata to fold into the emitted | ||
| // event's `fields` map. Populated by the runner's hook layer when | ||
| // AuditPayloadCapture has any flag enabled (issue #91 / FWS-8): | ||
|
|
@@ -1001,6 +1020,15 @@ func (a *AuditLogger) EmitLLMCall(ctx context.Context, args LLMCallAuditArgs) { | |
| if args.Cancelled { | ||
| evt.Event = AuditLLMCallCancelled | ||
| } | ||
| if args.Failed { | ||
| evt.Event = AuditLLMCallFailed | ||
| if args.ErrorText != "" { | ||
| if args.Fields == nil { | ||
| args.Fields = map[string]any{} | ||
| } | ||
| args.Fields["error"] = boundedErrorText(args.ErrorText) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low (privacy/design): |
||
| } | ||
| } | ||
| in, out := args.Usage.InputTokens, args.Usage.OutputTokens | ||
| evt.InputTokens = &in | ||
| evt.OutputTokens = &out | ||
|
|
@@ -1015,6 +1043,25 @@ func (a *AuditLogger) EmitLLMCall(ctx context.Context, args LLMCallAuditArgs) { | |
| a.EmitFromContext(ctx, evt) | ||
| } | ||
|
|
||
| // boundedErrorText prepares a provider error string for fields.error: | ||
| // ALWAYS secret-scrubbed — unlike prompt_messages/completion_text this field | ||
| // bypasses the payload-capture gate, so redaction can't be optional — then | ||
| // capped at 512 bytes on a rune boundary (a byte-slice cut could emit | ||
| // invalid UTF-8 into the audit JSON). 512 bytes carries the useful part of | ||
| // every provider validation message seen in practice. | ||
| func boundedErrorText(s string) string { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low/nit: rune-safe truncation. |
||
| s = RedactSecrets(s) | ||
| const capBytes = 512 | ||
| if len(s) <= capBytes { | ||
| return s | ||
| } | ||
| cut := capBytes | ||
| for cut > 0 && !utf8.RuneStart(s[cut]) { | ||
| cut-- | ||
| } | ||
| return s[:cut] + "…" | ||
| } | ||
|
|
||
| // EmitToolExec emits a tool_exec audit event tagged with the tool | ||
| // name + wall-clock duration. Routed through EmitFromContext so | ||
| // workflow-correlation fields auto-tag every tool execution when the | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Low/note (scope), not blocking. This guard is skill-registration-only and validates only top-level keys. So (a) an invalid key from an MCP-server tool schema or a builtin, and (b) a nested object property key (
properties.foo.properties.bad name) would still slip through and 400 the entire request. The primary vector — flat, platform-materialized skill params — is fully covered, and scoping to the defect source is reasonable. But if you want the guard to be exhaustively defensive, the natural home is at the provider boundary (validate the assembledtoolsarray right before it's sent), which would catch MCP/builtin keys and nested keys too. Fine to leave as-is for this PR; noting for the follow-up backlog.