Class: CardinalPermissionShim
- Inherits:
-
Object
- Object
- CardinalPermissionShim
- Defined in:
- lib/cardinal/permission_shim.rb
Constant Summary collapse
- PROTOCOL =
"2024-11-05"
Instance Method Summary collapse
-
#decide(args) ⇒ Object
POST the request, poll until answered, translate to the CLI's contract.
- #deny(message) ⇒ Object
- #get(path) ⇒ Object
- #handle(msg) ⇒ Object
- #http_json(klass, path, body) ⇒ Object
-
#initialize(input: $stdin, output: $stdout, base_url: ENV["CARDINAL_URL"], run_id: ENV["CARDINAL_RUN_ID"], timeout: ENV.fetch("CARDINAL_PERMISSION_TIMEOUT", "600").to_i) ⇒ CardinalPermissionShim
constructor
A new instance of CardinalPermissionShim.
- #post(path, body) ⇒ Object
- #reply(id, payload) ⇒ Object
- #run ⇒ Object
Constructor Details
#initialize(input: $stdin, output: $stdout, base_url: ENV["CARDINAL_URL"], run_id: ENV["CARDINAL_RUN_ID"], timeout: ENV.fetch("CARDINAL_PERMISSION_TIMEOUT", "600").to_i) ⇒ CardinalPermissionShim
Returns a new instance of CardinalPermissionShim.
17 18 19 20 21 22 23 |
# File 'lib/cardinal/permission_shim.rb', line 17 def initialize(input: $stdin, output: $stdout, base_url: ENV["CARDINAL_URL"], run_id: ENV["CARDINAL_RUN_ID"], timeout: ENV.fetch("CARDINAL_PERMISSION_TIMEOUT", "600").to_i) @input, @output = input, output @base_url, @run_id, @timeout = base_url, run_id, timeout @output.sync = true end |
Instance Method Details
#decide(args) ⇒ Object
POST the request, poll until answered, translate to the CLI's contract.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/cardinal/permission_shim.rb', line 61 def decide(args) created = post("/permission_requests", run_id: @run_id, tool_name: args["tool_name"].to_s, input: args["input"] || {}) return deny("Cardinal couldn't record the permission request — denied for safety.") unless created waited = 0 status, = created["status"], created["message"] while status == "pending" if waited >= @timeout post("/permission_requests/#{created["id"]}/answer", verdict: "deny", auto: "1") return deny("No answer from the user in time. Work around this without the command, or park with a QUESTION:.") end sleep 2 waited += 2 current = get("/permission_requests/#{created["id"]}") status, = current["status"], current["message"] if current end if status == "allowed" { "behavior" => "allow", "updatedInput" => args["input"] || {} } else deny(.to_s.empty? ? "The user denied this action." : "The user denied this: #{}") end end |
#deny(message) ⇒ Object
86 |
# File 'lib/cardinal/permission_shim.rb', line 86 def deny() = { "behavior" => "deny", "message" => } |
#get(path) ⇒ Object
96 97 98 |
# File 'lib/cardinal/permission_shim.rb', line 96 def get(path) http_json(Net::HTTP::Get, path, nil) end |
#handle(msg) ⇒ Object
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/cardinal/permission_shim.rb', line 35 def handle(msg) case msg["method"] when "initialize" reply(msg["id"], protocolVersion: msg.dig("params", "protocolVersion") || PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: "cardinal", version: "1.0" }) when "tools/list" reply(msg["id"], tools: [{ name: "permission", description: "Ask the Cardinal board's user to approve or deny a tool use.", inputSchema: { type: "object", properties: { tool_name: { type: "string" }, input: { type: "object" } }, required: ["tool_name"] } }]) when "tools/call" args = msg.dig("params", "arguments") || {} decision = decide(args) reply(msg["id"], content: [{ type: "text", text: decision.to_json }]) when "ping" reply(msg["id"], {}) else reply(msg["id"], {}) if msg["id"] # politely ack anything else that expects an answer end end |
#http_json(klass, path, body) ⇒ Object
100 101 102 103 104 105 106 107 108 109 |
# File 'lib/cardinal/permission_shim.rb', line 100 def http_json(klass, path, body) uri = URI("#{@base_url}#{path}") req = klass.new(uri.request_uri, "Content-Type" => "application/json", "Accept" => "application/json") req.body = body.to_json if body res = Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) { |h| h.request(req) } res.code.to_i.between?(200, 299) ? JSON.parse(res.body) : nil rescue StandardError => e warn "cardinal-shim: #{e.class}: #{e.}" nil end |
#post(path, body) ⇒ Object
92 93 94 |
# File 'lib/cardinal/permission_shim.rb', line 92 def post(path, body) http_json(Net::HTTP::Post, path, body) end |
#reply(id, payload) ⇒ Object
88 89 90 |
# File 'lib/cardinal/permission_shim.rb', line 88 def reply(id, payload) @output.puts({ jsonrpc: "2.0", id: id, result: payload }.to_json) end |
#run ⇒ Object
25 26 27 28 29 30 31 32 33 |
# File 'lib/cardinal/permission_shim.rb', line 25 def run while (line = @input.gets) line = line.strip next if line.empty? handle(JSON.parse(line)) end rescue Interrupt nil end |