hecks 3.14.0 → 4.0.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/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +2 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +3 -80
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +7 -13
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value.rb +13 -0
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +300 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +145 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +990 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +39 -7
- data/rust/host/src/payments/tests.rs +1427 -282
- data/rust/host/src/payments.rs +391 -112
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +483 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -1
- data/rust/parser/src/build/named_pattern.rs +4 -1
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -516
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +22 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +229 -112
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +230 -148
- data/rust/src/kernel/mod.rs +46 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +82 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/rust/host/src/approval.rs +0 -652
|
@@ -62,7 +62,7 @@ module Hecks
|
|
|
62
62
|
|
|
63
63
|
# Permission rules are a feature of `claude`: any other command would run unconfined.
|
|
64
64
|
def refuse_unconfinable!(words, profile)
|
|
65
|
-
return if profile.nil? ||
|
|
65
|
+
return if profile.nil? || words == default_command(profile)
|
|
66
66
|
|
|
67
67
|
raise Failed, "permission confinement applies only to the default claude command"
|
|
68
68
|
end
|
|
@@ -79,15 +79,14 @@ module Hecks
|
|
|
79
79
|
%w[claude -p --permission-mode] + [profile.permission_mode] + profile.tool_flags
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
# Runs `words`
|
|
83
|
-
#
|
|
82
|
+
# Runs `words` with only the environment the profile names, in its own process group so a
|
|
83
|
+
# timeout takes the agent's children with it. The reader starts first so an agent that
|
|
84
|
+
# writes before it has read its whole prompt cannot block both sides on a full pipe.
|
|
84
85
|
def run_confined(words, prompt, chdir, profile)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Open3.popen2e(profile.environment, *profile.confine(words),
|
|
86
|
+
Open3.popen2e(profile.environment, *words,
|
|
88
87
|
chdir: chdir, unsetenv_others: true, pgroup: true) do |stdin, out, waiter|
|
|
89
|
-
feed(stdin, prompt)
|
|
90
88
|
reader = Thread.new { out.read }
|
|
89
|
+
feed(stdin, prompt)
|
|
91
90
|
finish(waiter, reader, profile.timeout)
|
|
92
91
|
end
|
|
93
92
|
end
|
|
@@ -1,93 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "tmpdir"
|
|
4
|
-
|
|
5
3
|
module Hecks
|
|
6
4
|
module Adapters
|
|
7
|
-
# What one `Agent` run may do: which tools it holds, where it may write,
|
|
8
|
-
#
|
|
5
|
+
# What one `Agent` run may do: which tools it holds, where it may write, which environment
|
|
6
|
+
# variables it sees and how long it may run.
|
|
9
7
|
#
|
|
10
|
-
# Confinement
|
|
11
|
-
#
|
|
8
|
+
# Confinement is the permission rules of the `claude` command itself, so a run keeps the
|
|
9
|
+
# user's own login. Only the default `claude` command understands them; `Agent` refuses to
|
|
10
|
+
# run any other command under a profile.
|
|
12
11
|
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
12
|
# The variables every confined agent sees besides the ones its profile names.
|
|
22
13
|
BASE_ENV = %w[PATH HOME USER LOGNAME SHELL LANG LC_ALL TERM TMPDIR].freeze
|
|
23
14
|
|
|
24
|
-
# The tools that write files; a
|
|
15
|
+
# The tools that write files; a confined run holds them only through path rules.
|
|
25
16
|
WRITING_TOOLS = %w[Write Edit MultiEdit NotebookEdit].freeze
|
|
26
17
|
|
|
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
|
-
# The keywords `new` takes besides the four lists, with what each defaults to.
|
|
35
|
-
LIMIT_DEFAULTS = { timeout: nil, budget: nil, confinement: :sandbox }.freeze
|
|
36
|
-
|
|
37
18
|
# @return [Array<String>] the tools the agent is given
|
|
38
19
|
attr_reader :tools
|
|
39
|
-
# @return [Array<String>] the directories the agent may write
|
|
20
|
+
# @return [Array<String>] the directories the agent may write
|
|
40
21
|
attr_reader :writable
|
|
41
|
-
# @return [Symbol] one of `NETWORKS`
|
|
42
|
-
attr_reader :network
|
|
43
22
|
# @return [Array<String>] the environment variable names the agent may see besides `BASE_ENV`
|
|
44
23
|
attr_reader :env
|
|
45
24
|
# @return [Integer, nil] seconds the run may take; nil for no limit
|
|
46
25
|
attr_reader :timeout
|
|
47
26
|
# @return [Float, nil] the most the run may spend, in dollars, for an agent that takes one
|
|
48
27
|
attr_reader :budget
|
|
49
|
-
# @return [Symbol] one of `CONFINEMENTS`
|
|
50
|
-
attr_reader :confinement
|
|
51
28
|
|
|
52
29
|
# @param tools [Array<String>] the tools the agent is given
|
|
53
30
|
# @param writable [Array<String>] directories the agent may write
|
|
54
|
-
# @param network [Symbol] one of `NETWORKS`
|
|
55
31
|
# @param env [Array<String>] environment variable names to pass through
|
|
56
|
-
# @param
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
# @raise [ArgumentError] when `network` or `confinement` is not one it knows
|
|
60
|
-
def initialize(tools: [], writable: [], network: :none, env: [], **limits)
|
|
61
|
-
limits = LIMIT_DEFAULTS.merge(reject_unknown(limits))
|
|
62
|
-
ensure_known(:network, NETWORKS, network)
|
|
63
|
-
ensure_known(:confinement, CONFINEMENTS, limits[:confinement])
|
|
32
|
+
# @param timeout [Integer, nil] seconds the run may take
|
|
33
|
+
# @param budget [Float, nil] dollars it may spend
|
|
34
|
+
def initialize(tools: [], writable: [], env: [], timeout: nil, budget: nil)
|
|
64
35
|
@tools = tools.dup.freeze
|
|
65
36
|
@writable = writable.dup.freeze
|
|
66
|
-
@network = network
|
|
67
37
|
@env = env.dup.freeze
|
|
68
|
-
@timeout
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# @return [Boolean] whether this profile is enforced by the operating system's sandbox
|
|
72
|
-
def sandboxed?
|
|
73
|
-
confinement == :sandbox
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# @return [Boolean] whether this machine can apply this profile's confinement
|
|
77
|
-
def available?
|
|
78
|
-
!sandboxed? || File.executable?(SANDBOX)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# @param words [Array<String>] the agent's command
|
|
82
|
-
# @return [Array<String>] `words`, run under the sandbox with this profile's policy; the
|
|
83
|
-
# words unchanged when the confinement is the command's own permission rules
|
|
84
|
-
def confine(words)
|
|
85
|
-
sandboxed? ? [SANDBOX, "-p", policy, *words] : words
|
|
38
|
+
@timeout = timeout
|
|
39
|
+
@budget = budget
|
|
86
40
|
end
|
|
87
41
|
|
|
88
42
|
# @return [String] the `claude` permission mode this profile runs under
|
|
89
43
|
def permission_mode
|
|
90
|
-
|
|
44
|
+
"dontAsk"
|
|
91
45
|
end
|
|
92
46
|
|
|
93
47
|
# @param source [Hash{String => String}] the environment to take values from
|
|
@@ -96,79 +50,27 @@ module Hecks
|
|
|
96
50
|
source.to_h.slice(*BASE_ENV, *env)
|
|
97
51
|
end
|
|
98
52
|
|
|
99
|
-
# The `claude` flags this profile's tools and budget become.
|
|
53
|
+
# The `claude` flags this profile's tools and budget become. The writing tools are held only
|
|
54
|
+
# through an `Edit` rule per writable directory, which every file-writing tool answers to.
|
|
100
55
|
#
|
|
101
56
|
# @return [Array<String>]
|
|
102
57
|
def tool_flags
|
|
103
58
|
flags = tools.empty? ? [] : ["--tools", tools.join(","), "--allowedTools", *allowed_tools]
|
|
104
|
-
flags += ["--strict-mcp-config"]
|
|
59
|
+
flags += ["--strict-mcp-config"]
|
|
105
60
|
flags + (budget ? ["--max-budget-usd", budget.to_s] : [])
|
|
106
61
|
end
|
|
107
62
|
|
|
108
|
-
# @return [String] the sandbox policy: reads open but for credentials, writes where allowed
|
|
109
|
-
def policy
|
|
110
|
-
[
|
|
111
|
-
"(version 1)", "(allow default)",
|
|
112
|
-
"(deny file-write*)", write_rule, "(deny file-read*#{paths_clause(credential_paths)})",
|
|
113
|
-
*network_rules
|
|
114
|
-
].join("\n")
|
|
115
|
-
end
|
|
116
|
-
|
|
117
63
|
private
|
|
118
64
|
|
|
119
|
-
# The tools allowed without asking. A permissions-confined run holds the writing tools only
|
|
120
|
-
# through an `Edit` rule per writable directory, which every file-writing tool answers to.
|
|
121
65
|
def allowed_tools
|
|
122
|
-
return [tools.join(",")] if sandboxed?
|
|
123
|
-
|
|
124
66
|
tools - WRITING_TOOLS + writable.map { |path| "Edit(/#{real(path)}/**)" }
|
|
125
67
|
end
|
|
126
68
|
|
|
127
|
-
def write_rule
|
|
128
|
-
allowed = [real(Dir.tmpdir), *writable.map { |path| real(path) }]
|
|
129
|
-
devices = '(literal "/dev/null") (literal "/dev/tty") (regex #"^/dev/fd/")'
|
|
130
|
-
"(allow file-write*#{paths_clause(allowed)} #{devices})"
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def credential_paths
|
|
134
|
-
CREDENTIAL_PATHS.map { |path| File.join(Dir.home, path) }
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def paths_clause(paths)
|
|
138
|
-
paths.map { |path| " (subpath #{quote(path)})" }.join
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def network_rules
|
|
142
|
-
case network
|
|
143
|
-
when :any then []
|
|
144
|
-
when :https then ["(deny network*)", '(allow network-outbound (remote tcp "*:443") (remote udp "*:53"))',
|
|
145
|
-
'(allow network-outbound (remote unix-socket (path-literal "/private/var/run/mDNSResponder")))']
|
|
146
|
-
else ["(deny network*)"]
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def reject_unknown(limits)
|
|
151
|
-
unknown = limits.keys - LIMIT_DEFAULTS.keys
|
|
152
|
-
return limits if unknown.empty?
|
|
153
|
-
|
|
154
|
-
raise ArgumentError, "unknown keyword#{"s" if unknown.size > 1}: #{unknown.map(&:inspect).join(", ")}"
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def ensure_known(name, allowed, value)
|
|
158
|
-
return if allowed.include?(value)
|
|
159
|
-
|
|
160
|
-
raise ArgumentError, "#{name} must be one of #{allowed.inspect}, not #{value.inspect}"
|
|
161
|
-
end
|
|
162
|
-
|
|
163
69
|
def real(path)
|
|
164
70
|
File.realpath(path)
|
|
165
71
|
rescue SystemCallError
|
|
166
72
|
File.expand_path(path)
|
|
167
73
|
end
|
|
168
|
-
|
|
169
|
-
def quote(path)
|
|
170
|
-
%("#{path.gsub("\\", "\\\\\\\\").gsub('"', '\\"')}")
|
|
171
|
-
end
|
|
172
74
|
end
|
|
173
75
|
end
|
|
174
76
|
end
|
|
@@ -116,7 +116,7 @@ module Hecks
|
|
|
116
116
|
# The profile `--confine` runs the agent under: it writes only the candidates directory.
|
|
117
117
|
def confinement_profile
|
|
118
118
|
FileUtils.mkdir_p(@out_dir)
|
|
119
|
-
Hecks::Adapters::AgentProfile.new(
|
|
119
|
+
Hecks::Adapters::AgentProfile.new(tools: CONFINED_TOOLS, writable: [@out_dir],
|
|
120
120
|
timeout: CONFINED_TIMEOUT, budget: CONFINED_BUDGET)
|
|
121
121
|
end
|
|
122
122
|
|
|
@@ -43,7 +43,6 @@ module Hecks
|
|
|
43
43
|
gates = BootGates.new
|
|
44
44
|
load_bound_adapters!(registry)
|
|
45
45
|
Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) }
|
|
46
|
-
check_compute_rules_backstop!(registry)
|
|
47
46
|
|
|
48
47
|
gates.run!(:pre_verify, registry, directory)
|
|
49
48
|
registry.verify!
|
|
@@ -67,25 +66,6 @@ module Hecks
|
|
|
67
66
|
(bound + default).each { |name| load_adapter(registry, name) }
|
|
68
67
|
end
|
|
69
68
|
|
|
70
|
-
# A backstop only: fires when a translation declares a `computes`/
|
|
71
|
-
# `rekeys` rule but no persistence plugin is loaded to interpret it.
|
|
72
|
-
# Any loaded plugin's own compute-rules gate refuses earlier, by name,
|
|
73
|
-
# whenever one is loaded — this only runs when nothing was.
|
|
74
|
-
def check_compute_rules_backstop!(registry)
|
|
75
|
-
return if Ports::Persistence.plugins_loaded?
|
|
76
|
-
|
|
77
|
-
registry.translations.each do |translation|
|
|
78
|
-
translation.aggregates.each do |aggregate|
|
|
79
|
-
next if aggregate.computes.empty? && aggregate.rekeys.empty?
|
|
80
|
-
|
|
81
|
-
raise WiringError,
|
|
82
|
-
"cannot boot #{translation.domain}::#{aggregate.name}: a compute/rekey rule is declared, but no " \
|
|
83
|
-
"persistence plugin that can interpret it is loaded (e.g. require " \
|
|
84
|
-
"\"hecks/ports/persistence/plugins/era\")"
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
69
|
private
|
|
90
70
|
|
|
91
71
|
def load_adapter(registry, name)
|
data/lib/hecks/runtime/outbox.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "json"
|
|
|
2
2
|
require "securerandom"
|
|
3
3
|
require "time"
|
|
4
4
|
require_relative "event"
|
|
5
|
+
require_relative "value"
|
|
5
6
|
require_relative "../naming"
|
|
6
7
|
|
|
7
8
|
module Hecks
|
|
@@ -48,19 +49,11 @@ module Hecks
|
|
|
48
49
|
name: hash[:name],
|
|
49
50
|
aggregate: hash[:aggregate],
|
|
50
51
|
id: hash[:id],
|
|
51
|
-
payload: deep_symbolize(hash[:payload] || {}),
|
|
52
|
+
payload: Value.deep_symbolize(hash[:payload] || {}),
|
|
52
53
|
occurred_at: hash[:occurred_at],
|
|
53
54
|
correlation: hash[:correlation]
|
|
54
55
|
).emit!
|
|
55
56
|
end
|
|
56
|
-
|
|
57
|
-
def deep_symbolize(value)
|
|
58
|
-
case value
|
|
59
|
-
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
60
|
-
when Array then value.map { |element| deep_symbolize(element) }
|
|
61
|
-
else value
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
57
|
end
|
|
65
58
|
end
|
|
66
59
|
end
|
|
@@ -101,15 +101,7 @@ module Hecks
|
|
|
101
101
|
def spread(answer)
|
|
102
102
|
return { answered: answer } unless answer.is_a?(Hash)
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def deep_symbolize(value)
|
|
108
|
-
case value
|
|
109
|
-
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
110
|
-
when Array then value.map { |element| deep_symbolize(element) }
|
|
111
|
-
else value
|
|
112
|
-
end
|
|
104
|
+
Value.deep_symbolize(answer)
|
|
113
105
|
end
|
|
114
106
|
|
|
115
107
|
# The record, if there is one. A missing record is not an error here: the ask still
|
|
@@ -117,8 +117,8 @@ module Hecks
|
|
|
117
117
|
[domain, aggregate]
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
# Not every aggregate in a routed domain is Lambda-bound (a
|
|
121
|
-
#
|
|
120
|
+
# Not every aggregate in a routed domain is Lambda-bound (a persistence binding
|
|
121
|
+
# may name another adapter). Test the adapter's capability, not its name.
|
|
122
122
|
def remote_bound?(domain, aggregate)
|
|
123
123
|
adapter_name = Ports::Persistence::BindingPolicy.resolve(@registry, domain, aggregate).adapter
|
|
124
124
|
@registry.adapter_class(adapter_name) <= Ports::Persistence::RemoteRuntime
|
data/lib/hecks/runtime/value.rb
CHANGED
|
@@ -102,6 +102,19 @@ module Hecks
|
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# Converts every Hash key to a Symbol, at every depth of parsed JSON.
|
|
106
|
+
#
|
|
107
|
+
# @param value [Hash, Array, Object] a Hash or Array is walked; any other value is a leaf
|
|
108
|
+
# @return [Hash{Symbol => Object}, Array, Object] a new structure of the same shape with
|
|
109
|
+
# Symbol keys; a leaf is returned as it came
|
|
110
|
+
def self.deep_symbolize(value)
|
|
111
|
+
case value
|
|
112
|
+
when Hash then value.to_h { |key, item| [key.to_sym, deep_symbolize(item)] }
|
|
113
|
+
when Array then value.map { |item| deep_symbolize(item) }
|
|
114
|
+
else value
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
105
118
|
# `materialize`, but a single-attribute value object unwraps to its bare field.
|
|
106
119
|
# Opt-in for `group_by` rows; `materialize` keeps the wrapped shape callers depend on.
|
|
107
120
|
#
|
|
@@ -36,6 +36,7 @@ module Hecks
|
|
|
36
36
|
ir = shaped(exporter.call(@registry).fetch(@domain_name))
|
|
37
37
|
ir[:lineage] = shaped(exporter.lineage(@registry, @domain_name))
|
|
38
38
|
ir[:persistence] = shaped(exporter.persistence(@registry, @domain_name))
|
|
39
|
+
add_persistence_wiring(ir, exporter)
|
|
39
40
|
add_seams(ir, exporter)
|
|
40
41
|
add_edges_and_source(ir)
|
|
41
42
|
ir
|
|
@@ -43,8 +44,28 @@ module Hecks
|
|
|
43
44
|
|
|
44
45
|
private
|
|
45
46
|
|
|
47
|
+
# The adapters and the world that the host checks the persistence against.
|
|
48
|
+
def add_persistence_wiring(document, exporter)
|
|
49
|
+
document[:adapters] = adapters(exporter, document[:persistence])
|
|
50
|
+
document[:world] = world(exporter)
|
|
51
|
+
end
|
|
52
|
+
|
|
46
53
|
def shaped(payload) = self.class.json_shaped(payload)
|
|
47
54
|
|
|
55
|
+
# The bound persistence adapters' declarations, from the `.adapter` files under the
|
|
56
|
+
# source root's `lib/` and the domain's own directory, so the host can look them up.
|
|
57
|
+
def adapters(exporter, persistence)
|
|
58
|
+
files = exporter.adapter_files_under([File.join(RustBuild::ROOT, "lib"), File.dirname(@bluebook_dir)])
|
|
59
|
+
shaped(exporter.persistence_adapters(persistence, files))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The target's world persistence, from `<domain directory name>.world` beside the bluebooks
|
|
63
|
+
# (names only), so the host can check what the world binds against the adapters.
|
|
64
|
+
def world(exporter)
|
|
65
|
+
path = File.join(@bluebook_dir, "#{File.basename(File.dirname(@bluebook_dir))}.world")
|
|
66
|
+
shaped(exporter.persistence_world(path, @domain_name))
|
|
67
|
+
end
|
|
68
|
+
|
|
48
69
|
def add_seams(document, exporter)
|
|
49
70
|
seams = SEAMS.to_h { |seam| [seam, exporter.public_send(seam, @registry, @domain_name)] }
|
|
50
71
|
seams.each { |seam, value| document[seam] = shaped(value) unless value.empty? }
|
|
@@ -56,14 +77,12 @@ module Hecks
|
|
|
56
77
|
"hecksagon, not a deploy-time guess, names the identity verbs."
|
|
57
78
|
end
|
|
58
79
|
|
|
59
|
-
# Edges carry their own precompiled SQL, so a boot-time mint only executes it.
|
|
60
|
-
#
|
|
80
|
+
# Edges carry their own precompiled SQL, so a boot-time mint only executes it. The source
|
|
81
|
+
# text is verbatim: the era's integrity digest is
|
|
61
82
|
# the SHA256 of it, not of anything re-derived from the parsed IR.
|
|
62
83
|
def add_edges_and_source(document)
|
|
63
84
|
edges = Hecks::Projector::Exporter.translations(@registry).select { |edge| edge[:domain] == @domain_name }
|
|
64
85
|
document[:translations] = shaped(edges)
|
|
65
|
-
approvals = Hecks::Translation::ApprovalFile.read_all(@bluebook_dir)
|
|
66
|
-
document[:approvals] = shaped(approvals) unless approvals.empty?
|
|
67
86
|
document[:source_text] = Hecks::Runtime::EraCheck.source_text_for(
|
|
68
87
|
@registry.bluebooks.fetch(@domain_name), @bluebook_dir, registry: @registry
|
|
69
88
|
)
|
|
@@ -31,7 +31,6 @@ module Hecks
|
|
|
31
31
|
# @raise [Failure] when the target is not installed or a step fails
|
|
32
32
|
def call(argv)
|
|
33
33
|
domain = argv.first or raise Failure, "usage: hecks build_wasm <domain>"
|
|
34
|
-
return build_with_rust(domain) if ENV["HECKS_PARSER"] == "rust" && ENV["HECKS_CODEGEN"] == "rust"
|
|
35
34
|
|
|
36
35
|
require_target!
|
|
37
36
|
rust_dir = RustBuild.rust_dir
|
|
@@ -48,17 +47,6 @@ module Hecks
|
|
|
48
47
|
raise Failure, "project_rust failed for #{domain}" unless status.zero?
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
# With the Rust parser and codegen, `hecks-build` runs the same steps and writes the same
|
|
52
|
-
# outputs.
|
|
53
|
-
def build_with_rust(domain)
|
|
54
|
-
build_dir = File.join(ROOT, "rust", "build")
|
|
55
|
-
puts "== cargo build (hecks-build) =="
|
|
56
|
-
RustBuild.command!("cargo", "build", chdir: build_dir)
|
|
57
|
-
puts "== hecks-build #{domain} --wasm =="
|
|
58
|
-
RustBuild.command!(File.join(build_dir, "target", "debug", "hecks-build"), domain, "--wasm")
|
|
59
|
-
0
|
|
60
|
-
end
|
|
61
|
-
|
|
62
50
|
def require_target!
|
|
63
51
|
return if RustBuild.target_installed?(TARGET)
|
|
64
52
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "../rust_format"
|
|
4
5
|
|
|
5
6
|
module Hecks
|
|
6
7
|
module RustBuild
|
|
@@ -9,17 +10,8 @@ module Hecks
|
|
|
9
10
|
module WriteIfChanged
|
|
10
11
|
module_function
|
|
11
12
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# Every write is recorded against every open ancestor directory.
|
|
15
|
-
# `push_directory`/`pop_and_prune` are the same operation split for non-adjacent writes.
|
|
16
|
-
def track_directory(dir)
|
|
17
|
-
push_directory(dir)
|
|
18
|
-
yield
|
|
19
|
-
ensure
|
|
20
|
-
pop_and_prune(dir)
|
|
21
|
-
end
|
|
22
|
-
|
|
13
|
+
# Starts recording every path written or confirmed unchanged inside `dir`, against every open
|
|
14
|
+
# ancestor directory too; `pop_and_prune` closes it and deletes the files never touched.
|
|
23
15
|
def push_directory(dir)
|
|
24
16
|
FileUtils.mkdir_p(dir)
|
|
25
17
|
stack.push([File.expand_path(dir), Set.new])
|
|
@@ -56,6 +48,7 @@ module Hecks
|
|
|
56
48
|
# Returns whether it wrote.
|
|
57
49
|
def call(path, content)
|
|
58
50
|
record_touch(path)
|
|
51
|
+
content = RustFormat.call(content) if RustFormat.rust?(path)
|
|
59
52
|
# `.b` on both sides: String#== is encoding-aware, so UTF-8 vs ASCII-8BIT
|
|
60
53
|
# content can compare unequal even when the bytes are identical.
|
|
61
54
|
return false if File.exist?(path) && File.binread(path) == content.b
|
|
@@ -64,13 +57,20 @@ module Hecks
|
|
|
64
57
|
true
|
|
65
58
|
end
|
|
66
59
|
|
|
60
|
+
# Fills `tmp` from the block, then lays it out with rustfmt when `path` is Rust source.
|
|
61
|
+
def write_formatted(tmp, path, &)
|
|
62
|
+
File.open(tmp, "w", &)
|
|
63
|
+
File.write(tmp, RustFormat.call(File.read(tmp))) if RustFormat.rust?(path)
|
|
64
|
+
end
|
|
65
|
+
private_class_method :write_formatted
|
|
66
|
+
|
|
67
67
|
# Like `call`, for callers that build content by writing to an IO. Yields a sibling
|
|
68
68
|
# temp file, then renames it into place only if the bytes differ. Returns whether it wrote.
|
|
69
69
|
def block(path, &)
|
|
70
70
|
record_touch(path)
|
|
71
71
|
tmp = "#{path}.tmp-#{Process.pid}-#{rand(1_000_000)}"
|
|
72
72
|
begin
|
|
73
|
-
|
|
73
|
+
write_formatted(tmp, path, &)
|
|
74
74
|
changed = !(File.exist?(path) && FileUtils.compare_file(tmp, path))
|
|
75
75
|
FileUtils.mv(tmp, path) if changed
|
|
76
76
|
changed
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
# Runs generated Rust through rustfmt, so a generator writes what `cargo fmt --check` accepts.
|
|
7
|
+
#
|
|
8
|
+
# Every generator that writes a `.rs` file formats its text here before comparing it with the tree
|
|
9
|
+
# or writing it, so a drift check and a write see the same bytes. The edition is pinned in the
|
|
10
|
+
# call, not read from a `rustfmt.toml`, because a generator may run from any directory.
|
|
11
|
+
module RustFormat
|
|
12
|
+
# Raised when the rustfmt program cannot be found.
|
|
13
|
+
class Missing < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Raised when rustfmt fails for a reason that is not the text: a toolchain that will not
|
|
16
|
+
# install, say. Falling back to the unformatted text would write the wrong bytes without a word.
|
|
17
|
+
class Failed < StandardError; end
|
|
18
|
+
|
|
19
|
+
EDITION = "2021"
|
|
20
|
+
|
|
21
|
+
# The most passes `call` makes; a text still changing after them keeps the last result.
|
|
22
|
+
PASSES = 3
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# @param path [String] where the text will be written
|
|
27
|
+
# @return [Boolean] whether the path is Rust source
|
|
28
|
+
def rust?(path)
|
|
29
|
+
path.to_s.end_with?(".rs")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Formats one Rust source text.
|
|
33
|
+
#
|
|
34
|
+
# Text rustfmt cannot parse is returned as it came, with a warning on stderr: the compiler will
|
|
35
|
+
# name the real fault, and a generator should not be the thing that hides it. Any other failure
|
|
36
|
+
# of rustfmt raises, since the text is not the problem.
|
|
37
|
+
#
|
|
38
|
+
# @param text [String] Rust source
|
|
39
|
+
# @return [String] the same source as rustfmt lays it out
|
|
40
|
+
# @raise [Missing] when rustfmt is not installed
|
|
41
|
+
# @raise [Failed] when rustfmt fails for a reason other than the text
|
|
42
|
+
def call(text)
|
|
43
|
+
PASSES.times do
|
|
44
|
+
formatted = pass(text)
|
|
45
|
+
return formatted if formatted == text
|
|
46
|
+
|
|
47
|
+
text = formatted
|
|
48
|
+
end
|
|
49
|
+
text
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# One rustfmt run. rustfmt does not always reach a fixed point in one pass, so `call` repeats it
|
|
53
|
+
# until the text stops changing, as a later `cargo fmt --check` would have it.
|
|
54
|
+
#
|
|
55
|
+
# @param text [String] Rust source
|
|
56
|
+
# @return [String] the source after one pass, or unchanged when rustfmt cannot parse it
|
|
57
|
+
# @raise [Failed] when rustfmt fails for a reason other than the text
|
|
58
|
+
def pass(text)
|
|
59
|
+
out, err, status = Open3.capture3("rustfmt", "--edition", EDITION, "--emit", "stdout", stdin_data: text)
|
|
60
|
+
return out if status.success?
|
|
61
|
+
raise Failed, "rustfmt failed: #{err.lines.first&.strip}" unless err.include?("<stdin>")
|
|
62
|
+
|
|
63
|
+
warn "hecks: rustfmt could not format generated Rust, leaving it as generated: #{err.lines.first&.strip}"
|
|
64
|
+
text
|
|
65
|
+
rescue Errno::ENOENT
|
|
66
|
+
raise Missing, "rustfmt is not installed: generated Rust is formatted with it; run `rustup component add rustfmt`"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Formats the Rust texts of a path-to-text map and leaves the others alone.
|
|
70
|
+
#
|
|
71
|
+
# @param content [Hash{String => String}] absolute path to text
|
|
72
|
+
# @return [Hash{String => String}] the same map with each `.rs` text formatted
|
|
73
|
+
def files(content)
|
|
74
|
+
content.to_h { |path, text| [path, rust?(path) ? call(text) : text] }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -48,19 +48,13 @@ module Hecks
|
|
|
48
48
|
failure[:message].split("; ").all? { |chunk| matchers.any? { |regex| regex.match?(chunk) } }
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# Skips only exact, already-tracked wiring gaps;
|
|
51
|
+
# Skips only exact, already-tracked wiring gaps (`SKIPPED_DOMAIN_REASONS`); none is tracked
|
|
52
|
+
# now, so any WiringError is re-raised.
|
|
52
53
|
#
|
|
53
54
|
# @param error [Hecks::Runtime::WiringError] what generating a sequence raised
|
|
54
55
|
# @return [Symbol] a key of `SKIPPED_DOMAIN_REASONS`
|
|
55
56
|
def known_unfuzzable_wiring_gap(error)
|
|
56
|
-
|
|
57
|
-
when %r{a compute/rekey rule is declared}
|
|
58
|
-
# compute/rekey needs an era/Postgres migration, so it cannot boot under IsolatedBoot's
|
|
59
|
-
# Memory rebind. Real coverage lives in spec/adapters/driven/postgres_era/*_spec.rb.
|
|
60
|
-
:skip_compute_rekey
|
|
61
|
-
else
|
|
62
|
-
raise error
|
|
63
|
-
end
|
|
57
|
+
raise error
|
|
64
58
|
end
|
|
65
59
|
|
|
66
60
|
# Shrinks steps, then arguments, keeping a removal only while the same finding reproduces.
|
|
@@ -29,9 +29,7 @@ module Hecks
|
|
|
29
29
|
KNOWN_FUZZ_FINDINGS = {}.freeze
|
|
30
30
|
|
|
31
31
|
# Why a domain is skipped, by the reason `known_unfuzzable_wiring_gap` answers.
|
|
32
|
-
SKIPPED_DOMAIN_REASONS = {
|
|
33
|
-
skip_compute_rekey: "declares compute/rekey, which only an era/Postgres-bound boot can interpret"
|
|
34
|
-
}.freeze
|
|
32
|
+
SKIPPED_DOMAIN_REASONS = {}.freeze
|
|
35
33
|
|
|
36
34
|
# The persistence an ephemeral boot may use.
|
|
37
35
|
ADAPTERS = %i[memory sqlite postgres].freeze
|
|
@@ -30,17 +30,14 @@ module Hecks
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# @return [Integer] the exit status
|
|
33
|
-
def conclude(db,
|
|
33
|
+
def conclude(db, failed)
|
|
34
|
+
db.close
|
|
34
35
|
if failed
|
|
35
|
-
db.close
|
|
36
36
|
puts "AUDIT REFUSED."
|
|
37
37
|
return 1
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
record_approval(lineage, edge) if needs_approval && approve
|
|
42
|
-
db.close
|
|
43
|
-
print_passed(needs_approval, approve)
|
|
40
|
+
puts "AUDIT PASSED — review the samples above; intent is yours to approve."
|
|
44
41
|
0
|
|
45
42
|
end
|
|
46
43
|
|
|
@@ -79,28 +76,6 @@ module Hecks
|
|
|
79
76
|
puts " before: #{JSON.generate(sample[:before])}" if sample[:before]
|
|
80
77
|
puts " after: #{JSON.generate(sample[:after])}" if sample[:after]
|
|
81
78
|
end
|
|
82
|
-
|
|
83
|
-
# Binds to what was reviewed — this edge and the journal as it stands now — so it must
|
|
84
|
-
# live in the database, not the repo.
|
|
85
|
-
def record_approval(lineage, edge)
|
|
86
|
-
lineage.record_approval!(
|
|
87
|
-
from: edge.from, to: edge.to,
|
|
88
|
-
edge_digest: Hecks::Translation::Audit.edge_digest(edge)
|
|
89
|
-
)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def print_passed(needs_approval, approve)
|
|
93
|
-
if needs_approval && approve
|
|
94
|
-
puts "AUDIT PASSED — approval recorded in the database, bound to this edge and this journal."
|
|
95
|
-
puts "A journal that advances before the mint invalidates it; re-run with --approve if that happens."
|
|
96
|
-
elsif needs_approval
|
|
97
|
-
puts "AUDIT PASSED — this edge carries a compute or rekey rule, and the samples above are " \
|
|
98
|
-
"its ONLY verification."
|
|
99
|
-
puts "If they show what you intended, run again with --approve; the mint refuses until you do."
|
|
100
|
-
else
|
|
101
|
-
puts "AUDIT PASSED — review the samples above; intent is yours to approve."
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
79
|
end
|
|
105
80
|
end
|
|
106
81
|
end
|