terret 0.0.2 → 0.1.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 +4 -4
- data/config/bundle.yml +189 -0
- data/exe/trt +12 -0
- data/lib/terret/boot.rb +149 -0
- data/lib/terret/cli.rb +269 -11
- data/lib/terret/composition.rb +755 -0
- data/lib/terret/doctor.rb +183 -0
- data/lib/terret/home.rb +85 -0
- data/lib/terret/schema_gems.rb +22 -0
- data/lib/terret/version.rb +14 -0
- data/profiles/headless/patch.yml +27 -0
- data/profiles/headless/profile.yml +57 -0
- metadata +93 -7
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "composition"
|
|
4
|
+
|
|
5
|
+
module Terret
|
|
6
|
+
# `trt doctor` resolves a profile and validates every row's config against its
|
|
7
|
+
# plugin's Hames::Schema, without booting anything (docs/composition.md §9).
|
|
8
|
+
#
|
|
9
|
+
# It requires the composition's code and materializes each row (so !env,
|
|
10
|
+
# !setting and !ruby resolve to concrete values), then maps each row's plugin
|
|
11
|
+
# to its class and checks the materialized config against the class's schema.
|
|
12
|
+
# Nothing is mounted: doctor reports on a composition it never boots.
|
|
13
|
+
#
|
|
14
|
+
# Two semantics keep the exit status trustworthy, and both err the same way:
|
|
15
|
+
# a service with no schema is reported `unschema'd`, not failed, and an extra
|
|
16
|
+
# key WARNS rather than fails. Environment probes — does OPENROUTER_API_KEY
|
|
17
|
+
# resolve — print as informational lines and never as failures: doctor
|
|
18
|
+
# validates config, not the world. Exit status is 1 only when an enabled row's
|
|
19
|
+
# config is actually wrong.
|
|
20
|
+
module Doctor
|
|
21
|
+
# Returns a process exit status: 1 when an enabled row's config is wrong,
|
|
22
|
+
# 0 otherwise.
|
|
23
|
+
def self.run(resolved, allow_config_ruby: false, out:)
|
|
24
|
+
load_failures = require_code(resolved, allow_config_ruby)
|
|
25
|
+
# Settings resolve once, but a bad !setting/!ruby in settings: must not
|
|
26
|
+
# abort the whole run and hide the row table. It becomes its own error
|
|
27
|
+
# line; rows then materialize against empty settings, so any !setting in a
|
|
28
|
+
# row surfaces as that row's own error rather than a swallowed one.
|
|
29
|
+
settings, settings_error = begin
|
|
30
|
+
[Composition.materialize_settings(resolved.settings, allow_config_ruby: allow_config_ruby), nil]
|
|
31
|
+
rescue Composition::Error => e
|
|
32
|
+
[{}, e.message]
|
|
33
|
+
end
|
|
34
|
+
results = resolved.rows.map { |row| check(row, settings, allow_config_ruby) }
|
|
35
|
+
|
|
36
|
+
render(resolved, results, load_failures, settings_error, out)
|
|
37
|
+
bad = settings_error || results.any? { |r| !r[:disabled] && r[:status] == :error }
|
|
38
|
+
bad ? 1 : 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Requiring the composition's code, best-effort: a require that fails does
|
|
42
|
+
# not abort doctor. It surfaces as an info line for the file, and as the
|
|
43
|
+
# per-row "does not resolve" error for every class that file would define —
|
|
44
|
+
# which is more useful than a single aborted run.
|
|
45
|
+
#
|
|
46
|
+
# doctor is the SAFE preview — "validates config, not the world" — so a
|
|
47
|
+
# path-shaped require in a profile's plugins: (portable config from anywhere)
|
|
48
|
+
# is NOT executed to do that job: it surfaces as a load failure the same way
|
|
49
|
+
# a missing feature does, keeping `trt doctor <untrusted profile>` from being
|
|
50
|
+
# arbitrary code execution. A bundle's requires: ship inside an installed gem
|
|
51
|
+
# and are trusted (they may name a path to their own lib);
|
|
52
|
+
# --allow-config-ruby is the operator's consent to load a profile path too,
|
|
53
|
+
# exactly as at boot.
|
|
54
|
+
def self.require_code(resolved, allow_config_ruby)
|
|
55
|
+
refused, permitted = resolved.plugins.partition do |file|
|
|
56
|
+
!allow_config_ruby && !Composition.load_path_feature?(file)
|
|
57
|
+
end
|
|
58
|
+
failures = (resolved.requires + permitted).filter_map do |file|
|
|
59
|
+
require file
|
|
60
|
+
nil
|
|
61
|
+
rescue LoadError => e
|
|
62
|
+
[file, e.message]
|
|
63
|
+
end
|
|
64
|
+
failures + refused.map do |file|
|
|
65
|
+
[file, "refused: a filesystem path, not a load-path feature name; doctor does " \
|
|
66
|
+
"not execute untrusted requires — pass --allow-config-ruby to load it"]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# One row's verdict. The row id names the row in the table's first column,
|
|
71
|
+
# so validate is called WITHOUT a subject — the schema can name the row (its
|
|
72
|
+
# unit tests prove it), but here the column already does, and the doc's
|
|
73
|
+
# `error: sink must be a String` reads better without the id repeated.
|
|
74
|
+
def self.check(row, settings, allow_config_ruby)
|
|
75
|
+
base = { id: row.id, plugin: row.plugin, disabled: row.disabled }
|
|
76
|
+
config = Composition.materialize(row.config, settings: settings, allow_config_ruby: allow_config_ruby,
|
|
77
|
+
where: "row #{row.id.inspect}")
|
|
78
|
+
klass = constantize(row.plugin)
|
|
79
|
+
return base.merge(status: :error, detail: "#{row.plugin} does not resolve to a plugin class") unless klass
|
|
80
|
+
unless plugin_class?(klass)
|
|
81
|
+
# A name that resolves to a live constant that is not a plugin (String, a
|
|
82
|
+
# module, a typo hitting something real) is not "a plugin with no schema"
|
|
83
|
+
# — it is a wrong plugin:, and reporting it unschema'd would hide that.
|
|
84
|
+
return base.merge(status: :error, detail: "#{row.plugin} is not a plugin " \
|
|
85
|
+
"(its instances do not respond to #apply)")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
schema = klass.respond_to?(:config_schema) ? klass.config_schema : nil
|
|
89
|
+
return base.merge(status: :unschema, detail: nil) unless schema
|
|
90
|
+
|
|
91
|
+
# redact: a value here is a materialized !env/!setting/!ruby result and
|
|
92
|
+
# may be a secret; the detail names its type only, never its content.
|
|
93
|
+
result = schema.validate(config, redact: true)
|
|
94
|
+
status = if result.errors.any? then :error
|
|
95
|
+
elsif result.warnings.any? then :warn
|
|
96
|
+
else :ok
|
|
97
|
+
end
|
|
98
|
+
base.merge(status: status, detail: (result.errors + result.warnings).join("; "))
|
|
99
|
+
rescue Composition::Error => e
|
|
100
|
+
# A row whose !setting or !ruby cannot resolve is a config fault doctor
|
|
101
|
+
# owns, reported against the row rather than crashing every other row.
|
|
102
|
+
base.merge(status: :error, detail: e.message)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.constantize(name)
|
|
106
|
+
Object.const_get(name)
|
|
107
|
+
rescue NameError
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The plugin contract boot itself enforces (boot.rb): a class whose instances
|
|
112
|
+
# respond to #apply. Using the same test keeps doctor from red-flagging a
|
|
113
|
+
# composition boot would accept — including a third-party functional plugin
|
|
114
|
+
# that is not a Hames::Service.
|
|
115
|
+
def self.plugin_class?(klass)
|
|
116
|
+
klass.is_a?(Class) && klass.method_defined?(:apply)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# -- output ----------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def self.render(resolved, results, load_failures, settings_error, out)
|
|
122
|
+
out.puts "# doctor: profile #{resolved.profile.inspect}"
|
|
123
|
+
out.puts
|
|
124
|
+
if settings_error
|
|
125
|
+
out.puts "error #{Composition.one_line(settings_error)}"
|
|
126
|
+
out.puts
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Row ids are validated at resolution, but plugin names, error details,
|
|
130
|
+
# env markers and file paths are not — a newline in any of them could
|
|
131
|
+
# forge a table row, so every printed value goes through one_line. Widths
|
|
132
|
+
# are computed on the neutralized plugin so the table stays aligned.
|
|
133
|
+
plugins = results.to_h { |r| [r[:id], Composition.one_line(r[:plugin])] }
|
|
134
|
+
row_w = [results.map { |r| r[:id].length }.max || 3, 3].max
|
|
135
|
+
plugin_w = [plugins.values.map(&:length).max || 6, 6].max
|
|
136
|
+
out.puts "#{'row'.ljust(row_w)} #{'plugin'.ljust(plugin_w)} status"
|
|
137
|
+
results.each do |r|
|
|
138
|
+
out.puts "#{r[:id].ljust(row_w)} #{plugins[r[:id]].ljust(plugin_w)} #{status_text(r)}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
info = info_lines(resolved, load_failures)
|
|
142
|
+
return if info.empty?
|
|
143
|
+
|
|
144
|
+
out.puts
|
|
145
|
+
info.each { |line| out.puts "info #{Composition.one_line(line)}" }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.status_text(result)
|
|
149
|
+
detail = Composition.one_line(result[:detail].to_s)
|
|
150
|
+
text = case result[:status]
|
|
151
|
+
when :ok then "ok"
|
|
152
|
+
when :unschema then "unschema'd"
|
|
153
|
+
when :warn then "warn: #{detail}"
|
|
154
|
+
when :error then "error: #{detail}"
|
|
155
|
+
end
|
|
156
|
+
result[:disabled] ? "#{text} (disabled)" : text
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Informational, never a verdict: which !env markers this composition reads
|
|
160
|
+
# and whether each resolves, plus any code that would not load. The resolved
|
|
161
|
+
# value of a marker never appears — a doctor that printed a secret would be
|
|
162
|
+
# one nobody could run in front of other people.
|
|
163
|
+
def self.info_lines(resolved, load_failures)
|
|
164
|
+
lines = env_markers(resolved.rows).map { |name| "#{name}: #{ENV.key?(name) ? 'set' : 'unset'}" }
|
|
165
|
+
load_failures.each { |file, message| lines << "could not require #{file.inspect}: #{message}" }
|
|
166
|
+
lines
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.env_markers(rows)
|
|
170
|
+
names = []
|
|
171
|
+
rows.each { |row| collect_env(row.config, names) }
|
|
172
|
+
names.uniq
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.collect_env(value, names)
|
|
176
|
+
case value
|
|
177
|
+
when Composition::Tagged then names << value.argument if value.tag == "env"
|
|
178
|
+
when Hash then value.each_value { |v| collect_env(v, names) }
|
|
179
|
+
when Array then value.each { |v| collect_env(v, names) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/terret/home.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Terret
|
|
4
|
+
# Terret home: where profiles live (docs/composition.md §3).
|
|
5
|
+
#
|
|
6
|
+
# ~/.terret/
|
|
7
|
+
# ├── patch.yml # applies to every profile
|
|
8
|
+
# └── profiles/<name>/{profile.yml,patch.yml}
|
|
9
|
+
#
|
|
10
|
+
# TERRET_HOME overrides ~/.terret wholesale, which is what makes the layer
|
|
11
|
+
# stack testable — every composition test points it at a tmpdir — and what
|
|
12
|
+
# lets a deployment ship a home directory as an artifact rather than as
|
|
13
|
+
# instructions for populating a user's dotfiles.
|
|
14
|
+
#
|
|
15
|
+
# A home that does not hold the named profile falls back to the templates
|
|
16
|
+
# this gem ships (gems/terret/profiles), so `trt boot --profile headless`
|
|
17
|
+
# works on a machine whose home is empty. The home always wins where it has
|
|
18
|
+
# an opinion; the shipped templates are only the floor.
|
|
19
|
+
class Home
|
|
20
|
+
DEFAULT = "~/.terret"
|
|
21
|
+
SHIPPED = File.expand_path("../../profiles", __dir__)
|
|
22
|
+
|
|
23
|
+
# ENV is read here rather than at load time so a test can set TERRET_HOME
|
|
24
|
+
# after this file is required.
|
|
25
|
+
def self.resolve(path = nil)
|
|
26
|
+
return path if path.is_a?(Home)
|
|
27
|
+
|
|
28
|
+
env = ENV["TERRET_HOME"]
|
|
29
|
+
env = nil if env.nil? || env.empty?
|
|
30
|
+
new(path || env || DEFAULT)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :path
|
|
34
|
+
|
|
35
|
+
def initialize(path)
|
|
36
|
+
@path = File.expand_path(path.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def patch = File.join(path, "patch.yml")
|
|
40
|
+
def profile_dir(name) = File.join(path, "profiles", name.to_s)
|
|
41
|
+
def profile_config(name) = File.join(profile_dir(name), "profile.yml")
|
|
42
|
+
def profile_patch(name) = File.join(profile_dir(name), "patch.yml")
|
|
43
|
+
|
|
44
|
+
def shipped_profile_dir(name) = File.join(SHIPPED, name.to_s)
|
|
45
|
+
def shipped_profile_config(name) = File.join(shipped_profile_dir(name), "profile.yml")
|
|
46
|
+
def shipped_profile_patch(name) = File.join(shipped_profile_dir(name), "patch.yml")
|
|
47
|
+
|
|
48
|
+
# The pair of files a profile resolves to, home first and the shipped
|
|
49
|
+
# template as the floor. Either may be nil; a profile with neither does
|
|
50
|
+
# not exist.
|
|
51
|
+
#
|
|
52
|
+
# The two files are found independently on purpose. A home holding only a
|
|
53
|
+
# patch.yml is an operator who edited the one file the template told them
|
|
54
|
+
# to edit, and tying the patch's fate to a sibling profile.yml would drop
|
|
55
|
+
# it in silence — including when what it drops is a tightening of policy.
|
|
56
|
+
def profile_files(name)
|
|
57
|
+
config = [profile_config(name), shipped_profile_config(name)].find { |f| File.file?(f) }
|
|
58
|
+
shipped = config && config == shipped_profile_config(name)
|
|
59
|
+
patch = [profile_patch(name), (shipped_profile_patch(name) if shipped)]
|
|
60
|
+
.compact.find { |f| File.file?(f) }
|
|
61
|
+
[config, patch]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Profile names offered by this home and by the shipped templates.
|
|
65
|
+
def profile_names
|
|
66
|
+
[File.join(path, "profiles"), SHIPPED]
|
|
67
|
+
.flat_map { |d| Dir.glob(File.join(d, "*", "profile.yml")) }
|
|
68
|
+
.map { |f| File.basename(File.dirname(f)) }
|
|
69
|
+
.uniq.sort
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# dump-config prints these, so they are home-relative rather than absolute:
|
|
73
|
+
# a command whose whole purpose is "show me my config" should be safe to
|
|
74
|
+
# paste into an issue, and an absolute path names the operator.
|
|
75
|
+
def label(file)
|
|
76
|
+
f = file.to_s
|
|
77
|
+
return f.delete_prefix("#{path}/") if f.start_with?("#{path}/")
|
|
78
|
+
return "terret:#{f.delete_prefix("#{SHIPPED}/")}" if f.start_with?("#{SHIPPED}/")
|
|
79
|
+
|
|
80
|
+
f
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def to_s = path
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Terret
|
|
4
|
+
# Every gem whose services declare a Hames::Schema, as require paths. This is
|
|
5
|
+
# the single source `rake config:catalog` and the schema tests both read, so a
|
|
6
|
+
# newly configurable gem is added in one place. Split across two lists, a gem
|
|
7
|
+
# added to only one would be silently absent from the catalog with CI green.
|
|
8
|
+
#
|
|
9
|
+
# It is the base bundle's own requires plus the interface/adapter gems that
|
|
10
|
+
# ship schemas but are not part of terret-base (ws, mcp, morph).
|
|
11
|
+
SCHEMA_GEMS = %w[
|
|
12
|
+
terret/store/sqlite
|
|
13
|
+
terret/openrouter
|
|
14
|
+
terret/exec
|
|
15
|
+
terret/tools_std
|
|
16
|
+
terret/sandbox/docker
|
|
17
|
+
terret/ws
|
|
18
|
+
terret/acp
|
|
19
|
+
terret/mcp
|
|
20
|
+
terret/morph
|
|
21
|
+
].freeze
|
|
22
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Terret
|
|
4
|
+
# The meta-gem's own version — what `trt --version` prints and what
|
|
5
|
+
# terret.gemspec reads, so the number lives in exactly one place.
|
|
6
|
+
#
|
|
7
|
+
# Deliberately NOT Terret::VERSION: terret-core already owns that constant
|
|
8
|
+
# and it is terret-core's number. The 0.1.0 lockstep release aligns the two
|
|
9
|
+
# numbers, but they stay distinct constants — quietly redefining a sibling
|
|
10
|
+
# gem's version constant is not a thing this gem gets to do.
|
|
11
|
+
module Meta
|
|
12
|
+
VERSION = "0.1.0"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Rows this profile changes about terret-base. Applied after every bundle in
|
|
2
|
+
# the stack, before ~/.terret/patch.yml and before any --patch overlay.
|
|
3
|
+
#
|
|
4
|
+
# A patch targeting an existing id replaces that row's config WHOLESALE. It
|
|
5
|
+
# never deep-merges. So a patch that repoints the model has to carry the api
|
|
6
|
+
# key along with it, and `trt dump-config --profile headless` is how you see
|
|
7
|
+
# what a row currently holds before you replace it.
|
|
8
|
+
#
|
|
9
|
+
# A row with an id that is not in the stack yet is an insertion, and it has to
|
|
10
|
+
# say where it goes with `before:` or `after:` naming an existing row.
|
|
11
|
+
|
|
12
|
+
rows: []
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# UNCOMMENT TO RUN TOOLS DIRECTLY ON THIS HOST, UNSANDBOXED.
|
|
15
|
+
#
|
|
16
|
+
# Every Bash command, every spawned process, and every terminal this agent
|
|
17
|
+
# opens runs as you, on your machine, with your network. A prompt-injected
|
|
18
|
+
# instruction that reaches a tool call has nothing between it and the host but
|
|
19
|
+
# the allow list. See docs/security.md.
|
|
20
|
+
#
|
|
21
|
+
# This comment block is not a security control. What it is, is the difference
|
|
22
|
+
# between a decision made and a default inherited.
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# rows:
|
|
25
|
+
# - id: sandbox
|
|
26
|
+
# plugin: Terret::Exec::SandboxNone
|
|
27
|
+
# config: {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# The headless profile — the template Terret ships (docs/composition.md §3).
|
|
2
|
+
#
|
|
3
|
+
# Copy this directory to ~/.terret/profiles/<name>/ and edit it, or edit it in
|
|
4
|
+
# place under ~/.terret/profiles/headless/. A home that holds no copy falls
|
|
5
|
+
# back to this file, so `trt boot --profile headless` runs out of the box —
|
|
6
|
+
# which also means an unedited profile has no workspace and can read nothing.
|
|
7
|
+
#
|
|
8
|
+
# trt dump-config --profile headless # what the stack actually resolves to
|
|
9
|
+
# trt doctor --profile headless # what does not fit
|
|
10
|
+
# trt boot --profile headless # run it
|
|
11
|
+
|
|
12
|
+
bundles:
|
|
13
|
+
- terret # terret-base, always layer one
|
|
14
|
+
|
|
15
|
+
# Out-of-tree requires for code that is not a bundle. Bundles pull in their
|
|
16
|
+
# own gems; this is for a plugin you wrote and put on the load path yourself.
|
|
17
|
+
plugins: []
|
|
18
|
+
|
|
19
|
+
# The settings map has no schema of its own. Its whole job is to be the target
|
|
20
|
+
# of !setting references, so a value used by three rows is written once.
|
|
21
|
+
settings:
|
|
22
|
+
# EVERY FILESYSTEM TOOL IS CONTAINED TO THESE DIRECTORIES, and an empty list
|
|
23
|
+
# denies every file operation. That is the safe default and not a bug: there
|
|
24
|
+
# is no ungranted-but-permitted state. Add the absolute path of the project
|
|
25
|
+
# you want the agent to work in.
|
|
26
|
+
#
|
|
27
|
+
# workspace:
|
|
28
|
+
# - /Users/you/code/some-project
|
|
29
|
+
workspace: []
|
|
30
|
+
|
|
31
|
+
store:
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
# CHANGE THIS TO A DIRECTORY YOU OWN.
|
|
34
|
+
#
|
|
35
|
+
# The append-only session log. Everything the agent reads, says and
|
|
36
|
+
# does lands here in the clear — prompts, tool output, file contents.
|
|
37
|
+
# The default below is a placeholder that works on a laptop and is a
|
|
38
|
+
# poor choice anywhere else: /var/tmp is world-shared, and this exact
|
|
39
|
+
# path is predictable, so on a multi-user box another account can read
|
|
40
|
+
# your agent's whole history (and, if it gets there first, choose what
|
|
41
|
+
# your agent reads back). Point it inside your home or your Terret
|
|
42
|
+
# home, on a filesystem you can back up.
|
|
43
|
+
#
|
|
44
|
+
# Absolute paths only. A relative one resolves against whatever
|
|
45
|
+
# directory trt happened to be run from, which is rarely what you meant.
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
path: /var/tmp/terret/sessions.db
|
|
48
|
+
|
|
49
|
+
model:
|
|
50
|
+
# "provider/model". The provider is the adapter name a row registered;
|
|
51
|
+
# terret-base registers "openrouter", keyed from OPENROUTER_API_KEY.
|
|
52
|
+
main: openrouter/anthropic/claude-sonnet-4.5
|
|
53
|
+
|
|
54
|
+
sandbox:
|
|
55
|
+
# The container every spawned process runs inside. It needs whatever
|
|
56
|
+
# toolchain your agent's commands expect; the default is small on purpose.
|
|
57
|
+
image: ruby:slim
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: terret
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Obie Fernandez
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -23,16 +23,101 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.1'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: terret-exec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: terret-openrouter
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: terret-sandbox-docker
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.1'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.1'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: terret-store-sqlite
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.1'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.1'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: terret-tools-std
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.1'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.1'
|
|
96
|
+
description: 'The meta-gem, and the way a Terret is composed. Bundles ship ordered
|
|
97
|
+
config rows, profiles stack bundles, patches adjust rows by id, and Terret.boot
|
|
98
|
+
hands the result to the Hames loader -- so which plugins run, in what order, with
|
|
99
|
+
what config is a question YAML answers rather than Ruby. Ships terret-base (the
|
|
100
|
+
log, the harness, the model seam, the execution world sandboxed with the network
|
|
101
|
+
denied, the standard tool roster, and a policy floor that starts closed), the headless
|
|
102
|
+
profile template, and trt: boot, dump-config, doctor.'
|
|
29
103
|
email:
|
|
30
104
|
- obiefernandez@gmail.com
|
|
31
|
-
executables:
|
|
105
|
+
executables:
|
|
106
|
+
- trt
|
|
32
107
|
extensions: []
|
|
33
108
|
extra_rdoc_files: []
|
|
34
109
|
files:
|
|
110
|
+
- config/bundle.yml
|
|
111
|
+
- exe/trt
|
|
112
|
+
- lib/terret/boot.rb
|
|
35
113
|
- lib/terret/cli.rb
|
|
114
|
+
- lib/terret/composition.rb
|
|
115
|
+
- lib/terret/doctor.rb
|
|
116
|
+
- lib/terret/home.rb
|
|
117
|
+
- lib/terret/schema_gems.rb
|
|
118
|
+
- lib/terret/version.rb
|
|
119
|
+
- profiles/headless/patch.yml
|
|
120
|
+
- profiles/headless/profile.yml
|
|
36
121
|
homepage: https://terret.org
|
|
37
122
|
licenses:
|
|
38
123
|
- MIT
|
|
@@ -41,6 +126,7 @@ metadata:
|
|
|
41
126
|
source_code_uri: https://github.com/terret-org/terret
|
|
42
127
|
bug_tracker_uri: https://github.com/terret-org/terret/issues
|
|
43
128
|
rubygems_mfa_required: 'true'
|
|
129
|
+
terret: config/bundle.yml
|
|
44
130
|
rdoc_options: []
|
|
45
131
|
require_paths:
|
|
46
132
|
- lib
|
|
@@ -57,5 +143,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
143
|
requirements: []
|
|
58
144
|
rubygems_version: 4.0.16
|
|
59
145
|
specification_version: 4
|
|
60
|
-
summary: Ruby-native, model-agnostic agent harness
|
|
146
|
+
summary: 'Ruby-native, model-agnostic agent harness: profiles, boot, and the trt CLI'
|
|
61
147
|
test_files: []
|