vaultkit 0.1.2 → 1.0.0

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: 78b1d315e88a305ca599630dcb679921c4b7feb9cf6126892a845da8afc05895
4
- data.tar.gz: 5cb8b0aacc91385d26a681a5ab9725f260166952cfe190de7bfa47353e1b2fd6
3
+ metadata.gz: 7ef641bfa848899716802a548282d99ceb00882f2ebace64e73cad6a465e3f39
4
+ data.tar.gz: b45579cba40972b3500af392d872c226eec3499fa884179fa87dbeb3a0b7711e
5
5
  SHA512:
6
- metadata.gz: f123babdc6fea12f1e7a3adaa08a89d1181f82c0b11dfd73171d3a82a84680c9f68e55f2e724a02a232f34c2b8bdeea31c31f8e4a49a045dcccebb710daf3ad4
7
- data.tar.gz: 8ddfea885a78988b1c0cd74d6735364f5aab63d7312a639154956dd6e928035f5128b22fe12ccf422ccb9e7d5e45cb251a521dc6ab285bc011abd29b6b2bddb7
6
+ metadata.gz: 3ac9dd5b9b100dcdbf1a141f25bf1e686efcf9376dd5182aa3827c0b1457b571e65ba8f5fabe6a17a8473d3e4909306d74ef6003421ddf07f5f77e692848cc7d
7
+ data.tar.gz: f98f55a48d58315877e824663a8c9d4499542f661b838dadf2b5dd534ec66583b43dc4db25d75a55be187aabaa7ae92b2f7abc87634da61ba143f44bfb85d23b
data/bin/vkit CHANGED
@@ -4,7 +4,7 @@ require_relative "../lib/vkit"
4
4
  require_relative "../lib/vkit/cli/errors"
5
5
 
6
6
  begin
7
- Vkit::CLI::BaseCLI.start(ARGV)
7
+ Vkit::CLI.start(ARGV)
8
8
 
9
9
  rescue Vkit::CLI::Error => e
10
10
  warn "❌ #{e.message}"
@@ -168,6 +168,49 @@ module Vkit
168
168
  )
169
169
  end
170
170
  }
171
+
172
+ desc "agents SUBCOMMAND ...ARGS", "Manage agents and automation identities"
173
+ subcommand "agents", Class.new(Thor) {
174
+
175
+ # agents tokens SUBCOMMAND
176
+ desc "tokens SUBCOMMAND ...ARGS", "Manage agent tokens"
177
+ subcommand "tokens", Class.new(Thor) {
178
+
179
+ # agents tokens list
180
+ desc "list", "List tokens for an agent"
181
+ option :format, type: :string, default: "table", enum: %w[table json]
182
+ def list
183
+ Commands::AgentTokensListCommand.new.call(
184
+ agent: options[:agent],
185
+ format: options[:format]
186
+ )
187
+ end
188
+
189
+ # agents tokens create
190
+ desc "create", "Create a new agent token (automation identity)"
191
+ option :name, required: true, desc: "Human-readable name (e.g. billing-bot)"
192
+ option :expires_in, type: :string, desc: "Token lifetime (e.g. 1h, 24h, 30d)"
193
+ option :role, type: :string, default: "agent", desc: "Role assigned to this token"
194
+ def create
195
+ Commands::AgentTokensCreateCommand.new.call(
196
+ name: options[:name],
197
+ expires_in: options[:expires_in],
198
+ role: options[:role]
199
+ )
200
+ end
201
+
202
+ # agents tokens revoke
203
+ desc "revoke", "Revoke an agent token"
204
+ option :token, required: true, desc: "Token ID or prefix"
205
+ option :force, type: :boolean, default: false, desc: "Skip confirmation"
206
+ def revoke
207
+ Commands::AgentTokensRevokeCommand.new.call(
208
+ token: options[:token],
209
+ force: options[:force]
210
+ )
211
+ end
212
+ }
213
+ }
171
214
  end
172
215
  end
173
216
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Vkit
7
+ module CLI
8
+ module Commands
9
+ class AgentTokensCreateCommand < BaseCommand
10
+ def call(options)
11
+ with_auth do
12
+ user = credential_store.user
13
+ org = user["organization_slug"]
14
+
15
+ response =
16
+ authenticated_client.post(
17
+ "/api/v1/orgs/#{org}/agent_tokens",
18
+ body: build_body(options)
19
+ )
20
+
21
+ token = response.fetch("token")
22
+ secret = response.fetch("secret")
23
+
24
+ puts "🔐 AGENT TOKEN ISSUED (shown once)"
25
+ puts
26
+
27
+ puts "🧠 Name: #{token["name"]}"
28
+ puts "🎭 Role: #{token["role"]}"
29
+ puts "🆔 Token ID: #{token["id"]}"
30
+ puts "🏷 Prefix: #{token["prefix"]}"
31
+ puts "⏳ Expires At: #{token["expires_at"] || "never"}"
32
+ puts
33
+
34
+ puts "🔑 TOKEN"
35
+ puts "────────────────────────────────────────"
36
+ puts secret
37
+ puts "────────────────────────────────────────"
38
+ puts
39
+
40
+ puts "⚠️ Store this token securely. It cannot be retrieved again."
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def build_body(options)
47
+ body = {
48
+ name: options.fetch(:name),
49
+ role: options[:role],
50
+ }
51
+
52
+ if options[:expires_in]
53
+ body[:expires_at] = parse_expires_in(options[:expires_in]).iso8601
54
+ end
55
+
56
+ body
57
+ end
58
+
59
+ def parse_expires_in(value)
60
+ number = value.to_i
61
+
62
+ case value
63
+ when /\A\d+h\z/
64
+ Time.now.utc + (number * 60 * 60)
65
+ when /\A\d+d\z/
66
+ Time.now.utc + (number * 24 * 60 * 60)
67
+ else
68
+ raise "Invalid expires_in format (use 1h, 24h, 30d)"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../core/table_formatter"
5
+
6
+ module Vkit
7
+ module CLI
8
+ module Commands
9
+ class AgentTokensListCommand < BaseCommand
10
+ def call(options)
11
+ format = options[:format] || "table"
12
+
13
+ with_auth do
14
+ user = credential_store.user
15
+ org = user["organization_slug"]
16
+
17
+ response =
18
+ authenticated_client.get(
19
+ "/api/v1/orgs/#{org}/agent_tokens",
20
+ params: build_query(options)
21
+ )
22
+
23
+ tokens = response["tokens"] || []
24
+
25
+ print_result(tokens, format)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def build_query(options)
32
+ {}.tap do |q|
33
+ q[:name] = options[:agent] if options[:agent]
34
+ end
35
+ end
36
+
37
+ def print_result(tokens, format)
38
+ if tokens.empty?
39
+ puts "No tokens found."
40
+ return
41
+ end
42
+
43
+ case format
44
+ when "json"
45
+ puts JSON.pretty_generate(tokens)
46
+
47
+ when "table"
48
+ Vkit::Core::TableFormatter.render(
49
+ tokens.map do |t|
50
+ {
51
+ "ID" => t["id"],
52
+ "Agent" => t["name"],
53
+ "Prefix" => t["prefix"],
54
+ "Expires At" => t["expires_at"] || "never",
55
+ "Revoked At" => t["revoked_at"] || "-",
56
+ "Created At" => t["created_at"]
57
+ }
58
+ end
59
+ )
60
+
61
+ else
62
+ raise "Unknown format: #{format}"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Vkit
6
+ module CLI
7
+ module Commands
8
+ class AgentTokensRevokeCommand < BaseCommand
9
+ def call(options)
10
+ token_ref = options.fetch(:token)
11
+
12
+ with_auth do
13
+ user = credential_store.user
14
+ org = user["organization_slug"]
15
+
16
+ token = resolve_token!(org, token_ref)
17
+
18
+ unless options[:force]
19
+ confirm!(token)
20
+ end
21
+
22
+ authenticated_client.post(
23
+ "/api/v1/orgs/#{org}/agent_tokens/#{token["id"]}/revoke",
24
+ body: {}
25
+ )
26
+
27
+ puts "🛑 AGENT TOKEN REVOKED"
28
+ puts
29
+ puts "🧠 Name: #{token["name"]}"
30
+ puts "🏷 Prefix: #{token["prefix"]}"
31
+ puts "🆔 ID: #{token["id"]}"
32
+ puts
33
+ puts "✅ The token is no longer valid."
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def resolve_token!(org, token_ref)
40
+ response =
41
+ authenticated_client.get(
42
+ "/api/v1/orgs/#{org}/agent_tokens"
43
+ )
44
+
45
+ tokens = response["tokens"] || []
46
+
47
+ token =
48
+ tokens.find do |t|
49
+ t["id"] == token_ref || t["prefix"] == token_ref
50
+ end
51
+
52
+ raise "Agent token not found: #{token_ref}" unless token
53
+
54
+ token
55
+ end
56
+
57
+ def confirm!(token)
58
+ puts "⚠️ You are about to revoke this agent token:"
59
+ puts
60
+ puts "🧠 Name: #{token["name"]}"
61
+ puts "🏷 Prefix: #{token["prefix"]}"
62
+ puts "🆔 ID: #{token["id"]}"
63
+ puts
64
+ print "\nType 'revoke' to confirm: "
65
+
66
+ input = STDIN.gets&.strip
67
+ abort "Aborted." unless input == "revoke"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -16,8 +16,8 @@ module Vkit
16
16
  body: {}
17
17
  )
18
18
 
19
- rows = response["rows"] || []
20
- meta = response["meta"] || {}
19
+ rows = response.dig("rows", "rows") || []
20
+ meta = response.dig("rows", "meta") || {}
21
21
 
22
22
  print_result(rows, meta, format)
23
23
  end
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "fileutils"
5
5
  require_relative "../../policy/bundle_compiler"
6
+ require_relative "../../policy/validation_error"
6
7
 
7
8
  module Vkit
8
9
  module CLI
@@ -58,6 +59,9 @@ module Vkit
58
59
  puts " Checksum: #{bundle.dig("bundle", "checksum")}"
59
60
  puts " Output: #{out}"
60
61
  end
62
+ rescue Vkit::Policy::ValidationError => e
63
+ puts e.message
64
+ exit 1
61
65
  end
62
66
 
63
67
  private
data/lib/vkit/cli.rb CHANGED
@@ -1,4 +1,18 @@
1
1
  require "thor"
2
+ require_relative "version"
2
3
 
3
4
  require_relative "cli/commands"
4
5
  require_relative "cli/base_cli"
6
+
7
+ module Vkit
8
+ module CLI
9
+ def self.start(argv = ARGV)
10
+ if argv.include?("--version") || argv.include?("-v")
11
+ puts "vkit #{Vkit::VERSION}"
12
+ exit 0
13
+ end
14
+
15
+ BaseCLI.start(argv)
16
+ end
17
+ end
18
+ end
@@ -2,6 +2,7 @@ require "json"
2
2
  require "yaml"
3
3
  require "digest"
4
4
  require "time"
5
+ require_relative "policy_validator"
5
6
 
6
7
  module Vkit
7
8
  module Policy
@@ -40,6 +41,9 @@ module Vkit
40
41
  files.map do |f|
41
42
  data = YAML.load_file(f)
42
43
  raise "Policy file #{f} must be a Hash" unless data.is_a?(Hash)
44
+
45
+ PolicyValidator.validate!(data, file: File.basename(f))
46
+
43
47
  data.merge("__file" => File.basename(f))
44
48
  end
45
49
  end
@@ -0,0 +1,129 @@
1
+ module Vkit
2
+ module Policy
3
+ class PolicyValidator
4
+ ACTION_KEYS = %w[deny require_approval mask allow].freeze
5
+
6
+ def self.validate!(policy, file: nil)
7
+ prefix = file ? "In #{file}:\n\n" : ""
8
+
9
+ require_string!(policy, "id", prefix)
10
+ require_string!(policy, "description", prefix)
11
+
12
+ action = policy["action"]
13
+ unless action.is_a?(Hash)
14
+ raise ValidationError, <<~MSG
15
+ #{prefix}Invalid action format.
16
+
17
+ Expected:
18
+
19
+ action:
20
+ deny: true
21
+
22
+ Got:
23
+
24
+ action: #{action.inspect}
25
+
26
+ VaultKit requires `action` to be a mapping so it can attach
27
+ metadata like reason, ttl, approvals, and masking rules.
28
+ MSG
29
+ end
30
+
31
+ validate_action!(action, prefix)
32
+ true
33
+ end
34
+
35
+ def self.require_string!(hash, key, prefix)
36
+ value = hash[key]
37
+ return if value.is_a?(String) && !value.strip.empty?
38
+
39
+ raise ValidationError, <<~MSG
40
+ #{prefix}#{key} is required and must be a non-empty string.
41
+ MSG
42
+ end
43
+
44
+ def self.validate_action!(action, prefix)
45
+ intents = ACTION_KEYS.select { |k| action[k] == true }
46
+
47
+ if intents.empty?
48
+ raise ValidationError, <<~MSG
49
+ #{prefix}Action must specify exactly one intent.
50
+
51
+ Choose one of:
52
+
53
+ - deny
54
+ - require_approval
55
+ - mask
56
+ - allow
57
+
58
+ Example:
59
+
60
+ action:
61
+ deny: true
62
+ MSG
63
+ end
64
+
65
+ if intents.size > 1
66
+ raise ValidationError, <<~MSG
67
+ #{prefix}Action specifies multiple intents: #{intents.join(', ')}.
68
+
69
+ Only one intent may be set to true.
70
+ MSG
71
+ end
72
+
73
+ intent = intents.first
74
+
75
+ case intent
76
+ when "deny"
77
+ require_reason!(action, prefix)
78
+
79
+ when "require_approval"
80
+ require_reason!(action, prefix)
81
+ require_string!(action, "approver_role", prefix)
82
+
83
+ when "mask"
84
+ # masking config can be validated later
85
+
86
+ when "allow"
87
+ # nothing required
88
+ end
89
+
90
+ validate_ttl!(action["ttl"], prefix) if action.key?("ttl")
91
+ end
92
+
93
+ def self.require_reason!(action, prefix)
94
+ reason = action["reason"]
95
+ return if reason.is_a?(String) && !reason.strip.empty?
96
+
97
+ raise ValidationError, <<~MSG
98
+ #{prefix}action.reason is required and must be a non-empty string
99
+ when using deny or require_approval.
100
+ MSG
101
+ end
102
+
103
+ def self.validate_ttl!(ttl, prefix)
104
+ case ttl
105
+ when Integer
106
+ return
107
+ when String
108
+ return if ttl.match?(/\A\d+[smhd]\z/)
109
+
110
+ raise ValidationError, <<~MSG
111
+ #{prefix}Invalid ttl format.
112
+
113
+ Expected examples:
114
+ - "30m"
115
+ - "1h"
116
+ - "2d"
117
+
118
+ Got:
119
+ ttl: #{ttl.inspect}
120
+ MSG
121
+ else
122
+ raise ValidationError, <<~MSG
123
+ #{prefix}ttl must be a string duration (e.g. "1h") or an integer (seconds).
124
+ MSG
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,5 @@
1
+ module Vkit
2
+ module Policy
3
+ class ValidationError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vkit
4
+ VERSION = "1.0.0"
5
+ end
data/lib/vkit.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  module Vkit; end
2
2
 
3
+ require_relative "vkit/version.rb"
3
4
  require_relative "vkit/cli.rb"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vaultkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nnamdi Ogundu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-10 00:00:00.000000000 Z
11
+ date: 2026-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -40,6 +40,9 @@ files:
40
40
  - lib/vkit/cli/api/client.rb
41
41
  - lib/vkit/cli/base_cli.rb
42
42
  - lib/vkit/cli/commands.rb
43
+ - lib/vkit/cli/commands/agent_tokens_create_command.rb
44
+ - lib/vkit/cli/commands/agent_tokens_list_command.rb
45
+ - lib/vkit/cli/commands/agent_tokens_revoke_command.rb
43
46
  - lib/vkit/cli/commands/approval_command.rb
44
47
  - lib/vkit/cli/commands/base_command.rb
45
48
  - lib/vkit/cli/commands/datasource_command.rb
@@ -61,11 +64,14 @@ files:
61
64
  - lib/vkit/core/credential_store.rb
62
65
  - lib/vkit/core/table_formatter.rb
63
66
  - lib/vkit/policy/bundle_compiler.rb
67
+ - lib/vkit/policy/policy_validator.rb
64
68
  - lib/vkit/policy/schema/policy_bundle.schema.json
65
69
  - lib/vkit/policy/validate_bundle.rb
70
+ - lib/vkit/policy/validation_error.rb
66
71
  - lib/vkit/utils/banner.rb
67
72
  - lib/vkit/utils/config_loader.rb
68
73
  - lib/vkit/utils/logger.rb
74
+ - lib/vkit/version.rb
69
75
  homepage: https://vaultkit.io
70
76
  licenses:
71
77
  - Nonstandard