rails-agent-stack 0.1.1

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.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/README.md +16 -0
  3. data/.github/workflows/ci.yml +32 -0
  4. data/.gitignore +8 -0
  5. data/.rspec +3 -0
  6. data/AGENTS.md +18 -0
  7. data/CHANGELOG.md +40 -0
  8. data/Gemfile +15 -0
  9. data/MIT-LICENSE +21 -0
  10. data/README.md +228 -0
  11. data/Rakefile +8 -0
  12. data/app/assets/images/rails_agents/kip-logo.png +0 -0
  13. data/app/assets/images/rails_agents/kip-waving.png +0 -0
  14. data/app/assets/stylesheets/rails_agents/application.css +411 -0
  15. data/app/controllers/rails_agents/application_controller.rb +25 -0
  16. data/app/controllers/rails_agents/connect_controller.rb +36 -0
  17. data/app/controllers/rails_agents/dashboard_controller.rb +70 -0
  18. data/app/controllers/rails_agents/handshake_controller.rb +55 -0
  19. data/app/controllers/rails_agents/open_wire_inbound_controller.rb +153 -0
  20. data/app/controllers/rails_agents/pulls_controller.rb +42 -0
  21. data/app/controllers/rails_agents/registrations_controller.rb +96 -0
  22. data/app/controllers/rails_agents/schema_controller.rb +123 -0
  23. data/app/controllers/rails_agents/sessions_controller.rb +76 -0
  24. data/app/controllers/rails_agents/tool_bridge_controller.rb +67 -0
  25. data/app/views/layouts/rails_agents/application.html.erb +18 -0
  26. data/app/views/rails_agents/dashboard/show.html.erb +139 -0
  27. data/app/views/rails_agents/registrations/new.html.erb +83 -0
  28. data/app/views/rails_agents/sessions/new.html.erb +73 -0
  29. data/app/views/rails_agents/shared/_auth_brand.html.erb +20 -0
  30. data/config/routes.rb +34 -0
  31. data/docs/AGENTS.md +220 -0
  32. data/docs/PRD.md +89 -0
  33. data/docs/assets/hero-banner.png +0 -0
  34. data/examples/support/agent.rb +14 -0
  35. data/examples/support/channels/.keep +0 -0
  36. data/examples/support/evals/smoke.yml +4 -0
  37. data/examples/support/knowledge/shipping.md +1 -0
  38. data/examples/support/memory.rb +3 -0
  39. data/examples/support/prompt.md +9 -0
  40. data/examples/support/skills/.keep +0 -0
  41. data/examples/support/tools/.keep +0 -0
  42. data/exe/rails-agents +6 -0
  43. data/lib/generators/rails_agents/agent/agent_generator.rb +80 -0
  44. data/lib/generators/rails_agents/agent/templates/agent.rb.tt +14 -0
  45. data/lib/generators/rails_agents/agent/templates/agent_database.rb.tt +42 -0
  46. data/lib/generators/rails_agents/agent/templates/channels/slack.rb.tt +8 -0
  47. data/lib/generators/rails_agents/agent/templates/evals/smoke.yml.tt +4 -0
  48. data/lib/generators/rails_agents/agent/templates/imports.yml.tt +9 -0
  49. data/lib/generators/rails_agents/agent/templates/memory.rb.tt +6 -0
  50. data/lib/generators/rails_agents/agent/templates/prompt.md.tt +10 -0
  51. data/lib/generators/rails_agents/agent/templates/prompt_database.md.tt +14 -0
  52. data/lib/generators/rails_agents/install/INSTALL +10 -0
  53. data/lib/generators/rails_agents/install/install_generator.rb +72 -0
  54. data/lib/generators/rails_agents/install/templates/AGENTS.md.tt +46 -0
  55. data/lib/generators/rails_agents/install/templates/initializer.rb.tt +8 -0
  56. data/lib/generators/rails_agents/install/templates/library_README.md.tt +28 -0
  57. data/lib/generators/rails_agents/install/templates/library_manifest.yml.tt +5 -0
  58. data/lib/generators/rails_agents/install/templates/rails_agents_autoload.rb.tt +10 -0
  59. data/lib/rails-agent-stack.rb +4 -0
  60. data/lib/rails_agents/background_agent.rb +15 -0
  61. data/lib/rails_agents/base.rb +294 -0
  62. data/lib/rails_agents/chat_agent.rb +15 -0
  63. data/lib/rails_agents/cli.rb +245 -0
  64. data/lib/rails_agents/client.rb +216 -0
  65. data/lib/rails_agents/configuration.rb +41 -0
  66. data/lib/rails_agents/creation_agent.rb +10 -0
  67. data/lib/rails_agents/credentials_writer.rb +38 -0
  68. data/lib/rails_agents/engine.rb +19 -0
  69. data/lib/rails_agents/knowledge_agent.rb +10 -0
  70. data/lib/rails_agents/library_imports.rb +163 -0
  71. data/lib/rails_agents/local_auth.rb +56 -0
  72. data/lib/rails_agents/local_sync.rb +113 -0
  73. data/lib/rails_agents/monitoring_agent.rb +10 -0
  74. data/lib/rails_agents/open_wire_adapter.rb +31 -0
  75. data/lib/rails_agents/operations_agent.rb +10 -0
  76. data/lib/rails_agents/railtie.rb +14 -0
  77. data/lib/rails_agents/tasks.rake +8 -0
  78. data/lib/rails_agents/version.rb +5 -0
  79. data/lib/rails_agents/workflow_agent.rb +11 -0
  80. data/lib/rails_agents.rb +57 -0
  81. data/rails-agent-stack.gemspec +49 -0
  82. data/rails-agents.code-workspace +25 -0
  83. data/spec/generators/agent_generator_spec.rb +36 -0
  84. data/spec/rails_agents/base_spec.rb +202 -0
  85. data/spec/rails_agents/client_spec.rb +121 -0
  86. data/spec/rails_agents/configuration_spec.rb +55 -0
  87. data/spec/rails_agents/library_imports_spec.rb +80 -0
  88. data/spec/rails_agents/local_sync_spec.rb +63 -0
  89. data/spec/rails_agents/taxonomy_spec.rb +52 -0
  90. data/spec/spec_helper.rb +27 -0
  91. metadata +229 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open_wire"
4
+
5
+ module RailsAgents
6
+ # Open-Wire channel adapter — thin wrapper so agents don't talk HTTP directly.
7
+ module OpenWireAdapter
8
+ module_function
9
+
10
+ def client(base_url: nil, api_key: nil)
11
+ ::OpenWire::Client.new(base_url: base_url, api_key: api_key)
12
+ end
13
+
14
+ def verify_inbound!(secret:, body:, headers:)
15
+ ::OpenWire::Webhook.verify_and_parse!(
16
+ secret: secret,
17
+ body: body,
18
+ headers: headers
19
+ )
20
+ end
21
+
22
+ def reply!(installation_id:, to:, text:, thread_id: nil, client: nil)
23
+ (client || self.client).send_message(
24
+ installation_id: installation_id,
25
+ to: to,
26
+ text: text,
27
+ thread_id: thread_id
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ # Coordinates unpredictable real-world work across people, tools, and changing conditions.
5
+ class OperationsAgent < Base
6
+ def self.agent_kind
7
+ :operations
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load "rails_agents/tasks.rake"
7
+ end
8
+
9
+ generators do
10
+ require "generators/rails_agents/install/install_generator"
11
+ require "generators/rails_agents/agent/agent_generator"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rails_agents do
4
+ desc "Install Rails Agents (alias for rails g rails_agents:install)"
5
+ task install: :environment do
6
+ sh "bin/rails generate rails_agents:install"
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ # Multi-step orchestration agents (onboarding, lead qualification, pipelines).
5
+ # Prefer a stable +session_id+ so each step continues the same run context.
6
+ class WorkflowAgent < Base
7
+ def self.agent_kind
8
+ :workflow
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_agents/version"
4
+ require "rails_agents/configuration"
5
+ require "rails_agents/client"
6
+ require "rails_agents/credentials_writer"
7
+ require "rails_agents/local_auth"
8
+ require "rails_agents/local_sync"
9
+ require "rails_agents/library_imports"
10
+ require "rails_agents/base"
11
+ require "rails_agents/knowledge_agent"
12
+ require "rails_agents/creation_agent"
13
+ require "rails_agents/workflow_agent"
14
+ require "rails_agents/operations_agent"
15
+ require "rails_agents/monitoring_agent"
16
+ require "rails_agents/chat_agent"
17
+ require "rails_agents/background_agent"
18
+ require "rails_agents/cli"
19
+
20
+ begin
21
+ require "open_wire"
22
+ require "rails_agents/open_wire_adapter"
23
+ rescue LoadError
24
+ # open-wire gem optional until path/RubyGems dependency is installed
25
+ end
26
+
27
+ if defined?(Rails::Engine)
28
+ require "rails_agents/engine"
29
+ end
30
+
31
+ if defined?(Rails::Railtie)
32
+ require "rails_agents/railtie"
33
+ end
34
+
35
+ module RailsAgents
36
+ TAXONOMY_TYPES = %i[knowledge workflow operations monitoring].freeze
37
+ LEGACY_TAXONOMY_TYPES = %i[creation].freeze
38
+
39
+ class << self
40
+ attr_writer :config
41
+
42
+ def config
43
+ @config ||= Configuration.new
44
+ end
45
+
46
+ def configure
47
+ yield config
48
+ end
49
+
50
+ def reset!
51
+ @config = Configuration.new
52
+ end
53
+ end
54
+ end
55
+
56
+ # PRD uses RailsAgent - alias to RailsAgents for compatibility
57
+ RailsAgent = RailsAgents unless defined?(RailsAgent)
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rails_agents/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rails-agent-stack"
7
+ spec.version = RailsAgents::VERSION
8
+ spec.authors = ["Rails Agent Team"]
9
+ spec.email = ["hello@railsagents.dev"]
10
+
11
+ spec.summary = "Rails AI agents framework — fullstack Ruby on Rails agents " \
12
+ "(ActiveAgent / RubyLLM alternative)."
13
+ spec.description = "Rails Agent is the most advanced agentic platform for Ruby on Rails. " \
14
+ "Mount /agents, scaffold agent directories, attach BYOK model credentials, " \
15
+ "and build with instructions, tools, connectors, channels, skills, packages, " \
16
+ "knowledge, memory, and guardrails. Sync, run, deploy, and monitor on " \
17
+ "Rails Agent Cloud — a production Rails agent framework and better " \
18
+ "alternative to RubyLLM and ActiveAgent for fullstack Rails AI agents."
19
+ spec.homepage = "https://rails-agent.com"
20
+ spec.license = "MIT"
21
+ spec.required_ruby_version = ">= 3.2.0"
22
+
23
+ spec.metadata["homepage_uri"] = spec.homepage
24
+ spec.metadata["documentation_uri"] = "https://rails-agent.com/docs/getting-started"
25
+ spec.metadata["source_code_uri"] = "https://github.com/Tiny-Bubble-Company/rails-agents"
26
+ spec.metadata["bug_tracker_uri"] = "https://github.com/Tiny-Bubble-Company/rails-agents/issues"
27
+ spec.metadata["changelog_uri"] = "https://github.com/Tiny-Bubble-Company/rails-agents/releases"
28
+ spec.metadata["rubygems_mimetype"] = "application/x-rubygem"
29
+
30
+ spec.files = Dir.chdir(__dir__) do
31
+ `git ls-files -z`.split("\x0").reject do |f|
32
+ f == "Gemfile.lock" || f.start_with?("spec/fixtures/")
33
+ end
34
+ rescue StandardError
35
+ Dir["{app,config,exe,lib,docs}/**/*", "exe/*", "*.{md,gemspec,rake}", "MIT-LICENSE", "AGENTS.md"]
36
+ end
37
+
38
+ spec.bindir = "exe"
39
+ spec.executables = ["rails-agents"]
40
+ spec.require_paths = ["lib"]
41
+
42
+ spec.add_dependency "railties", ">= 7.0"
43
+ spec.add_dependency "thor", "~> 1.3"
44
+ spec.add_dependency "open-wire", "~> 0.1"
45
+
46
+ spec.add_development_dependency "rake", "~> 13.0"
47
+ spec.add_development_dependency "rspec", "~> 3.13"
48
+ spec.add_development_dependency "webmock", "~> 3.23"
49
+ end
@@ -0,0 +1,25 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": ".",
5
+ "name": "rails-agents"
6
+ },
7
+ {
8
+ "path": "../rails-agents-cloud",
9
+ "name": "rails-agents-cloud"
10
+ },
11
+ {
12
+ "path": "../rails-agents-boilerplate"
13
+ },
14
+ {
15
+ "path": "../rails-agents-ops"
16
+ },
17
+ {
18
+ "path": "../rails-agents-design"
19
+ },
20
+ {
21
+ "path": "../open-wire",
22
+ "name": "open-wire"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Agent generator templates" do
6
+ templates_root = File.expand_path("../../lib/generators/rails_agents/agent/templates", __dir__)
7
+
8
+ it "default agent template uses BYOK model DSL and taxonomy base class" do
9
+ content = File.read(File.join(templates_root, "agent.rb.tt"))
10
+ expect(content).to include("<%= agent_base_class %>")
11
+ expect(content).to include("credential: :company_openai")
12
+ expect(content).not_to include("model :auto")
13
+ end
14
+
15
+ it "database template scaffolds KnowledgeAgent with order and product tools" do
16
+ content = File.read(File.join(templates_root, "agent_database.rb.tt"))
17
+ expect(content).to include("KnowledgeAgent")
18
+ expect(content).to include("lookup_order")
19
+ expect(content).to include("search_products")
20
+ expect(content).to include("Order.find_by")
21
+ expect(content).to include("Product.where")
22
+ end
23
+
24
+ it "database prompt documents demo orders and BYOK" do
25
+ content = File.read(File.join(templates_root, "prompt_database.md.tt"))
26
+ expect(content).to include("ORD-DEMO-1001")
27
+ expect(content).to include("BYOK")
28
+ end
29
+
30
+ it "scaffolds an explicit workspace Library imports manifest" do
31
+ content = File.read(File.join(templates_root, "imports.yml.tt"))
32
+
33
+ expect(content).to include("imports: []")
34
+ expect(content).to include("library/skills/triage.rb")
35
+ end
36
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "fileutils"
5
+ require "pathname"
6
+ require "tmpdir"
7
+
8
+ RSpec.describe RailsAgents::Base do
9
+ let(:agent_dir) { Pathname.new(Dir.mktmpdir("agent")) }
10
+
11
+ after do
12
+ FileUtils.rm_rf(agent_dir)
13
+ end
14
+
15
+ let(:support_agent) do
16
+ Class.new(described_class) do
17
+ model :gpt_5_mini, provider: :openai, credential: :company_openai
18
+ memory :conversation, provider: :mem0, recall: 5
19
+ knowledge_from "knowledge/**/*"
20
+
21
+ tool :lookup_order do |order_id:|
22
+ { id: order_id, status: "shipped" }
23
+ end
24
+
25
+ skill :triage, from: "skills/triage.rb"
26
+ channel :slack
27
+ end.tap do |klass|
28
+ klass.instance_variable_set(:@agent_directory, agent_dir)
29
+ end
30
+ end
31
+
32
+ let(:client) { instance_double(RailsAgents::Client) }
33
+
34
+ before do
35
+ FileUtils.mkdir_p(agent_dir.join("knowledge"))
36
+ File.write(agent_dir.join("knowledge/faq.md"), "# FAQ")
37
+ end
38
+
39
+ it "stores DSL configuration on the class" do
40
+ expect(support_agent.model_setting).to eq(:gpt_5_mini)
41
+ expect(support_agent.model_provider).to eq(:openai)
42
+ expect(support_agent.model_credential).to eq(:company_openai)
43
+ expect(support_agent.memory_setting).to eq(:conversation)
44
+ expect(support_agent.memory_provider).to eq(:mem0)
45
+ expect(support_agent.memory_recall).to eq(5)
46
+ expect(support_agent.knowledge_glob).to eq("knowledge/**/*")
47
+ expect(support_agent.tool_definitions.keys).to include(:lookup_order)
48
+ expect(support_agent.skill_definitions[:triage]).to eq("skills/triage.rb")
49
+ expect(support_agent.channel_definitions).to include(:slack)
50
+ end
51
+
52
+ it "supports legacy model :auto without provider metadata" do
53
+ legacy = Class.new(described_class) { model :auto }
54
+ instance = legacy.new(client: client)
55
+ allow(client).to receive(:create_run).and_return({ "id" => "run_1" })
56
+
57
+ legacy.new(client: client).run("Hi")
58
+
59
+ expect(client).to have_received(:create_run).with(
60
+ hash_including(metadata: hash_including(model: :auto, kind: :base))
61
+ )
62
+ end
63
+
64
+ it "supports keyword trigger inputs like order_id:" do
65
+ allow(client).to receive(:create_run).and_return({ "id" => "run_kw" })
66
+
67
+ support_agent.run(order_id: 1, client: client)
68
+
69
+ expect(client).to have_received(:create_run).with(
70
+ hash_including(message: "order_id: 1")
71
+ )
72
+ end
73
+
74
+ it "rejects mixing a message string with keyword inputs" do
75
+ expect {
76
+ support_agent.run("hello", order_id: 1, client: client)
77
+ }.to raise_error(ArgumentError, /either a message string or keyword/)
78
+ end
79
+
80
+ it "executes tool blocks" do
81
+ result = support_agent.tool_definitions[:lookup_order].call(order_id: 42)
82
+ expect(result[:status]).to eq("shipped")
83
+ end
84
+
85
+ it "collects knowledge paths relative to agent directory" do
86
+ agent = support_agent.new(client: client)
87
+ expect(agent.knowledge_paths).to eq(["knowledge/faq.md"])
88
+ end
89
+
90
+ it "includes kind and structured model in run metadata" do
91
+ allow(client).to receive(:create_run).and_return({ "id" => "run_abc" })
92
+
93
+ support_agent.new(client: client).run("Hello", session_id: "sess_1")
94
+
95
+ expect(client).to have_received(:create_run).with(
96
+ hash_including(
97
+ agent: agent_dir.basename.to_s,
98
+ message: "Hello",
99
+ session_id: "sess_1",
100
+ metadata: hash_including(
101
+ kind: :base,
102
+ model: {
103
+ name: :gpt_5_mini,
104
+ provider: :openai,
105
+ credential: :company_openai
106
+ },
107
+ memory: {
108
+ type: :conversation,
109
+ provider: :mem0,
110
+ recall: 5
111
+ }
112
+ )
113
+ )
114
+ )
115
+ end
116
+
117
+ it "creates a run via the cloud client" do
118
+ allow(client).to receive(:create_run).and_return({ "id" => "run_abc" })
119
+
120
+ result = support_agent.new(client: client).run("Hello", session_id: "sess_1")
121
+
122
+ expect(result.run_id).to eq("run_abc")
123
+ end
124
+
125
+ it "deploys via the cloud client" do
126
+ allow(client).to receive(:deploy).and_return({ "id" => "dep_1" })
127
+
128
+ response = support_agent.new(client: client).deploy
129
+
130
+ expect(client).to have_received(:deploy).with(
131
+ agent: agent_dir.basename.to_s,
132
+ bundle_path: agent_dir.to_s
133
+ )
134
+ expect(response["id"]).to eq("dep_1")
135
+ end
136
+
137
+ it "syncs knowledge via the cloud client" do
138
+ allow(client).to receive(:sync_knowledge).and_return({ "synced" => 1 })
139
+
140
+ support_agent.new(client: client).sync_knowledge
141
+
142
+ expect(client).to have_received(:sync_knowledge).with(
143
+ agent: agent_dir.basename.to_s,
144
+ paths: ["knowledge/faq.md"]
145
+ )
146
+ end
147
+
148
+ it "streams run output" do
149
+ allow(client).to receive(:create_run).and_return({ "id" => "run_1" })
150
+ allow(client).to receive(:stream_run).and_yield({ "content" => "Hi" }).and_yield({ "content" => " there" })
151
+
152
+ result = support_agent.run("Hello", client: client)
153
+ expect(result.output).to eq("Hi there")
154
+ end
155
+
156
+ it "prefers done output_text over token chunks when draining stream" do
157
+ allow(client).to receive(:create_run).and_return({ "id" => "run_3" })
158
+ allow(client).to receive(:stream_run)
159
+ .and_yield({ "content" => "partial" })
160
+ .and_yield({
161
+ "output_text" => "Final markdown",
162
+ "output" => "Final markdown",
163
+ "format" => "markdown",
164
+ "items" => [],
165
+ "status" => "succeeded"
166
+ })
167
+
168
+ result = support_agent.run("Hello", client: client)
169
+ expect(result.output).to eq("Final markdown")
170
+ end
171
+
172
+ it "exposes Eve-aligned output_text / items from sync create_run" do
173
+ allow(client).to receive(:create_run).and_return({
174
+ "object" => "run",
175
+ "data" => {
176
+ "id" => "run_2",
177
+ "output" => "| Name | Qty |\n| --- | --- |\n| Tee | 2 |",
178
+ "output_text" => "| Name | Qty |\n| --- | --- |\n| Tee | 2 |",
179
+ "output_data" => nil,
180
+ "items" => [
181
+ {
182
+ "type" => "message",
183
+ "role" => "assistant",
184
+ "content" => "| Name | Qty |\n| --- | --- |\n| Tee | 2 |",
185
+ "format" => "markdown"
186
+ }
187
+ ],
188
+ "format" => "markdown",
189
+ "status" => "succeeded"
190
+ }
191
+ })
192
+
193
+ result = support_agent.run("List products", client: client)
194
+ expect(result.run_id).to eq("run_2")
195
+ expect(result.output_text).to include("Tee")
196
+ expect(result.output).to eq(result.output_text)
197
+ expect(result.format).to eq("markdown")
198
+ expect(result.items.first["type"]).to eq("message")
199
+ expect(result.output_data).to be_nil
200
+ expect(result.status).to eq("succeeded")
201
+ end
202
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe RailsAgents::Client do
6
+ let(:config) do
7
+ RailsAgents.config.tap do |c|
8
+ c.api_key = "test-key"
9
+ c.api_base = "https://cloud.rails-agent.com"
10
+ c.project_id = "proj_123"
11
+ end
12
+ end
13
+
14
+ subject(:client) { described_class.new(config: config) }
15
+
16
+ describe "#create_run" do
17
+ it "POSTs to /api/v1/runs with auth" do
18
+ stub_request(:post, "https://cloud.rails-agent.com/api/v1/runs")
19
+ .with(
20
+ headers: { "Authorization" => "Bearer test-key" },
21
+ body: hash_including("agent" => "support", "message" => "Hello")
22
+ )
23
+ .to_return(status: 200, body: { id: "run_1" }.to_json, headers: { "Content-Type" => "application/json" })
24
+
25
+ response = client.create_run(agent: "support", message: "Hello")
26
+ expect(response["id"]).to eq("run_1")
27
+ end
28
+
29
+ it "extracts nested API error messages" do
30
+ stub_request(:post, "https://cloud.rails-agent.com/api/v1/runs")
31
+ .to_return(
32
+ status: 401,
33
+ body: { error: { message: "Invalid API key" } }.to_json,
34
+ headers: { "Content-Type" => "application/json" }
35
+ )
36
+
37
+ expect do
38
+ client.create_run(agent: "support", message: "Hello")
39
+ end.to raise_error(RailsAgents::Client::Error, "Invalid API key")
40
+ end
41
+ end
42
+
43
+ describe "#deploy" do
44
+ it "POSTs to /api/v1/deploys" do
45
+ stub_request(:post, "https://cloud.rails-agent.com/api/v1/deploys")
46
+ .to_return(status: 200, body: { id: "dep_1", status: "queued" }.to_json)
47
+
48
+ response = client.deploy(agent: "support")
49
+ expect(response["status"]).to eq("queued")
50
+ end
51
+ end
52
+
53
+ describe "#install_channel" do
54
+ it "POSTs to /api/v1/channels/:kind/install" do
55
+ stub_request(:post, "https://cloud.rails-agent.com/api/v1/channels/slack/install")
56
+ .to_return(status: 200, body: { url: "https://oauth.example" }.to_json)
57
+
58
+ response = client.install_channel(kind: "slack")
59
+ expect(response["url"]).to include("oauth")
60
+ end
61
+ end
62
+
63
+ describe "#sync_knowledge" do
64
+ it "POSTs to /api/v1/knowledge/sync" do
65
+ stub_request(:post, "https://cloud.rails-agent.com/api/v1/knowledge/sync")
66
+ .to_return(status: 200, body: { synced: 2 }.to_json)
67
+
68
+ response = client.sync_knowledge(agent: "support", paths: %w[faq.md])
69
+ expect(response["synced"]).to eq(2)
70
+ end
71
+ end
72
+
73
+ describe "#sync_files" do
74
+ it "PUTs to /api/v1/agents/:id/files" do
75
+ stub_request(:put, "https://cloud.rails-agent.com/api/v1/agents/support/files")
76
+ .with(body: hash_including("files" => kind_of(Array)))
77
+ .to_return(status: 200, body: { data: [] }.to_json)
78
+
79
+ response = client.sync_files(agent: "support", files: [{ "path" => "app/agents/support/agent.rb", "content" => "class Support; end" }])
80
+ expect(response["data"]).to eq([])
81
+ end
82
+ end
83
+
84
+ describe "#handshake" do
85
+ it "POSTs to /api/v1/auth/handshake without bearer" do
86
+ stub_request(:post, "https://cloud.rails-agent.com/api/v1/auth/handshake")
87
+ .with { |req| !req.headers["Authorization"] }
88
+ .to_return(status: 201, body: { data: { api_key: "ra_x" } }.to_json)
89
+
90
+ response = client.handshake(email: "a@b.com", password: "password1")
91
+ expect(response.dig("data", "api_key")).to eq("ra_x")
92
+ end
93
+ end
94
+
95
+ describe "#logs" do
96
+ it "GETs /api/v1/logs" do
97
+ stub_request(:get, %r{https://cloud.rails-agent.com/api/v1/logs})
98
+ .to_return(status: 200, body: { entries: [] }.to_json)
99
+
100
+ expect(client.logs(agent: "support")["entries"]).to eq([])
101
+ end
102
+ end
103
+
104
+ describe "#traces" do
105
+ it "GETs /api/v1/traces" do
106
+ stub_request(:get, %r{https://cloud.rails-agent.com/api/v1/traces})
107
+ .to_return(status: 200, body: { traces: [] }.to_json)
108
+
109
+ expect(client.traces["traces"]).to eq([])
110
+ end
111
+ end
112
+
113
+ describe "#evals" do
114
+ it "GETs /api/v1/evals" do
115
+ stub_request(:get, %r{https://cloud.rails-agent.com/api/v1/evals})
116
+ .to_return(status: 200, body: { evals: [] }.to_json)
117
+
118
+ expect(client.evals["evals"]).to eq([])
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe RailsAgents::Configuration do
6
+ subject(:config) { described_class.new }
7
+
8
+ it "defaults api_base to cloud.rails-agent.com" do
9
+ expect(config.api_base).to eq("https://cloud.rails-agent.com")
10
+ end
11
+
12
+ it "defaults dashboard_base to cloud.rails-agent.com" do
13
+ expect(config.dashboard_base).to eq("https://cloud.rails-agent.com")
14
+ end
15
+
16
+ it "builds api_v1_base under /api/v1" do
17
+ expect(config.api_v1_base).to eq("https://cloud.rails-agent.com/api/v1")
18
+ end
19
+
20
+ it "reads api_key from ENV" do
21
+ with_env("RAILS_AGENTS_API_KEY" => "test-key") do
22
+ expect(described_class.new.api_key).to eq("test-key")
23
+ end
24
+ end
25
+
26
+ it "reads project_id from ENV" do
27
+ with_env("RAILS_AGENTS_PROJECT_ID" => "prj_test") do
28
+ expect(described_class.new.project_id).to eq("prj_test")
29
+ end
30
+ end
31
+
32
+ it "ignores RAILS_AGENTS_API_BASE env (cloud host is fixed)" do
33
+ with_env("RAILS_AGENTS_API_BASE" => "https://example.invalid") do
34
+ expect(described_class.new.api_base).to eq("https://cloud.rails-agent.com")
35
+ end
36
+ end
37
+
38
+ it "reports configured when api_key is present" do
39
+ config.api_key = "abc"
40
+ expect(config).to be_configured
41
+ end
42
+
43
+ it "reports not configured when api_key is blank" do
44
+ config.api_key = ""
45
+ expect(config).not_to be_configured
46
+ end
47
+
48
+ def with_env(vars)
49
+ old = vars.keys.to_h { |k| [k, ENV[k]] }
50
+ vars.each { |k, v| ENV[k] = v }
51
+ yield
52
+ ensure
53
+ old.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
54
+ end
55
+ end