Pedal Assist Coding logo

Pedal Assist Coding

Archives
RSS Feed
CommonsWare
Log in
Subscribe
August 27, 2026

Thursdays with Koog: Permissions

In Which the Author Tries to Lock Himself Down

A month ago, in a past "Thursdays with Koog" issue, I described Koog's framework for tools. Today, I'd like to talk a bit about how Knosh works with tools, in particular how things get secured.

Knosh sticks to Koog's tools-as-classes pattern, and so it has a bunch of SimpleTool subclasses like GetWorkingDirectoryTool:

package com.commonsware.knosh.tools

import ai.koog.agents.core.tools.SimpleTool
import ai.koog.serialization.typeToken
import com.commonsware.knosh.common.RuntimeContext
import kotlinx.serialization.Serializable

// Raw string is exempt from MaxLineLength per project Detekt config (excludeRawStrings: true)
private val DESCRIPTION =
  """
  You MUST use this tool to get the current working directory. You do not have access to a shell, Bash, or any command-line tools — do not guess or assume the working directory.

  Usage:
  - Takes no arguments.
  - The returned path is absolute and can be used to resolve relative paths or to
    understand the root from which other tools operate.
  """
    .trimIndent()

/** Tool that returns the current working directory as an absolute filesystem path string. Takes no meaningful input. */
public class GetWorkingDirectoryTool(private val context: RuntimeContext) :
  SimpleTool<GetWorkingDirectoryTool.Args>(
    argsType = typeToken<Args>(),
    name = "get-working-directory",
    description = DESCRIPTION,
  ) {
  /** Arguments for [GetWorkingDirectoryTool]. There are no meaningful arguments; pass an empty object. */
  @Serializable public class Args

  /**
   * Returns the current working directory as an absolute filesystem path.
   *
   * @param args unused
   * @return the absolute path of the current working directory
   */
  override suspend fun execute(args: Args): String = context.workingDir.toString()
}

(from Knosh 0.3.0)

KnoshToolSet has a list of all of those tools:

  private val tools: List<Tool<*, *>> =
    listOf(
      CreateDirectoryTool(dispatcher, context),
      DeleteFileTool(dispatcher, context),
      FileInfoTool(dispatcher, context),
      GetWorkingDirectoryTool(context),
      GlobTool(dispatcher, context),
      GrepTool(dispatcher, context),
      ListDirectoryTreeTool(dispatcher, context),
      MoveContentTool(dispatcher, context),
      TextEditTool(dispatcher, context),
      TextReadTool(dispatcher, context),
      TextWriteTool(dispatcher, context),
      WebFetchTool(httpClient),
    )

Here, dispatcher is a CoroutineDispatcher for doing work asynchronously, and context is a RuntimeContext that provides access to things that need to be overridden in tests, such as environment variables and the filesystem.

However, not all agents need all tools. If you have an agent that is solely for code reviews, you might not want it to have the ability to write to files, delete files, etc. Hence, in the frontmatter for an agent definition, you can enable and disable tools in a permission structure:

permission:
  text-write: "deny"
  web-fetch: "allow"

Each tool has its unique identifier (e.g., text-write maps to TextWriteTool). By default, tools are allowed, but those an agent flags as deny get removed from the list.

OpenCode — whose permission system Knosh adapts — also supports ask, which prompts the user at the time of tool use whether to allow it. Knosh is a one-shot coding agent, designed for non-interactive use, so it does not support ask.

However, tools that manipulate the filesystem need more fine-grained access controls than a top-level allow/deny flag. By default, files inside of the current working directory can be accessed, while files elsewhere cannot. But, you can further constrain that if desired:

permission:
  text-edit:
    "*": "deny"
    "*.kt": "allow"
    "*.kts": "allow"
    "/Users/alice/projects/my-app/*": "allow"

Inside Knosh, since Koog's Tool is an abstract class, we can use the decorator pattern. PermissionCheckDecorator wraps a Knosh tool and enforces permissions before allowing the tool itself to its job:

/**
 * A [Tool] decorator that enforces both agent-level and command-level permissions and logs each tool call.
 *
 * Wraps [wrapped] transparently, forwarding all [execute] calls after permission checks pass. On each successful
 * execution, emits an INFO log event with the tool name as the message and `args`/`result` attributes. When
 * [logFullToolCalls] is `false`, long content in args is redacted via [LoggableContent.logContent] and result strings
 * that are longer than 200 characters or contain a newline are truncated: the text up to the first newline or first 80
 * characters (whichever is shorter) is kept, followed by `<redacted: N chars>`.
 *
 * Enforces a dual-gate permission model: both the agent's permissions and the command's permissions must allow a tool
 * invocation for it to proceed. First, [agentConfig]'s permissions are checked. Then, [commandPermissions] and
 * [commandExternalDirectories] are checked independently. Either gate may throw [ToolException.ValidationFailure] via
 * [fail] if a permission is denied.
 */
internal class PermissionCheckDecorator<A, B>(
  private val wrapped: Tool<A, B>,
  internal val agentConfig: AgentConfig,
  private val commandPermissions: Map<String, List<ToolPermission>> = emptyMap(),
  private val commandExternalDirectories: Map<String, Boolean> = emptyMap(),
  private val logFullToolCalls: Boolean = false,
  private val workingDir: Path = System.getProperty("user.dir").toPath(),
  private val userHome: Path = System.getProperty("user.home").toPath(),
) : Tool<A, B>(argsType = wrapped.argsType, resultType = wrapped.resultType, descriptor = wrapped.descriptor) {
    // whole lotta code
}

This allows the whole permission system to be abstracted out, tested in isolation, etc.

This works nicely for tools that are known in advance. Things get a bit interesting for tools that are not known in advance, notably MCP servers. In next week's "Thursdays with Koog", I'll talk about the soon-to-be-released Knosh 0.4.0, what MCP is, and how Knosh supports MCP.

Don't miss what's next. Subscribe to Pedal Assist Coding:
Older → Qwen 3.8: Quality Sans Speed

Add a comment:

Posting this comment will subscribe you to this newsletter with the email address you enter.
Bluesky
androiddev.social
commonsware.com
Powered by Buttondown, the easiest way to start and grow your newsletter.