toolkami 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64639f6ea3dcaaf4b8219a203547c7943b1e3c381f35c61ca92f2e3201cf91b5
4
- data.tar.gz: 8bfbd595cccc94c18b48d9f39146be1eeba7205eb4f4f4e8f46ae7dbbd6f03cd
3
+ metadata.gz: 3b45509729bb7cf4488cd7c980b82f8591d8100b487e3f843468a366e6a30740
4
+ data.tar.gz: dd187ced737c9521f5f574c8f8b3c34cd535328a288e74a0c8800a1fd6c1db00
5
5
  SHA512:
6
- metadata.gz: e1fc05ed56ce38e6e0786d158e7bd9547b4c00dde65e7e6c078fbef195adb68a537468bb16e6df53cbd68f4b5c824c0b1f9c463b620e0f8ef6fda94ca4fa916f
7
- data.tar.gz: 80bb9141963a9cc25d5b1e03b96e711b3e0873354829aaa06b0742ddedfd23e11e005ea8aa139d8557e17c874bc3253348f97deb54b0e7ffc73aa0efe7f0472d
6
+ metadata.gz: 30434ef57423cd7c0e0941ea888a745e93da510ef16ffd5bbda03beb136af884e48efa7e289da28858829b1798ddace2fa1b627eaa0665f58951061f924719be
7
+ data.tar.gz: 904f06fc249716daed81da20bcaaea02ca0c99f5ce0d34ffe079f18ffb7677bd8a87dad436df133be28ea3f5fbdeea1c8813d63b8599c1debc3b34b4af426f14
data/README.md CHANGED
@@ -8,7 +8,7 @@ Ruby gRPC client for the local `toolkami` daemon.
8
8
  gem install toolkami
9
9
  ```
10
10
 
11
- The packaged gem downloads the matching Linux x64 daemon from the GitHub release for the gem version on first client start. Repository checkouts do not download a daemon; they use the local Cargo debug binary instead.
11
+ The packaged gem downloads the matching Linux x64 daemon and guest-agent from the GitHub release for the gem version on first client start. Repository checkouts do not download release binaries; they use the local Cargo debug binary instead.
12
12
 
13
13
  ## Usage
14
14
 
@@ -21,6 +21,77 @@ puts client.ping("ruby").message
21
21
  client.close
22
22
  ```
23
23
 
24
+ ## Daemon Launch and NBD Permissions
25
+
26
+ The daemon requires read/write access to NBD block devices (`/dev/nbd*`).
27
+
28
+ - Default behavior: `Client.open(...)` reuses an existing daemon; if unavailable, it auto-launches one.
29
+ - If your user lacks NBD permissions, auto-launch fails with an actionable error.
30
+ - To disable auto-launch and require an already-running daemon:
31
+
32
+ ```ruby
33
+ client = Toolkami::Client.open(
34
+ tcp_address: "127.0.0.1:50061",
35
+ auto_launch: false
36
+ )
37
+ ```
38
+
39
+ - To explicitly opt into privileged launch:
40
+
41
+ ```ruby
42
+ client = Toolkami::Client.open(
43
+ tcp_address: "127.0.0.1:50061",
44
+ use_sudo: true
45
+ )
46
+ ```
47
+
48
+ ## Spawn/Restore with Folder Mounts
49
+
50
+ ```ruby
51
+ require "toolkami"
52
+
53
+ client = Toolkami::Client.open(tcp_address: "127.0.0.1:50061")
54
+
55
+ image = client.build(
56
+ Toolkami::V1::BuildRequest.new(
57
+ dockerfile_text: "FROM ubuntu:24.04\nWORKDIR /workspace",
58
+ dockerfile_bytes_max: 64_000
59
+ )
60
+ )
61
+
62
+ spawned = client.spawn(
63
+ Toolkami::V1::SpawnRequest.new(
64
+ image_id: image.image_id,
65
+ mounts: [
66
+ Toolkami::V1::FolderMount.new(
67
+ source_host_path: "/abs/path/to/repo",
68
+ target_path: "/workspace/repo"
69
+ # access_mode defaults to READ_ONLY when omitted
70
+ )
71
+ ]
72
+ )
73
+ )
74
+
75
+ snapshot = client.snapshot(
76
+ Toolkami::V1::SnapshotRequest.new(instance_id: spawned.instance_id)
77
+ )
78
+
79
+ restored = client.restore(
80
+ Toolkami::V1::RestoreRequest.new(
81
+ snapshot_id: snapshot.snapshot_id,
82
+ mounts: [
83
+ Toolkami::V1::FolderMount.new(
84
+ source_host_path: "/abs/path/to/repo",
85
+ target_path: "/workspace/repo"
86
+ )
87
+ ]
88
+ )
89
+ )
90
+
91
+ puts restored.instance_id
92
+ client.close
93
+ ```
94
+
24
95
  ## Development
25
96
 
26
97
  ```bash
@@ -14,11 +14,16 @@ module Toolkami
14
14
  def initialize(
15
15
  tcp_address: nil,
16
16
  startup_attempts_max: nil,
17
- startup_delay_s: nil
17
+ startup_delay_s: nil,
18
+ auto_launch: true,
19
+ use_sudo: nil
18
20
  )
19
21
  @tcp_address = Daemon.resolve_tcp_address(tcp_address)
20
22
  @startup_attempts_max = Daemon.resolve_startup_attempts_max(startup_attempts_max)
21
23
  @startup_delay_s = Daemon.resolve_startup_delay_s(startup_delay_s)
24
+ Assert.that(auto_launch == true || auto_launch == false, "auto_launch must be boolean")
25
+ @auto_launch = auto_launch
26
+ @use_sudo = Daemon.resolve_use_sudo(use_sudo)
22
27
  @daemon_launch = nil
23
28
  @stub = build_stub(@tcp_address)
24
29
  end
@@ -71,11 +76,19 @@ module Toolkami
71
76
 
72
77
  def start
73
78
  return self if ready?
79
+ unless @auto_launch
80
+ raise "toolkami daemon is not reachable at #{@tcp_address} and auto_launch is disabled"
81
+ end
74
82
 
75
83
  daemon_bin_path = Daemon.resolve_bin_path
84
+ preflight_error = Daemon.nbd_access_preflight
85
+ if !preflight_error.nil? && !@use_sudo
86
+ raise "#{preflight_error}; set use_sudo=true or start daemon with proper permissions"
87
+ end
76
88
  @daemon_launch = Daemon.launch(
77
89
  daemon_bin_path: daemon_bin_path,
78
- tcp_address: @tcp_address
90
+ tcp_address: @tcp_address,
91
+ use_sudo: @use_sudo
79
92
  )
80
93
  @stub = build_stub(@tcp_address)
81
94
  wait_for_ready
@@ -10,6 +10,7 @@ module Toolkami
10
10
  STARTUP_DELAY_S_DEFAULT = 0.1
11
11
  STDERR_CHUNKS_MAX = 8
12
12
  TCP_ADDRESS_DEFAULT = "127.0.0.1:50061"
13
+ USE_SUDO_DEFAULT = false
13
14
 
14
15
  def self.assert_bin_path(daemon_bin_path)
15
16
  Assert.that(daemon_bin_path.is_a?(String), "daemon_bin_path must be a String")
@@ -18,15 +19,30 @@ module Toolkami
18
19
  true
19
20
  end
20
21
 
21
- def self.launch(daemon_bin_path:, tcp_address:)
22
+ def self.launch(daemon_bin_path:, tcp_address:, use_sudo: false)
22
23
  assert_bin_path(daemon_bin_path)
23
24
  daemon_stderr_read, daemon_stderr_write = IO.pipe
24
- daemon_pid = Process.spawn(
25
- { "TOOLKAMI_TCP_ADDRESS" => tcp_address },
26
- daemon_bin_path,
27
- out: File::NULL,
28
- err: daemon_stderr_write
29
- )
25
+ daemon_env = daemon_env_resolve(daemon_bin_path, tcp_address)
26
+ daemon_pid =
27
+ if use_sudo
28
+ env_args = daemon_env.map { |key, value| "#{key}=#{value}" }
29
+ Process.spawn(
30
+ "sudo",
31
+ "-n",
32
+ "env",
33
+ *env_args,
34
+ daemon_bin_path,
35
+ out: File::NULL,
36
+ err: daemon_stderr_write
37
+ )
38
+ else
39
+ Process.spawn(
40
+ daemon_env,
41
+ daemon_bin_path,
42
+ out: File::NULL,
43
+ err: daemon_stderr_write
44
+ )
45
+ end
30
46
  daemon_stderr_write.close
31
47
 
32
48
  Assert.that(daemon_pid.positive?, "daemon pid must be positive")
@@ -97,6 +113,37 @@ module Toolkami
97
113
  delay_s
98
114
  end
99
115
 
116
+ def self.resolve_use_sudo(use_sudo)
117
+ return USE_SUDO_DEFAULT if use_sudo.nil?
118
+
119
+ Assert.that(use_sudo == true || use_sudo == false, "use_sudo must be boolean")
120
+ use_sudo
121
+ end
122
+
123
+ def self.daemon_env_resolve(daemon_bin_path, tcp_address)
124
+ daemon_env = { "TOOLKAMI_TCP_ADDRESS" => tcp_address }
125
+ guest_agent_bin_path = File.join(File.dirname(daemon_bin_path), "toolkami-guest-agent")
126
+ if File.file?(guest_agent_bin_path) && File.executable?(guest_agent_bin_path)
127
+ daemon_env["TOOLKAMI_GUEST_AGENT_HOST_BIN_PATH"] = guest_agent_bin_path
128
+ end
129
+ daemon_env
130
+ end
131
+
132
+ def self.nbd_access_preflight
133
+ has_device = false
134
+ (0..15).each do |index|
135
+ path = "/dev/nbd#{index}"
136
+ next unless File.exist?(path)
137
+ next unless File.blockdev?(path)
138
+
139
+ has_device = true
140
+ return nil if File.readable?(path) && File.writable?(path)
141
+ end
142
+ return "daemon requires NBD devices (/dev/nbd*) but none were found" unless has_device
143
+
144
+ "daemon requires read/write access to /dev/nbd*; run a privileged daemon or grant device permissions"
145
+ end
146
+
100
147
  def self.daemon_failure_message(daemon_status, daemon_stderr)
101
148
  Assert.that(!daemon_status.nil?, "daemon_status must not be nil")
102
149
  daemon_status_text =
@@ -16,11 +16,15 @@ module Toolkami
16
16
  def self.daemon_bin_path_install
17
17
  platform_assert
18
18
  daemon_bin_path = Release.daemon_bin_path_packaged_resolve
19
- return daemon_bin_path if daemon_bin_path_ready?(daemon_bin_path)
19
+ guest_agent_bin_path = Release.guest_agent_bin_path_packaged_resolve
20
+ return daemon_bin_path if daemon_bin_path_ready?(daemon_bin_path) && daemon_bin_path_ready?(guest_agent_bin_path)
20
21
 
21
- daemon_bytes = daemon_bytes_download
22
+ daemon_bytes = daemon_bytes_download(Release.release_artifact_url_resolve(Release.package_version_resolve))
23
+ guest_agent_bytes = daemon_bytes_download(Release.release_guest_agent_artifact_url_resolve(Release.package_version_resolve))
22
24
  daemon_bin_file_write(daemon_bin_path, daemon_bytes)
25
+ daemon_bin_file_write(guest_agent_bin_path, guest_agent_bytes)
23
26
  daemon_bin_path_assert(daemon_bin_path)
27
+ daemon_bin_path_assert(guest_agent_bin_path)
24
28
  daemon_bin_path
25
29
  end
26
30
 
@@ -35,12 +39,12 @@ module Toolkami
35
39
  raise "toolkami supports only linux x64 for automatic daemon installation"
36
40
  end
37
41
 
38
- def self.daemon_bytes_download
39
- package_version = Release.package_version_resolve
40
- release_artifact_url = Release.release_artifact_url_resolve(package_version)
42
+ def self.daemon_bytes_download(release_artifact_url)
41
43
  response = http_response_fetch(URI(release_artifact_url))
42
44
  daemon_bytes = response.body
43
45
 
46
+ Assert.that(release_artifact_url.is_a?(String), "release_artifact_url must be a String")
47
+ Assert.that(release_artifact_url.start_with?("https://"), "release_artifact_url must be HTTPS")
44
48
  Assert.that(daemon_bytes.is_a?(String), "daemon_bytes must be a String")
45
49
  Assert.that(!daemon_bytes.empty?, "daemon_bytes must not be empty")
46
50
  Assert.that(daemon_bytes.bytesize <= DOWNLOAD_SIZE_BYTES_MAX, "daemon_bytes must be bounded")
@@ -100,7 +104,10 @@ module Toolkami
100
104
  daemon_directory_path = File.dirname(daemon_bin_path)
101
105
  daemon_temp_path = "#{daemon_bin_path}.tmp"
102
106
 
103
- Assert.that(daemon_bin_path.end_with?("/toolkami"), "daemon_bin_path must end with /toolkami")
107
+ Assert.that(
108
+ daemon_bin_path.end_with?("/toolkami") || daemon_bin_path.end_with?("/toolkami-guest-agent"),
109
+ "daemon_bin_path must end with supported binary name"
110
+ )
104
111
  Assert.that(daemon_bytes.bytesize.positive?, "daemon_bytes must not be empty")
105
112
  FileUtils.mkdir_p(daemon_directory_path)
106
113
  File.binwrite(daemon_temp_path, daemon_bytes)
@@ -5,7 +5,8 @@ require_relative "version"
5
5
 
6
6
  module Toolkami
7
7
  module Release
8
- RELEASE_ASSET_NAME = "toolkami-linux-x64"
8
+ RELEASE_DAEMON_ASSET_NAME = "toolkami-linux-x64"
9
+ RELEASE_GUEST_AGENT_ASSET_NAME = "toolkami-guest-agent-linux-x64"
9
10
  RELEASE_BASE_URL = "https://github.com/aperoc/toolkami/releases"
10
11
 
11
12
  def self.daemon_bin_path_checkout_resolve
@@ -24,6 +25,14 @@ module Toolkami
24
25
  daemon_bin_path
25
26
  end
26
27
 
28
+ def self.guest_agent_bin_path_packaged_resolve
29
+ guest_agent_bin_path = File.expand_path("../../bin/toolkami-guest-agent", __dir__)
30
+
31
+ Assert.that(guest_agent_bin_path.end_with?("/bin/toolkami-guest-agent"), "packaged guest-agent path must end with /bin/toolkami-guest-agent")
32
+ Assert.that(guest_agent_bin_path.include?("/sdk/toolkami/ruby/") || guest_agent_bin_path.include?("/gems/toolkami-"), "packaged guest-agent path must target sdk or gem install")
33
+ guest_agent_bin_path
34
+ end
35
+
27
36
  def self.package_root_path_resolve
28
37
  package_root_path = File.expand_path("../..", __dir__)
29
38
 
@@ -44,7 +53,18 @@ module Toolkami
44
53
  def self.release_artifact_url_resolve(package_version)
45
54
  release_artifact_url = URI.join(
46
55
  "#{RELEASE_BASE_URL}/",
47
- "download/v#{package_version}/#{RELEASE_ASSET_NAME}"
56
+ "download/v#{package_version}/#{RELEASE_DAEMON_ASSET_NAME}"
57
+ ).to_s
58
+
59
+ Assert.that(package_version.is_a?(String), "package_version must be a String")
60
+ Assert.that(release_artifact_url.include?("/v#{package_version}/"), "release_artifact_url must include version")
61
+ release_artifact_url
62
+ end
63
+
64
+ def self.release_guest_agent_artifact_url_resolve(package_version)
65
+ release_artifact_url = URI.join(
66
+ "#{RELEASE_BASE_URL}/",
67
+ "download/v#{package_version}/#{RELEASE_GUEST_AGENT_ASSET_NAME}"
48
68
  ).to_s
49
69
 
50
70
  Assert.that(package_version.is_a?(String), "package_version must be a String")
@@ -7,7 +7,7 @@ require 'google/protobuf'
7
7
  require 'toolkami/v1/common_pb'
8
8
 
9
9
 
10
- descriptor_data = "\n\x1atoolkami/v1/instance.proto\x12\x0btoolkami.v1\x1a\x18toolkami/v1/common.proto\"n\n\x0cSpawnRequest\x12\x10\n\x08image_id\x18\x01 \x01(\t\x12\x16\n\tcpu_count\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x17\n\nmemory_mib\x18\x03 \x01(\rH\x01\x88\x01\x01\x42\x0c\n\n_cpu_countB\r\n\x0b_memory_mib\"L\n\rSpawnResponse\x12\x13\n\x0binstance_id\x18\x01 \x01(\t\x12&\n\x08vm_state\x18\x02 \x01(\x0e\x32\x14.toolkami.v1.VmState\"%\n\x0eRestoreRequest\x12\x13\n\x0bsnapshot_id\x18\x01 \x01(\t\"N\n\x0fRestoreResponse\x12\x13\n\x0binstance_id\x18\x01 \x01(\t\x12&\n\x08vm_state\x18\x02 \x01(\x0e\x32\x14.toolkami.v1.VmState\"\xaf\x01\n\x0b\x45xecRequest\x12\x13\n\x0binstance_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63ommand_text\x18\x02 \x01(\t\x12\x19\n\x11\x63ommand_bytes_max\x18\x03 \x01(\r\x12\x17\n\ntimeout_ms\x18\x04 \x01(\rH\x00\x88\x01\x01\x12\x18\n\x10stdout_bytes_max\x18\x05 \x01(\r\x12\x18\n\x10stderr_bytes_max\x18\x06 \x01(\rB\r\n\x0b_timeout_ms\"^\n\x0c\x45xecResponse\x12\x11\n\texit_code\x18\x01 \x01(\x05\x12\x13\n\x0bstdout_text\x18\x02 \x01(\t\x12\x13\n\x0bstderr_text\x18\x03 \x01(\t\x12\x11\n\ttimed_out\x18\x04 \x01(\x08\x62\x06proto3"
10
+ descriptor_data = "\n\x1atoolkami/v1/instance.proto\x12\x0btoolkami.v1\x1a\x18toolkami/v1/common.proto\"\x98\x01\n\x0cSpawnRequest\x12\x10\n\x08image_id\x18\x01 \x01(\t\x12\x16\n\tcpu_count\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x17\n\nmemory_mib\x18\x03 \x01(\rH\x01\x88\x01\x01\x12(\n\x06mounts\x18\x04 \x03(\x0b\x32\x18.toolkami.v1.FolderMountB\x0c\n\n_cpu_countB\r\n\x0b_memory_mib\"v\n\rSpawnResponse\x12\x13\n\x0binstance_id\x18\x01 \x01(\t\x12&\n\x08vm_state\x18\x02 \x01(\x0e\x32\x14.toolkami.v1.VmState\x12\x14\n\x0c\x66s_branch_id\x18\x03 \x01(\t\x12\x12\n\nfs_root_id\x18\x04 \x01(\t\"O\n\x0eRestoreRequest\x12\x13\n\x0bsnapshot_id\x18\x01 \x01(\t\x12(\n\x06mounts\x18\x02 \x03(\x0b\x32\x18.toolkami.v1.FolderMount\"d\n\x0fRestoreResponse\x12\x13\n\x0binstance_id\x18\x01 \x01(\t\x12&\n\x08vm_state\x18\x02 \x01(\x0e\x32\x14.toolkami.v1.VmState\x12\x14\n\x0c\x66s_branch_id\x18\x03 \x01(\t\"\xaf\x01\n\x0b\x45xecRequest\x12\x13\n\x0binstance_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63ommand_text\x18\x02 \x01(\t\x12\x19\n\x11\x63ommand_bytes_max\x18\x03 \x01(\r\x12\x17\n\ntimeout_ms\x18\x04 \x01(\rH\x00\x88\x01\x01\x12\x18\n\x10stdout_bytes_max\x18\x05 \x01(\r\x12\x18\n\x10stderr_bytes_max\x18\x06 \x01(\rB\r\n\x0b_timeout_ms\"^\n\x0c\x45xecResponse\x12\x11\n\texit_code\x18\x01 \x01(\x05\x12\x13\n\x0bstdout_text\x18\x02 \x01(\t\x12\x13\n\x0bstderr_text\x18\x03 \x01(\t\x12\x11\n\ttimed_out\x18\x04 \x01(\x08\"o\n\x0b\x46olderMount\x12\x18\n\x10source_host_path\x18\x01 \x01(\t\x12\x13\n\x0btarget_path\x18\x02 \x01(\t\x12\x31\n\x0b\x61\x63\x63\x65ss_mode\x18\x03 \x01(\x0e\x32\x1c.toolkami.v1.MountAccessMode*w\n\x0fMountAccessMode\x12!\n\x1dMOUNT_ACCESS_MODE_UNSPECIFIED\x10\x00\x12\x1f\n\x1bMOUNT_ACCESS_MODE_READ_ONLY\x10\x01\x12 \n\x1cMOUNT_ACCESS_MODE_READ_WRITE\x10\x02\x62\x06proto3"
11
11
 
12
12
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
13
  pool.add_serialized_file(descriptor_data)
@@ -20,5 +20,7 @@ module Toolkami
20
20
  RestoreResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.RestoreResponse").msgclass
21
21
  ExecRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.ExecRequest").msgclass
22
22
  ExecResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.ExecResponse").msgclass
23
+ FolderMount = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.FolderMount").msgclass
24
+ MountAccessMode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.MountAccessMode").enummodule
23
25
  end
24
26
  end
@@ -7,7 +7,7 @@ require 'google/protobuf'
7
7
  require 'toolkami/v1/common_pb'
8
8
 
9
9
 
10
- descriptor_data = "\n\x1atoolkami/v1/snapshot.proto\x12\x0btoolkami.v1\x1a\x18toolkami/v1/common.proto\"&\n\x0fSnapshotRequest\x12\x13\n\x0binstance_id\x18\x01 \x01(\t\"\'\n\x10SnapshotResponse\x12\x13\n\x0bsnapshot_id\x18\x01 \x01(\t\"\x9f\x01\n\tDiffEntry\x12\x11\n\tpath_text\x18\x01 \x01(\t\x12#\n\x04kind\x18\x02 \x01(\x0e\x32\x15.toolkami.v1.DiffKind\x12\x17\n\nsize_bytes\x18\x03 \x01(\x04H\x00\x88\x01\x01\x12\x1d\n\x10hash_sha256_text\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\r\n\x0b_size_bytesB\x13\n\x11_hash_sha256_text\"L\n\x0b\x44iffRequest\x12\x13\n\x0bsnapshot_id\x18\x01 \x01(\t\x12\x13\n\x0binstance_id\x18\x02 \x01(\t\x12\x13\n\x0b\x65ntries_max\x18\x03 \x01(\r\"J\n\x0c\x44iffResponse\x12\'\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x16.toolkami.v1.DiffEntry\x12\x11\n\ttruncated\x18\x02 \x01(\x08\x62\x06proto3"
10
+ descriptor_data = "\n\x1atoolkami/v1/snapshot.proto\x12\x0btoolkami.v1\x1a\x18toolkami/v1/common.proto\"&\n\x0fSnapshotRequest\x12\x13\n\x0binstance_id\x18\x01 \x01(\t\"q\n\x10SnapshotResponse\x12\x13\n\x0bsnapshot_id\x18\x01 \x01(\t\x12\x12\n\nfs_root_id\x18\x02 \x01(\t\x12\x1e\n\x11parent_fs_root_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x14\n\x12_parent_fs_root_id\"\x9f\x01\n\tDiffEntry\x12\x11\n\tpath_text\x18\x01 \x01(\t\x12#\n\x04kind\x18\x02 \x01(\x0e\x32\x15.toolkami.v1.DiffKind\x12\x17\n\nsize_bytes\x18\x03 \x01(\x04H\x00\x88\x01\x01\x12\x1d\n\x10hash_sha256_text\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\r\n\x0b_size_bytesB\x13\n\x11_hash_sha256_text\"L\n\x0b\x44iffRequest\x12\x13\n\x0bsnapshot_id\x18\x01 \x01(\t\x12\x13\n\x0binstance_id\x18\x02 \x01(\t\x12\x13\n\x0b\x65ntries_max\x18\x03 \x01(\r\"J\n\x0c\x44iffResponse\x12\'\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x16.toolkami.v1.DiffEntry\x12\x11\n\ttruncated\x18\x02 \x01(\x08\x62\x06proto3"
11
11
 
12
12
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
13
  pool.add_serialized_file(descriptor_data)
@@ -1,3 +1,3 @@
1
1
  module Toolkami
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toolkami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aperoc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-15 00:00:00.000000000 Z
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-protobuf