Conversation
AiToolCall.arguments/result and AiToolCallRequest.arguments stored tool
arguments and results as plain-text JSON, so a credential passed to or
returned by a tool was persisted in clear. Before each audit write in
ExecTool and AgentRunner, AuditRedactor now normalizes the value as the
audit serializes it and masks as ***redacted*** the value under every
key matching a built-in secret-name list (password, passwd, passphrase,
pwd, secret, token but not its lowercase plural, api/private/access key,
credential, authorization) or a name a deployment adds through
ai_audit_redact_pattern, plus the value of {name|key, value} pairs. The
property can only add names, so no value of it switches masking off.
The MCP row now records the arguments the service ran with (filtered to
the exposed schema, fixed parameters applied) instead of the raw client
input. The service, the MCP client and the model still get the real
values; resume() still dispatches from pendingState.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Business summary
Every MCP
tools/calland every agent tool call writes an audit row (moqui.ai.AiToolCall), and an approval-gated agent call also writes a pendingAiToolCallRequest. These rows stored the tool arguments and the full tool result as plain-text JSON. As a result, a credential passed to a tool (an API client secret, an access token) or returned by one was persisted in clear text, readable by anyone who can read those entities.With this change, the value under any credential-like key is stored as
***redacted***. The rest of the row is unchanged, and the service, the MCP client and the model still get the real values.What changed
New
org.moqui.ai.AuditRedactorbuilds the masked copy for the audit:JsonOutput, read back). That way bean properties, iterator items,ExpandoandMap.Entryvalues are covered exactly as they would be written.***redacted***the value under every key that matches the secret-name pattern, and thevalueof a{name|key: <secret name>, value: …}pair (HTTP headers, metafields).nullstaysnull, and the input is never modified. Containers nested more than 32 levels deep are masked whole.The built-in names always apply:
pass(?:word|wd|phrase)|pwd|secret|token(?-i:(?!s))|api[-_]?key|private[-_]?key|access[-_]?key|credential|authorization, matched case-insensitively anywhere in the key. Thetoken(?-i:(?!s))part skips only the lowercase plural, so LLM usage counts stay readable (tokensIn,totalTokensOut,maxTokens, such as those inget_ai_spendresults), whileaccessToken,tokenIdandtokenStringare still masked.New
ai_audit_redact_patterndefault-property (empty) inMoquiConf.xml. A deployment sets it to a regex of extra names (for example,consumerKey|signingKey), and those names are added to the built-in list. The property can only add names:script/ai/mcp/ExecTool.groovy: the audit row now records the arguments the backing service actually ran with (filtered to the exposedinputSchema,fixedparameters applied) instead of the raw client input. Both arguments and result go throughAuditRedactor. The filtering now happens before the authentication gate, so refused calls are recorded the same way. The authenticated execution path is unchanged.AgentRunner.groovy: all four audit writes are masked:dispatchTool: arguments and result.rememberFact: arguments.resume(): arguments.AiToolCallRequest.argumentsat the approval gate.resume()still dispatches fromAiAgentRun.pendingState, so an approved call runs with the real values.Docs: design decision 19 in the MCP design spec,
ai_audit_redact_patternin the configuration reference, a new §6 in the security model on what masking does and does not cover, field descriptions onAiToolCall/AiToolCallRequest, and AGENTS.md check 3.Why the MCP row now stores the filtered arguments
The raw input can include keys that never reach the service, and a client's attempted override of a
fixedparameter. That means the row could disagree with what actually ran. For example, onmaina call to the fixture toolecho_fixedwithrepeat: 5is audited asrepeat: 5, although the service ran with the fixedrepeat=2. Recording the map the service actually received makes the row accurate. It also limits the stored keys to declared parameter names, which is where name-based masking is reliable. The trade-off is that out-of-schema keys a client sends are no longer recorded.Not covered (by design, see security model §6)
AiAgentRun.pendingStateandAiConversationMessagekeep the real values because both are load-bearing.resume()dispatches frompendingState(which is cleared on resume), and the conversation transcript is replayed to the model.errorText, and a secret inside a string value (for example, a JSON document returned as a single string).{settingTypeEnumId, settingValue}. Only{name|key, value}pairs are recognized.cookie,sessionId,jwt,consumerKey,signingKeyorencryptionKey, and the pluralaccessTokens. A deployment adds the ones its tools use throughai_audit_redact_pattern.Validation
This followed TDD twice:
passphrase/pwd, marker-on-failure) were also written first. They failed against the first version of the helper, then passed.All runs used
./gradlew :runtime:component:moqui-ai:testin an isolated copy of the dev runtime (its own transaction journal, withruntime/component/moqui-aipointing at this branch) against a local MySQL dev database:origin/mainb485c64AuditRedactorTestscases and 7 call-level tests. The call-level tests assert on the persisted rows:McpCallTests×3: secret-named argument and result fields at the top level and nested in a Map and a List, where the row holds the marker instead of the value whilestructuredContentkeeps the real value; the refused call; the filtered arguments.AgentRunnerTests: the model still receives the real result.AiApprovalTests×2: the pendingAiToolCallRequestis masked and the approved call still runs with the real value; the rejected-call row is masked.AiContextTests: aremembercall with an extra secret-named key.Expando, iterators andMap.Entryvalues leaked; and{name, value}headers leaked. After the fix, a rerun shows none of those leak, no property value switches masking off, and 8 threads flipping the property concurrently produce no unmasked value.AiComposerTests(catalog content absent) and one is inNotNakedSeedTests(it needs another component's seed data). The 8 skips are live provider tests that need API keys.AiApprovalTests"A1 regression…" failed once in 9 runs and passed on rerun. This is pre-existing and unrelated to this change.run()startsgenerate#ConversationTitleasynchronously for an untitled conversation, and that thread can take the scripted response from the shared staticMockProviderqueue before the agent loop does. Nothing changed here runs before that first model call.fake-secret-<nanoTime>). No real credentials were used./mcp/jsonon a running server. The transport screen is unchanged; the tests drivedispatch#Requestandexec#Tooland read the persisted rows back from the database.Deploy note
This change adds a class under
src/main(AuditRedactor) and changesAgentRunner. Rebuild the component jar (./gradlew :runtime:component:moqui-ai:jar) before restarting a server.🤖 Generated with Claude Code