@argszero/cordis-plugin-sandbox-grant-advisor 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # @argszero/cordis-plugin-sandbox-grant-advisor
2
2
 
3
3
  Turns a sandbox environment failure that has **no path forward** into a
4
- diagnosis the model — and the user reading the transcript — can act on. Three
4
+ diagnosis the model — and the user reading the transcript — can act on. Four
5
5
  signatures, one mechanism:
6
6
 
7
7
  ```
8
8
  SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\ws) # Windows workspace ACL
9
9
  PTY shell exited during startup # persistent shell × confining mode
10
10
  [exit code: -1073741502] (0xC0000142) # a confined child that never started
11
+ [exit code: 1] sandbox: { mode: "workspace-write", denied: true }
12
+ # denied INSIDE the workspace
11
13
  ```
12
14
 
13
15
  **This plugin is the stopgap for "the error does not name the outstanding
@@ -15,7 +17,7 @@ condition".** It repairs nothing: no ACL is written, no privilege is requested,
15
17
  nothing is elevated, no environment variable is set for another process, no
16
18
  preset is installed and no mode is changed.
17
19
 
18
- ## The three failures it recognizes
20
+ ## The four failures it recognizes
19
21
 
20
22
  The first two are recognized on the public **`tools/post-execute`** waterfall
21
23
  (`@deepseek-ai/dsh-tools`) from the failure text. That seam is the one that has
@@ -24,9 +26,9 @@ all three of what a diagnosis needs: the failure
24
26
  `isError` result), an agent identity to attribute it to (`exec.agent`), and a
25
27
  channel that speaks to the model in the same step (`PostToolDecision`'s
26
28
  `additionalContexts`, which the agent loop turns into a durable user-role message
27
- — `packages/core/agent-loop/src/tool-calls.ts`). The third is recognized at the
28
- **same seam** from the canonical value of a result the pipeline calls a
29
- *success*, for a reason §3 gives in full.
29
+ — `packages/core/agent-loop/src/tool-calls.ts`). The third and fourth are
30
+ recognized at the **same seam** from the canonical value of a result the pipeline
31
+ calls a *success*, for reasons §3 and §4 give in full.
30
32
 
31
33
  That seam, not `ctx.sandbox.confine`: `confine(argv, policy, signal)` sees the
32
34
  confinement failure too, but its signature carries no agent, so a wrapper could
@@ -437,10 +439,13 @@ the advisory never names the dead directory.
437
439
 
438
440
  ### 3. A confined child that never started (`native-init`)
439
441
 
440
- Five reports of one exit code: [`#7876`] and [`#8193`] (the packaged desktop
442
+ Seven reports of one exit code: [`#7876`] and [`#8193`] (the packaged desktop
441
443
  app), [`#8313`] (the same version and the same mode run as the desktop app versus
442
- the Web UI launched from a terminal, which works) and [`#7877`] (MSYS2 / Git
443
- Bash) — and [`#8208`], which found the mechanism the console cases share. All are `0xC0000142`
444
+ the Web UI launched from a terminal, which works), [`#8334`] (the same code with
445
+ the authorization side attached: the grant **succeeded** and every confined child
446
+ still died) and [`#7877`] (MSYS2 / Git
447
+ Bash) — and [`#8208`], which found the mechanism the console cases share, and
448
+ [`#8336`], which measured the creation flags that mechanism turns on. All are `0xC0000142`
444
449
  `STATUS_DLL_INIT_FAILED` — the Windows
445
450
  loader terminated the process while it was initializing its native images, i.e.
446
451
  **before the program's entry point**. A command that ran and then failed exits
@@ -546,10 +551,37 @@ Two producers have been measured under a confining mode:
546
551
  ordinary path (`process.ts:567`, i.e. `0x404`); `CREATE_NO_WINDOW`
547
552
  (`0x08000000`) is **not a constant anywhere in that source**. Those flags are
548
553
  fatal under a console-less runner and harmless under one that owns a console —
549
- so it is the console, not the flag list, that decides. `0.7.x` wrote the
550
- two-set version of that list and called the flags "a necessary ingredient ...
551
- the host process image is what turns it fatal"; both halves were wrong, and
552
- `0.8.0` replaces them.
554
+ so **there** the console decides. `0.7.x` wrote the two-set version of that
555
+ list and called the flags "a necessary ingredient ... the host process image is
556
+ what turns it fatal"; both halves were wrong, and `0.8.0` replaces them.
557
+
558
+ **`0.11.0` and earlier concluded too much from those three sets** — "it is the
559
+ console and not the flag list that decides" — and [`#8336`] measured the case
560
+ that falsifies it: with a console-owning host, a restricted token and the Low
561
+ integrity level, varying only the creation flags, `CREATE_NO_WINDOW` and
562
+ `CREATE_NEW_CONSOLE` both died with `0xC0000142`, while `0`, `DETACHED_PROCESS`
563
+ and `CREATE_NEW_PROCESS_GROUP` reached the program. So the console decides
564
+ **whether a flag that merely shares the inherited console is fatal**, and a
565
+ flag that forces the child to create one of its own is fatal regardless. The
566
+ pair inside that matrix is what makes it a rule rather than a list of forbidden
567
+ flags: the same `CREATE_NO_WINDOW` on an *unrestricted* token reached the
568
+ program, and `STARTF_USESHOWWINDOW` with `SW_HIDE` — how the harness hides a
569
+ window without isolating a console — was harmless on the restricted one.
570
+ `0.12.0` replaces the withdrawn sentence with the rule and that matrix.
571
+
572
+ **The `windowsHide` suspicion, answered.** It is the first thing a search turns
573
+ up for this failure, and it points at the wrong component: the flag appears
574
+ once on the *ordinary* subprocess path
575
+ (`packages/subprocess/subprocess-local/src/spawn.ts:472`,
576
+ `windowsHide: platform === 'win32'`), which starts the runner rather than the
577
+ confined child, and the restricted spawn defines no `CREATE_NO_WINDOW` constant
578
+ at all. It is **not** set in `dsh-jobs` — a search there finds nothing, and
579
+ every `windowsHide` in the tree sits on a host-side or ordinary spawn. And
580
+ where it *is* set, [`#8208`] measured it both ways on a host that works: with
581
+ and without `windowsHide`, the confined `pwsh` reached exit `0` with its own
582
+ stdout intact. The reason is the rule again — a windowless console is still a
583
+ console, and that is what the child inherits; what decides is whether the host
584
+ owns a console **object**, not whether it owns a window.
553
585
 
554
586
  **One arm was applied, measured, and rejected.** Putting `DETACHED_PROCESS` on
555
587
  the *restricted child* removes its console request and does stop the crash —
@@ -621,6 +653,110 @@ the single shape that happened to be measured first. It never
621
653
  offers `danger-full-access` as a fix and never suggests a sandbox setting be
622
654
  relaxed.
623
655
 
656
+ ### 4. Denied inside the workspace (`workspace-denial`, added in 0.11.0)
657
+
658
+ [#423] is one report and its own follow-up, and it is the **other end of the
659
+ backend §1 is about**. There the workspace grant could not be applied at all and
660
+ every command died before it ran; here the grant **was** applied — on the
661
+ workspace root, once — and part of the tree still refuses writes, forever. The
662
+ report's shape: under `workspace-write` on Windows, a command writing into a
663
+ subdirectory that was created or **moved in from outside** the session (an
664
+ installer, an editor, another harness running under its own account) is denied,
665
+ while the same command against a directory the harness itself created succeeds.
666
+
667
+ ```
668
+ [exit code: 1] sandbox: { mode: "workspace-write", denied: true }
669
+ ```
670
+
671
+ **The signature is a value, not a message**, and this family is invisible from
672
+ the error path for the same structural reason §3 is: a denied command exits
673
+ nonzero, and the shipped shell tools report a nonzero exit as a finished run
674
+ rather than as `isError`
675
+ (`packages/shell/tool-pwsh/src/render.ts` reports `[exit code: N]` and drops a
676
+ denial marker). The fact therefore arrives in `ToolExecutionSuccess.value`,
677
+ where `tool-bash` / `tool-pwsh` project what the sandbox executor stamped
678
+ (`packages/shell/bash-sandbox/src/index.ts`, `pwsh-sandbox`): the mode the call
679
+ actually ran under, whether the backend's own refusal dialect appears in the
680
+ **captured stderr**, and the enforcement that applied. Reading the executors'
681
+ own stamp rather than a sentence means this plugin is reporting the harness's
682
+ reading of its own sandbox, not a guess about a line of output.
683
+
684
+ **What the advisory says.** Four things, and one it refuses:
685
+
686
+ - **That retrying is provably useless.** The host-side grant is written once, on
687
+ the workspace **root**, and relies on Windows ACE inheritance to reach the
688
+ tree beneath it. Writing an inherited ACE into an *already-existing* child
689
+ needs `WRITE_DAC` on that child; where the caller does not hold it, Windows
690
+ skips the child silently — no error, no return value, no log line. The backend
691
+ then checks only the root (`hasExactGrant(workspaceRoot)` in
692
+ `packages/sandbox/sandbox-windows-acl/src/acl.ts`) and returns early once the
693
+ grant is there, which it is from the first call onwards. The descendants that
694
+ missed the propagation are never revisited — not later in this session, not in
695
+ any later one.
696
+ - **Which objects miss it, and how many.** It is a fact about *who created
697
+ them*: objects the harness creates inherit the ACE, and objects that already
698
+ existed do not. [#423] measured 170 of 729 objects missing it, **including
699
+ root-level files** — so "write at the workspace root instead" is not a safe
700
+ move either.
701
+ - **The discriminator, and its second half.** The advisory prints the path it
702
+ keyed on beside the root it tested it against, so the reader can audit the
703
+ claim instead of taking a statement about two strings on faith. Then the
704
+ measurement a single check gets wrong: reading and listing use the **normal**
705
+ token while writing and deleting use the **restricted** (low-integrity) one,
706
+ and both sides must pass — so an object whose DACL names only
707
+ `Administrators`/`SYSTEM` plus the capability SID is refused on the read side
708
+ too, and looks fine to a check that merely greps for the capability SID.
709
+ [#423] measured exactly that on a `.cache` directory.
710
+ - **The second measured variant of the same shape.** The coverage that is
711
+ missing can be the mandatory-integrity **label** rather than a DACL entry —
712
+ reported in the same thread on 2026-09-29, against a `0.2.0-rc.1` install,
713
+ under the same root-only short-circuit. From inside a session the two are
714
+ indistinguishable; from outside, the repository's own diagnosis skill
715
+ separates them (`diagnose-windows-sandbox-acl`, 0.2.0 and later, reports
716
+ `hasExactDeny()` for the DACL half and `LOW_LABEL` — `S-1-16-4096` — for the
717
+ label half).
718
+ - **No repair command.** The obvious one, a recursive `icacls /grant` for the
719
+ capability SID, is refused by Windows itself with `ERROR_NONE_MAPPED` (1332) —
720
+ the tool cannot map that SID to a name, so a grant that must name it never
721
+ reaches the child. The line that *would* work needs `WRITE_DAC` on the object,
722
+ which is the right in question. The advisory states the mechanism, names the
723
+ ceiling, and says out loud that it prints no command because this project has
724
+ no Windows host to verify one on — the same standard §1's standing-grant
725
+ section is held to, and for the same reason.
726
+
727
+ **What it refuses to explain, and why that is the design.** A denial is the
728
+ *sanctioned* outcome in three other situations, and advising about a missing
729
+ inherited grant in any of them would be a confidently wrong cause:
730
+
731
+ - **Outside the workspace** — a confining sandbox denying a path outside its
732
+ writable roots is the whole point, and the denial surface's one-shot escalation
733
+ offer is correct there.
734
+ - **Under `read-only`** — that mode denies every write by construction, so an
735
+ in-workspace denial under it is the mode working.
736
+ - **A runner failure** — the executor refuses to call a run denied when the
737
+ runner itself failed, and such a value is left alone for the same reason.
738
+
739
+ What is left is exactly the anomaly: a denial under `workspace-write` of a path
740
+ inside the session's own workspace. That is why the family reads the mode off
741
+ the **value** (the executor stamped the mode it actually ran under, so no policy
742
+ lookup can disagree with it), tests containment against the root the policy
743
+ resolver reports, and speaks only when **every** absolute path the command's own
744
+ arguments name lies under that root. A command that names an outside path as
745
+ well — an interpreter under `C:\Program Files`, an output directory on another
746
+ volume — is refused rather than guessed at, and so is a command naming only
747
+ relative paths: in both cases the plugin cannot say *which* path was denied, and
748
+ silence is the fail-closed direction. The platform gate is real here and cannot
749
+ be dropped: the mechanism is ACE inheritance, which no other backend has.
750
+
751
+ **On a denial it cannot finish placing** — a root the policy resolver does not
752
+ report, or no mounted policy service at all — the plugin withholds the advisory
753
+ and says so once on the host log. That is the disclosure rule §1's mode gate
754
+ follows as well: silence alone would make "the sandbox is not the cause"
755
+ indistinguishable from "this plugin could not tell".
756
+
757
+ [#423] is a Discussion (the repository has issues disabled), and the reply
758
+ covering this family is posted there.
759
+
624
760
  ## What it does with a recognized failure
625
761
 
626
762
  1. **One durable advisory per agent, per family.** An agent that hits two
@@ -631,10 +767,12 @@ relaxed.
631
767
  `warn` line, so the fact survives outside the transcript too.
632
768
  2. **A disclosure when it withholds.** The PTY and native-init advisories are
633
769
  only sent when the
634
- resolved mode actually confines. If the mode is `danger-full-access`, or
635
- cannot be resolved at all (no `sandboxPolicy` service mounted, no agent
636
- session, a resolver that throws), the failure is left exactly as it was
637
- **and the host log says so once**. Silence alone would make "the sandbox is
770
+ resolved mode actually confines, and the workspace-denial advisory only when
771
+ the workspace root is resolvable — the fact its containment claim is tested
772
+ against. If the mode is `danger-full-access`, or either fact cannot be
773
+ resolved at all (no `sandboxPolicy` service mounted, no agent session, a
774
+ resolver that throws, a policy without a root), the failure is left exactly as
775
+ it was **and the host log says so once**. Silence alone would make "the sandbox is
638
776
  not the cause" and "this plugin could not tell" indistinguishable from the
639
777
  outside. Withholding is never a guess: an unresolvable mode is *not* an
640
778
  invitation to fall back to the deployment default.
@@ -649,8 +787,8 @@ relaxed.
649
787
  refused twice — while a session can always make progress by spending the
650
788
  budget it has.
651
789
 
652
- **Why the blocking half does not extend to the two mode-gated families** (it
653
- is ACL-only by construction, in the parameter type): the ACL remedy is a
790
+ **Why the blocking half covers one family only** (it is ACL-only by
791
+ construction, in the parameter type): the ACL remedy is a
654
792
  command
655
793
  the user can run *while the session continues*, so refusing further identical
656
794
  calls cannot make the session unfinishable — spending the budget always lets
@@ -659,8 +797,11 @@ relaxed.
659
797
  native-init remedy is a launch fix on the user's side — whose one in-session
660
798
  part, rewriting an MSYS2 command, the model does by calling a *different*
661
799
  command, which has a different call key and is therefore never the call being
662
- refused. Refusing calls in those families could only pad a session that is
663
- already unable to do the thing being refused.
800
+ refused. The workspace-internal denial is not covered either, and for a
801
+ stronger version of the same reason: its remedy is not a command at all — the
802
+ object was skipped when the grant was written and nothing revisited it — so
803
+ refusing calls could only pad a session that is already unable to do the thing
804
+ being refused.
664
805
 
665
806
  ## Install
666
807
 
@@ -734,6 +875,15 @@ than one that stays silent.
734
875
  `[exit code: -1073741502]`, or any value that is not the shipped shell
735
876
  projection (`kind: 'foreground'`), is not this family — a line of text can
736
877
  never be mistaken for a loader status.
878
+ - **A denial is not this family unless the plugin can place it.** A denial under
879
+ `read-only`, under `danger-full-access`, or on a host that is not Windows; a
880
+ value the executor marked as a runner failure (`denied` is not set when the
881
+ runner itself failed); a command naming a path outside the workspace; a command
882
+ naming an outside path as well as an inside one; and a command naming only
883
+ relative paths — all six are refused, and the first three are the *sanctioned*
884
+ outcomes rather than puzzles. The plugin reads the executors' own stamp rather
885
+ than a sentence, so a command whose output contains `denied: true` is not this
886
+ family either.
737
887
  - **Only one advisory per agent, per family.** The environment is explained
738
888
  once; repeating it per failed command would be noise competing with the
739
889
  failure itself.
@@ -742,9 +892,11 @@ than one that stays silent.
742
892
 
743
893
  - **The Windows path itself cannot be witnessed on macOS**, where this plugin
744
894
  was built. What the test suite proves is the decision layer — classification
745
- of all three families (the third from the producer's own canonical value,
895
+ of all four families (the third from the producer's own canonical value,
746
896
  built by the suite with the reported `-1073741502` and the report's stderr
747
- line), the once-per-agent-per-family rule, the sandbox-mode gate
897
+ line; the fourth from the executors' own stamp, built by the suite with the
898
+ mode, the `denied` flag and the refusal dialect the executor matches), the
899
+ once-per-agent-per-family rule, the sandbox-mode gate
748
900
  and its fail-closed behaviour, the fail-fast budget and its self-feeding
749
901
  guard, and the wiring to a real cordis `Context` and the real `ToolRuntime` —
750
902
  driven by fixtures that throw the producers' exact error shapes
@@ -754,7 +906,14 @@ than one that stays silent.
754
906
  a given Windows host reproduces the PTY startup failure; those are the user's
755
907
  one-line experiment and the reporter's own control, and both advisories say
756
908
  where they stop. The native-init family is the one that needs no Windows to be
757
- faithful, because what it reads is a number inside a JSON value.
909
+ faithful, because what it reads is a number inside a JSON value. The
910
+ workspace-denial family is exercised the same way and **does** need the
911
+ platform fact, which the suite supplies by stubbing `process.platform` for the
912
+ arms that need `win32` and restoring it — the plugin reads the real fact rather
913
+ than a config knob, because a knob would be a backdoor into a shipped decision.
914
+ What that proves is the decision layer against the producers' stamped values;
915
+ the ACE-inheritance path itself is still unwitnessed here, and the advisory
916
+ says as much by shipping no repair command.
758
917
  - **The standing-grant section is source-level, and the out-of-tree reach is the
759
918
  reporter's measurement.** What this section states about the backend's own
760
919
  behaviour — that the workspace grant is standing, that nothing in the dispose or
@@ -785,7 +944,7 @@ than one that stays silent.
785
944
  outside the seam this plugin subscribes to. The report and its proposed fix
786
945
  stay with the maintainers; all this plugin can do is explain the provisioning
787
946
  failure that shares its root.
788
- - **The real fix is upstream, in all three families.** For the ACL failure,
947
+ - **The real fix is upstream, in all four families.** For the ACL failure,
789
948
  `grantWrite` already computes `hasExactGrant` / `hasExactDeny` /
790
949
  `hasExactLabel` and discards which one was false, so the diagnostic that turns
791
950
  a 52-minute detour into one line belongs at that site. For the PTY failure,
@@ -794,7 +953,12 @@ than one that stays silent.
794
953
  the runner should be launched with the environment its own execution needs
795
954
  (`ELECTRON_RUN_AS_NODE=1` when `argv[0]` is an Electron binary — [`#7876`]'s
796
955
  three candidate fixes) or refuse, in a checkable way, an MSYS2 program under a
797
- restricted token. This plugin is the stopgap for all three.
956
+ restricted token. For the workspace-internal denial the site is the same
957
+ `grantWrite`: its early return asks only whether the **root** already carries
958
+ the ACE, so the descendants that missed the propagation are never repaired —
959
+ the check would have to look past the root, or the denial surface would have to
960
+ say *which* path was refused instead of only that one was. This plugin is the
961
+ stopgap for all four.
798
962
 
799
963
  ## Compatibility
800
964
 
@@ -885,6 +1049,7 @@ the current runtime cannot distinguish rather than counting it as a pass.
885
1049
  [discussion #7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
886
1050
  [discussion #7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
887
1051
  [discussion #8208]: https://github.com/deepseek-ai/deepseek-harness/discussions/8208
1052
+ [#423]: https://github.com/deepseek-ai/deepseek-harness/discussions/423
888
1053
  [#7750]: https://github.com/deepseek-ai/deepseek-harness/discussions/7750
889
1054
  [#7771]: https://github.com/deepseek-ai/deepseek-harness/discussions/7771
890
1055
  [#7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
@@ -906,3 +1071,5 @@ the current runtime cannot distinguish rather than counting it as a pass.
906
1071
  [#8313]: https://github.com/deepseek-ai/deepseek-harness/discussions/8313
907
1072
  [#8314]: https://github.com/deepseek-ai/deepseek-harness/discussions/8314
908
1073
  [#8322]: https://github.com/deepseek-ai/deepseek-harness/discussions/8322
1074
+ [#8334]: https://github.com/deepseek-ai/deepseek-harness/discussions/8334
1075
+ [#8336]: https://github.com/deepseek-ai/deepseek-harness/discussions/8336
package/cordis.patch.yml CHANGED
@@ -105,7 +105,7 @@
105
105
  # running one.
106
106
  #
107
107
  # A THIRD family is recognized from a value, not from text (#7876, #7877,
108
- # #8193, #8208, #8313):
108
+ # #8193, #8208, #8313, #8336, #8334):
109
109
  #
110
110
  # [exit code: -1073741502] (0xC0000142 STATUS_DLL_INIT_FAILED)
111
111
  #
@@ -124,7 +124,51 @@
124
124
  # or spawned without a console), carries the one conversion the model can make
125
125
  # itself (rewrite the work as PowerShell or `cmd`), names the remedy #8193
126
126
  # measured — a real node.exe host, which the desktop ships — and says plainly
127
- # which producers it does not know. Like the second family, it is mode-gated.
127
+ # which producers it does not know. It also states the one rule the whole family
128
+ # follows from, because a reader without it reaches for the wrong flag: in a
129
+ # restricted token a console can be INHERITED but not CREATED, so a creation
130
+ # flag that forces the child to make one of its own — `CREATE_NO_WINDOW`,
131
+ # `CREATE_NEW_CONSOLE` — is fatal on its own, while `DETACHED_PROCESS` (which
132
+ # declines a console rather than creating one) is not; #8336 measured that
133
+ # matrix on one machine, and #8208 measured `windowsHide` both ways on a host
134
+ # that works, so the advisory answers that suspicion with where the flag
135
+ # actually appears — the ordinary subprocess path, not the confined one — and
136
+ # with why a windowless console is still an inheritable console. Like the second
137
+ # family, it is mode-gated.
138
+ #
139
+ # A FOURTH family is the other half of the FIRST one's backend (#423). There the
140
+ # grant could not be applied at all; here it WAS applied — on the workspace root,
141
+ # once — and Windows ACE inheritance silently skipped the objects whose DACL the
142
+ # caller could not write, so commands writing into a subdirectory created or moved
143
+ # in from outside (an installer, an editor, another harness under its own account)
144
+ # are denied forever while the same command against a directory the harness itself
145
+ # created succeeds. The backend's provisioning check short-circuits on the root
146
+ # (`hasExactGrant` returns early once the root carries the ACE), so those
147
+ # descendants are never revisited. It is read from a value rather than from text,
148
+ # like the third family and for the same reason (a denial exits nonzero and the
149
+ # shipped shell tools report that as a finished run), but it needs no bespoke code:
150
+ # the executors stamp the denial onto the value as
151
+ #
152
+ # sandbox: { mode: "workspace-write", denied: true }
153
+ #
154
+ # produced by matching the backend's own refusal dialect in the captured stderr —
155
+ # so this plugin reads the harness's own reading of its own sandbox. It speaks
156
+ # only when the host is Windows, the stamped mode is `workspace-write`, and EVERY
157
+ # absolute path the command names lies inside the session's workspace: a denial
158
+ # anywhere else (outside the workspace, under `read-only`, a runner failure) is
159
+ # the sandbox working as designed and is left alone. The advisory prints the path
160
+ # it keyed on beside the root it tested it against, says that retrying is provably
161
+ # useless, gives the DISCRIMINATOR — reading/listing use the normal token while
162
+ # writing/deleting use the restricted one, and both sides must pass, so an object
163
+ # whose DACL names only Administrators/SYSTEM plus the capability SID looks granted
164
+ # to a check that just greps for the SID — names the second measured variant of
165
+ # the same shape (the coverage missing on the mandatory-integrity LABEL rather
166
+ # than on the DACL, separated from outside a session by the repository's own
167
+ # `diagnose-windows-sandbox-acl`), and states the Windows ceiling that closes the
168
+ # obvious repair (a recursive `icacls /grant` for a capability SID is refused with
169
+ # ERROR_NONE_MAPPED (1332), because that SID has no name to map). It ships NO
170
+ # repair command, for the reason the standing-grant section ships none: this
171
+ # project has no Windows host to verify a line on.
128
172
 
129
173
  - insert:
130
174
  - id: sandbox-grant-advisor