Skip to content

Merge develop - #26

Merged
p4nda merged 49 commits into
mainfrom
develop
Aug 5, 2026
Merged

Merge develop#26
p4nda merged 49 commits into
mainfrom
develop

Conversation

@p4nda

@p4nda p4nda commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

No description provided.

p4nda and others added 30 commits July 28, 2026 08:35
Under enforcing, ai_tools_t held `map` only on the project and home types, so
any mmap of a file the agent created under /tmp failed EACCES. That blocked
dotnet restore/build (it mmaps a shared-memory mutex under /tmp/.dotnet/shm) and
git in a /tmp working tree -- the "sandbox limitation" the 0.8.0 notes recorded.

Add an optional policy group, tmpmap, carrying the one rule
`allow ai_tools_t ai_tools_tmp_t:file map;`. It is off by default because it
widens the surface around world-writable /tmp; the grant is mmap-at-all, not
executable mapping (PROT_EXEC additionally needs file:execute, which the domain
does not hold on its tmp type, and /tmp is noexec), so it cannot run code from
/tmp. The need is not dotnet-specific -- anything that mmaps a temp file (git,
SQLite) hits the same wall.

The negative-test surface follows: avc-denials.sh gains a GRP-007 probe that
mmaps a /tmp file (expected denied while the group is off), and avc-analyze.sh
buckets a map-on-ai_tools_tmp_t denial as group-disabled, matched on the
permission rather than the type (read/write on that type are core-granted; an
execute denial stays NEW).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… ones prebuilt

Give operators a first-class way to toggle the optional SELinux policy groups on
an installed host, and ship prebuilt only the groups that have earned it.

A new ai-tools-admin selinux subcommand -- list-groups, enable-group <name>,
disable-group <name> -- loads a prebuilt module via semodule, needing no source
checkout or selinux-policy-devel. The group set, descriptions, and a per-group
stability field are single-sourced in selinux-groups.lib.sh, shared with
selinux/install-selinux.sh (which still compiles a group from source), so the two
cannot drift; install-selinux.sh's local registry and its stale, name-only-
semodule-l matcher are replaced by the library.

Only STABLE groups (the core plus tmpmap) ship prebuilt: their .pp are committed
and packaged, and the tarball CONTENT, the build image's COPY, and install.sh all
select them by glob over the committed .pp, so a stable group's binary cannot be
silently dropped from the RPM. EXPERIMENTAL groups (systemd/pkgmgmt/netadmin/
podman) are unaudited drafts, kept uncommitted; ai-tools-admin enable-group of one
refuses and points at the source compile-and-verify workflow (install-selinux.sh
+ the avc/ bring-up loop) rather than loading unaudited policy.

unit/selinux-groups.sh pins the registry accessors, the stability field, the
validity/experimental predicates, and registry<->filesystem lockstep (a stable
group has a committed .pp, an experimental one must not), skipping the lockstep
outside a source checkout so the SELinux-free RPM selftest container passes.
perms.sh asserts the deployed library; the docs (CLAUDE.md map, confinement/tests/
providers rules, the SELinux README, and rpm-packaging incl. a platform-scope
note) follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…--providers

ai-tools --providers reported agents and integrations but said nothing about the
SELinux layer, so an operator whose dotnet build failed with an opaque EACCES had
no pointer to the cause.

On a host where SELinux is not Disabled, --providers now adds a SELinux policy
groups section: the core module's load state and each loaded optional group, read
unprivileged via a single `semodule -l` (degrading to a
`sudo ai-tools-admin selinux list-groups` pointer if the store is not readable
unprivileged), keyed off the shared selinux-groups.lib.sh registry. When the
dotnet integration is enabled under Enforcing but the tmpmap group is not loaded,
it warns that restore/build will fail on the /tmp mmap and names the fix,
`ai-tools-admin selinux enable-group tmpmap` -- surfaced where the operator checks
status. Read-only and additive; the man page and cli rule follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t datestamp

The naming form still read feature/<ticket>-<name> in the flow diagram and the
prose while the worked examples had moved to the ATR-plus-datestamp shape, so the
abstract rule and the examples disagreed.

State the convention once, matching the examples: feature/<ticket>-<yyMMdd>-<name>,
where <ticket> defaults to ATR when no tracker id applies and <yyMMdd> is the
short two-digit-year datestamp the branch was cut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VERSION is already 0.8.1; add the matching %changelog entry so the version
invariant holds (check-version.sh: VERSION == newest %changelog head) and the
release can be tagged. The entry leads with the fix it closes -- the 0.8.0
SELinux-enforcing dotnet limitation, resolved by the tmpmap group -- then the
supporting additions. A patch release: the mmap grant fixes a documented
limitation rather than adding a headline feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rpm-selftest and release jobs build and run podman containers, and the
runner's podman defaults to an ancient crun (0.17, OCI spec 1.0.0) that rejects
the runtime spec podman generates -- "crun: unknown version specified" at
container create, before any build step. The same runner ships a modern runc
(spec 1.2.1).

Force runc as podman's default OCI runtime via an /etc/containers/containers.conf.d
drop-in in both container jobs (build, run, and sign all use it), and pin those
jobs to ubuntu-22.04 so the runtime versions stay stable. The container-free
shellcheck job stays on ubuntu-latest to keep a current ShellCheck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pm drops it)

The rpm-selftest is green on Rocky 9 but red on Rocky 10:

  /var/opt/ai-tools: expected root:ai-tools 2750, got root:ai-tools 750
  /var/opt/ai-tools/sandbox-projects: expected 2770, got 770

The directories are packaged %dir %attr(2750/2770, root, ai-tools), and on EL10
the owner and group apply but the setgid bit is dropped on install. It is not a
shell-layer issue: on an EL10.2 host install -d -m 2750, the _fixperms chmod
(a+rX,u+w,g-w,o-w), and the %post setfacl all preserve setgid; it is lost inside
rpm's install of the %attr directory (rpm 4.19+ behavior). It surfaced now only
because the EL10 selftest finally runs to completion after the runc fix; the
setgid governs the SANDBOX_GROUP inheritance the collaborative-ownership model
relies on, so the loss is a real bug, not a test nit.

Re-assert 2750/2770 in ai-tools-base %post, beside the setfacl block that already
touches these dirs. Idempotent, and a no-op on EL9 where rpm kept the setgid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nal-groups

Feature/ATR-260729-selinux-optional-groups
A session's largest avoidable cost is output nobody reads: five of every six
lines of a default git log, an unbounded tree walk, a progress bar redrawn
hundreds of times. Rule sets under /usr/local/lib/ai-tools/filters.d narrow a
matching command so the tool itself prints less, and a companion pass strips
terminal control bytes from what it printed. Filtering is on by default;
operator.conf AI_TOOLS_FILTERS selects the sets, and an empty value rules the
whole layer out -- the strip as well as the rewrite.

This trades tokens, never access. The harness re-runs its permission rules on
the rewritten command, so an allow entry must still match it and a deny entry
still overrides; a rule reaches only a command of fully-parsed shape, and
cancels itself when the agent already passed a flag about the same thing.
Every failure direction resolves to the command as the agent wrote it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… events

Claude Code's adapter onto the shared filter engine: one hook body dispatched
on its argument, declared in settings.json as PreToolUse and PostToolUse on
Bash. Only the JSON shapes are Claude Code's, so a second agent reuses every
rule set with an adapter of its own.

It returns no permission decision, so the harness runs its full permission
pipeline on the rewritten command. A rewrite carries the original tool input
forward with only the command changed, and an output replacement preserves
every key it did not filter, so a timeout or a background flag the agent set
survives -- down to the trailing newlines, which a sentinel carries through
the capture. The adapter fails soft where the security gates fail closed: a
missing library, an absent jq, or a malformed event leaves the command running
exactly as written, which is the safe answer for a pure output path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… set

dotnet build, publish, restore, run and test carry --nologo -v q, which drops
the per-project restore chatter and the target summary while keeping errors and
warnings -- the part the agent acts on. A rule cancels itself when the command
already sets a verbosity or logger flag.

The SDK's verbosity has no environment-variable form, so it belongs in a
command rule rather than in the session-env fragment beside DOTNET_NOLOGO. The
rules ship with this package for the reason that fragment does: they are .NET
knowledge, and they install and are removed with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three hooks the agent package ships parse their event JSON with jq,
and each takes a silent `|| exit 0` path without it: the ownership
handback stops returning agent-written files to the operator, the session
hooks stop reclaiming .git and stop the session-end sweep, and the command
filters stop filtering. The first two are ownership guarantees, so a host
without jq loses them with no error -- Requires rather than Recommends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An install that rewrites a config the operator owns needs to leave two
different copies behind, and every config file wants the same two: a .bak
of what they had, which is the only thing that restores their settings if
a rewrite is valid but wrong, and a .shipped baseline to hand-merge from
when the rewrite could not run at all -- an RPM-installed host has no
checkout to copy from.

Both are date-stamped and neither ever overwrites an earlier copy, so
running the installer twice in one day keeps the first, which is the run
the operator usually wants back. The jq gate lives here too: jq is a
package dependency, so the JSON paths built on this report a broken
install rather than degrading, and it is checked where it is needed
rather than at source time -- this library is sourced on every launch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A kept settings.json is the one control-plane file an upgrade does not
overwrite, so a hook added in a new version reached no existing host: the
hook body and its data installed, the declaration that invokes them did
not, and the feature sat inert with only the verification phase to say so.

An install now merges the shipped hook declarations into a kept file,
adding only what is missing and leaving the permission rules it was kept
for untouched. A merge backs the file up first and names every addition;
a merge that cannot run leaves the file byte-identical and drops the
shipped baseline beside it to merge from by hand.

The merge lives in conf.lib.sh beside the sidecar handling it uses,
returning what happened instead of printing it, so the installer carries
only the wording and the unit test sources the deployed library like the
other library tests. Every way the merge can go wrong is quiet -- a
dropped permission array changes what runs, a skipped event leaves a hook
declared nowhere, an under-named report hides the edit from the operator
reading the install log -- so the test asserts all three, plus that the
two sidecars stay distinct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A kept operator.conf never gains a key a new version documents, so an
operator upgrading into this release would not learn AI_TOOLS_FILTERS
exists -- the block is in the shipped file and reaches no existing host.

New options are now named at install time, with the shipped baseline
dropped alongside to copy the documentation from and a diff suggested
against it. Deliberately a report and not a merge: with the present/absent
grammar an absent key already means its default, so what a stale file
costs is knowledge rather than behaviour, which does not justify rewriting
prose whose layout is the operator's -- least of all in the file carrying
the operator list. A key already set, or deliberately commented out,
counts as seen and is not re-announced on every upgrade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The launch allowlist had its own parse loop copied into three components
-- the wrapper, the CLI, and the chown helper -- so the rule for what a
line means lived in three places and could drift. A line the wrapper
resolves but the chown helper does not is a project the agent launches in
whose files never come back.

One parser now serves all three, and the file gains what every other
config already had: end-of-line comments, and quotes for a path holding a
space or a literal #. Every existing shape parses exactly as before, which
the unit test pins first -- a line that stops resolving silently drops a
project from the gate. The wrapper and the CLI refuse to start without the
library rather than falling back to a private parser, and the chown helper
aborts on its bare source, since a parser that matches nothing would
decline every hand-back in silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The install enabled linger and then waited for the account's user-bus
SOCKET FILE to appear, but a socket left behind by a manager that has
exited is present while every connection to it is refused -- so the wait
passed and both systemctl --user calls failed with "Connection refused",
leaving the auto-update timer never started.

Readiness is now asked of the system manager, which is authoritative and
needs no bus, after starting user@<uid>.service -- the documented,
idempotent way to have a manager exist for a nologin account. The calls
themselves go over systemd's machine transport, where root is already
authorized; dropping into the account with sudo instead needs that
account's own bus to accept a connection it refuses from a process root
switched into, so a healthy manager read as an absent one, in the install
and in the suite's own check alike. The sudo form stays as a fallback for
a systemd without the transport.

A host whose manager still does not come up keeps the enablement symlink,
so the timer starts at next boot, and every command printed for the
operator to paste carries sudo -- each needs root and lands in an
unprivileged shell. Group enrolment reports what the run actually did
rather than announcing "adding" on every re-install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The group prompts described the shipped default rather than the host, so
an operator running an upgrade with a group enabled -- tmpmap, say, which
the dotnet integration needs -- saw "Enable? [n]" beside it and no
indication it was already loaded. Answering No never disabled it (this
step only ever adds modules), but the prompt read as though it would.

Loaded groups are now named before the gate that skips the whole section,
which is the default answer and where the question actually arises, and
each is marked in the list with the verb that does remove one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… guard can fire

install-selinux.sh runs under IFS=$'\n\t' while the labelling report's
fields are space-separated, so `read verdict subject detail wanted` put
each whole line into ${verdict} and every case missed. That silently
disabled the guard it feeds: `bad` never matched, so the install could
never abort on an entrypoint that failed to take ai_tools_exec_t -- the
one label whose absence leaves a session running unconfined. It also
explains "no agent path found to label" on a host whose paths had just
been labelled successfully.

semanage's "already defined, modifying instead" goes to stdout, which is
where that function emits its parsed report, so those notices were being
read as verdicts too. Both streams are now dropped; the -m fallback is
the handling.

The renderer also says which situation it is in rather than covering two
with one line: an empty report means no agent resolved from the manifests
at all, so nothing grants the entrypoint its type and a session refuses to
launch at the transition preflight -- worth naming the commands that show
which agents are enabled and re-apply the label. A verdict the renderer
does not know is reported rather than dropped. Commands printed for the
operator to paste carry sudo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rting

Every install forced a full recursive relabel (restorecon -RF) of every
registered project, rewriting the context of every file in every tree --
node_modules, .git and build output included -- whether or not it was
already correct. On a host with several large projects that is the slow
part of the install, and it buys nothing: a file created inside a
labelled directory inherits that directory's type, so a labelled tree
stays labelled on its own.

The primitive now takes the effort its caller needs. A first-time claim
still converts in full, which is what a tree of user_home_t files
requires; the installer's sweep repairs instead, fixing what drifted and
skipping what already matches. Drift is still caught, and an unchanged
tree costs a read-only walk rather than a write per file. The semanage
rule is still asserted in both modes, so a type change from a policy bump
reaches existing projects, and its "already defined" notice no longer
prints as though it were an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The install's output described the tool's internals rather than the
operator's situation, and ran the sections together.

Control-plane assertions open their own section instead of trailing the
previous file's keep-or-reset prompt, where they read as part of it.
Managed-asset status lines sit indented under the directory heading that
introduces them and name the asset alone, so a listing reads as entries of
that directory rather than a flat list repeating the directory each time.

Module and policy-group sets print as a bracketed, bold [a, b] -- space-
separated names blur into the prose around them and hide how many there
are. Both joins are done by hand: "$*" uses only the first character of
IFS, and paste -d cycles through its delimiters, so a ", " separator
silently loses its space either way.

The relaunch reminder prints while the install is still running, so it
says when it applies -- an operator reading "exit and relaunch"
mid-install has nothing to relaunch yet. semanage's "already defined,
modifying instead" is silenced at the last call site that still leaked it,
and the status line says whether the context was labelled or re-applied.
Group management points at the shipped ai-tools-admin when it is
installed, rather than a checkout path the host may not keep. Every
suggested command that needs root carries sudo, including the timer
verification, whose machine spec also needs its .host suffix to resolve,
and the closing pointers name the file an operator actually tunes and the
CLI manual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The host options had only their inline comments. This gives them a manual:
the KEY=value grammar, every option with what present and absent each
mean, and two worked examples. Written to the happy path -- every option
is optional, an absent key keeps its default, and a fresh install needs no
edits.

Installed by both the from-source path and the RPM, asserted by the
permissions test, and pointed at from the install's closing summary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implement command output filtering and enhance .NET SDK verbosity control
Enhance config management with backups, merges, and reporting
A minimally seeded operator.conf -- the one `ai-tools-admin operator add`
writes -- reported every option the shipped file documents as new, because the
indented `#   KEY=value` examples in that file's own grammar header counted as
mentions. A comment indented past one space is prose and mentions nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
settings.json ships %config(noreplace), so an upgrade keeps the permission
rules a host tuned instead of overwriting them and rpm parks this version's
copy alongside as .rpmnew.

`sudo ai-tools-admin postupgrade` reconciles what an upgrade leaves, one
treatment per file: hook declarations are merged into settings.json after
listing exactly what will be added and confirming, with a dated .bak written
first; operator.conf and the sudoers grant are reported and never written.
Each package's %post points at the command whenever a .rpmnew is waiting, so a
hook that installs with nothing to invoke it, or an option a host has not seen,
is signposted rather than discovered later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…elog

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
p4nda and others added 19 commits August 1, 2026 03:22
Drives the deployed `ai-tools-admin postupgrade` from dispatch through the
registry to each treatment, asserting which one every file got: settings.json
merged with its permission rules intact and a dated .bak written first,
operator.conf reported and byte-identical afterwards, the sudoers grant shown
and neither written nor dropped -- its fixture is a grant of everything to
everyone, so a silent adoption fails loudly. The cleanup prompt is the fourth
property: it may default to yes only once the two files match.

The command reconciles a registry of absolute control-plane paths, so it takes
AI_TOOLS_POSTUPGRADE_ROOT to prefix them and reach a fixture tree in the
testdir instead. That is a root-only hook of the same standing as
AI_TOOLS_ALLOWLIST -- the helper is reachable only as root, sudo strips the
name, and a caller who could set it may already edit those files outright --
and is unset in production. The agent-side half of the pair is already
deployed: no input this command reads is agent-writable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new-option report reads a `#KEY=` line as an option the operator has
already seen. operator.conf documents its own grammar with indented `#   KEY=`
examples, so the distinction decides whether a freshly seeded file looks like
it already knows every option there is -- pinned here in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The per-install project sweep had switched from `restorecon -RF` to `-R`
("repair" mode) on the premise that a forced pass rewrites every file. It
does not: `restorecon` writes only a file whose context differs, so `-RF`
is already idempotent on a labelled tree. The one thing `-F` adds is
resetting a file that carries a customizable or explicitly foreign
context -- exactly the drift an install-time sweep exists to catch.

Dropping it meant a fixture copied into a claimed project with a
preserved system context (a `cp -a` / `tar --selinux` of a real /run,
/var, ... path, which only an unconfined actor can apply) was no longer
corrected, so it kept a system label the confined agent cannot read. The
next session's workspace walk then denied on every such path, and
setroubleshootd amplified that into a host-wide CPU flood.

Restore the force and drop the now-purposeless convert/repair split:
`ai_tools_label_project` always uses `restorecon -FR`, and the installer
sweep re-asserts the label idempotently while still correcting drift.

Also normalize every `restorecon -RF` in the tree to `-FR` -- identical
behavior, but it no longer reads like a destructive `rm -Rf`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enhance upgrade reconciliation and comment handling in configs
The dotnet command rules inserted `--nologo -v q` between the verb and the
agent's own arguments. Placed ahead of a positional, `--nologo` makes .NET 10's
`dotnet run <file>.cs` stop resolving the file as a file-based app -- the
command fails with "Couldn't find a project to run" -- so the rewrite changed
what the command did, not merely how much it printed.

The banner `--nologo` suppressed is already handled globally by DOTNET_NOLOGO
(session-env.d/dotnet.env.sh), so it was redundant on every verb. Dropping it
from build, publish, restore, run and test keeps the `-v q` verbosity savings
while leaving the command's behaviour intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coreutils that preset a file's label before creating it -- sed -i, cp,
mv, install via setfscreatecon(3) -- were denied process:setfscreate on
ai_tools_t, so libselinux returned EACCES and printed "failed to set
default file creation context" to stderr on every such command. In the
0.9.x .NET bring-up the noise is most visible interleaved through dotnet
build and NuGet restore output, but it affects any confined command that
presets a context.

The permission is target-less and grants no new labeling reach: the
files_tmp_filetrans rule already transitions new /tmp inodes to
ai_tools_tmp_t and file-creation checks are unchanged, so the inode is
born the same type whether or not the context is preset. One grant covers
writes under ai_tools_project_t, ai_tools_tmp_t and ai_tools_home_t.

Asserted from the agent side in avc-testsuite.sh: sed -i on both a
project and a /tmp file must emit no setfscreate warning under enforcing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Part of the 0.9.x .NET sandbox bring-up. MSBuild keeps worker nodes
resident between invocations by default and holds each build task's
assemblies mapped; a subsequent build in the same solution then fails
with a file lock on the prior project's output (dotnet/msbuild#6461).
All operators share one ai-tools UID, so these back-to-back builds
collide readily.

MSBUILDDISABLENODEREUSE=1 -- upstream's standing workaround until the
task-host fix lands -- starts fresh nodes per invocation, trading a small
cold-start cost for deterministic builds. Pure runtime env; no SELinux
policy involved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continues the 0.9.x .NET sandbox bring-up. The CoreCLR host materialises
its apphost and runs its W^X JIT by mapping an anonymous memfd (tmpfs_t)
PROT_EXEC. ai_tools_t already holds process:execmem (shared with V8) but
no execute on a tmpfs file object, so anything that produces or runs a
native host is blocked at build or launch:

  needs apphost  console apps, ASP.NET Core, worker/hosted services,
                 xunit.v3 (out-of-process testhost), single-file publish
  does not       class-library builds, and in-process test runners
                 (MSTest on Microsoft.Testing.Platform)

Two optional groups back a full .NET workflow and are disjoint -- a build-
and-run project enables both:

  tmpmap   ai_tools_tmp_t:file map        NuGet restore + build (the shm
                                          mutex under /tmp/.dotnet/shm)
  apphost  tmpfs_t:file map+execute       apphost / JIT of an executable

  sudo selinux/install-selinux.sh enable-group apphost

Threat model: apphost permits fileless in-memory execution but confers no
new privilege. execmem already concedes arbitrary in-process native code;
the exec'd image stays in ai_tools_t with no entrypoint to a more
privileged domain, DAC and the type system unchanged, and /tmp stays
noexec regardless. It ships disabled and experimental until an enforcing
bring-up trims the rule and scopes it to a private memfd type.

Wiring is registry-driven (selinux-groups.lib.sh), so enable/disable/
list-groups, tests/unit/selinux-groups.sh and the AVC harness need no
per-group code; adds the GRP-008 enforce-verification probe and the
analyzer's tmpfs_t attribution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The .NET bring-up adds apphost beside tmpmap, so the optional groups are
now a mix of stable and experimental and can no longer be labelled
"experimental" wholesale:

  - print the section and each group's (stable|experimental) tag before
    the skip gate, so the prompt follows its explanation
  - offer an already-loaded group for recompile-from-source (build_pp,
    picking up an edited .te/.fc in a source checkout)
  - report the full loaded set, not just this run's additions, so a kept
    group does not read as disabled
  - render every labelled path with a consistent green check
  - fix two IFS newline-join defects ("${arr[*]}" and a two-arg say under
    IFS=$'\n\t') that split a summary line and de-indented a command

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
offer_selinux becomes the single labelling point: it drives
install-selinux.sh on both paths -- the install action on accept, the
relabel action on a declined-but-loaded host -- and install.sh drops its
separate ai-tools-relabel-agent pass, which double-labelled the entrypoint
on accept and printed inconsistently against the styled output. The
declined path now gets a full restorecon (entrypoint, projects, conf)
rather than the entrypoint alone, so post-upgrade bin_t drift on a project
is repaired too. The helper is unchanged and still serves ai-tools
--relabel and the post-upgrade relabel watcher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the 0.9.x .NET bring-up items to the unreleased 0.9.0 entry -- the
apphost optional group, the SELinux warning fix, the MSBuild node-reuse
fix, the single install relabel, and the clearer optional-group prompt --
with a closing pointer to enable tmpmap and apphost for .NET workloads.
Reader-facing lines; the mechanism stays in the commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t binaries

Continues the 0.9.x .NET bring-up. With tmpmap (restore/build) and apphost
(JIT/apphost memfd) an executable project builds and runs, but the runtime
is still blocked on two fronts the base domain does not cover -- both kept
out of the base because the agent itself needs neither:

  IPC (benign)  the diagnostic unix socket and CLR debug FIFO under /tmp,
                the .local test-host socket, and connecting to it. The base
                transitions new /tmp files but not sock_file/fifo_file (so
                those default to tmp_t and cannot be created), and grants
                create_stream_socket_perms but not connectto. That is why
                multi-node MSBuild hung on its worker pipes and dotnet test
                (Microsoft.Testing.Platform) got EACCES reaching its test
                host. Adds getsid and /proc/sys/net reads too.
  exec (gated)  executing a native host the agent BUILT from the project
                tree (apphost, testhost, R2R via dotnet exec) needs execute
                on ai_tools_project_t -- on-disk native code the sandbox
                wrote, run as a new process image. No new privilege
                (execmem already concedes in-process native code,
                execute_no_trans keeps the child in ai_tools_t), but the
                reason the module is off by default and experimental;
                execmod covers an R2R image relocated in place.

One module, two sections, so a .NET host enables it with a single
enable-group; the exec section splits out cleanly if IPC-without-execute
is ever wanted. Registry-driven like the other groups; adds the GRP-009
probe, the analyzer's netcore attribution, and the ai-tools status nudge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A dedicated path-scoped rule for running .NET in the sandbox: the three
optional groups (tmpmap, apphost, netcore), a project-type -> group map,
and the enforcing bring-up denial breakdown that shaped netcore's
benign/sensitive split. Trims the duplicated group prose in
providers.rule.md to a pointer and registers the rule in the CLAUDE.md
component map, single-sourcing the story.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add netcore to the unreleased 0.9.0 entry -- dotnet test, multi-node
MSBuild, and running a built binary -- and fold it into the closing .NET
guidance (tmpmap, apphost, netcore). Reader-facing; mechanism is in the
commit and dotnet.rule.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…x-apphost

Enhance .NET support with SELinux adjustments and new features
An out-of-process xUnit/VSTest test host binds an ephemeral TCP port and
the runner connects back to it, so `dotnet test` for xunit.v3-style
projects fails under enforcing with an EACCES on connect(2) that the
existing unix-socket and diagnostic-FIFO rules do not cover.

Add corenet_tcp_connect_generic_port(ai_tools_t) to netcore §1. It is the
same benign IPC class as the self:unix_stream_socket connectto already in
the group -- the sandbox reaching its own child over loopback, not a new
host surface -- so it stays in the runtime-IPC half, not the sensitive
native-execution half.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude Code's file-write path stamps a group/mask execute bit on files it
creates. On a data file this surfaced as -rw-rwx---+ (an ACL mask of rwx),
which ai-tools-chown's world-only `o=` and unclaim's `g-w` both left in place.
Harmless as an ACL mask, but tar/zip store st_mode (whose group bits are the
mask), so an archive extracted without ACLs turned it into a real group-execute
bit on non-executable files.

Both now clamp the group class keyed on the owner-execute bit (the only exec
bit git records): a data file (owner rw) drops the stray execute to group rw,
while a genuine script (owner rwx) keeps group r-x. chown strips execute only,
so on an ACL'd file the mask stays rw and the agent can still edit next turn;
unclaim's relative g-w[,g-x] leaves any setuid/setgid bit untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Running an operator command (--project-*/--sandbox-*/--lockdown/--reclaim/
--relabel) as a user not listed in OPERATORS in operator.conf used to proceed
through the registry writes and confirm prompts, then fail deep in a root
helper (ai-tools-lockdown: "not in allowed projects for current operator") and
roll back -- a misleading flow that never names the actual cause.

Add a require_operator preflight before dispatch that refuses such a command up
front and points at the fix: sudo ai-tools-admin operator add <user>.
operator.conf is 644, so the unprivileged CLI reads OPERATORS directly, and
enrollment there takes effect on the next command. The informational commands
(--help/--version/--list/--providers) stay open, so an unenrolled user can
still read usage and inspect the host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the two fixes in this release: the stray group-execute strip on the
ownership handback and unclaim, and the up-front operator-membership gate in
the CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@p4nda
p4nda merged commit 091663c into main Aug 5, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant