feat: plane-2 rules read declared param roles instead of guessing args
Add ParamRole and a defaulted Tool.paramRoles to the tool contract; built-in tools declare which params carry effects: file_read/write/edit -> path=PATH, shell -> argv=EXEC_COMMAND. PathContainmentRule / ExecInterpreterRule / NetworkHostRule now inspect only the declared params when roles are present, and fall back to the looksLikePath / leading-token / extractHost heuristic only for un-enriched (e.g. custom) tools - so a custom FILE_WRITE tool is still containment-checked. Shared extractParamStrings flattens String and List<*> (argv) values. SessionOrchestrator.runPlane2Assessment resolves the tool once and passes tool.paramRoles into the assessment input. This retires the arg-guessing from the first-party path: a slashy edit 'content' arg is no longer mis-flagged as a path.
This commit is contained in:
+3
-2
@@ -599,13 +599,14 @@ abstract class SessionOrchestrator(
|
|||||||
): RiskSummary? {
|
): RiskSummary? {
|
||||||
val assessor = toolCallAssessor ?: return null
|
val assessor = toolCallAssessor ?: return null
|
||||||
val policy = workspacePolicy ?: return null
|
val policy = workspacePolicy ?: return null
|
||||||
val capabilities = toolRegistry?.resolve(toolName)?.requiredCapabilities ?: emptySet()
|
val tool = toolRegistry?.resolve(toolName)
|
||||||
val assessment = assessor.assess(
|
val assessment = assessor.assess(
|
||||||
ToolCallAssessmentInput(
|
ToolCallAssessmentInput(
|
||||||
request = request,
|
request = request,
|
||||||
capabilities = capabilities,
|
capabilities = tool?.requiredCapabilities ?: emptySet(),
|
||||||
workspace = policy,
|
workspace = policy,
|
||||||
probe = worldProbe,
|
probe = worldProbe,
|
||||||
|
paramRoles = tool?.paramRoles ?: emptyMap(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
emit(
|
emit(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.correx.core.toolintent
|
|||||||
import com.correx.core.events.events.ToolCallObservation
|
import com.correx.core.events.events.ToolCallObservation
|
||||||
import com.correx.core.events.events.ToolRequest
|
import com.correx.core.events.events.ToolRequest
|
||||||
import com.correx.core.events.risk.RiskAction
|
import com.correx.core.events.risk.RiskAction
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import com.correx.core.validation.model.ValidationIssue
|
import com.correx.core.validation.model.ValidationIssue
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ data class ToolCallAssessmentInput(
|
|||||||
val capabilities: Set<ToolCapability>,
|
val capabilities: Set<ToolCapability>,
|
||||||
val workspace: WorkspacePolicy,
|
val workspace: WorkspacePolicy,
|
||||||
val probe: WorldProbe,
|
val probe: WorldProbe,
|
||||||
|
val paramRoles: Map<String, ParamRole> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ToolCallAssessment(
|
data class ToolCallAssessment(
|
||||||
|
|||||||
+10
-4
@@ -6,6 +6,7 @@ import com.correx.core.toolintent.ToolCallAssessment
|
|||||||
import com.correx.core.toolintent.ToolCallAssessmentInput
|
import com.correx.core.toolintent.ToolCallAssessmentInput
|
||||||
import com.correx.core.toolintent.ToolCallRule
|
import com.correx.core.toolintent.ToolCallRule
|
||||||
import com.correx.core.toolintent.maxAction
|
import com.correx.core.toolintent.maxAction
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import com.correx.core.validation.model.ValidationIssue
|
import com.correx.core.validation.model.ValidationIssue
|
||||||
import com.correx.core.validation.model.ValidationSeverity
|
import com.correx.core.validation.model.ValidationSeverity
|
||||||
@@ -49,10 +50,15 @@ class ExecInterpreterRule(interpreters: Set<String>) : ToolCallRule {
|
|||||||
return ToolCallAssessment(issues = issues, observations = observations, disposition = disposition)
|
return ToolCallAssessment(issues = issues, observations = observations, disposition = disposition)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun executables(input: ToolCallAssessmentInput): List<String> =
|
private fun executables(input: ToolCallAssessmentInput): List<String> {
|
||||||
input.request.parameters.values
|
val declared = input.paramRoles.filterValues { it == ParamRole.EXEC_COMMAND }.keys
|
||||||
.filterIsInstance<String>()
|
val raws = if (declared.isNotEmpty()) {
|
||||||
.mapNotNull { leadingToken(it) }
|
declared.flatMap { extractParamStrings(input.request.parameters[it]) }
|
||||||
|
} else {
|
||||||
|
input.request.parameters.values.filterIsInstance<String>()
|
||||||
|
}
|
||||||
|
return raws.mapNotNull { leadingToken(it) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun leadingToken(raw: String): String? =
|
private fun leadingToken(raw: String): String? =
|
||||||
raw.split(TOKEN_DELIMITERS).firstOrNull { it.isNotBlank() }
|
raw.split(TOKEN_DELIMITERS).firstOrNull { it.isNotBlank() }
|
||||||
|
|||||||
+10
-4
@@ -6,6 +6,7 @@ import com.correx.core.toolintent.ToolCallAssessment
|
|||||||
import com.correx.core.toolintent.ToolCallAssessmentInput
|
import com.correx.core.toolintent.ToolCallAssessmentInput
|
||||||
import com.correx.core.toolintent.ToolCallRule
|
import com.correx.core.toolintent.ToolCallRule
|
||||||
import com.correx.core.toolintent.maxAction
|
import com.correx.core.toolintent.maxAction
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import com.correx.core.validation.model.ValidationIssue
|
import com.correx.core.validation.model.ValidationIssue
|
||||||
import com.correx.core.validation.model.ValidationSeverity
|
import com.correx.core.validation.model.ValidationSeverity
|
||||||
@@ -64,10 +65,15 @@ class NetworkHostRule(
|
|||||||
return ToolCallAssessment(issues = issues, observations = observations, disposition = disposition)
|
return ToolCallAssessment(issues = issues, observations = observations, disposition = disposition)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hosts(input: ToolCallAssessmentInput): List<String> =
|
private fun hosts(input: ToolCallAssessmentInput): List<String> {
|
||||||
input.request.parameters.values
|
val declared = input.paramRoles.filterValues { it == ParamRole.NETWORK_TARGET }.keys
|
||||||
.filterIsInstance<String>()
|
val raws = if (declared.isNotEmpty()) {
|
||||||
.mapNotNull { extractHost(it) }
|
declared.flatMap { extractParamStrings(input.request.parameters[it]) }
|
||||||
|
} else {
|
||||||
|
input.request.parameters.values.filterIsInstance<String>()
|
||||||
|
}
|
||||||
|
return raws.mapNotNull { extractHost(it) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractHost(raw: String): String? {
|
private fun extractHost(raw: String): String? {
|
||||||
if (!raw.contains("://")) return null
|
if (!raw.contains("://")) return null
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.correx.core.toolintent.rules
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens a raw tool-call parameter value into candidate strings for rule
|
||||||
|
* inspection. A declared param may arrive as a plain String (e.g. a path) or a
|
||||||
|
* List<*> (e.g. shell `argv`); both are reduced to strings.
|
||||||
|
*/
|
||||||
|
internal fun extractParamStrings(value: Any?): List<String> = when (value) {
|
||||||
|
is String -> listOf(value)
|
||||||
|
is List<*> -> value.mapNotNull { it?.toString() }
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
+11
-4
@@ -6,6 +6,7 @@ import com.correx.core.toolintent.ToolCallAssessment
|
|||||||
import com.correx.core.toolintent.ToolCallAssessmentInput
|
import com.correx.core.toolintent.ToolCallAssessmentInput
|
||||||
import com.correx.core.toolintent.ToolCallRule
|
import com.correx.core.toolintent.ToolCallRule
|
||||||
import com.correx.core.toolintent.maxAction
|
import com.correx.core.toolintent.maxAction
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import com.correx.core.validation.model.ValidationIssue
|
import com.correx.core.validation.model.ValidationIssue
|
||||||
import com.correx.core.validation.model.ValidationSeverity
|
import com.correx.core.validation.model.ValidationSeverity
|
||||||
@@ -75,10 +76,16 @@ class PathContainmentRule : ToolCallRule {
|
|||||||
return ToolCallAssessment(issues = issues, observations = observations, disposition = disposition)
|
return ToolCallAssessment(issues = issues, observations = observations, disposition = disposition)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun candidatePaths(input: ToolCallAssessmentInput): List<String> =
|
private fun candidatePaths(input: ToolCallAssessmentInput): List<String> {
|
||||||
input.request.parameters.values
|
val declared = input.paramRoles.filterValues { it == ParamRole.PATH }.keys
|
||||||
.filterIsInstance<String>()
|
return if (declared.isNotEmpty()) {
|
||||||
.filter { it.looksLikePath() }
|
declared.flatMap { extractParamStrings(input.request.parameters[it]) }
|
||||||
|
} else {
|
||||||
|
input.request.parameters.values
|
||||||
|
.filterIsInstance<String>()
|
||||||
|
.filter { it.looksLikePath() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun String.looksLikePath(): Boolean =
|
private fun String.looksLikePath(): Boolean =
|
||||||
isNotBlank() && (startsWith("/") || startsWith("~") || contains("/") || contains(".."))
|
isNotBlank() && (startsWith("/") || startsWith("~") || contains("/") || contains(".."))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.correx.core.events.types.SessionId
|
|||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.events.types.ToolInvocationId
|
import com.correx.core.events.types.ToolInvocationId
|
||||||
import com.correx.core.toolintent.rules.ExecInterpreterRule
|
import com.correx.core.toolintent.rules.ExecInterpreterRule
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
@@ -86,4 +87,24 @@ class ExecInterpreterRuleTest {
|
|||||||
assertEquals(RiskAction.PROMPT_USER, r.disposition)
|
assertEquals(RiskAction.PROMPT_USER, r.disposition)
|
||||||
assertEquals("INTERPRETER_EXECUTION", r.issues.single().code)
|
assertEquals("INTERPRETER_EXECUTION", r.issues.single().code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `declared EXEC_COMMAND role flags an argv list invoking python`() {
|
||||||
|
val r = rule.assess(
|
||||||
|
input(mapOf("argv" to listOf("python", "foo.py")))
|
||||||
|
.copy(paramRoles = mapOf("argv" to ParamRole.EXEC_COMMAND)),
|
||||||
|
)
|
||||||
|
assertEquals(RiskAction.PROMPT_USER, r.disposition)
|
||||||
|
assertEquals("INTERPRETER_EXECUTION", r.issues.single().code)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `declared EXEC_COMMAND role ignores non-declared params`() {
|
||||||
|
val r = rule.assess(
|
||||||
|
input(mapOf("argv" to listOf("ls", "-la"), "note" to "python is great"))
|
||||||
|
.copy(paramRoles = mapOf("argv" to ParamRole.EXEC_COMMAND)),
|
||||||
|
)
|
||||||
|
assertEquals(RiskAction.PROCEED, r.disposition)
|
||||||
|
assertTrue(r.issues.isEmpty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.correx.core.events.types.SessionId
|
|||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.events.types.ToolInvocationId
|
import com.correx.core.events.types.ToolInvocationId
|
||||||
import com.correx.core.toolintent.rules.PathContainmentRule
|
import com.correx.core.toolintent.rules.PathContainmentRule
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
@@ -85,4 +86,47 @@ class PathContainmentRuleTest {
|
|||||||
assertEquals(RiskAction.PROCEED, r.disposition)
|
assertEquals(RiskAction.PROCEED, r.disposition)
|
||||||
assertTrue(r.observations.isEmpty())
|
assertTrue(r.observations.isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `declared PATH role flags outside-workspace path arg`() {
|
||||||
|
val r = rule.assess(input("/tmp/scratch.txt", FakeProbe()).copy(paramRoles = mapOf("path" to ParamRole.PATH)))
|
||||||
|
assertEquals(RiskAction.PROMPT_USER, r.disposition)
|
||||||
|
assertEquals("PATH_OUTSIDE_WORKSPACE", r.issues.single().code)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `declared PATH role ignores a slashy non-path param`() {
|
||||||
|
val r = rule.assess(
|
||||||
|
ToolCallAssessmentInput(
|
||||||
|
request = ToolRequest(
|
||||||
|
ToolInvocationId("i"), SessionId("s"), StageId("st"), "file_write",
|
||||||
|
mapOf("path" to "/work/project/A.kt", "content" to "import a/b/c"),
|
||||||
|
),
|
||||||
|
capabilities = setOf(ToolCapability.FILE_WRITE),
|
||||||
|
workspace = WorkspacePolicy(workspace, listOf(Path.of("/etc"))),
|
||||||
|
probe = FakeProbe(),
|
||||||
|
paramRoles = mapOf("path" to ParamRole.PATH),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(RiskAction.PROCEED, r.disposition)
|
||||||
|
assertTrue(r.issues.isEmpty())
|
||||||
|
assertEquals(1, r.observations.size) // only the declared path param, not the slashy content
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no declared roles still flags a slashy content param via fallback`() {
|
||||||
|
val r = rule.assess(
|
||||||
|
ToolCallAssessmentInput(
|
||||||
|
request = ToolRequest(
|
||||||
|
ToolInvocationId("i"), SessionId("s"), StageId("st"), "file_write",
|
||||||
|
mapOf("content" to "/tmp/evil.txt"),
|
||||||
|
),
|
||||||
|
capabilities = setOf(ToolCapability.FILE_WRITE),
|
||||||
|
workspace = WorkspacePolicy(workspace, listOf(Path.of("/etc"))),
|
||||||
|
probe = FakeProbe(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(RiskAction.PROMPT_USER, r.disposition)
|
||||||
|
assertEquals("PATH_OUTSIDE_WORKSPACE", r.issues.single().code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.correx.core.tools.contract
|
||||||
|
|
||||||
|
enum class ParamRole { PATH, EXEC_COMMAND, NETWORK_TARGET }
|
||||||
@@ -10,5 +10,6 @@ interface Tool {
|
|||||||
val parametersSchema: JsonObject
|
val parametersSchema: JsonObject
|
||||||
val tier: Tier
|
val tier: Tier
|
||||||
val requiredCapabilities: Set<ToolCapability>
|
val requiredCapabilities: Set<ToolCapability>
|
||||||
|
val paramRoles: Map<String, ParamRole> get() = emptyMap()
|
||||||
fun validateRequest(request: ToolRequest): ValidationResult
|
fun validateRequest(request: ToolRequest): ValidationResult
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -4,6 +4,7 @@ import com.correx.core.approvals.Tier
|
|||||||
import com.correx.core.events.events.ToolRequest
|
import com.correx.core.events.events.ToolRequest
|
||||||
import com.correx.core.events.types.ToolInvocationId
|
import com.correx.core.events.types.ToolInvocationId
|
||||||
import com.correx.core.tools.contract.FileAffectingTool
|
import com.correx.core.tools.contract.FileAffectingTool
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.Tool
|
import com.correx.core.tools.contract.Tool
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import com.correx.core.tools.contract.ToolExecutor
|
import com.correx.core.tools.contract.ToolExecutor
|
||||||
@@ -74,6 +75,7 @@ class FileEditTool(
|
|||||||
}
|
}
|
||||||
override val tier: Tier = Tier.T3
|
override val tier: Tier = Tier.T3
|
||||||
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
|
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
|
||||||
|
override val paramRoles: Map<String, ParamRole> = mapOf("path" to ParamRole.PATH)
|
||||||
|
|
||||||
override fun affectedPaths(request: ToolRequest): Set<Path> {
|
override fun affectedPaths(request: ToolRequest): Set<Path> {
|
||||||
val pathString = request.parameters["path"] as? String ?: return emptySet()
|
val pathString = request.parameters["path"] as? String ?: return emptySet()
|
||||||
|
|||||||
+2
@@ -2,6 +2,7 @@ package com.correx.infrastructure.tools.filesystem
|
|||||||
|
|
||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.events.events.ToolRequest
|
import com.correx.core.events.events.ToolRequest
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.Tool
|
import com.correx.core.tools.contract.Tool
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import com.correx.core.tools.contract.ToolExecutor
|
import com.correx.core.tools.contract.ToolExecutor
|
||||||
@@ -47,6 +48,7 @@ class FileReadTool(
|
|||||||
}
|
}
|
||||||
override val tier: Tier = Tier.T1
|
override val tier: Tier = Tier.T1
|
||||||
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_READ)
|
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_READ)
|
||||||
|
override val paramRoles: Map<String, ParamRole> = mapOf("path" to ParamRole.PATH)
|
||||||
|
|
||||||
override fun validateRequest(request: ToolRequest): ValidationResult {
|
override fun validateRequest(request: ToolRequest): ValidationResult {
|
||||||
val pathString = request.parameters["path"] as? String
|
val pathString = request.parameters["path"] as? String
|
||||||
|
|||||||
+2
@@ -4,6 +4,7 @@ import com.correx.core.approvals.Tier
|
|||||||
import com.correx.core.events.events.ToolRequest
|
import com.correx.core.events.events.ToolRequest
|
||||||
import com.correx.core.events.types.ToolInvocationId
|
import com.correx.core.events.types.ToolInvocationId
|
||||||
import com.correx.core.tools.contract.FileAffectingTool
|
import com.correx.core.tools.contract.FileAffectingTool
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.Tool
|
import com.correx.core.tools.contract.Tool
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import com.correx.core.tools.contract.ToolExecutor
|
import com.correx.core.tools.contract.ToolExecutor
|
||||||
@@ -60,6 +61,7 @@ class FileWriteTool(
|
|||||||
}
|
}
|
||||||
override val tier: Tier = Tier.T2
|
override val tier: Tier = Tier.T2
|
||||||
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
|
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
|
||||||
|
override val paramRoles: Map<String, ParamRole> = mapOf("path" to ParamRole.PATH)
|
||||||
|
|
||||||
override fun affectedPaths(request: ToolRequest): Set<Path> {
|
override fun affectedPaths(request: ToolRequest): Set<Path> {
|
||||||
val pathString = request.parameters["path"] as? String ?: return emptySet()
|
val pathString = request.parameters["path"] as? String ?: return emptySet()
|
||||||
|
|||||||
+2
@@ -2,6 +2,7 @@ package com.correx.infrastructure.tools.shell
|
|||||||
|
|
||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.events.events.ToolRequest
|
import com.correx.core.events.events.ToolRequest
|
||||||
|
import com.correx.core.tools.contract.ParamRole
|
||||||
import com.correx.core.tools.contract.Tool
|
import com.correx.core.tools.contract.Tool
|
||||||
import com.correx.core.tools.contract.ToolCapability
|
import com.correx.core.tools.contract.ToolCapability
|
||||||
import com.correx.core.tools.contract.ToolExecutor
|
import com.correx.core.tools.contract.ToolExecutor
|
||||||
@@ -42,6 +43,7 @@ class ShellTool(
|
|||||||
override val tier: Tier = Tier.T2
|
override val tier: Tier = Tier.T2
|
||||||
override val requiredCapabilities: Set<ToolCapability> =
|
override val requiredCapabilities: Set<ToolCapability> =
|
||||||
setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN)
|
setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN)
|
||||||
|
override val paramRoles: Map<String, ParamRole> = mapOf("argv" to ParamRole.EXEC_COMMAND)
|
||||||
|
|
||||||
override fun validateRequest(request: ToolRequest): ValidationResult {
|
override fun validateRequest(request: ToolRequest): ValidationResult {
|
||||||
val argv = when (val raw = request.parameters["argv"]) {
|
val argv = when (val raw = request.parameters["argv"]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user