openui-rails 0.1.0 → 0.2.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.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2011-2024 Thesys Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,15 @@
1
+ {
2
+ "runtime_package": "@openuidev/browser-bundle",
3
+ "runtime_version": "0.2.1",
4
+ "runtime_integrity": "sha384-GNNBzicRNkuscleA9e7/4cz6JwE9PE4BRKJ1udKWNzBh4DTD2/zpfOz4p91n8sW0",
5
+ "library": "openuiChatLibrary",
6
+ "exports": {
7
+ "library.prompt.txt": "prompt()",
8
+ "library.spec.json": "toSpec()"
9
+ },
10
+ "sha256": {
11
+ "library.prompt.txt": "7a334672957298e60543d80650615102b934bc88f69115e8602770a988514c3d",
12
+ "library.spec.json": "2c36842674a3d1f3a06deddd90304c67da65fc83686dd45e51d1b67d4a2178ad",
13
+ "openui.LICENSE.txt": "b6bdd2d3d161722fb3f3afc039114cfa9f3236f036e1c98e0853bc3a85f6cd9e"
14
+ }
15
+ }
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tempfile"
5
+
6
+ module OpenUI
7
+ module Rails
8
+ # Task implementation for installing exports of the pinned default library.
9
+ class DefaultLibrary # :nodoc:
10
+ DIRECTORY = File.expand_path("default_library", __dir__)
11
+ NAMES = %w[library.spec.json library.prompt.txt openui.LICENSE.txt].freeze
12
+
13
+ def self.files(directory: DIRECTORY)
14
+ provenance = JSON.parse(File.read(File.join(directory, "provenance.json")))
15
+ integrity = RuntimePackage::FILES.fetch("package/dist/openui-bundle.min.js").last
16
+ unless provenance.fetch("runtime_version") == RuntimeAsset::PINNED_VERSION && provenance.fetch("runtime_integrity") == integrity
17
+ raise ConfigurationError, "default library exports do not match the pinned runtime"
18
+ end
19
+
20
+ NAMES.to_h do |name|
21
+ content = File.binread(File.join(directory, name))
22
+ unless Digest::SHA256.hexdigest(content) == provenance.fetch("sha256").fetch(name)
23
+ raise ConfigurationError, "default library export checksum mismatch; reinstall the gem"
24
+ end
25
+ [ name, content ]
26
+ end
27
+ rescue JSON::ParserError, KeyError, SystemCallError
28
+ raise ConfigurationError, "default library exports are unavailable or invalid; reinstall the gem"
29
+ end
30
+
31
+ def self.prepare(root:, configuration: OpenUI::Rails.configuration, force: false)
32
+ root = File.realpath(root)
33
+ exports = files
34
+ targets = [ configuration.library_spec_path, configuration.prompt_path ].map { |path| safe_path(path, root) }
35
+ targets << safe_path(File.join(File.dirname(targets.last), "openui.LICENSE.txt"), root)
36
+ if targets.uniq.size != targets.size || targets.any? { |path| targets.any? { |other| path.start_with?("#{other}/") } }
37
+ raise ConfigurationError, "prompt and spec need distinct file paths"
38
+ end
39
+ contents = targets.zip(NAMES.map { |name| exports.fetch(name) }).to_h
40
+ if !force && contents.any? { |path, content| File.exist?(path) && File.binread(path) != content }
41
+ raise ConfigurationError, "existing library files differ; preserve your custom library or explicitly use openui:library:prepare[true]"
42
+ end
43
+
44
+ contents.each do |path, content|
45
+ next if File.file?(path) && File.binread(path) == content
46
+
47
+ FileUtils.mkdir_p(File.dirname(path))
48
+ Tempfile.create(".openui-library-", File.dirname(path)) do |temporary|
49
+ temporary.chmod(0o644)
50
+ temporary.binmode
51
+ temporary.write(content)
52
+ temporary.flush
53
+ if force
54
+ File.rename(temporary.path, path)
55
+ else
56
+ File.link(temporary.path, path)
57
+ end
58
+ end
59
+ end
60
+ configuration.reset!
61
+ targets
62
+ rescue Errno::EEXIST
63
+ raise ConfigurationError, "library file appeared during preparation; rerun without overwriting it"
64
+ end
65
+
66
+ def self.safe_path(path, root)
67
+ raise ConfigurationError, "configure both library_spec_path and prompt_path before preparing the library" if path.nil?
68
+
69
+ target = File.expand_path(path, root)
70
+ unless !File.symlink?(target) && (!File.exist?(target) || File.file?(target))
71
+ raise ConfigurationError, "library outputs must be regular files inside the application root"
72
+ end
73
+
74
+ ancestor = File.dirname(target)
75
+ ancestor = File.dirname(ancestor) until File.exist?(ancestor) || File.symlink?(ancestor)
76
+ real = File.realpath(ancestor)
77
+ unless File.directory?(ancestor) && (real == root || real.start_with?("#{root}/"))
78
+ raise ConfigurationError, "library output directory resolves outside the application root"
79
+ end
80
+ File.join(real, target.delete_prefix(ancestor).delete_prefix("/"))
81
+ end
82
+ private_class_method :safe_path
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenUI
4
+ module Rails
5
+ # Read-only task diagnostics. Never executes application authorization,
6
+ # makes HTTP requests, or prints file contents and exception messages.
7
+ class Doctor # :nodoc:
8
+ Check = Struct.new(:name, :status, :message, keyword_init: true)
9
+ Report = Struct.new(:checks, keyword_init: true) do
10
+ def ok? = checks.none? { |check| check.status == :fail }
11
+
12
+ def to_s
13
+ lines = checks.map { |check| "[#{check.status.to_s.upcase}] #{check.name}: #{check.message}" }
14
+ lines << (ok? ? "Required local checks passed. Review all UNVERIFIED checks." : "Setup has required failures. Fix them and rerun openui:doctor.")
15
+ lines.join("\n")
16
+ end
17
+ end
18
+
19
+ def initialize(root:, configuration: OpenUI::Rails.configuration, routes:)
20
+ @root = File.expand_path(root)
21
+ @configuration = configuration
22
+ @routes = routes
23
+ end
24
+
25
+ def run
26
+ @checks = []
27
+ valid = check_configuration
28
+ check_route
29
+ check_assets
30
+ check_library_identity if valid
31
+ record("browser", :unverified, "Verify Stimulus registration, rendering, stylesheet loading, and event handling in a browser.")
32
+ record("authorization", :unverified, "Review actor resolution, tenant-scoped surfaces, and global or per-action authorizers. Callbacks were not executed; missing authorization denies actions.")
33
+ record("idempotency", :unverified, "Verify atomic shared storage for multiple workers, TTL limits, and business constraints.")
34
+ record("recovery", :unverified, "Verify authorized subscriptions, canonical snapshots, and host-owned fallback UI.")
35
+ Report.new(checks: @checks.freeze)
36
+ end
37
+
38
+ private
39
+
40
+ def record(name, status, message)
41
+ @checks << Check.new(name: name, status: status, message: message).freeze
42
+ end
43
+
44
+ def check_configuration
45
+ @configuration.validate!
46
+ prompt = @configuration.prompt_path
47
+ raise ConfigurationError if prompt.nil? || !File.file?(prompt) || File.zero?(prompt)
48
+
49
+ record("configuration", :pass, "Library files and numeric limits are valid.")
50
+ true
51
+ rescue OpenUI::Rails::Error, SystemCallError, ArgumentError, TypeError
52
+ record("configuration", :fail, "Review library paths, nonempty prompt, manifest JSON, asset settings, and numeric limits. For the default library, run openui:library:prepare.")
53
+ false
54
+ end
55
+
56
+ def check_route
57
+ endpoint = @configuration.actions_endpoint
58
+ raise ConfigurationError unless endpoint.is_a?(String) && endpoint.start_with?("/") && !endpoint.start_with?("//") && !endpoint.match?(/[?#]/)
59
+
60
+ raise ConfigurationError unless endpoint.end_with?("/actions")
61
+
62
+ mounts = @routes.routes.select { |route| route.app.respond_to?(:app) && route.app.app == Engine }
63
+ mount_path = endpoint.delete_suffix("/actions")
64
+ mount_path = "/" if mount_path.empty?
65
+ matching = mounts.any? do |route|
66
+ route.path.spec.to_s == mount_path && (route.verb.empty? || route.verb.include?("POST"))
67
+ end
68
+ if matching
69
+ record("actions route", :pass, "Static engine mount matches actions_endpoint. Request constraints were not executed.")
70
+ elsif mounts.any? { |route| route.path.spec.to_s.match?(/[:*]/) }
71
+ record("actions route", :unverified, "The engine has a dynamic mount. Verify actions_endpoint and request constraints in the host.")
72
+ else
73
+ raise ConfigurationError
74
+ end
75
+ rescue OpenUI::Rails::Error, ArgumentError
76
+ record("actions route", :fail, "Mount OpenUI::Rails::Engine and match actions_endpoint to that mount's /actions path.")
77
+ end
78
+
79
+ def default_assets?
80
+ @configuration.runtime_assets == :self_hosted && @configuration.runtime_url.nil?
81
+ end
82
+
83
+ def check_assets
84
+ unless default_assets? && @configuration.runtime_stylesheet_url.nil?
85
+ urls = [ RuntimeAsset.url(@configuration), RuntimeAsset.stylesheet_url(@configuration) ]
86
+ hashes = [ RuntimeAsset.integrity(@configuration), RuntimeAsset.stylesheet_integrity(@configuration) ]
87
+ raise ConfigurationError unless urls.all? { |url| url.is_a?(String) && !url.empty? } &&
88
+ hashes.all? { |value| value.is_a?(String) && value.match?(/\Asha(?:256|384|512)-[A-Za-z0-9+\/=]+\z/) }
89
+
90
+ record("runtime assets", :unverified, "Custom or external URLs and integrity settings exist. No network request or content verification was performed.")
91
+ return
92
+ end
93
+
94
+ raise ConfigurationError unless RuntimeAsset.version(@configuration) == RuntimeAsset::PINNED_VERSION
95
+
96
+ public_root = File.join(@root, "public")
97
+ url = @configuration.local_runtime_path
98
+ raise ConfigurationError unless url.is_a?(String) && url.start_with?("/") && !url.match?(/[?#]/)
99
+
100
+ directory = File.expand_path(url.delete_prefix("/"), public_root)
101
+ raise ConfigurationError unless directory.start_with?("#{public_root}/")
102
+
103
+ RuntimePackage::FILES.each_value do |suffix, expected|
104
+ path = File.join(directory, "#{RuntimeAsset::FILE_PREFIX}-#{RuntimeAsset::PINNED_VERSION}.#{suffix}")
105
+ inside = File.file?(path) && File.realpath(path).start_with?("#{File.realpath(@root)}/public/")
106
+ verified = inside && "sha384-#{Digest::SHA384.file(path).base64digest}" == expected
107
+ sri = case suffix
108
+ when "js" then RuntimeAsset.integrity(@configuration)
109
+ when "css" then RuntimeAsset.stylesheet_integrity(@configuration)
110
+ else expected
111
+ end
112
+ verified &&= valid_integrity?(path, sri)
113
+ record("runtime #{suffix}", verified ? :pass : :fail,
114
+ verified ? "Matches the pinned checksum and configured integrity." : "Missing asset, checksum mismatch, or incorrect integrity override. Run openui:runtime:fetch and review integrity settings.")
115
+ end
116
+ rescue OpenUI::Rails::Error, SystemCallError, ArgumentError, TypeError
117
+ record("runtime assets", :fail, "Review runtime version, paths, URLs, stylesheet, and integrity hashes. Keep default assets inside public/ and run openui:runtime:fetch.")
118
+ end
119
+
120
+ def valid_integrity?(path, value)
121
+ return false unless value.is_a?(String) && value.match?(/\Asha(?:256|384|512)-[A-Za-z0-9+\/=]+\z/)
122
+
123
+ algorithm = value.split("-", 2).first
124
+ "#{algorithm}-#{Digest.const_get(algorithm.upcase).file(path).base64digest}" == value
125
+ end
126
+
127
+ def check_library_identity
128
+ unless default_assets?
129
+ record("library identity", :unverified, "Verify the configured prompt/spec against the actual custom runtime library.")
130
+ return
131
+ end
132
+
133
+ files = DefaultLibrary.files
134
+ matching = @configuration.library_fingerprint == LibraryManifest.parse(files.fetch("library.spec.json")).fingerprint &&
135
+ File.binread(@configuration.prompt_path) == files.fetch("library.prompt.txt")
136
+ record("library identity", matching ? :pass : :fail,
137
+ matching ? "Prompt and spec match the pinned default chat library." : "The default runtime needs its matching prompt/spec. Preserve custom files or use openui:library:prepare; custom libraries need a matching custom runtime.")
138
+ rescue OpenUI::Rails::Error, SystemCallError, ArgumentError, TypeError
139
+ record("library identity", :fail, "Cannot verify the pinned default library exports. Review the gem installation and configured files.")
140
+ end
141
+ end
142
+ end
143
+ end
@@ -123,7 +123,7 @@ module OpenUI
123
123
  # Human-readable identifier, e.g. "support@1.2.0+1a2b3c4d5e6f".
124
124
  attr_reader :identifier
125
125
 
126
- # Return the names declared in the components collection.
126
+ # Return names from an array or name-keyed components collection.
127
127
  #
128
128
  # @return [Array<String>]
129
129
  def component_names = names_for(:components)
@@ -178,6 +178,7 @@ module OpenUI
178
178
  keys = COLLECTION_KEYS.fetch(collection)
179
179
  keys.each do |key|
180
180
  value = document[key]
181
+ return value.keys if value.is_a?(Hash)
181
182
  next unless value.is_a?(Array)
182
183
 
183
184
  return value.filter_map { |entry| name_of(entry) }
@@ -1,5 +1,5 @@
1
1
  module OpenUI
2
2
  module Rails
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/openui/rails.rb CHANGED
@@ -27,7 +27,9 @@ require "openui/rails/actions"
27
27
  require "openui/rails/adapters/ruby_llm"
28
28
  require "openui/rails/runtime_asset"
29
29
  require "openui/rails/runtime_package"
30
+ require "openui/rails/default_library"
30
31
  require "openui/rails/surface"
31
32
  require "openui/rails/view_helper"
32
33
  require "openui/rails/configuration"
34
+ require "openui/rails/doctor"
33
35
  require "openui/rails/engine"
@@ -1,6 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :openui do
4
+ desc "Check local OpenUI setup without writes or network requests"
5
+ task doctor: :environment do
6
+ report = OpenUI::Rails::Doctor.new(root: ::Rails.root, routes: ::Rails.application.routes).run
7
+ puts report
8
+ exit 1 unless report.ok?
9
+ end
10
+
11
+ namespace :library do
12
+ desc "Prepare the pinned default chat prompt and spec (pass true to overwrite)"
13
+ task :prepare, [ :force ] => :environment do |_task, args|
14
+ unless [ nil, "true", "false" ].include?(args[:force])
15
+ raise OpenUI::Rails::ConfigurationError, "force must be true or false"
16
+ end
17
+ paths = OpenUI::Rails::DefaultLibrary.prepare(root: ::Rails.root, force: args[:force] == "true")
18
+ paths.each { |path| puts "Prepared #{path}" }
19
+ puts "Default library ready; commit the prompt and spec together."
20
+ end
21
+ end
22
+
4
23
  namespace :runtime do
5
24
  desc "Vendor the checksum-locked OpenUI JS, CSS and license (requires npm)"
6
25
  task :fetch, [ :destination ] => :environment do |_task, args|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openui-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Wang
@@ -49,6 +49,7 @@ files:
49
49
  - CODE_OF_CONDUCT.md
50
50
  - CONFIGURATION.md
51
51
  - CONTRIBUTING.md
52
+ - DEVELOPMENT.md
52
53
  - LICENSE
53
54
  - README.md
54
55
  - RELEASING.md
@@ -73,6 +74,12 @@ files:
73
74
  - lib/openui/rails/actions/submission.rb
74
75
  - lib/openui/rails/adapters/ruby_llm.rb
75
76
  - lib/openui/rails/configuration.rb
77
+ - lib/openui/rails/default_library.rb
78
+ - lib/openui/rails/default_library/library.prompt.txt
79
+ - lib/openui/rails/default_library/library.spec.json
80
+ - lib/openui/rails/default_library/openui.LICENSE.txt
81
+ - lib/openui/rails/default_library/provenance.json
82
+ - lib/openui/rails/doctor.rb
76
83
  - lib/openui/rails/engine.rb
77
84
  - lib/openui/rails/errors.rb
78
85
  - lib/openui/rails/json_value.rb