Skip to content

Tags: hashgraph-online/hol-guard

Tags

extension-distribution-fc270a183382f0e567b556bb94c7d957d00a8ebf-40281634882.1

Toggle extension-distribution-fc270a183382f0e567b556bb94c7d957d00a8ebf-40281634882.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(plugin): check and convert plugins against the Agent Plugins spec (

#6021)

* feat(plugin): check and convert plugins against the Agent Plugins spec

`hol-guard plugin check --spec agent-plugins-1.0` reports whether a plugin
root conforms to the Agent Plugins 1.0.0 spec: the closed plugin.json field
set, skill names and descriptions, and each mcp.json server's transport,
command, cwd, env and headers. Every finding names the spec section it
breaks and whether a client would reject the plugin or only the component.

`hol-guard plugin convert --to <runtime>` writes one runtime's package. The
new agent-plugins target emits plugin.json and mcp.json with each server's
type, rewrites runtime root placeholders to `${PLUGIN_ROOT}`, leaves out
env, headers and servers that cannot be expressed portably (and says so),
then checks the result against the spec.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* docs(plugin): document Agent Plugins check and convert

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(plugin): keep converted MCP servers runnable and report what clients skip

Servers whose files the package does not carry, whose args read user
variables, or that need a working folder or ${PLUGIN_DATA} are left out
of converted configs and reported instead of shipping broken. Agent
Plugins sources keep their plugin root in Claude Code and Gemini CLI.
The check now rejects non-string or oversized skill descriptions, more
skills than it can check, unreadable entries and malformed server URLs,
and prints what clients do with each finding. The checker is split into
smaller modules.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(plugins): carry only plain plugin files with converted servers

A server that names the whole plugin root, reaches outside it with . or ..
segments, goes through a symlinked folder, or names a file at the path of the
generated plugin.json or mcp.json is now left out of the Agent Plugins
package. A working folder inside the plugin travels with its server. Two
different files at one package path stop packaging with
plugin_kit_package_path_conflict, and a file reached twice is copied once
before it counts against the size budget.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(plugin-kit): report a server whose working folder would ship empty

A cwd folder that exists but holds no file the package walk keeps (empty,
or every file excluded) no longer counts as bundled, so the server is left
out of mcp.json and reported as not portable instead of pointing at a
folder the package does not contain.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(plugin-kit): keep excluded and oversized server files out of Agent Plugins packages

A file a server names now counts as bundled only when the package walk
found it, so an excluded file makes the server not portable instead of
shipping. A server file too large to copy fails the package instead of
leaving the server in mcp.json without it.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* test(plugin-kit): cover an oversized server file inside a skill folder and clean up large fixtures

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-f6274d1e36425b56663f2e91e8456f464103ee85-40251784315.1

Toggle extension-distribution-f6274d1e36425b56663f2e91e8456f464103ee85-40251784315.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(release): ship a Linux ARM64 native wheel on the GitHub release (#…

…6009)

Linux ARM64 installs got only the universal wheel from PyPI, so Guard had
no native runtime there and every protected command failed with
native_unavailable. Releases now build a manylinux_2_17_aarch64 wheel
around the static aarch64-unknown-linux-musl runtime on the ubuntu-24.04-arm
runner and attach it to the GitHub release only, the same tier as Windows
ARM64. PyPI staging leaves it out.

The updater now treats manylinux_2_17_aarch64 as a published GitHub-only
platform, so `hol-guard update` on a pure-wheel Linux ARM64 install fetches
that wheel and checks it against the native-wheels.json digests embedded in
the PyPI pure wheel, exactly as it does for Windows ARM64. Doctor and update
payloads on Linux ARM64 now point to `hol-guard update` instead of reporting
an unsupported platform.

Pull request canaries skip the new runner, and repair runs of sources that
predate this change keep their original wheel set.

Fixes #5923

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-f82dccec4fd168df4d41a2c0cd22515bfa84e783-40247332380.1

Toggle extension-distribution-f82dccec4fd168df4d41a2c0cd22515bfa84e783-40247332380.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(command): refuse a contained run that hands the shell a directory…

… search path (#5996)

* test(command): pin that a contained run is never handed a directory search path

A bare relative `cd dashboard` is admitted into the workspace sandbox as the
directory of that name below the run cwd. Elsewhere the decision will only
read a relative operand that way against proof that the caller exported no
CDPATH, so the containment reading looked like the loose one.

It is not. The launcher builds the run environment from an allow-list, and
CDPATH is on no list: it never reaches the sandbox, whichever way the caller
spells it. A CDPATH the command sets itself is already refused in
`cwd::locations`. Between them the search a shell would make cannot happen
inside the sandbox, so the operand resolves to exactly the directory the
shape names.

That proof lived only in the gap between two files. Pin it with a test over
`environment::filter`, and say in `cd::below` and `cwd::locations` which
property the reading rests on, so a name added to the allow-list later has to
answer for it rather than quietly undoing the containment shape.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(command): refuse a contained run that hands the shell a directory search path

The containment shape reads a bare relative `cd dashboard` as the directory of
that name below the run's cwd. The claim behind that reading was that CDPATH
cannot reach the sandbox: the launcher builds the run environment from an
allow-list and CDPATH is on no list.

That covers an inherited CDPATH, not one the command carries. The launcher runs
the command text as written, so `CDPATH=/ cd etc && ls` sets the search path
inside the sandbox, the shell finds `/etc` before the workspace's own `etc`, and
`ls` lists a directory the shape never read. Only `cwd::depths` noticed the
name, and only to withhold a parent step, so a command that spends no `..` was
admitted on the wrong directory.

Screen it in the shape instead. `cd::sets_search_path` refuses a segment that
sets CDPATH, in a leading assignment or in the arguments of a builtin that
exports one, matched without case because zsh ties the same search to a
lowercase `cdpath` and `export cdpath=...` there sets both. A command that only
names the variable — greps for it, echoes it, reads a file called after it —
sets nothing and stays contained.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(command): read every spelling that sets a directory search path

The screen that refuses a contained `cd` when the command gives the shell its
own `CDPATH` read only `export`, `declare`, `typeset`, `setenv` and `set`, and
read a name up to its first `=`. `readonly CDPATH=/` and `local cdpath=/` set
the variable as much as `export` does and are already carried as inert shell
state, and `CDPATH+=/` appends, which from unset is the same as setting it.
Either spelling put a search path in front of a bare relative `cd` operand that
the shape then read against the workspace.

The builtins that set a variable now include `readonly` and `local`, the name
is read with a trailing `+` off, and the segment's own leading assignments are
read as written, so an appending assignment ahead of the command is seen
whether or not the parser carries it as an assignment.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-b036d45cd416161e1afc10710b43234dc19823df-40256982471.1

Toggle extension-distribution-b036d45cd416161e1afc10710b43234dc19823df-40256982471.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(runtime): derive and publish the policy verifier key natively (#…

…6011)

* feat(runtime): derive and publish the policy verifier key natively

The resident refuses to serve until the derived verifier key exists, so
provisioning that key can never be a resident call. This adds a one-shot
`provision-verifier-key --stdin` arm to the runtime CLI, which takes the
policy master on stdin — never on argv, where another local process could
read it — derives the verifier key, and publishes it under the runtime's
private state directory.

Creation never replaces an existing key. On POSIX the publish is a
`hard_link`, which fails with `AlreadyExists` rather than overwriting; on
Windows it is a rename issued with `FILE_RENAME_REPLACE_IF_EXISTS` cleared,
which reports the same. A key that is already there is read back and
compared in constant time: a master that derives to something else is an
explicit `native_policy_verifier_key_mismatch`, not a silent verifier reset.

Nothing calls the new arm yet; the Python provisioning path still owns the
callers and moves over separately.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(runtime): keep a failed verifier key write out of the final path

Publishing the derived key hard-links a private temporary into place so it
can never replace an existing key. Any link error other than AlreadyExists
was read as a filesystem without links, which dropped a full disk, a denied
directory or an I/O error into the fallback that creates the final path
itself. If that write then failed, a short policy-verifier.key was left
behind, and the error was answered with confirm_existing: callers saw
native_policy_verifier_key_invalid or native_policy_verifier_key_missing
rather than the write failure, and every later provisioning and the
resident's own startup check rejected the stub until someone deleted it.

Fall back only for a link the filesystem refuses outright (Unsupported,
EPERM, EOPNOTSUPP, ENOSYS) and report every other link error. In the
fallback, track whether this call created the file: a write that fails
after our own exclusive create removes it again, a create that fails with a
key already there is still the race that confirm_existing settles, and a
create that fails with nothing there returns the write failure.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* test(runtime): give each verifier key test its own temporary directory

A process id and a counter both repeat across runs, so a directory left
behind by a failed assertion could be read as a later run's state and turn a
Created into a Verified or a key mismatch. TempDir names a directory nothing
else can reach and removes it however the test ends.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-ac1a0b998b5b184399b7dd85658d27482295b2e1-40254853811.1

Toggle extension-distribution-ac1a0b998b5b184399b7dd85658d27482295b2e1-40254853811.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(connect): keep polling for device approval when one request goes …

…unanswered (#6005)

* fix(connect): keep polling for device approval when one request goes unanswered

A single token poll that timed out or hit a reset connection ended
`hol-guard connect --headless` with "Guard authorization failed: timed
out", even though the device code was still valid and the user could
still approve it. RFC 8628 section 3.5 asks clients to back off and keep
polling after a connection timeout.

Unanswered polls now double the interval (capped at 60 seconds) and keep
waiting until the device code deadline. Non-transient URL errors such as
certificate failures, and managed-network policy blocks, still fail at
once.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(connect): never poll for device approval after the wait window closes

Cap the backoff sleep after an unanswered poll at the remaining wait
window and check the deadline again before the next request, so a long
backoff cannot start a late poll.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(connect): keep the server polling interval when backing off

A device-code interval above the 60-second backoff cap was lowered to 60
after an unanswered poll, so the next request came sooner than Guard
Cloud asked. The backoff now never drops below the current interval.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-3083c57d24c81e48c56e115e2161c415d53a51e6-40263449108.1

Toggle extension-distribution-3083c57d24c81e48c56e115e2161c415d53a51e6-40263449108.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(guard): check project agent hooks in background workspace assess…

…ments (#6014)

* feat(guard): check project agent hooks in background workspace assessments

Workspace assessments now read the hook configuration files agents load from the worktree root and run each hook command through the shared launch screen. A hook that pipes a download into a shell, sends the environment to the network or runs under sudo is listed as a dashboard warning with its event and configuration path, never the command.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(guard): read top-level hook events only where agents allow them and bound arguments

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(guard): name every cause of an incomplete hook check in the dashboard

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-1229abfd52d2ac39ad61470b230969cd20eeec6b-40260404875.1

Toggle extension-distribution-1229abfd52d2ac39ad61470b230969cd20eeec6b-40260404875.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(extensions): stop a failed policy save from blocking every later …

…save (#6015)

* fix(extensions): stop a failed policy save from blocking every later save

A save that failed after writing its prepared transition, but before the
vault anchor moved, left that row in place. Every later read reported the
authority as needing recovery, preview refused with authority_unavailable
before apply's own recovery could run, and the daemon only ever read under
a shared lease, which cannot roll anything back. Nothing cleared it, so the
operator stayed locked out of extension policy and extensions resolved
blocked.

- The failing save now takes its own prepared row back at once. The
  resident abandons a row the caller names by its transition digest without
  waiting out the retry window, and still keeps it if the anchor moved.
- Preview reads once more as the writer before refusing, so a row a crashed
  save left behind is rolled back on the next attempt.
- The daemon's periodic refresh reads as the writer when its shared read
  reports recovery required, so an affected install heals on its own.
- A protected read now replaces a non-protected snapshot at the same
  revision, and apply refusals are logged by exception type.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* test(extensions): model crashed saves where a test needs a prepared row

A save that fails in process now takes its own prepared row back, so
the tests that need a row left behind simulate a process that died
after preparing. The self-heal preview test stops at target metadata
instead of depending on pack preparation.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* docs(runtime): note that a failed save names its own row for rollback

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-0561efc939e0b93c983d390884e6baae156e1783-40249131205.1

Toggle extension-distribution-0561efc939e0b93c983d390884e6baae156e1783-40249131205.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(command): walk a project's own skills tree without review (#5994)

* fix(command): walk a project's own skills tree without review

A checkout ships `.agents/skills`, `.claude/skills` or `.codex/skills`
beside its code, and an agent finds its skills by walking that tree. A
plain listing of the session folder already prints those names, so
`ls .agents` was admitted while `find .agents -maxdepth 3 -type f -print`
asked for approval — the walk that reads the project's own skills had to
stop on an approval row.

Extend `skill_listing_root`, the names-only listing root every one of
those callers shares, with the tree a checkout carries itself. It reuses
the directory rules `project_skill_listing_directory` already carries
(the `.agents`/`.claude`/`.codex` marker, a `skills` child, no hidden
component below it, and an `.agents` root that holds nothing but
`skills`) and adds the credential-store screen the home tree keeps, so
`find`, `ls` and the read-argument screen answer the same way. Each
document below the tree still keeps its own read proof, and a hidden
project directory that is not a skills tree still asks.

Regression cases: `find-project-agents-root`,
`find-project-skills-tree` and `find-project-agents-root-then-listing`
in `tests/fixtures/benign_false_positive_gate.json`, plus
`rust/crates/guard-command/tests/pretool_find_project_skill_root.rs` for
the negatives the gate's fixture home cannot build.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* test(command): drop the hidden-directory negative the exact-safe route already answers

`find .worktrees -maxdepth 2 -type f -print` was asserted to keep review,
but a names-only walk of an inert hidden directory is already admitted by
the exact-safe command route, with or without the project skills tree. The
assertion said nothing about the new admission, which is bounded by
`project_skill_listing_directory` and the credential-store screen that the
remaining two negatives cover.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(command): screen what a descending skill listing reaches, not only its root

`skill_listing_root` screened the operand: a hidden folder or a credential
store named as the root is refused, and the home tree additionally refuses a
hidden component below `skills`. `find` was given that same screen and then
walked. So `find .agents/skills -type f` printed `.agents/skills/.private` and
`.agents/skills/credentials`, the two names the screen refuses when either is
named as the root, and the tree the operand rules declined to list was listed
from one level up.

A listing that descends now has to clear the bounded metadata walk as well,
which is what `ls -R` of the `.agent` root has always had to do. `find` asks
for it on every root, because it prints a hidden entry at any depth its
expression reaches. `ls` without `-R` prints no hidden name and descends
nowhere, so its route is unchanged and a plain listing of the same tree stays
silent.

The misplaced doc comment above `skill_link_resolves_over_home`, which
described `skill_listing_root`, now sits on the function it describes.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(command): keep the home skill listing the home trees already had

The descending screen was applied to every skill root, including the two
home trees, where it refuses a hidden descendant on its name alone. A skill
directory holding a `.cache` or a `.venv` is the ordinary case there, so
`find ~/.agents/skills/review-loop -type f` started asking — a listing that
has been silent, and that `pretool_skill_listings` pins.

Apply the walk where this change opened the listing: the tree a checkout
carries itself. The hidden folder a home tree reaches is a gap that predates
this change, and closing it needs a screen that reads the names it finds
rather than one that refuses every hidden entry.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-408a8a0d7f1519210b3e88f45c4c4021240a3717-40247834337.1

Toggle extension-distribution-408a8a0d7f1519210b3e88f45c4c4021240a3717-40247834337.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(claude): store a tool-call permission notice with its credentials…

… masked (#6001)

* fix(claude): store a tool-call permission notice with its credentials masked

A Claude Code review writes a signed permission notice and a pending row
holding the command that asked. Both kept the raw text, so a bearer token
or API key in a curl header sat in `sync_state` in plain text.

The notice's command only ties the notice to its pending row and is never
an approval binding (that rides on `artifact_hash`), so the store now masks
`command` and `raw_command_text` on `tool_call` notices before signing, the
same way the review queue stores the command. Both the direct hook lane and
the daemon's fallback writer go through this one store step. Other artifact
types keep their command, since a `tool_action_request` command feeds the
saved policy's exact-match context.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* test(runtime): build the masked bearer fixture at runtime

The credential-masking test spelled a synthetic bearer token inside a
curl command literal, which the curl-auth-header rule flags. Build the
command from a separate token binding and record the earlier commit's
fingerprint in .gitleaksignore.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(claude): mask credentials passed as command arguments in tool-call notices

`redact_text` masks an assignment only at the start of a line, so a
notice for `curl --api-key=... url` or `tool --password ... run` still
stored the value in both signed rows. `redact_command_text` adds an
argument pass: a value whose name carries a sensitive fragment is masked
(`--api-key=*****`, `'X-Api-Key: *****'`, and the argument after a bare
`--password`), quoted values are masked to their closing quote, and shell
operators are never masked. The notice and its pending row use it, so
they still agree when the prompt is marked seen.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(claude): mask a credential argument from its first sensitive separator

A header value ending in base64 padding, such as
`X-Api-Key: <value>=`, put the padding `=` first in the search, so the
whole value was read as the name and left in place. The value now starts
at the first `=` or `:` whose name is sensitive, which also keeps
`--header=X-Auth-Token:<value>` masked.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

extension-distribution-368d35e44cfc9db17f1a59b9dc1a20e70730d5ab-40258252066.1

Toggle extension-distribution-368d35e44cfc9db17f1a59b9dc1a20e70730d5ab-40258252066.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(native): read logged launches, rooted find -exec and looped inlin…

…e programs as review (#6007)

* fix(native): read logged launches, rooted find -exec and looped inline programs

Three shell shapes from #5931 failed native evaluation and were blocked:
a background launch logged to a file (`node server.js > out.log 2>&1 &`),
a `find` rooted at a quoted parameter with `-exec basename {} \;` piped to
`sort` and followed by `shasum`, and a `for` loop that runs `node -e` on
each matched file. They now reach review.

- A redirected launch is read when every redirect names a literal target,
  nothing reads input, and the text without redirects models exactly.
- `find -exec` accepts `$NAME/` or `${NAME}/` inside double quotes as a
  root path.
- Loop bodies are split outside quotes; each inline interpreter launch
  must name a single program source, and a loop that changes directory
  or uses a computed command word keeps the floor.
- Assigning a plain lowercase shell variable is no longer read as moving
  data.

Commands where a part could run something else (shell reading a file,
unnamed redirect targets, preloads, `sh -c` under a parameter root) keep
the evaluation-failed floor.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(native): keep the floor for written scripts, prefixed cd and expanded values

- A redirect that writes a file the rest of the text names is no longer
  read as a log, so `printf '...' > run.js && node run.js` keeps the
  evaluation failure.
- The directory-change check reads past `builtin`, `command`, `time`,
  `nohup` and `exec`, and refuses `eval`, `source` and quoted or computed
  command words.
- A plain shell variable may expand a parameter only as the quoted root of
  a path, so `x=$SECRET` still counts as moving data.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(native): count globbed and partly quoted names of a written file

The write-then-run check compared names spelled exactly, so a launch of
the file a redirect just wrote through `r*.js`, `ru"n.js"`, `r\un.js` or
`"$f"` still read as a log. Names now compare with quotes and escapes
removed, and a word that could expand to the name (an unquoted glob or
brace, or a parameter outside single quotes other than `$!`) counts as
naming it.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(native): match a written module by the names an interpreter resolves

`node run` loads `run.js`, and `node ./lib` or `node .` load an
`index.js`, so a redirect that writes one of those files and a launch that
names it that way still ran a program the scan never read. A module-like
target now matches its stem, any directory on its path, and `.` or `..`.
Plain output such as `/tmp/x.log` keeps exact-name matching, so
`npm run dev > /tmp/dev.log &` is not caught by its stem.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(native): read directory launches and operator-joined redirect targets

`node ./` and `node lib/` load an `index.js` but left an empty name, so a
redirect that wrote that file still read as a log. A word now names its
last non-empty component, and one with none left names the directory as
`.` does. A redirect target also ends at the first shell operator, and a
process-substitution target counts as named.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(native): find redirects glued to a quoted word

A redirect written straight after a closing quote, as in printf 'x'>run.js, is now read as a write to run.js, so a following launch of that file keeps its evaluation failure.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

* fix(native): list the compound launch module and accept the containment floor

The command source manifest now names native_command_unmodeled_compound.rs. A written script launched in the foreground meets workspace containment first where a sandbox exists, which is stricter than review, so the negative controls accept that floor as well as the evaluation failure.

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>

---------

Signed-off-by: ZCF <291624129+zerocodefast@users.noreply.github.com>