8000 T75 — Load the classifier's one ruleset inside the guest kernel so a VM host decides a host-address box's verdict per box · Issue #1812 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T75 — Load the classifier's one ruleset inside the guest kernel so a VM host decides a host-address box's verdict per box #1812

Closed

Description

@norrietaylor

Task

slice size difficulty model who estimated lines
Host-address boxes under deny-all L unknown ground strongest an agent 940 (380 code, 560 tests)

On a VM-backed host, the guest kernel accepts and loads the classifier table the native installer renders, byte for byte the installer's own render for the same parameters, the digest taken over the exact bytes written to nft's stdin in both the guest and the native installer's load, never a re-render; a deny-all host-address box's outbound probe is refused, its DNS to the gateway included (no resolver carve-out on a VM host until gominimal/inbox#897), and its session shows host_ip_enforcement per_box; per_box rests on the daemon's own control connect to a loopback listener it holds succeeding and, immediately after, a deny-subtree probe child's connect to that listener failing fast with EHOSTUNREACH, EACCES or EPERM in every family probed (IPv4 in the guest, ::1 too wherever IPv6 loopback is enabled); a guest whose check or load failed, whose table is gone behind its marker, whose control leg fails, or whose probe connects, times out or sees any other errno reports none, refuses the deny-all box again and says why in the bundle.

Covers:

  • NET-079

Layers:

  • daemon:
    • .minimal/minimal.toml: locked_commit moves, by hand and never through mip update, to the gominimal/pkgs commit whose guest kernel builds in every expression the rendered table uses (NFT_SOCKET is the one the kernel lacks today) and whose microvm-rootfs closure pins Alpine's nftables apk and its missing libraries; nothing nft-related is packed into the initramfs
    • crates/minimald/src/net/classifier.rs: a guest-only table load, run after the daemon's leaf and the deny and allow subtrees exist. The installer is compiled into minimald from scripts/install-host-classifier.sh itself (include_str of that one file, never a copy) and fed on stdin to /bin/bash -s, invoked by absolute path from the read-only rootfs with its environment cleared (no BASH_ENV, no ENV, no inherited PATH) and every parameter passed as argv after --print-ruleset, never interpolated into the script text, so no installer file is staged anywhere a box could reach; the render takes the node-plane identity and the host-address cohort identity as two separate inputs, and the guest's tree root. Wherever the table renders a resolver carve-out (the native lane), its target is minimald's live answerer: the address and port it actually bound (ServerState::zone_answerer_port(), on loopback), never constants; a re-bind re-renders the rule, or the render refuses a stale target. The rendered bytes go unchanged, by absolute path and with the same cleared environment, to the rootfs's root-owned /usr/sbin/nft first as nft -c -f -, then as nft -f - in one transaction; the logged sha256 covers exactly the bytes written to nft's stdin, never a re-render. The marker is created only after that load succeeds and stays only the fact that the load ran; any failure writes no marker and logs the cause and nft's error. On a VM-backed host the guest render carries no resolver carve-out and no dstnat rule, and logs one line at render: "deny-all host-address box on VM host: no resolver carve-out (follow-up gominimal/inbox#897)"; a deny-all box's udp and tcp 53 to the gateway is refused like any other destination. In the guest, decide() and recorded() (before each host-address launch) decide per box only on effect: with no marker nothing is spawned and the guest does not decide; with a marker, nft list must show table inet minimal_class and its deny_out chain, and the effect probe must read rejected for per_box. The probe: immediately before it, a control leg connects from the daemon's own cgroup, outside the deny subtree, to a loopback listener the daemon holds on a port other than the answerer's, and that connect must succeed, or the probe is inconclusive, reports none and logs the cause; then a short-lived probe child placed in a leaf of the deny subtree connect()s to the same listener, once per family the table is inet over: 127.0.0.1 always, and ::1 wherever IPv6 loopback is enabled (the guest boots with ipv6.disable=1, so it probes IPv4 only). A family reads rejected only on EHOSTUNREACH, EACCES or EPERM within a short deadline (the design §5.7 active reject: icmpx admin-prohibited surfaces as EHOSTUNREACH on IPv4, ICMP code 13, and EACCES on IPv6, ICMPv6 code 1; EPERM is an output-hook verdict); any other errno, ECONNREFUSED and ENETUNREACH included, a success or a timeout reads none, and any probed family that does not read rejected makes the whole probe none, because a v6 bypass is a full bypass. A probe that reads none refuses a deny-all box again. The native decision is T48's (T48 — Classify host-address boxes apart from node-plane traffic and enforce their deny-all #1805) and is not changed here
    • crates/minimald/src/main.rs: runs the guest load between entering the daemon's leaf and the start-time decide and record, and holds the loopback listener the effect probe connects to, so host_ip_enforcement flips to per_box, and the guest's refusal of a deny-all host-address box lifts, only through a decision that a successful load, a listed table and a fast-rejected probe produce
    • crates/minimald/src/net/provider.rs: in a guest that decides per box, a deny-all host-address box gets no resolver: the node DNS layer (the gateway's resolver) applies no per-box name rule to host-address boxes, so nothing is carved out until gominimal/inbox#897 lands; a guest that does not decide keeps the node's DNS layer
    • scripts/install-host-classifier.sh: the install mode's load pipes render_ruleset's output straight into nft -f - instead of through a temp file and prints the sha256 of exactly the bytes it piped, so both lanes load, and digest, one render function's bytes; native rule text unchanged; the guest render omits the carve-out per the amended ruling (--no-resolver-carve-out); the install records the carve-out address and port it loaded beside the marker and clears that record with the marker
    • crates/minimald/src/server.rs: a process-wide cell holds the answerer's actually bound address and port, set beside set_zone_answerer_port and cleared when the answerer is released, so a released answerer makes native deny-all launches stale; classifier.rs refuses a native deny-all launch as TableNotEffective "stale carve-out", naming both values, when the recorded target differs from the live bind, there is no live bind, or the table carries a carve-out with no record, and the install hint re-renders with --answerer-address/--answerer-port set to the live bind
    • cargo nextest run -p minimald guest_table_is_the_installers_rendered_ruleset
    • cargo nextest run -p minimald ruleset_digest_covers_bytes_piped_to_nft_in_both_lanes
    • cargo nextest run -p minimald guest_installer_runs_with_cleared_env_and_argv_params
    • cargo nextest run -p minimald guest_effect_probe_decides_per_box
    • cargo nextest run -p minimald guest_effect_probe_inconclusive_without_live_listener
    • cargo nextest run -p minimald guest_render_takes_node_plane_and_cohort_separately
    • cargo nextest run -p minimald guest_marker_written_only_after_the_table_loads
    • cargo nextest run -p minimald guest_load_failure_keeps_deny_all_refused
    • cargo nextest run -p minimald guest_decide_rechecks_table_not_only_marker
    • cargo nextest run -p minimald carve_out_targets_live_answerer_bind
    • cargo nextest run -p minimald native_deny_all_refused_when_carve_out_target_is_stale
    • cargo nextest run -p minimald native_deny_all_refused_with_no_live_answerer
    • cargo nextest run -p minimald release_clears_live_answerer_cell
    • cargo nextest run -p minimald guest_deny_all_renders_no_resolver_carve_out
  • vmhost:
    • crates/minvmd/tests/guest_classifier_integration.rs (new): boots a VM through the supervisor path; asserts the guest's nft -c -f accepted the rendered text against the guest kernel and fails with nft's own error naming the missing expression when it did not; asserts the digest of the bytes the guest wrote to nft's stdin equals the digest the native installer's load path prints for the same parameters, and the sha256 of its --print-ruleset output; asserts inet minimal_class is listed in the guest; starts a deny-all host-address box whose outbound probe to an address off the guest is refused, whose udp and tcp 53 to the gateway (100.64.0.1) is refused, and whose session record shows host_ip_enforcement per_box; records the effect probe's observed errno per probed family and asserts it is in EHOSTUNREACH, EACCES and EPERM, expecting EHOSTUNREACH on IPv4 and, where IPv6 loopback is enabled, EACCES on IPv6
    • cargo nextest run -p minvmd guest_kernel_accepts_rendered_ruleset (deferred: vm)
    • cargo nextest run -p minvmd guest_loaded_ruleset_matches_native_print_ruleset (deferred: vm)
    • cargo nextest run -p minvmd guest_classifier_table_present (deferred: vm)
    • cargo nextest run -p minvmd guest_deny_all_probe_refused (deferred: vm)
    • cargo nextest run -p minvmd guest_deny_all_dns_to_gateway_refused (deferred: vm)
    • cargo nextest run -p minvmd guest_deny_all_box_answers_inbound_through_the_proxy (deferred: vm)
    • cargo nextest run -p minvmd guest_host_ip_enforcement_per_box_after_load (deferred: vm)
    • cargo nextest run -p minvmd guest_effect_probe_errno_per_family (deferred: vm)

Waits on:

  • T48 — Classify host-address boxes apart from node-plane traffic and enforce their deny-all #1631
  • waits on a gominimal/pkgs issue (to be filed): guest kernel nft expressions + nftables apk in microvm-rootfs
  • assumes the node-plane and cohort identities collapse to the guest's one address only on an un-enrolled host (local-minvmd); an enrolled guest passes the design §4.2 addresses as the two inputs
  • assumes the guest already meets the inescapability requirement: each box's cgroup-namespace root is its own leaf, no box writes cgroupfs, no box holds CAP_NET_ADMIN, and nft is root-owned on the read-only rootfs while the installer text never touches a file
Plan block
### T75 — Load the classifier's one ruleset inside the guest kernel so a VM host decides a host-address box's verdict per box
  slice: S11
  kind: task
  covers: NET-079
  size: L
  est: 380 code, 560 tests
  executor: agent
  difficulty: unknown ground
  model: strongest
  diagnostics: the bundle's guest daemon log tail carries one line at boot with nft -c's verdict on the rendered table and nft's own error when it refuses, one line with the load's outcome and the sha256 of the exact text handed to nft, and one line per launch whose table recheck or effect probe failed behind a marker, naming which, the control leg's outcome, and each probed family's observed errno, success or timeout, so a VM host still refusing deny-all host-address boxes reads why in a bundle
  observability: one info log line at guest boot when the table loads and the marker is written, naming the ruleset digest; one error line naming the cause when the check or the load fails; one warn line when a launch's recheck finds the table gone, its control leg fails, or a probed family connects, times out or fails with an errno outside EHOSTUNREACH, EACCES and EPERM; the existing per-launch classifier line then reads host_ip_enforcement per_box for a guest box
  layers:
    - daemon:
      - .minimal/minimal.toml: locked_commit moves, by hand and never through mip update, to the gominimal/pkgs commit whose guest kernel builds in every expression the rendered table uses (NFT_SOCKET is the one the kernel lacks today) and whose microvm-rootfs closure pins Alpine's nftables apk and its missing libraries; nothing nft-related is packed into the initramfs
      - crates/minimald/src/net/classifier.rs: a guest-only table load, run after the daemon's leaf and the deny and allow subtrees exist. The installer is compiled into minimald from scripts/install-host-classifier.sh itself (include_str of that one file, never a copy) and fed on stdin to /bin/bash -s, invoked by absolute path from the read-only rootfs with its environment cleared (no BASH_ENV, no ENV, no inherited PATH) and every parameter passed as argv after --print-ruleset, never interpolated into the script text, so no installer file is staged anywhere a box could reach; the render takes the node-plane identity and the host-address cohort identity as two separate inputs, and the guest's tree root. Wherever the table renders a resolver carve-out (the native lane), its target is minimald's live answerer: the address and port it actually bound (ServerState::zone_answerer_port(), on loopback), never constants; a re-bind re-renders the rule, or the render refuses a stale target. The rendered bytes go unchanged, by absolute path and with the same cleared environment, to the rootfs's root-owned /usr/sbin/nft first as nft -c -f -, then as nft -f - in one transaction; the logged sha256 covers exactly the bytes written to nft's stdin, never a re-render. The marker is created only after that load succeeds and stays only the fact that the load ran; any failure writes no marker and logs the cause and nft's error. On a VM-backed host the guest render carries no resolver carve-out and no dstnat rule, and logs one line at render: "deny-all host-address box on VM host: no resolver carve-out (follow-up gominimal/inbox#897)"; a deny-all box's udp and tcp 53 to the gateway is refused like any other destination. In the guest, decide() and recorded() (before each host-address launch) decide per box only on effect: with no marker nothing is spawned and the guest does not decide; with a marker, nft list must show table inet minimal_class and its deny_out chain, and the effect probe must read rejected for per_box. The probe: immediately before it, a control leg connects from the daemon's own cgroup, outside the deny subtree, to a loopback listener the daemon holds on a port other than the answerer's, and that connect must succeed, or the probe is inconclusive, reports none and logs the cause; then a short-lived probe child placed in a leaf of the deny subtree connect()s to the same listener, once per family the table is inet over: 127.0.0.1 always, and ::1 wherever IPv6 loopback is enabled (the guest boots with ipv6.disable=1, so it probes IPv4 only). A family reads rejected only on EHOSTUNREACH, EACCES or EPERM within a short deadline (the design §5.7 active reject: icmpx admin-prohibited surfaces as EHOSTUNREACH on IPv4, ICMP code 13, and EACCES on IPv6, ICMPv6 code 1; EPERM is an output-hook verdict); any other errno, ECONNREFUSED and ENETUNREACH included, a success or a timeout reads none, and any probed family that does not read rejected makes the whole probe none, because a v6 bypass is a full bypass. A probe that reads none refuses a deny-all box again. The native decision is T48's (#1805) and is not changed here
      - crates/minimald/src/main.rs: runs the guest load between entering the daemon's leaf and the start-time decide and record, and holds the loopback listener the effect probe connects to, so host_ip_enforcement flips to per_box, and the guest's refusal of a deny-all host-address box lifts, only through a decision that a successful load, a listed table and a fast-rejected probe produce
      - crates/minimald/src/net/provider.rs: in a guest that decides per box, a deny-all host-address box gets no resolver: the node DNS layer (the gateway's resolver) applies no per-box name rule to host-address boxes, so nothing is carved out until gominimal/inbox#897 lands; a guest that does not decide keeps the node's DNS layer
      - scripts/install-host-classifier.sh: the install mode's load pipes render_ruleset's output straight into nft -f - instead of through a temp file and prints the sha256 of exactly the bytes it piped, so both lanes load, and digest, one render function's bytes; native rule text unchanged; the guest render omits the carve-out per the amended ruling (--no-resolver-carve-out); the install records the carve-out address and port it loaded beside the marker and clears that record with the marker
      - crates/minimald/src/server.rs: a process-wide cell holds the answerer's actually bound address and port, set beside set_zone_answerer_port and cleared when the answerer is released, so a released answerer makes native deny-all launches stale; classifier.rs refuses a native deny-all launch as TableNotEffective "stale carve-out", naming both values, when the recorded target differs from the live bind, there is no live bind, or the table carries a carve-out with no record, and the install hint re-renders with --answerer-address/--answerer-port set to the live bind
      test: cargo nextest run -p minimald guest_table_is_the_installers_rendered_ruleset
      test: cargo nextest run -p minimald ruleset_digest_covers_bytes_piped_to_nft_in_both_lanes
      test: cargo nextest run -p minimald guest_installer_runs_with_cleared_env_and_argv_params
      test: cargo nextest run -p minimald guest_effect_probe_decides_per_box
      test: cargo nextest run -p minimald guest_effect_probe_inconclusive_without_live_listener
      test: cargo nextest run -p minimald guest_render_takes_node_plane_and_cohort_separately
      test: cargo nextest run -p minimald guest_marker_written_only_after_the_table_loads
      test: cargo nextest run -p minimald guest_load_failure_keeps_deny_all_refused
      test: cargo nextest run -p minimald guest_decide_rechecks_table_not_only_marker
      test: cargo nextest run -p minimald carve_out_targets_live_answerer_bind
      test: cargo nextest run -p minimald native_deny_all_refused_when_carve_out_target_is_stale
      test: cargo nextest run -p minimald native_deny_all_refused_with_no_live_answerer
      test: cargo nextest run -p minimald release_clears_live_answerer_cell
      test: cargo nextest run -p minimald guest_deny_all_renders_no_resolver_carve_out
    - vmhost:
      - crates/minvmd/tests/guest_classifier_integration.rs (new): boots a VM through the supervisor path; asserts the guest's nft -c -f accepted the rendered text against the guest kernel and fails with nft's own error naming the missing expression when it did not; asserts the digest of the bytes the guest wrote to nft's stdin equals the digest the native installer's load path prints for the same parameters, and the sha256 of its --print-ruleset output; asserts inet minimal_class is listed in the guest; starts a deny-all host-address box whose outbound probe to an address off the guest is refused, whose udp and tcp 53 to the gateway (100.64.0.1) is refused, and whose session record shows host_ip_enforcement per_box; records the effect probe's observed errno per probed family and asserts it is in EHOSTUNREACH, EACCES and EPERM, expecting EHOSTUNREACH on IPv4 and, where IPv6 loopback is enabled, EACCES on IPv6
      test (deferred: vm): cargo nextest run -p minvmd guest_kernel_accepts_rendered_ruleset
      test (deferred: vm): cargo nextest run -p minvmd guest_loaded_ruleset_matches_native_print_ruleset
      test (deferred: vm): cargo nextest run -p minvmd guest_classifier_table_present
      test (deferred: vm): cargo nextest run -p minvmd guest_deny_all_probe_refused
      test (deferred: vm): cargo nextest run -p minvmd guest_deny_all_dns_to_gateway_refused
      test (deferred: vm): cargo nextest run -p minvmd guest_deny_all_box_answers_inbound_through_the_proxy
      test (deferred: vm): cargo nextest run -p minvmd guest_host_ip_enforcement_per_box_after_load
      test (deferred: vm): cargo nextest run -p minvmd guest_effect_probe_errno_per_family
  produces: none
  consumes:
    - crates/minimald/src/net/classifier.rs
    - crates/minimald/src/session_host.rs
    - crates/minimald/src/net/answerer.rs
  proof: On a VM-backed host, the guest kernel accepts and loads the classifier table the native installer renders, byte for byte the installer's own render for the same parameters, the digest taken over the exact bytes written to nft's stdin in both the guest and the native installer's load, never a re-render; a deny-all host-address box's outbound probe is refused, its DNS to the gateway included (no resolver carve-out on a VM host until gominimal/inbox#897), and its session shows host_ip_enforcement per_box; per_box rests on the daemon's own control connect to a loopback listener it holds succeeding and, immediately after, a deny-subtree probe child's connect to that listener failing fast with EHOSTUNREACH, EACCES or EPERM in every family probed (IPv4 in the guest, ::1 too wherever IPv6 loopback is enabled); a guest whose check or load failed, whose table is gone behind its marker, whose control leg fails, or whose probe connects, times out or sees any other errno reports none, refuses the deny-all box again and says why in the bundle.
  depends on:
    - blocked by #1631
    - waits on a gominimal/pkgs issue (to be filed): guest kernel nft expressions + nftables apk in microvm-rootfs
    - assumes the node-plane and cohort identities collapse to the guest's one address only on an un-enrolled host (local-minvmd); an enrolled guest passes the design §4.2 addresses as the two inputs
    - assumes the guest already meets the inescapability requirement: each box's cgroup-namespace root is its own leaf, no box writes cgroupfs, no box holds CAP_NET_ADMIN, and nft is root-owned on the read-only rootfs while the installer text never touches a file

Design ruling

Filed from the architecture review of #1805 (T48, #1631). On a VM-backed host a deny-all host-address box is not enforced: nothing loads the inet minimal_class table in the guest kernel, while on native Linux scripts/install-host-classifier.sh loads it. T48's interim stands as it is: on a VM-backed host a deny-all host-address box is refused, and every other host-address box runs with host_ip_enforcement none, surfaced at every session start. #1806 (T73) only surfaces that verdict, so the fix is this task.

The permanent fix loads the same ruleset text inside the guest kernel, from one definition: the installer's own --print-ruleset output, never a second copy. nft reaches the guest as Alpine's nftables apk in the microvm-rootfs closure, pinned by that closure's per-apk digests, rather than as a static binary in the initramfs. The installer reaches the guest compiled into minimald from the one script file and piped to bash -s, so no installer file is staged. A netlink nftables writer inside minimald is the fallback, taken only if the image size or a no-shell-out policy rules the nft route out. Enforcement in the host's egress gate is ruled out: past the VM's SNAT every host-address box shares the one guest source address, so the host cannot tell them apart.

Conditions on the design:

  • The guest decides per_box only on effect, at start and before each host-address launch.
    • A control leg goes first. The daemon connects from its own cgroup, outside the deny subtree, to a loopback listener it holds on a port other than the answerer's. That connect must succeed. If it fails, the probe is inconclusive: the guest reports none and logs the cause.
    • Immediately after, a short-lived probe child in a deny-subtree leaf connects to the same listener, once per family. The table is inet. The guest boots with ipv6.disable=1, so it probes IPv4 only. Wherever IPv6 loopback is enabled, ::1 is probed too.
    • A family reads as rejected only on EHOSTUNREACH, EACCES or EPERM within a short deadline (the design §5.7 active reject). icmpx admin-prohibited surfaces as EHOSTUNREACH on IPv4 (ICMP code 13) and EACCES on IPv6 (ICMPv6 code 1). EPERM is an output-hook verdict.
    • Any other errno (ECONNREFUSED and ENETUNREACH included), a success, or a timeout reads as none. A family that connects makes the whole probe none, because a v6 bypass is a full bypass. none refuses a deny-all box again.
    • The table and its deny_out chain must also be listed by nft list. The presence marker is only the fact that the load ran.
    • The same native check belongs to T48 (#1805), not to this task.
  • The installer runs by absolute path from the read-only rootfs (/bin/bash, /usr/sbin/nft) with its environment cleared: no BASH_ENV, no ENV, no inherited PATH. Every parameter is passed as argv, never interpolated into the script text.
  • The digest covers the exact bytes written to nft's stdin, never a re-render. The native installer's load path pipes the output of the same render function, and the digest test covers both lanes.
  • The render takes the node-plane identity and the host-address cohort identity as two separate inputs. They collapse to the guest's one address only on an un-enrolled host (local-minvmd). An enrolled guest passes the design §4.2 addresses.
  • nft is root-owned on the read-only rootfs, and no box holds CAP_NET_ADMIN. The guest already meets the inescapability requirement: each box's cgroup-namespace root is its own leaf, and no box writes cgroupfs.
  • The resolver carve-out is by address and port, 127.0.0.1:7656, never loopback-wide. The dstnat rule matches only deny-subtree sockets sending to 127.0.0.1 on udp 53. It never catches loopback-wide :53 and never touches the node plane's DNS.

Acceptance:

  • the guest kernel accepts the rendered text under nft -c -f, and a missing kernel option fails loudly;
  • the text the guest loads is byte-identical to the native installer's --print-ruleset output for the same parameters;
  • the table is present in the guest;
  • a deny-all probe from a host-address box is refused;
  • the deny-all box's DNS to the gateway is refused (no carve-out on a VM host until gominimal/inbox#897);
  • host_ip_enforcement flips to per_box only after the load succeeds;
  • the VM refusal of deny-all host-address boxes lifts only once the load succeeds.

This task waits on a gominimal/pkgs change that is not filed yet. The guest kernel has to build in NFT_SOCKET, and microvm-rootfs has to carry the nftables apk closure. locked_commit then moves to a commit that carries both.

Design ruling, amended

The architecture review of #1886 found no conformant resolver for a deny-all host-address box on a VM host. The guest's node DNS layer, gvproxy at the gateway, forwards every non-zone name upstream and answers no sibling host-address name. So on a VM host the guest table renders no resolver carve-out and no dstnat rule, and logs that at render. The deny-all box's udp and tcp 53 to the gateway is refused, and a KVM test asserts it.

The native carve-out renders from minimald's live answerer bind, never constants, and follows a re-bind. T90 later moves that source to the manager-held answerer.

The in-guest per-box DNS gate, and restoring the resolver tests, is gominimal/inbox#897. That issue also records the wider gap: an allow-list host-address box on a VM host resolves any name today.

Activity

  1. added
    kind:taskA task the plan skill rendered; the body carries ## Task
    size:L500 to 900 net lines; one PR, and the ceiling
    model:strongestThe strongest model available: unknown ground, or large work that needs care
    on Oct 2, 2026
  2. norrietaylor commented on Oct 3, 2026

    @norrietaylor
    MemberAuthor

    The pkgs prerequisite this task waits on is https://github.com/gominimal/inbox/issues/773. Its scope now also covers CONFIG_NFT_CT and CONFIG_NF_CONNTRACK_MARK, because the classifier ruleset from #1805 classifies flows by conntrack mark. Move locked_commit only to a pkgs commit whose guest kernel carries all three options, and whose nft -c check passes on the full rendered ruleset.

  3. minimal-foundry commented on Oct 3, 2026

    @minimal-foundry
    Contributor

    Done. #1886 merged.

    branch agent/1812-load-the-classifier-s-one-ruleset-inside-the-gue · epic #1437 · PR #1886

    Updated 2026-10-28T06:17:32.453Z.

  4. added
    needs-humanAn agent handed off; a human must act, then clear this label. Set by defer and handoffs.
    on Oct 3, 2026
  5. removed
    needs-humanAn agent handed off; a human must act, then clear this label. Set by defer and handoffs.
    on Oct 3, 2026
  6. added
    needs-humanAn agent handed off; a human must act, then clear this label. Set by defer and handoffs.
    on Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:taskA task the plan skill rendered; the body carries ## Taskmodel:strongestThe strongest model available: unknown ground, or large work that needs careneeds-humanAn agent handed off; a human must act, then clear this label. Set by defer and handoffs.size:L500 to 900 net lines; one PR, and the ceiling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0