hecks 3.1.2 → 3.1.3
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/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +36 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +70 -0
- data/examples/pizzas/bluebook/pizzas.bluebook +138 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +21 -0
- data/examples/pizzas/bluebook/pizzas.world +3 -0
- data/examples/pizzas/bluebook/translations/2-77625c.bluebook +6 -0
- data/examples/pizzas/oidc.json +24 -0
- data/examples/pizzas/pizzas_behaviors.hecksagon +19 -0
- data/lib/hecks/cli/console.rb +5 -2
- data/lib/hecks/doors/cli_runner.rb +6 -0
- data/lib/hecks/projector/cli_projector.rb +51 -22
- data/lib/hecks/quality_control/adapters/agent.rb +63 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +165 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +23 -9
- data/lib/hecks/version.rb +1 -1
- data/rust/host/HECKS_RELEASE +1 -1
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88b11874249737158a9451d66aa0842ef88fb3f1ff047cced68d97b2ce9fdc5f
|
|
4
|
+
data.tar.gz: 2872e381ce8e7fe5d46ef7a7623134dc23d923e57bdf194758effedaa1cc4671
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76a39ab361adfb4e07bb558cb917d46bf3d7b6bb21340ff9789cb7b6c10be47a3197df4f394e8da73129feeb5b8439d9401f5e83b8620e0a512fce966c891377
|
|
7
|
+
data.tar.gz: 58f1514450a234f3807897401daf7db410739463a8603bf58b7d73e548f14424211b4d1c5f3af1dc7421b769f39582e15c6f22188932af3de054961eed2cb641
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Driving adapter standing in for a Stripe webhook handler: boots Pizzas and calls
|
|
4
|
+
# Dispatcher#dispatch_port, which translates the payment into PizzaPaymentReceived.
|
|
5
|
+
#
|
|
6
|
+
# examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb
|
|
7
|
+
|
|
8
|
+
$LOAD_PATH.unshift File.expand_path("../../../../lib", __dir__)
|
|
9
|
+
|
|
10
|
+
require "hecks"
|
|
11
|
+
|
|
12
|
+
DOMAIN = File.expand_path("../..", __dir__)
|
|
13
|
+
RUNTIME = Hecks.boot(DOMAIN)
|
|
14
|
+
|
|
15
|
+
# Unique per run: identity is never minted, so a repeated name would be refused as AlreadyExists.
|
|
16
|
+
NAME = "StripeDemoMargherita-#{Process.pid}-#{rand(10_000)}".freeze
|
|
17
|
+
|
|
18
|
+
# Set the stage: an order Purchase's `given` rules will accept.
|
|
19
|
+
order = Order.create_pizza(name: { value: NAME }, pizza: { price_cents: { cents: 1200 }, size: { value: "large" } })
|
|
20
|
+
order.add_topping(topping: { value: "Basil" }, amount: { value: 3 })
|
|
21
|
+
|
|
22
|
+
puts "Before payment: #{order.status}, customer=#{order.customer_name.inspect}"
|
|
23
|
+
|
|
24
|
+
# The webhook: a real handler would verify a Stripe signature; a reference is always a bare id.
|
|
25
|
+
RUNTIME.dispatch_port(
|
|
26
|
+
"Pizzas", "Order", "PaymentGateway", "Receive",
|
|
27
|
+
flat: {
|
|
28
|
+
name: NAME,
|
|
29
|
+
customer_name: { value: "Chris" },
|
|
30
|
+
amount: { cents: 1200 }
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
sold = Order.find(NAME)
|
|
35
|
+
puts "After payment: #{sold.status}, customer=#{sold.customer_name.to_h}"
|
|
36
|
+
puts "Events: #{sold.events.map(&:name).join(', ')}"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Hecks.behaviors "Pizzas" do
|
|
2
|
+
vision "Examples of how a pizza is built, sold, and found through this domain."
|
|
3
|
+
|
|
4
|
+
# `pizzas_behaviors.hecksagon` is a Memory-persisted sibling of
|
|
5
|
+
# `pizzas.hecksagon` that lives OUTSIDE bluebook/, so an ordinary
|
|
6
|
+
# directory boot of examples/pizzas never globs it up alongside the
|
|
7
|
+
# real one.
|
|
8
|
+
loads "pizzas.bluebook", "../pizzas_behaviors.hecksagon"
|
|
9
|
+
|
|
10
|
+
test "CreatePizza puts a fresh pizza on the menu, available for sale" do
|
|
11
|
+
tests "CreatePizza", on: "Order"
|
|
12
|
+
input name: "Margherita", pizza: { price_cents: { cents: 1200 }, size: "large" }
|
|
13
|
+
expect status: "available", customer_name: nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
test "AddTopping refuses more than ten toppings" do
|
|
17
|
+
tests "AddTopping", on: "Order"
|
|
18
|
+
setup "CreatePizza", name: "Loaded", pizza: { price_cents: { cents: 1500 }, size: "large" }
|
|
19
|
+
setup "AddTopping", name: "Loaded", topping: "Basil", amount: 1
|
|
20
|
+
setup "AddTopping", name: "Loaded", topping: "Olives", amount: 1
|
|
21
|
+
setup "AddTopping", name: "Loaded", topping: "Mushroom", amount: 1
|
|
22
|
+
setup "AddTopping", name: "Loaded", topping: "Onion", amount: 1
|
|
23
|
+
setup "AddTopping", name: "Loaded", topping: "Pepper", amount: 1
|
|
24
|
+
setup "AddTopping", name: "Loaded", topping: "Sausage", amount: 1
|
|
25
|
+
setup "AddTopping", name: "Loaded", topping: "Bacon", amount: 1
|
|
26
|
+
setup "AddTopping", name: "Loaded", topping: "Pineapple", amount: 1
|
|
27
|
+
setup "AddTopping", name: "Loaded", topping: "Anchovy", amount: 1
|
|
28
|
+
setup "AddTopping", name: "Loaded", topping: "Jalapeno", amount: 1
|
|
29
|
+
input name: "Loaded", topping: "Extra Cheese", amount: 1
|
|
30
|
+
expect refused: "at most 10 toppings"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
test "Purchase refuses a pizza with no toppings on it" do
|
|
34
|
+
tests "Purchase", on: "Order"
|
|
35
|
+
setup "CreatePizza", name: "Bare", pizza: { price_cents: { cents: 900 }, size: "small" }
|
|
36
|
+
input name: "Bare", customer_name: "Chris", amount: { cents: 900 }
|
|
37
|
+
expect refused: "at least one topping"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
test "Purchase sells the pizza and closes its lifecycle" do
|
|
41
|
+
tests "Purchase", on: "Order"
|
|
42
|
+
setup "CreatePizza", name: "Sold Out", pizza: { price_cents: { cents: 1200 }, size: "large" }
|
|
43
|
+
setup "AddTopping", name: "Sold Out", topping: "Basil", amount: 2
|
|
44
|
+
input name: "Sold Out", customer_name: "Chris", amount: { cents: 1200 }
|
|
45
|
+
expect emits: ["PizzaPurchased"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
test "a sold pizza is no longer Available" do
|
|
49
|
+
tests "Available", on: "Order", kind: :query
|
|
50
|
+
setup "CreatePizza", name: "Fresh", pizza: { price_cents: { cents: 1000 }, size: "small" }
|
|
51
|
+
setup "CreatePizza", name: "Also Sold", pizza: { price_cents: { cents: 1100 }, size: "small" }
|
|
52
|
+
setup "AddTopping", name: "Also Sold", topping: "Basil", amount: 1
|
|
53
|
+
setup "Purchase", name: "Also Sold", customer_name: "Chris", amount: { cents: 1100 }
|
|
54
|
+
expect count: 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
test "CostingLessThan finds a pizza under the ceiling named" do
|
|
58
|
+
tests "CostingLessThan", on: "Order", kind: :query
|
|
59
|
+
setup "CreatePizza", name: "Bargain", pizza: { price_cents: { cents: 800 }, size: "small" }
|
|
60
|
+
setup "CreatePizza", name: "Splurge", pizza: { price_cents: { cents: 1800 }, size: "large" }
|
|
61
|
+
input ceiling: { cents: 1000 }
|
|
62
|
+
expect count: 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
test "Available's own row can be checked directly, not just counted" do
|
|
66
|
+
tests "Available", on: "Order", kind: :query
|
|
67
|
+
setup "CreatePizza", name: "Only One", pizza: { price_cents: { cents: 950 }, size: "small" }
|
|
68
|
+
expect status: "available", name: { value: "Only One" }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Hecks.bluebook "Pizzas" do
|
|
2
|
+
vision "Put toppings on a pizza and sell it to a customer."
|
|
3
|
+
supporting
|
|
4
|
+
|
|
5
|
+
aggregate "Order" do
|
|
6
|
+
description "An order that gathers toppings on a pizza and is eventually sold to a customer."
|
|
7
|
+
|
|
8
|
+
attribute :name, PizzaName
|
|
9
|
+
attribute :pizza, Pizza
|
|
10
|
+
attribute :toppings, list_of(Topping)
|
|
11
|
+
# Optional: unknown until `Purchase` sets it.
|
|
12
|
+
attribute :customer_name, CustomerName, optional: true
|
|
13
|
+
|
|
14
|
+
identified_by :name
|
|
15
|
+
|
|
16
|
+
value_object "PizzaName" do
|
|
17
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
18
|
+
|
|
19
|
+
invariant("a pizza is named") { !value.to_s.empty? }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
value_object "Price" do
|
|
23
|
+
attribute :cents, Integer
|
|
24
|
+
|
|
25
|
+
invariant("a price is never negative") { cents >= 0 }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
value_object "CustomerName" do
|
|
29
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
30
|
+
|
|
31
|
+
invariant("a customer is named") { !value.to_s.empty? }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
value_object "ToppingName" do
|
|
35
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
36
|
+
|
|
37
|
+
invariant("a topping is named") { !value.to_s.empty? }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
value_object "ToppingAmount" do
|
|
41
|
+
attribute :value, Integer
|
|
42
|
+
|
|
43
|
+
invariant("an amount is positive") { value.positive? }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
value_object "Topping" do
|
|
47
|
+
attribute :name, String, pattern: '[^ \t\n\r]'
|
|
48
|
+
attribute :amount, Integer
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A named, reusable closed set as a sibling value object; the inline `one_of(...)` shorthand
|
|
52
|
+
# cannot be spelled inside a `value_object` block.
|
|
53
|
+
value_object "Size" do
|
|
54
|
+
attribute :value, String, one_of: ["small", "large"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# What was ordered; who ordered it and payment state live on Order.
|
|
58
|
+
value_object "Pizza" do
|
|
59
|
+
attribute :price_cents, Price
|
|
60
|
+
attribute :size, Size
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
lifecycle :status, default: "available" do
|
|
64
|
+
transition "Purchase" => "sold", from: "available"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
query "Available" do
|
|
68
|
+
where(status: "available")
|
|
69
|
+
order_by :name
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Both price queries reach through the nested Pizza value object with a dotted path.
|
|
73
|
+
query "CostingLessThan" do
|
|
74
|
+
attribute :ceiling, Price
|
|
75
|
+
where(:"pizza.price_cents.cents" => { lt: :ceiling })
|
|
76
|
+
order_by :name
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A literal, not a kwarg: exercises the path where the where-clause number is stringified.
|
|
80
|
+
query "Expensive" do
|
|
81
|
+
where(:"pizza.price_cents.cents" => { gt: 1000 })
|
|
82
|
+
order_by :name
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
command "CreatePizza" do
|
|
86
|
+
role "Chef"
|
|
87
|
+
goal "Put a new pizza on the menu"
|
|
88
|
+
|
|
89
|
+
attribute :name, PizzaName
|
|
90
|
+
attribute :pizza, Pizza
|
|
91
|
+
|
|
92
|
+
sets :name
|
|
93
|
+
sets :pizza
|
|
94
|
+
|
|
95
|
+
emits PizzaCreated
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# AddTopping has no transition, so `from:` is its only lifecycle guard.
|
|
99
|
+
command "AddTopping", from: "available" do
|
|
100
|
+
role "Chef"
|
|
101
|
+
goal "Customize a pizza with an ingredient"
|
|
102
|
+
|
|
103
|
+
reference_to Order
|
|
104
|
+
attribute :topping, ToppingName
|
|
105
|
+
attribute :amount, ToppingAmount
|
|
106
|
+
|
|
107
|
+
given("at most 10 toppings") { toppings.size < 10 }
|
|
108
|
+
|
|
109
|
+
sets :toppings, append: { name: :topping, amount: :amount }
|
|
110
|
+
|
|
111
|
+
emits ToppingAdded
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
command "Purchase", from: "available" do
|
|
115
|
+
role "Customer"
|
|
116
|
+
goal "Buy the pizza"
|
|
117
|
+
|
|
118
|
+
reference_to Order
|
|
119
|
+
attribute :amount, Price
|
|
120
|
+
|
|
121
|
+
given("a pizza needs at least one topping") { toppings.size.positive? }
|
|
122
|
+
given("a payment was actually made") { amount.cents.positive? }
|
|
123
|
+
# Checks the payment itself is plausible (e.g. an underpayment), not whether one happened.
|
|
124
|
+
given("the payment covers the price") { amount.cents >= pizza.price_cents.cents }
|
|
125
|
+
|
|
126
|
+
# The lifecycle transition sets status; a `sets` on the lifecycle field is refused at build.
|
|
127
|
+
sets :customer_name
|
|
128
|
+
|
|
129
|
+
emits PizzaPurchased
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A payment arriving is an external fact; the business rules stay on Purchase.
|
|
134
|
+
policy "OnPizzaPaymentReceived" do
|
|
135
|
+
on Order::PizzaPaymentReceived
|
|
136
|
+
trigger Order::Purchase
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Hecks.hecksagon "Pizzas" do
|
|
2
|
+
# Role declarations only enforce access once Governance can check them; this standalone
|
|
3
|
+
# deploy attaches and binds Governance itself.
|
|
4
|
+
attaches "Governance"
|
|
5
|
+
Pizzas::Order.persisted_by("Memory")
|
|
6
|
+
|
|
7
|
+
# The primary port: an outside adapter (a payment webhook) translates an external fact into
|
|
8
|
+
# our vocabulary; business rules stay on Purchase, reached via OnPizzaPaymentReceived.
|
|
9
|
+
Pizzas::Order.port "PaymentGateway" do
|
|
10
|
+
operation "Receive" do
|
|
11
|
+
attribute :customer_name, CustomerName
|
|
12
|
+
attribute :amount, Price
|
|
13
|
+
emits "PizzaPaymentReceived"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Hecks.hecksagon "Governance" do
|
|
19
|
+
Governance::RoleAssignment.persisted_by("Memory")
|
|
20
|
+
Governance::RoleTransition.persisted_by("Memory")
|
|
21
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"audience": "Pizzas",
|
|
3
|
+
"scopes": [
|
|
4
|
+
{
|
|
5
|
+
"scope": "pizzas:order.add_topping",
|
|
6
|
+
"verb": "Pizzas::Order.AddTopping",
|
|
7
|
+
"role": "Chef"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"scope": "pizzas:order.create_pizza",
|
|
11
|
+
"verb": "Pizzas::Order.CreatePizza",
|
|
12
|
+
"role": "Chef"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"scope": "pizzas:order.purchase",
|
|
16
|
+
"verb": "Pizzas::Order.Purchase",
|
|
17
|
+
"role": "Customer"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"roles": [
|
|
21
|
+
"Chef",
|
|
22
|
+
"Customer"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Memory-bound sibling of pizzas.hecksagon: `pizzas.behaviors` boots a fresh runtime per
|
|
2
|
+
# test, so it loads this sibling instead of the one beside the bluebook.
|
|
3
|
+
Hecks.hecksagon "Pizzas" do
|
|
4
|
+
attaches "Governance"
|
|
5
|
+
Pizzas::Order.persisted_by("Memory")
|
|
6
|
+
|
|
7
|
+
Pizzas::Order.port "PaymentGateway" do
|
|
8
|
+
operation "Receive" do
|
|
9
|
+
attribute :customer_name, CustomerName
|
|
10
|
+
attribute :amount, Price
|
|
11
|
+
emits "PizzaPaymentReceived"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Hecks.hecksagon "Governance" do
|
|
17
|
+
Governance::RoleAssignment.persisted_by("Memory")
|
|
18
|
+
Governance::RoleTransition.persisted_by("Memory")
|
|
19
|
+
end
|
data/lib/hecks/cli/console.rb
CHANGED
|
@@ -47,12 +47,15 @@ module Hecks
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# @param runtime [Runtime] a booted domain
|
|
50
|
-
# @return [String] each chapter's vision and its aggregates' commands
|
|
50
|
+
# @return [String] each chapter's vision, and its aggregates' descriptions and commands
|
|
51
51
|
def overview(runtime)
|
|
52
52
|
runtime.registry.bluebooks.each_value.map do |bluebook|
|
|
53
53
|
aggregates = bluebook.aggregates.map do |aggregate|
|
|
54
54
|
commands = aggregate.commands.map { |command| "#{Naming.snake(command.hecks_name)}!" }.sort.join(", ")
|
|
55
|
-
|
|
55
|
+
description = aggregate.description.to_s.strip
|
|
56
|
+
lines = [" #{aggregate.name}: #{commands}"]
|
|
57
|
+
lines << " #{description}" unless description.empty?
|
|
58
|
+
lines.join("\n")
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
"\n #{bluebook.name} — #{bluebook.vision}\n#{aggregates.join("\n")}\n"
|
|
@@ -120,6 +120,7 @@ module Hecks
|
|
|
120
120
|
|
|
121
121
|
name = argv.first
|
|
122
122
|
return { answer: [cli[:usage], 0] } if name.nil? || %w[--help -h help].include?(name)
|
|
123
|
+
return { answer: [all_usage(bluebook, options), 0] } if name == "--all"
|
|
123
124
|
|
|
124
125
|
asking, name, argv = entry_word(cli, argv)
|
|
125
126
|
return { answer: [cli[:usage], 1] } if name.nil?
|
|
@@ -141,6 +142,11 @@ module Hecks
|
|
|
141
142
|
launcher: launcher }
|
|
142
143
|
end
|
|
143
144
|
|
|
145
|
+
# The usage with the internal commands and queries listed too, which `--all` asks for.
|
|
146
|
+
def all_usage(bluebook, options)
|
|
147
|
+
Projector.call(:cli, bluebook: bluebook, options: options.merge(all: true))[:usage]
|
|
148
|
+
end
|
|
149
|
+
|
|
144
150
|
# Reads the entry words of a line: whether it asks a query, the bare name, and the words
|
|
145
151
|
# from the name on.
|
|
146
152
|
#
|
|
@@ -403,41 +403,70 @@ module Hecks
|
|
|
403
403
|
out = ["#{bluebook.name} — #{bluebook.vision}", "",
|
|
404
404
|
" #{program} <command>! [name=value …] do something",
|
|
405
405
|
" #{program} query <query> [name=value …] read something", ""]
|
|
406
|
-
|
|
407
|
-
out << "commands:"
|
|
408
|
-
out.concat(listing(commands) { |spec| spec[:summary] })
|
|
409
|
-
out << ""
|
|
410
|
-
out << "queries (nothing here changes anything):"
|
|
411
|
-
out.concat(listing(questions) { |spec| first_sentence(spec[:summary]) })
|
|
406
|
+
out.concat(tables(commands, questions, all: options[:all], notes: aggregate_notes(bluebook)))
|
|
412
407
|
out << ""
|
|
413
408
|
out << " #{program} <command> --help what one command wants, and every way it refuses"
|
|
409
|
+
out.concat(all_hint(program, commands, questions)) unless options[:all]
|
|
414
410
|
out << " a command is called with its aggregate — #{example_qualified(commands)}"
|
|
415
411
|
out.join("\n")
|
|
416
412
|
end
|
|
417
413
|
|
|
414
|
+
# The commands table then the queries table.
|
|
415
|
+
def tables(commands, questions, all:, notes:)
|
|
416
|
+
["commands:", *listing(commands, all: all, notes: notes) { |spec| spec[:summary] }, "",
|
|
417
|
+
"queries (nothing here changes anything):",
|
|
418
|
+
*listing(questions, all: all, notes: notes) { |spec| first_sentence(spec[:summary]) }]
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# The line saying the internal commands and queries were left out and how to list them,
|
|
422
|
+
# or no line when there are none.
|
|
423
|
+
def all_hint(program, commands, questions)
|
|
424
|
+
hidden = (commands.values + questions.values).count { |spec| spec[:internal] }
|
|
425
|
+
return [] if hidden.zero?
|
|
426
|
+
|
|
427
|
+
[" #{program} --all also list the #{hidden} internal commands and queries"]
|
|
428
|
+
end
|
|
429
|
+
|
|
418
430
|
# The command or question lines of the help. A domain with more than one aggregate is listed
|
|
419
431
|
# under a heading per aggregate, so related commands sit together; the heading is the prefix
|
|
420
432
|
# every call to them carries, so the lines under it leave it out. The bookkeeping a run
|
|
421
|
-
# records about itself (`internal`: system-role commands and port operations) is
|
|
422
|
-
#
|
|
423
|
-
# list, each name in full.
|
|
424
|
-
|
|
433
|
+
# records about itself (`internal`: system-role commands and port operations) is left out,
|
|
434
|
+
# since a person never types them; `all` lists them as names only. A single-aggregate
|
|
435
|
+
# domain keeps the plain list, each name in full.
|
|
436
|
+
#
|
|
437
|
+
# @param notes [Hash{String => String}] a line of prose per aggregate, shown under its heading
|
|
438
|
+
def listing(specs, all: false, notes: {}, &description)
|
|
425
439
|
shown, internal = specs.values.partition { |spec| !spec[:internal] }
|
|
426
|
-
|
|
427
|
-
grouped = groups.length > 1
|
|
440
|
+
grouped = shown.map { |spec| spec[:group] }.uniq.length > 1
|
|
428
441
|
named = shown.to_h { |spec| [spec, entry_name(spec, grouped)] }
|
|
442
|
+
lines = listed_rows(shown, named, grouped, notes, &description)
|
|
443
|
+
lines.concat(internal_lines(internal)) if all && !internal.empty?
|
|
444
|
+
lines
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# The rows of one table: one line per spec, under a heading per aggregate when grouped.
|
|
448
|
+
def listed_rows(shown, named, grouped, notes, &description)
|
|
429
449
|
width = named.values.map(&:length).max.to_i
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
end
|
|
436
|
-
else
|
|
437
|
-
shown.each { |spec| lines << " #{named[spec].ljust(width)} #{yield(spec)}#{alias_note(spec)}" }
|
|
450
|
+
row = ->(spec, indent) { "#{indent}#{named[spec].ljust(width)} #{description.call(spec)}#{alias_note(spec)}" }
|
|
451
|
+
return shown.map { |spec| row.call(spec, " ") } unless grouped
|
|
452
|
+
|
|
453
|
+
shown.group_by { |spec| spec[:group] }.flat_map do |group, members|
|
|
454
|
+
[*heading_lines(group, notes), *members.map { |spec| row.call(spec, " ") }]
|
|
438
455
|
end
|
|
439
|
-
|
|
440
|
-
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# The lines that open an aggregate's group: its heading, then its note when it has one.
|
|
459
|
+
def heading_lines(group, notes)
|
|
460
|
+
return [] unless group
|
|
461
|
+
|
|
462
|
+
[" #{heading(group)}", *notes[group]&.then { |note| " #{note}" }]
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# The first sentence of each aggregate's description, keyed by aggregate name; an aggregate
|
|
466
|
+
# with no description has no entry.
|
|
467
|
+
def aggregate_notes(bluebook)
|
|
468
|
+
bluebook.aggregates.to_h { |aggregate| [aggregate.hecks_name, first_sentence(aggregate.description)] }
|
|
469
|
+
.reject { |_, note| note.empty? }
|
|
441
470
|
end
|
|
442
471
|
|
|
443
472
|
# The aggregate's own name as a heading: the prefix of every call to the lines under it.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "shellwords"
|
|
5
|
+
require_relative "agent_profile"
|
|
5
6
|
|
|
6
7
|
module Hecks
|
|
7
8
|
module Adapters
|
|
@@ -27,10 +28,11 @@ module Hecks
|
|
|
27
28
|
# The command an ask runs.
|
|
28
29
|
#
|
|
29
30
|
# @param command [String, Array<String>, nil] a command line, or its words
|
|
30
|
-
# @
|
|
31
|
-
|
|
31
|
+
# @param profile [AgentProfile, nil] when given, the default command takes its tools, budget
|
|
32
|
+
# @return [Array<String>] `command` as words; `QA_MINER_AGENT` else the default when nil
|
|
33
|
+
def command_for(command = nil, profile = nil)
|
|
32
34
|
command ||= ENV.fetch("QA_MINER_AGENT", nil)
|
|
33
|
-
return
|
|
35
|
+
return default_command(profile) unless command
|
|
34
36
|
|
|
35
37
|
command.is_a?(String) ? Shellwords.split(command) : command
|
|
36
38
|
end
|
|
@@ -41,11 +43,13 @@ module Hecks
|
|
|
41
43
|
# @param command [String, Array<String>, nil] the agent's command; see `command_for`
|
|
42
44
|
# @param chdir [String] the directory the agent works in
|
|
43
45
|
# @param log [String, nil] a file the agent's output is appended to
|
|
46
|
+
# @param profile [AgentProfile, nil] what the run may do; nil runs it as the caller could
|
|
44
47
|
# @return [String] the agent's output
|
|
45
|
-
# @raise [Failed] when the agent cannot start or exits non-zero
|
|
46
|
-
def ask(prompt:, chdir:, command: nil, log: nil)
|
|
47
|
-
words = command_for(command)
|
|
48
|
-
|
|
48
|
+
# @raise [Failed] when the agent cannot start, runs past its timeout or exits non-zero
|
|
49
|
+
def ask(prompt:, chdir:, command: nil, log: nil, profile: nil)
|
|
50
|
+
words = command_for(command, profile)
|
|
51
|
+
refuse_unconfinable!(words, profile)
|
|
52
|
+
output, status = run(words, prompt, chdir, profile)
|
|
49
53
|
File.open(log, "a") { |file| file.puts(output) } if log
|
|
50
54
|
return output if status.success?
|
|
51
55
|
|
|
@@ -53,6 +57,58 @@ module Hecks
|
|
|
53
57
|
rescue SystemCallError => e
|
|
54
58
|
raise Failed, "agent could not start (#{words.first}): #{e.message}"
|
|
55
59
|
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Permission rules are a feature of `claude`: any other command would run unconfined.
|
|
64
|
+
def refuse_unconfinable!(words, profile)
|
|
65
|
+
return if profile.nil? || profile.sandboxed? || words == default_command(profile)
|
|
66
|
+
|
|
67
|
+
raise Failed, "permission confinement applies only to the default claude command"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def run(words, prompt, chdir, profile)
|
|
71
|
+
return run_confined(words, prompt, chdir, profile) if profile
|
|
72
|
+
|
|
73
|
+
Open3.capture2e(*words, stdin_data: prompt, chdir: chdir)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def default_command(profile)
|
|
77
|
+
return DEFAULT_COMMAND unless profile
|
|
78
|
+
|
|
79
|
+
%w[claude -p --permission-mode] + [profile.permission_mode] + profile.tool_flags
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Runs `words` under the profile's sandbox with only the environment it names, in its own
|
|
83
|
+
# process group so a timeout takes the agent's children with it.
|
|
84
|
+
def run_confined(words, prompt, chdir, profile)
|
|
85
|
+
raise Failed, "no sandbox here; refusing to run an agent unconfined" unless profile.available?
|
|
86
|
+
|
|
87
|
+
Open3.popen2e(profile.environment, *profile.confine(words),
|
|
88
|
+
chdir: chdir, unsetenv_others: true, pgroup: true) do |stdin, out, waiter|
|
|
89
|
+
feed(stdin, prompt)
|
|
90
|
+
reader = Thread.new { out.read }
|
|
91
|
+
finish(waiter, reader, profile.timeout)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def feed(stdin, prompt)
|
|
96
|
+
stdin.write(prompt)
|
|
97
|
+
rescue Errno::EPIPE
|
|
98
|
+
nil
|
|
99
|
+
ensure
|
|
100
|
+
stdin.close
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def finish(waiter, reader, timeout)
|
|
104
|
+
unless waiter.join(timeout)
|
|
105
|
+
Process.kill("KILL", -waiter.pid)
|
|
106
|
+
waiter.join
|
|
107
|
+
reader.join
|
|
108
|
+
raise Failed, "agent timed out after #{timeout}s"
|
|
109
|
+
end
|
|
110
|
+
[reader.value, waiter.value]
|
|
111
|
+
end
|
|
56
112
|
end
|
|
57
113
|
end
|
|
58
114
|
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
# What one `Agent` run may do: which tools it holds, where it may write, what it may reach on
|
|
8
|
+
# the network, which environment variables it sees and how long it may run.
|
|
9
|
+
#
|
|
10
|
+
# Confinement uses the macOS sandbox. Where there is none, a confined run refuses to start
|
|
11
|
+
# rather than run unconfined.
|
|
12
|
+
class AgentProfile
|
|
13
|
+
# What the network rule lets through: nothing, outbound HTTPS and name lookups, or anything.
|
|
14
|
+
NETWORKS = %i[none https any].freeze
|
|
15
|
+
|
|
16
|
+
# How a run is confined: under the operating system's sandbox, or by the permission rules
|
|
17
|
+
# of the `claude` command itself. Only the second leaves the user's own login usable, since
|
|
18
|
+
# the sandbox refuses the keychain and `~/.claude`.
|
|
19
|
+
CONFINEMENTS = %i[sandbox permissions].freeze
|
|
20
|
+
|
|
21
|
+
# The variables every confined agent sees besides the ones its profile names.
|
|
22
|
+
BASE_ENV = %w[PATH HOME USER LOGNAME SHELL LANG LC_ALL TERM TMPDIR].freeze
|
|
23
|
+
|
|
24
|
+
# The tools that write files; a permissions-confined run holds them only through path rules.
|
|
25
|
+
WRITING_TOOLS = %w[Write Edit MultiEdit NotebookEdit].freeze
|
|
26
|
+
|
|
27
|
+
# Home-relative places an agent may not read: credentials and the harness's own state.
|
|
28
|
+
CREDENTIAL_PATHS = %w[
|
|
29
|
+
.ssh .aws .gnupg .claude .config/gh .config/op .gem/credentials .netrc Library/Keychains
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
SANDBOX = "/usr/bin/sandbox-exec"
|
|
33
|
+
|
|
34
|
+
# @return [Array<String>] the tools the agent is given
|
|
35
|
+
attr_reader :tools
|
|
36
|
+
# @return [Array<String>] the directories the agent may write, besides the temp directory
|
|
37
|
+
attr_reader :writable
|
|
38
|
+
# @return [Symbol] one of `NETWORKS`
|
|
39
|
+
attr_reader :network
|
|
40
|
+
# @return [Array<String>] the environment variable names the agent may see besides `BASE_ENV`
|
|
41
|
+
attr_reader :env
|
|
42
|
+
# @return [Integer, nil] seconds the run may take; nil for no limit
|
|
43
|
+
attr_reader :timeout
|
|
44
|
+
# @return [Float, nil] the most the run may spend, in dollars, for an agent that takes one
|
|
45
|
+
attr_reader :budget
|
|
46
|
+
# @return [Symbol] one of `CONFINEMENTS`
|
|
47
|
+
attr_reader :confinement
|
|
48
|
+
|
|
49
|
+
# @param tools [Array<String>] the tools the agent is given
|
|
50
|
+
# @param writable [Array<String>] directories the agent may write
|
|
51
|
+
# @param network [Symbol] one of `NETWORKS`
|
|
52
|
+
# @param env [Array<String>] environment variable names to pass through
|
|
53
|
+
# @param timeout [Integer, nil] seconds the run may take
|
|
54
|
+
# @param budget [Float, nil] dollars the run may spend
|
|
55
|
+
# @param confinement [Symbol] one of `CONFINEMENTS`; `network` applies to the sandbox only
|
|
56
|
+
# @raise [ArgumentError] when `network` or `confinement` is not one it knows
|
|
57
|
+
def initialize(tools: [], writable: [], network: :none, env: [], timeout: nil, budget: nil,
|
|
58
|
+
confinement: :sandbox)
|
|
59
|
+
unless NETWORKS.include?(network)
|
|
60
|
+
raise ArgumentError, "network must be one of #{NETWORKS.inspect}, not #{network.inspect}"
|
|
61
|
+
end
|
|
62
|
+
unless CONFINEMENTS.include?(confinement)
|
|
63
|
+
raise ArgumentError, "confinement must be one of #{CONFINEMENTS.inspect}, not #{confinement.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@tools = tools.dup.freeze
|
|
67
|
+
@writable = writable.dup.freeze
|
|
68
|
+
@network = network
|
|
69
|
+
@env = env.dup.freeze
|
|
70
|
+
@timeout = timeout
|
|
71
|
+
@budget = budget
|
|
72
|
+
@confinement = confinement
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Boolean] whether this profile is enforced by the operating system's sandbox
|
|
76
|
+
def sandboxed?
|
|
77
|
+
confinement == :sandbox
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Boolean] whether this machine can apply this profile's confinement
|
|
81
|
+
def available?
|
|
82
|
+
!sandboxed? || File.executable?(SANDBOX)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param words [Array<String>] the agent's command
|
|
86
|
+
# @return [Array<String>] `words`, run under the sandbox with this profile's policy; the
|
|
87
|
+
# words unchanged when the confinement is the command's own permission rules
|
|
88
|
+
def confine(words)
|
|
89
|
+
sandboxed? ? [SANDBOX, "-p", policy, *words] : words
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [String] the `claude` permission mode this profile runs under
|
|
93
|
+
def permission_mode
|
|
94
|
+
sandboxed? ? "acceptEdits" : "dontAsk"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @param source [Hash{String => String}] the environment to take values from
|
|
98
|
+
# @return [Hash{String => String}] only the variables this profile lets the agent see
|
|
99
|
+
def environment(source = ENV)
|
|
100
|
+
source.to_h.slice(*BASE_ENV, *env)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The `claude` flags this profile's tools and budget become.
|
|
104
|
+
#
|
|
105
|
+
# @return [Array<String>]
|
|
106
|
+
def tool_flags
|
|
107
|
+
flags = tools.empty? ? [] : ["--tools", tools.join(","), "--allowedTools", *allowed_tools]
|
|
108
|
+
flags += ["--strict-mcp-config"] unless sandboxed?
|
|
109
|
+
flags + (budget ? ["--max-budget-usd", budget.to_s] : [])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [String] the sandbox policy: reads open but for credentials, writes where allowed
|
|
113
|
+
def policy
|
|
114
|
+
[
|
|
115
|
+
"(version 1)", "(allow default)",
|
|
116
|
+
"(deny file-write*)", write_rule, "(deny file-read*#{paths_clause(credential_paths)})",
|
|
117
|
+
*network_rules
|
|
118
|
+
].join("\n")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# The tools allowed without asking. A permissions-confined run holds the writing tools only
|
|
124
|
+
# through an `Edit` rule per writable directory, which every file-writing tool answers to.
|
|
125
|
+
def allowed_tools
|
|
126
|
+
return [tools.join(",")] if sandboxed?
|
|
127
|
+
|
|
128
|
+
tools - WRITING_TOOLS + writable.map { |path| "Edit(/#{real(path)}/**)" }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def write_rule
|
|
132
|
+
allowed = [real(Dir.tmpdir), *writable.map { |path| real(path) }]
|
|
133
|
+
devices = '(literal "/dev/null") (literal "/dev/tty") (regex #"^/dev/fd/")'
|
|
134
|
+
"(allow file-write*#{paths_clause(allowed)} #{devices})"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def credential_paths
|
|
138
|
+
CREDENTIAL_PATHS.map { |path| File.join(Dir.home, path) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def paths_clause(paths)
|
|
142
|
+
paths.map { |path| " (subpath #{quote(path)})" }.join
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def network_rules
|
|
146
|
+
case network
|
|
147
|
+
when :any then []
|
|
148
|
+
when :https then ["(deny network*)", '(allow network-outbound (remote tcp "*:443") (remote udp "*:53"))',
|
|
149
|
+
'(allow network-outbound (remote unix-socket (path-literal "/private/var/run/mDNSResponder")))']
|
|
150
|
+
else ["(deny network*)"]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def real(path)
|
|
155
|
+
File.realpath(path)
|
|
156
|
+
rescue SystemCallError
|
|
157
|
+
File.expand_path(path)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def quote(path)
|
|
161
|
+
%("#{path.gsub('\\', '\\\\\\\\').gsub('"', '\\"')}")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -16,23 +16,28 @@ module Hecks
|
|
|
16
16
|
# The command behind `hecks quality_control target.mine_combinations`: asks an agent to mine the
|
|
17
17
|
# adversarial corpus for new domain combinations, then checks them. It is opt-in and never run
|
|
18
18
|
# by `hecks quality_control sweep.tick` (an agent call costs money and answers differently each
|
|
19
|
-
# run).
|
|
20
|
-
# `--
|
|
21
|
-
# `--
|
|
19
|
+
# run). `--brief` prints the agent's prompt and stops; `--from <dir>` re-checks an earlier run;
|
|
20
|
+
# `--against <domain>` narrows the census; `--agent "cmd"` reads the prompt on stdin;
|
|
21
|
+
# `--confine` limits the default agent to writing the run's candidates directory.
|
|
22
22
|
#
|
|
23
23
|
# The agent defaults to `claude -p` (`QA_MINER_AGENT` overrides); candidates that boot are
|
|
24
24
|
# checked by `hecks quality_control target.check_generated_domains --source`, whose report is
|
|
25
25
|
# this command's report. Exit 0: all clean. 2: a finding (see
|
|
26
|
-
# `.claude/skills/hecks_qa/SKILL.md`). 1: an operational
|
|
27
|
-
# error.
|
|
26
|
+
# `.claude/skills/hecks_qa/SKILL.md`). 1: an operational error.
|
|
28
27
|
class QaMineCombinations
|
|
29
28
|
EXIT_OK = 0
|
|
30
29
|
EXIT_ERROR = 1
|
|
31
30
|
RESULT_MARKER = "QA_GENERATED_RESULT "
|
|
32
31
|
|
|
32
|
+
# What `--confine` lets the default agent do: read anything, write the candidates directory,
|
|
33
|
+
# run for twenty minutes and spend two dollars.
|
|
34
|
+
CONFINED_TOOLS = %w[Read Glob Grep Write Edit].freeze
|
|
35
|
+
CONFINED_TIMEOUT = 1200
|
|
36
|
+
CONFINED_BUDGET = 2.0
|
|
37
|
+
|
|
33
38
|
USAGE = "usage: hecks quality_control mine_combinations [--candidates N] [--rust] [--seeds K] " \
|
|
34
39
|
"[--steps M] " \
|
|
35
|
-
"[--adversarial F] [--repair-rounds R] [--agent CMD] [--from <candidates-dir>] " \
|
|
40
|
+
"[--adversarial F] [--repair-rounds R] [--agent CMD] [--confine] [--from <candidates-dir>] " \
|
|
36
41
|
"[--against <domain> …] [--brief]"
|
|
37
42
|
|
|
38
43
|
Miner = Hecks::Fuzzing::CombinationMiner
|
|
@@ -65,7 +70,8 @@ module Hecks
|
|
|
65
70
|
@out_dir = @options[:from] || File.join(@run_dir, "candidates")
|
|
66
71
|
@agent_log = File.join(@run_dir, "agent.log")
|
|
67
72
|
@agent = Hecks::Adapters::Agent.new
|
|
68
|
-
@
|
|
73
|
+
@profile = confinement_profile if @options[:confine]
|
|
74
|
+
@command = @agent.command_for(@options[:agent], @profile)
|
|
69
75
|
corpus = @options[:against].empty? ? Miner.corpus_paths(@root) : @options[:against]
|
|
70
76
|
@brief = Miner.brief(corpus, root: @root, bug_titles: Miner.recent_bug_titles(@root))
|
|
71
77
|
prompt = Miner.prompt(@root, @brief, count: @options[:candidates], out_dir: @out_dir)
|
|
@@ -89,7 +95,7 @@ module Hecks
|
|
|
89
95
|
|
|
90
96
|
def parse(argv)
|
|
91
97
|
options = { candidates: 3, rust: false, seeds: 5, steps: 25, adversarial: 0.3, repair_rounds: 1,
|
|
92
|
-
agent: nil, from: nil, brief: false, against: [] }
|
|
98
|
+
agent: nil, from: nil, brief: false, confine: false, against: [] }
|
|
93
99
|
until argv.empty?
|
|
94
100
|
arg = argv.shift
|
|
95
101
|
if %w[-h --help].include?(arg)
|
|
@@ -108,6 +114,7 @@ module Hecks
|
|
|
108
114
|
options[key] = reader.call(argv.shift)
|
|
109
115
|
when "--rust" then options[:rust] = true
|
|
110
116
|
when "--brief" then options[:brief] = true
|
|
117
|
+
when "--confine" then options[:confine] = true
|
|
111
118
|
when "--against" then options[:against] << File.expand_path(argv.shift)
|
|
112
119
|
else abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
|
|
113
120
|
end
|
|
@@ -124,12 +131,19 @@ module Hecks
|
|
|
124
131
|
|
|
125
132
|
# Returns nil when the agent finished, else why it did not.
|
|
126
133
|
def ask_agent(prompt)
|
|
127
|
-
@agent.ask(prompt: prompt, command: @command, chdir: @root, log: @agent_log)
|
|
134
|
+
@agent.ask(prompt: prompt, command: @command, chdir: @root, log: @agent_log, profile: @profile)
|
|
128
135
|
nil
|
|
129
136
|
rescue Hecks::Adapters::Agent::Failed => e
|
|
130
137
|
e.message
|
|
131
138
|
end
|
|
132
139
|
|
|
140
|
+
# The profile `--confine` runs the agent under: it writes only the candidates directory.
|
|
141
|
+
def confinement_profile
|
|
142
|
+
FileUtils.mkdir_p(@out_dir)
|
|
143
|
+
Hecks::Adapters::AgentProfile.new(confinement: :permissions, tools: CONFINED_TOOLS, writable: [@out_dir],
|
|
144
|
+
timeout: CONFINED_TIMEOUT, budget: CONFINED_BUDGET)
|
|
145
|
+
end
|
|
146
|
+
|
|
133
147
|
def boot_error(candidate, boot_root)
|
|
134
148
|
dir = Hecks::Fuzzing::DomainGenerator.write(
|
|
135
149
|
{ "source" => File.read(candidate[:bluebook]), "aggregates" => [], "policies" => [] },
|
data/lib/hecks/version.rb
CHANGED
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.1.
|
|
1
|
+
3.1.3
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hecks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.1.
|
|
4
|
+
version: 3.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Young
|
|
@@ -35,6 +35,14 @@ executables:
|
|
|
35
35
|
extensions: []
|
|
36
36
|
extra_rdoc_files: []
|
|
37
37
|
files:
|
|
38
|
+
- examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb
|
|
39
|
+
- examples/pizzas/bluebook/pizzas.behaviors
|
|
40
|
+
- examples/pizzas/bluebook/pizzas.bluebook
|
|
41
|
+
- examples/pizzas/bluebook/pizzas.hecksagon
|
|
42
|
+
- examples/pizzas/bluebook/pizzas.world
|
|
43
|
+
- examples/pizzas/bluebook/translations/2-77625c.bluebook
|
|
44
|
+
- examples/pizzas/oidc.json
|
|
45
|
+
- examples/pizzas/pizzas_behaviors.hecksagon
|
|
38
46
|
- exe/hecks
|
|
39
47
|
- lib/hecks.rb
|
|
40
48
|
- lib/hecks/adapters.rb
|
|
@@ -618,6 +626,7 @@ files:
|
|
|
618
626
|
- lib/hecks/projector/target.rb
|
|
619
627
|
- lib/hecks/quality_control/adapters/agent.adapter
|
|
620
628
|
- lib/hecks/quality_control/adapters/agent.rb
|
|
629
|
+
- lib/hecks/quality_control/adapters/agent_profile.rb
|
|
621
630
|
- lib/hecks/quality_control/adapters/angle_tools.adapter
|
|
622
631
|
- lib/hecks/quality_control/adapters/angle_tools.rb
|
|
623
632
|
- lib/hecks/quality_control/adapters/clearance_recorder.rb
|