Skip to content

Commit 451d47c

Browse files
krukowCopilot
andcommitted
feat(tool-set): add github.copilot-sdk.tool-set namespace + isolated preset
Source-qualified tool filter constructors (builtin/mcp/custom + builtins vector form) plus isolated-builtins / isolated for parity with upstream BuiltInTools.Isolated. Bare "*" is rejected at construction time. Adds fdef specs in instrument.clj for every public fn so integration tests with instrumentation enabled catch contract violations. Mirrors upstream nodejs/src/toolSet.ts from PR github/copilot-sdk#1428 github/copilot-sdk#1428 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 125746b commit 451d47c

4 files changed

Lines changed: 210 additions & 1 deletion

File tree

script/validate_docs.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"github.copilot-sdk.specs"
4343
"github.copilot-sdk.instrument"
4444
"github.copilot-sdk.tools"
45+
"github.copilot-sdk.tool-set"
4546
"github.copilot-sdk.generated.event-specs"
4647
"github.copilot-sdk.generated.coerce"})
4748

src/github/copilot_sdk/instrument.clj

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
(stest/unstrument)"
1515
(:require [clojure.spec.alpha :as s]
1616
[clojure.spec.test.alpha :as stest]
17-
[github.copilot-sdk.specs :as specs]))
17+
[github.copilot-sdk.specs :as specs]
18+
;; Ensure namespaces hosting public fns referenced by `register-fdef!`
19+
;; are loaded before `stest/instrument` runs (otherwise the missing
20+
;; var would be silently skipped, leaving an instrumentation gap).
21+
[github.copilot-sdk.tool-set]))
1822

1923
;; -----------------------------------------------------------------------------
2024
;; Single-source registry for fdefs
@@ -566,6 +570,30 @@
566570
:args (s/cat :client ::specs/client :params map?)
567571
:ret map?)
568572

573+
;; -----------------------------------------------------------------------------
574+
;; Tool filter helpers (upstream PR #1428)
575+
;; -----------------------------------------------------------------------------
576+
577+
(register-fdef! github.copilot-sdk.tool-set/valid-name?
578+
:args (s/cat :name any?)
579+
:ret boolean?)
580+
581+
(register-fdef! github.copilot-sdk.tool-set/builtin
582+
:args (s/cat :name string?)
583+
:ret string?)
584+
585+
(register-fdef! github.copilot-sdk.tool-set/mcp
586+
:args (s/cat :name string?)
587+
:ret string?)
588+
589+
(register-fdef! github.copilot-sdk.tool-set/custom
590+
:args (s/cat :name string?)
591+
:ret string?)
592+
593+
(register-fdef! github.copilot-sdk.tool-set/builtins
594+
:args (s/cat :names (s/coll-of string?))
595+
:ret (s/coll-of string? :kind vector?))
596+
569597
;; -----------------------------------------------------------------------------
570598
;; Instrument all public API functions
571599
;; -----------------------------------------------------------------------------
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
(ns github.copilot-sdk.tool-set
2+
"Helpers for building source-qualified tool filter patterns used by
3+
`:available-tools` and `:excluded-tools` in session configs.
4+
5+
The runtime matches each tool reference against patterns of the form
6+
`\"<source>:<name>\"` where source is one of `builtin`, `mcp`, or
7+
`custom` and name is either a literal tool name or the wildcard
8+
`\"*\"`. The bare wildcard `\"*\"` (no source) is intentionally
9+
rejected by the SDK — apps must explicitly opt into a source so an
10+
absent source can never silently grant access to unexpected tools.
11+
12+
Mirrors upstream `ToolSet` and `BuiltInTools` from
13+
`nodejs/src/toolSet.ts` (upstream PR #1428).
14+
15+
Examples:
16+
17+
(tool-set/builtin \"ask_user\") ;; => \"builtin:ask_user\"
18+
(tool-set/builtin \"*\") ;; => \"builtin:*\"
19+
(tool-set/mcp \"*\") ;; => \"mcp:*\"
20+
(tool-set/builtins [\"task\" \"skill\"]) ;; => [\"builtin:task\" \"builtin:skill\"]
21+
22+
;; Ready-to-use: every built-in that is safely session-bounded.
23+
tool-set/isolated ;; => [\"builtin:ask_user\" \"builtin:task_complete\" ...]")
24+
25+
(def ^:private valid-name-regex
26+
"Allowed characters in a tool name segment (mirrors upstream `nameRe`)."
27+
#"^[a-zA-Z0-9_-]+$")
28+
29+
(defn valid-name?
30+
"True when `name` is a valid tool-name segment — alphanumeric plus
31+
`_` and `-`, or the literal wildcard `\"*\"`. Returns false for any
32+
non-string input."
33+
[name]
34+
(and (string? name)
35+
(or (= "*" name)
36+
(some? (re-matches valid-name-regex name)))))
37+
38+
(defn- validate! [source name]
39+
(when-not (valid-name? name)
40+
(throw (ex-info (format "Invalid %s tool name: %s" source (pr-str name))
41+
{:source source :name name}))))
42+
43+
(defn builtin
44+
"Returns the filter pattern `\"builtin:<name>\"`. `name` may be the
45+
wildcard `\"*\"`. Throws on invalid names."
46+
[name]
47+
(validate! "builtin" name)
48+
(str "builtin:" name))
49+
50+
(defn mcp
51+
"Returns the filter pattern `\"mcp:<name>\"`. `name` may be the
52+
wildcard `\"*\"`. Throws on invalid names."
53+
[name]
54+
(validate! "mcp" name)
55+
(str "mcp:" name))
56+
57+
(defn custom
58+
"Returns the filter pattern `\"custom:<name>\"`. `name` may be the
59+
wildcard `\"*\"`. Throws on invalid names."
60+
[name]
61+
(validate! "custom" name)
62+
(str "custom:" name))
63+
64+
(defn builtins
65+
"Returns a vector of `\"builtin:<name>\"` patterns, one per entry in
66+
`names`. Throws on the first invalid name."
67+
[names]
68+
(mapv builtin names))
69+
70+
(def isolated-builtins
71+
"Names of the built-in tools that are safely session-bounded (no host
72+
I/O). Mirrors upstream `BuiltInTools.Isolated`. Use [[isolated]] for
73+
the source-qualified form ready to drop into `:available-tools`."
74+
["ask_user"
75+
"task_complete"
76+
"exit_plan_mode"
77+
"task"
78+
"read_agent"
79+
"write_agent"
80+
"list_agents"
81+
"send_inbox"
82+
"context_board"
83+
"skill"])
84+
85+
(def isolated
86+
"Source-qualified `\"builtin:<name>\"` patterns for every tool in
87+
[[isolated-builtins]]. Drop directly into `:available-tools`:
88+
89+
(copilot/create-session client
90+
{:available-tools tool-set/isolated})"
91+
(builtins isolated-builtins))
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
(ns github.copilot-sdk.tool-set-test
2+
"Unit tests for github.copilot-sdk.tool-set — the source-qualified tool
3+
filter helpers introduced by upstream PR #1428."
4+
(:require [clojure.test :refer [deftest is testing]]
5+
[github.copilot-sdk.tool-set :as tool-set]))
6+
7+
(deftest valid-name?-tests
8+
(testing "alphanumeric, underscore, hyphen are valid"
9+
(is (true? (tool-set/valid-name? "ask_user")))
10+
(is (true? (tool-set/valid-name? "task-complete")))
11+
(is (true? (tool-set/valid-name? "Tool123")))
12+
(is (true? (tool-set/valid-name? "a"))))
13+
(testing "literal wildcard is valid"
14+
(is (true? (tool-set/valid-name? "*"))))
15+
(testing "wildcards in the middle of a name are invalid"
16+
(is (false? (tool-set/valid-name? "ask*"))))
17+
(testing "spaces, colons, dots, slashes are invalid"
18+
(is (false? (tool-set/valid-name? "ask user")))
19+
(is (false? (tool-set/valid-name? "builtin:ask_user")))
20+
(is (false? (tool-set/valid-name? "ask.user")))
21+
(is (false? (tool-set/valid-name? "ask/user"))))
22+
(testing "empty string and non-strings are invalid"
23+
(is (false? (tool-set/valid-name? "")))
24+
(is (false? (tool-set/valid-name? nil)))
25+
(is (false? (tool-set/valid-name? :ask_user)))
26+
(is (false? (tool-set/valid-name? 42)))))
27+
28+
(deftest builtin-tests
29+
(testing "produces source-qualified pattern"
30+
(is (= "builtin:ask_user" (tool-set/builtin "ask_user")))
31+
(is (= "builtin:*" (tool-set/builtin "*"))))
32+
(testing "rejects invalid name"
33+
(is (thrown-with-msg? clojure.lang.ExceptionInfo
34+
#"Invalid builtin tool name"
35+
(tool-set/builtin "bad name")))
36+
(is (thrown-with-msg? clojure.lang.ExceptionInfo
37+
#"Invalid builtin tool name"
38+
(tool-set/builtin "")))))
39+
40+
(deftest mcp-tests
41+
(testing "produces source-qualified pattern"
42+
(is (= "mcp:my_server" (tool-set/mcp "my_server")))
43+
(is (= "mcp:*" (tool-set/mcp "*"))))
44+
(testing "rejects invalid name"
45+
(is (thrown-with-msg? clojure.lang.ExceptionInfo
46+
#"Invalid mcp tool name"
47+
(tool-set/mcp "bad:name")))))
48+
49+
(deftest custom-tests
50+
(testing "produces source-qualified pattern"
51+
(is (= "custom:reviewer" (tool-set/custom "reviewer")))
52+
(is (= "custom:*" (tool-set/custom "*"))))
53+
(testing "rejects invalid name"
54+
(is (thrown-with-msg? clojure.lang.ExceptionInfo
55+
#"Invalid custom tool name"
56+
(tool-set/custom "has space")))))
57+
58+
(deftest builtins-tests
59+
(testing "maps over names and returns a vector"
60+
(is (= ["builtin:task" "builtin:skill"]
61+
(tool-set/builtins ["task" "skill"])))
62+
(is (vector? (tool-set/builtins ["task"]))))
63+
(testing "empty input yields empty vector"
64+
(is (= [] (tool-set/builtins []))))
65+
(testing "throws on the first invalid entry"
66+
(is (thrown-with-msg? clojure.lang.ExceptionInfo
67+
#"Invalid builtin tool name"
68+
(tool-set/builtins ["ok" "bad name"])))))
69+
70+
(deftest isolated-builtins-tests
71+
(testing "matches upstream BuiltInTools.Isolated exactly"
72+
(is (= ["ask_user"
73+
"task_complete"
74+
"exit_plan_mode"
75+
"task"
76+
"read_agent"
77+
"write_agent"
78+
"list_agents"
79+
"send_inbox"
80+
"context_board"
81+
"skill"]
82+
tool-set/isolated-builtins))))
83+
84+
(deftest isolated-tests
85+
(testing "is the source-qualified form of isolated-builtins"
86+
(is (= (mapv #(str "builtin:" %) tool-set/isolated-builtins)
87+
tool-set/isolated)))
88+
(testing "every entry starts with builtin: prefix"
89+
(is (every? #(.startsWith ^String % "builtin:") tool-set/isolated))))

0 commit comments

Comments
 (0)