Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.