Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9537242
fix(review): attribute permission_change findings and exempt new test…
ivankuznetsov Sep 23, 2026
5832f20
test: add incident aggregate timing headroom
ivankuznetsov Sep 25, 2026
64da1c4
Merge remote-tracking branch 'refs/remotes/origin/main' into hive-bab…
ivankuznetsov Sep 27, 2026
01aa23d
fix(bot): centralize brainstorm document location in the parser
ivankuznetsov Sep 21, 2026
3b39a0f
fix(review): diff reviewer prompts against merge-base, not branch tips
ivankuznetsov Sep 17, 2026
2a9ba2c
test(opencode): isolate export capture limit fixture
ivankuznetsov Sep 25, 2026
30070d2
docs(review): record merge-base diff scope
ivankuznetsov Sep 29, 2026
4d5aa43
fix(ci): restore incident timing headroom
ivankuznetsov Sep 30, 2026
aea497f
refactor(bot): encapsulate idea-draft state decisions in IdeaDraftStore
ivankuznetsov Sep 22, 2026
c428397
fix(bot): deep-freeze idea-draft commit_snapshot String leaves
ivankuznetsov Oct 2, 2026
df5150d
fix(ci): cover expired idea draft commit
ivankuznetsov Oct 2, 2026
971ae87
fix(ci): cover expired idea draft guard
ivankuznetsov Oct 2, 2026
20252b2
fix(eval): clear a stale default report when inherited RAKEOPT is rej…
ivankuznetsov Oct 9, 2026
be97197
fix(agent-cli-runtime): keep probe overlay merges from restoring scru…
ivankuznetsov Oct 12, 2026
662d350
docs(agent-cli-runtime): document the probe request overlay-key contract
ivankuznetsov Oct 12, 2026
ec33d4b
fix(agent-cli-runtime): keep probe credential keys from restoring scr…
ivankuznetsov Oct 12, 2026
fbc65c0
fix(ci): restore incident timing headroom
ivankuznetsov Oct 12, 2026
f438725
Merge #1530 into patrol batch
ivankuznetsov Oct 15, 2026
e6204a5
Merge #1525 into patrol batch
ivankuznetsov Oct 15, 2026
03eb8b9
Merge #1513 into patrol batch
ivankuznetsov Oct 15, 2026
8efbe2f
Merge #1516 into patrol batch
ivankuznetsov Oct 15, 2026
84bea59
Merge #1461 into patrol batch
ivankuznetsov Oct 15, 2026
5afbae8
Merge #1468 into patrol batch
ivankuznetsov Oct 15, 2026
02d4ef7
docs(wiki): drop incident-budget notes already on main
ivankuznetsov Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bin/hive-eval
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ if path
end

if inherited_rake_dry_run?(ENV["RAKEOPT"])
# A rejected inherited RAKEOPT must not leave a stale default report behind,
# matching the other pre-run guards that exit before the shared removal point.
# Custom report paths are still only cleared after full validation, below.
remove_report!(report_path) if report_path == default_report
warn "hive-eval: inherited RAKEOPT may not enable rake dry-run"
exit 1
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,26 @@ def child_environment(profile, request, env:)
selected[key] = value.to_s
end
end
%w[
BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH RUBYLIB RUBYOPT
].each { |key| selected[key] = nil }
profile.env_bin_override_keys.each do |key|
selected[key] = env[key].to_s unless env[key].to_s.empty?
end
request.credential_environment_keys.each do |key|
selected[key] = env[key].to_s unless env[key].to_s.empty?
end
selected.merge(request.environment).freeze
# The overlay is the only caller-supplied channel into the child
# environment. Filtering here keeps the denylist cleanup final even if
# a request is built with keys outside the validated overlay contract.
request.environment.each do |key, value|
next unless OPENCODE_OVERLAY_ENVIRONMENT_KEYS.include?(key)

selected[key] = value.to_s
end
# The denylist scrub must stay last: every caller-controlled channel
# above (credential keys, executable overrides, the overlay) copies
# selected values into the child environment, so no selection may
# reintroduce a scrubbed runtime injection key.
SCRUBBED_RUNTIME_ENVIRONMENT_KEYS.each { |key| selected[key] = nil }
selected.freeze
end
private_class_method :child_environment

Expand Down
30 changes: 27 additions & 3 deletions components/agent-cli-runtime/lib/agent_cli_runtime/values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ module AgentCliRuntime
OPENCODE_DISABLE_AUTOUPDATE OPENCODE_PURE
].freeze

# Environment keys that Ruby, Bundler, and RubyGems honor at startup to
# inject load paths. The OpenCode probe scrubs them from every child
# environment, so request constructors must refuse to select them back as
# credential keys.
SCRUBBED_RUNTIME_ENVIRONMENT_KEYS = %w[
BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH RUBYLIB RUBYOPT
].freeze

module Immutable
module_function

Expand Down Expand Up @@ -301,6 +309,11 @@ def initialize(request:, working_directory:, invocation_root:,
raise ArgumentError,
"credential environment keys cannot override the OpenCode overlay: #{reserved.join(', ')}"
end
scrubbed = keys & SCRUBBED_RUNTIME_ENVIRONMENT_KEYS
unless scrubbed.empty?
raise ArgumentError,
"credential environment keys cannot restore scrubbed runtime injection keys: #{scrubbed.join(', ')}"
end
normalized_bash_patterns = Immutable.strings(bash_patterns)
if normalized_bash_patterns.any?(&:empty?)
raise ArgumentError, "OpenCode bash patterns must be non-empty"
Expand Down Expand Up @@ -339,13 +352,24 @@ def initialize(profile:, route:, variant: nil, environment: {},
configured_variants: nil,
executable: nil)
parsed_route = route.is_a?(Route) ? route : Route.parse(route)
normalized_environment = Immutable.hash(environment)
unallowed = normalized_environment.keys - OPENCODE_OVERLAY_ENVIRONMENT_KEYS
unless unallowed.empty?
raise ArgumentError,
"ProbeRequest environment keys must be OpenCode overlay keys: #{unallowed.join(', ')}"
end
normalized_credential_keys = Immutable.strings(credential_environment_keys)
scrubbed = normalized_credential_keys & SCRUBBED_RUNTIME_ENVIRONMENT_KEYS
unless scrubbed.empty?
raise ArgumentError,
"ProbeRequest credential environment keys cannot restore scrubbed runtime injection keys: #{scrubbed.join(', ')}"
end
super(
profile: profile,
route: parsed_route,
variant: variant.nil? ? nil : Immutable.string(variant),
environment: Immutable.hash(environment),
credential_environment_keys:
Immutable.strings(credential_environment_keys),
environment: normalized_environment,
credential_environment_keys: normalized_credential_keys,
credential_file_staged: credential_file_staged == true,
configured_variants:
configured_variants.nil? ? nil : Immutable.strings(configured_variants),
Expand Down
111 changes: 111 additions & 0 deletions components/agent-cli-runtime/test/opencode_preparation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,54 @@ def test_probe_environment_explicitly_unsets_ambient_unselected_values
ENV.delete("UNSELECTED_PROBE_SECRET")
end

def test_probe_request_rejects_environment_keys_outside_the_overlay_contract
profile = AgentCliRuntime::Profiles.fetch(:opencode)
%w[
BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH RUBYLIB RUBYOPT
].each do |key|
error = assert_raises(ArgumentError) do
AgentCliRuntime::ProbeRequest.new(
profile:, route: "anthropic/claude-sonnet-4-5",
environment: { key => "restored-injection" }
)
end
assert_match(/must be OpenCode overlay keys/, error.message)
assert_includes error.message, key
end
end

def test_probe_child_environment_keeps_scrubbed_runtime_keys_nil_across_the_overlay_merge
profile = AgentCliRuntime::Profiles.fetch(:opencode)
request = AgentCliRuntime::ProbeRequest.new(
profile:, route: "anthropic/claude-sonnet-4-5",
environment: {
"XDG_CONFIG_HOME" => "/fixture/config",
"XDG_DATA_HOME" => "/fixture/data",
"TMPDIR" => "/fixture/tmp"
}
)

child = AgentCliRuntime::OpenCode::Probe.send(
:child_environment, profile, request,
env: {
"PATH" => ENV.fetch("PATH"),
"HOME" => "/fixture/home",
"RUBYOPT" => "-r/ambient/missing",
"GEM_HOME" => "/fixture/gems"
}
)

%w[
BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH RUBYLIB RUBYOPT
].each do |key|
assert_nil child[key], "expected #{key} to stay scrubbed in the child environment"
end
assert_equal "/fixture/config", child.fetch("XDG_CONFIG_HOME")
assert_equal "/fixture/data", child.fetch("XDG_DATA_HOME")
assert_equal "/fixture/tmp", child.fetch("TMPDIR")
assert_equal "/fixture/home", child.fetch("HOME")
end

def test_credential_environment_keys_cannot_override_overlay_controls
error = assert_raises(ArgumentError) do
AgentCliRuntime::OpenCodePreparationRequest.new(
Expand All @@ -627,6 +675,69 @@ def test_credential_environment_keys_cannot_override_overlay_controls
assert_match(/cannot override the OpenCode overlay/, error.message)
end

def test_probe_request_rejects_scrubbed_runtime_keys_as_credential_keys
profile = AgentCliRuntime::Profiles.fetch(:opencode)
%w[
BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH RUBYLIB RUBYOPT
].each do |key|
error = assert_raises(ArgumentError) do
AgentCliRuntime::ProbeRequest.new(
profile:, route: "anthropic/claude-sonnet-4-5",
credential_environment_keys: [ key ]
)
end
assert_match(/cannot restore scrubbed runtime injection keys/, error.message)
assert_includes error.message, key
end
end

def test_preparation_request_rejects_scrubbed_runtime_keys_as_credential_keys
%w[
BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH RUBYLIB RUBYOPT
].each do |key|
error = assert_raises(ArgumentError) do
AgentCliRuntime::OpenCodePreparationRequest.new(
request: AgentCliRuntime::Request.new(profile: :opencode, prompt: "test"),
working_directory: Dir.pwd,
invocation_root: File.join(Dir.tmpdir, "unused-opencode-root"),
credential_environment_keys: [ key ]
)
end
assert_match(/cannot restore scrubbed runtime injection keys/, error.message)
assert_includes error.message, key
end
end

def test_probe_child_environment_keeps_scrubbed_runtime_keys_nil_across_credential_selection
profile = AgentCliRuntime::Profiles.fetch(:opencode)
request = AgentCliRuntime::ProbeRequest.new(
profile:, route: "anthropic/claude-sonnet-4-5",
environment: {
"XDG_CONFIG_HOME" => "/fixture/config"
},
credential_environment_keys: [ "ANTHROPIC_API_KEY" ]
)

child = AgentCliRuntime::OpenCode::Probe.send(
:child_environment, profile, request,
env: {
"PATH" => ENV.fetch("PATH"),
"RUBYOPT" => "-r/ambient/missing",
"GEM_HOME" => "/ambient/gems",
"ANTHROPIC_API_KEY" => "secret-canary"
}
)

%w[
BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH RUBYLIB RUBYOPT
].each do |key|
assert_nil child[key],
"expected #{key} to stay scrubbed despite ambient values"
end
assert_equal "/fixture/config", child.fetch("XDG_CONFIG_HOME")
assert_equal "secret-canary", child.fetch("ANTHROPIC_API_KEY")
end

def test_route_probe_is_fail_soft_when_the_binary_is_missing
request = AgentCliRuntime::ProbeRequest.new(
profile: :opencode,
Expand Down
Loading
Loading