feat(windows): define native authority boundary
This commit is contained in:
parent
da1da3bfb8
commit
a1a6b4200e
|
|
@ -0,0 +1,531 @@
|
|||
# ADR-0004: Windows native authority packaging
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-17
|
||||
- Implementation status: the provider-neutral TypeScript base port and
|
||||
fail-closed host capability are implemented; the platform-package shape and
|
||||
protected runtime-projection type state are selected and implemented. The
|
||||
fail-closed package loader exists but is disabled by a null release artifact
|
||||
commitment; the platform package, native implementation, release integration,
|
||||
and real-Windows proof do not exist, so all Windows controls remain unavailable
|
||||
|
||||
## Context
|
||||
|
||||
REA's experimental Windows x64 Ghidra boundary cannot establish three security
|
||||
properties with Node's public filesystem and child-process APIs alone:
|
||||
|
||||
- atomic process creation inside a dedicated kill-on-close Job Object;
|
||||
- handle-based path admission that rejects disallowed reparse points and retains
|
||||
stable file identity; and
|
||||
- creation and readback verification of protected private DACLs.
|
||||
|
||||
The existing fallback uses bounded `taskkill /T /F`, pathname-based Node file
|
||||
operations, and POSIX mode requests. These are useful P0 mechanics, but they are
|
||||
not evidence for the stronger Windows authority claims.
|
||||
|
||||
REA installation must remain compiler-free. Setup may not install Visual
|
||||
Studio Build Tools, Python, node-gyp, PowerShell modules, or another runtime.
|
||||
The packed CLI is also verified with lifecycle scripts disabled, so native
|
||||
support cannot depend on an install hook.
|
||||
|
||||
## Decision drivers
|
||||
|
||||
1. Keep Win32 handles, structures, constants, and calling conventions outside
|
||||
domain, application, Ghidra, CLI, and MCP contracts.
|
||||
2. Prevent target or launcher code from running before authority is established.
|
||||
3. Work from an npm installation without a local compiler or install script.
|
||||
4. Fail closed with typed capability diagnostics when the exact native artifact
|
||||
is absent, unsupported, unreadable, or incompatible.
|
||||
5. Preserve the current limited P0 behavior without allowing it to satisfy a
|
||||
native-authority capability claim.
|
||||
6. Verify the published artifact and the real Windows behavior independently.
|
||||
|
||||
## Options considered
|
||||
|
||||
### Source-built Node-API addon
|
||||
|
||||
Rejected. It requires a compiler toolchain during installation or setup and
|
||||
would make availability depend on mutable operator build state.
|
||||
|
||||
### Source-owned helper executable
|
||||
|
||||
Rejected. A helper adds an authenticated protocol, helper-process ownership,
|
||||
version negotiation, output capture, and crash cleanup without improving the
|
||||
executable identity binding: it must still call `CreateProcessW` with a path.
|
||||
The addon can perform blocking Win32 waits and pipe reads on source-owned native
|
||||
threads and cross into JavaScript only through public Node-API operations.
|
||||
|
||||
### Prebuilt Windows Node-API addon in a platform package
|
||||
|
||||
Selected. Node-API provides an ABI-stable interface for the
|
||||
handle and DACL operations when the addon uses only Node-API/node-addon-api
|
||||
rather than V8, libuv, or Node internals. Release CI builds the Windows x64
|
||||
artifacts. A platform-specific npm package declares
|
||||
`os: ["win32"]` and `cpu: ["x64"]`; the main package references its exact
|
||||
version as an optional dependency. Neither package has an install script or a
|
||||
source-build fallback, and the platform package contains no helper executable.
|
||||
|
||||
This follows the platform-package selection pattern used by mature native npm
|
||||
packages while deliberately omitting their node-gyp fallback. It also fits
|
||||
REA's existing package verifier, which exercises installs with scripts disabled
|
||||
and with optional dependencies omitted.
|
||||
|
||||
## Boundary
|
||||
|
||||
The TypeScript side exposes one provider-neutral external-adapter port with
|
||||
three cohesive capabilities:
|
||||
|
||||
```text
|
||||
ProviderProcess / PrivateRuntimeRoot / authorized file operations
|
||||
|
|
||||
WindowsNativeAuthority port
|
||||
/ | \
|
||||
owned process admitted handle sealed runtime
|
||||
lifetime and identity projection
|
||||
|
|
||||
optional Windows x64 Node-API adapter package
|
||||
```
|
||||
|
||||
The port returns typed observations and failures. It never returns raw Win32
|
||||
handles, pointers, SIDs, access masks, or platform DTOs. Resource values own an
|
||||
explicit idempotent `close` operation; the composition root also registers an
|
||||
environment cleanup hook as crash/teardown defense, not as the normal lifetime
|
||||
API.
|
||||
|
||||
The TypeScript port is an application-owned interface. Its operations are
|
||||
limited to:
|
||||
|
||||
- `admitPath`, returning an opaque, closeable admitted-file identity plus
|
||||
normalized observations needed for policy decisions;
|
||||
- `createPrivateRoot`, consuming admitted source trees/files and returning an
|
||||
opaque, closeable sealed projection with manifest, entry-point, launch, and
|
||||
verified owner/access-policy observations; and
|
||||
- `spawnOwnedProcess`, consuming only that sealed projection plus stdio limits
|
||||
and an absolute startup deadline, and returning provider-neutral process I/O
|
||||
and lifetime operations. Source paths, executable identities, arguments, and
|
||||
environment values cannot be supplied or changed at this boundary.
|
||||
|
||||
Each operation returns the existing `Result` shape with a dedicated tagged
|
||||
failure union covering unavailable capability, invalid input, policy rejection,
|
||||
identity drift, access denial, resource exhaustion, timeout, native contract
|
||||
mismatch, and unexpected native failure. No native adapter may export platform
|
||||
DTOs directly to its callers.
|
||||
|
||||
The application-owned contract lives in
|
||||
`src/application/WindowsNativeAuthority.ts`. It fixes the public names
|
||||
`WindowsNativeAuthority`, `WindowsAdmittedFile`,
|
||||
`WindowsAdmittedDirectory`, `WindowsPrivateRoot`, and
|
||||
`WindowsOwnedProcess`. Module-owned wrappers register backend tokens in a
|
||||
shared weak registry and reject resources created by another authority
|
||||
instance, aliased by another backend result, or already closing/closed. File
|
||||
and directory admission are distinct types, so a
|
||||
directory cannot be supplied as an executable and a file cannot be supplied as
|
||||
the parent of a private root. The backend must continuously drain process
|
||||
output and retain no more than the committed byte limits; callers receive
|
||||
detached snapshots with retained bytes, total byte counts, and truncation
|
||||
flags. Process completion and close are typed results, and every native
|
||||
resource owns an explicit idempotent close operation. The built-in unavailable
|
||||
implementation fails all three operations closed and reports no loaded
|
||||
identity; the Windows capability report continues to advertise no native
|
||||
authority.
|
||||
|
||||
Successful path admission is deliberately narrower than an opaque native
|
||||
identity string. The adapter must return the exact requested path, the
|
||||
handle-derived final diagnostic path, the admitted `ntfs` filesystem tag, a
|
||||
lowercase 16-hex-digit volume identity, a lowercase 32-hex-digit file identity,
|
||||
and the regular-file digest where applicable. The volume identity is the
|
||||
zero-padded hexadecimal representation of the unsigned `FILE_ID_INFO`
|
||||
`VolumeSerialNumber` numeric value. The file identity is the lowercase
|
||||
hexadecimal encoding of `FileId.Identifier[0]` through
|
||||
`FileId.Identifier[15]` in array order. The composite identity is scoped to the
|
||||
retained handle and admitted volume; it is not a durable global identifier and
|
||||
must not authorize a later reopened object.
|
||||
|
||||
The application boundary parses the runtime-shaped report, rejects unknown or
|
||||
malformed fields, derives one canonical string encoding, and closes the
|
||||
returned backend resource before reporting a native-contract mismatch. This
|
||||
proves only the shape and canonical encoding of what the adapter reports. The
|
||||
trusted native implementation and real-Windows verifier must separately prove
|
||||
that the filesystem tag and identity bytes came from handle-based filesystem
|
||||
inspection and `GetFileInformationByHandleEx(FileIdInfo)`. TypeScript schema
|
||||
validation cannot prove Win32 provenance or prevent a malicious native module
|
||||
from inventing well-shaped values. This is contract hardening only: it does not
|
||||
implement `CreateFileW` traversal or make the host capability available.
|
||||
|
||||
The sealed-root observation applies the same boundary discipline to #290. A
|
||||
native result is not published as a `WindowsPrivateRoot` until the application
|
||||
parses a strict readback report for five object classes: the root directory,
|
||||
read-only directories, read-only files, writable directories, and writable
|
||||
files. Each class commits its object kind, explicit-versus-inherited ACE
|
||||
disposition, propagation flags, verified object count, descriptor digest, and
|
||||
path-to-policy manifest digest. One enclosing security manifest binds all five
|
||||
classes to both the sealed projection manifest and launch commitment. Its
|
||||
versioned canonical input contains `version`, `projectionManifestSha256`,
|
||||
`launchSha256`, and the five ordered class records, each limited to
|
||||
`verifiedObjectCount` and `policyManifestSha256`.
|
||||
|
||||
Root and existing immutable objects require explicit protected DACLs. Directory
|
||||
policies distinguish inheritable container/object ACEs from non-propagating
|
||||
leaf-file policies. Writable directories require an explicit protected policy;
|
||||
files created beneath them report inherited ACEs from that protected parent.
|
||||
Every class must state current-user ownership, no additional trustee or broad
|
||||
allow entry, SYSTEM full control, and an explicit OWNER RIGHTS denial of
|
||||
`WRITE_DAC` and `WRITE_OWNER`. Root and immutable objects admit only
|
||||
current-user read/execute; writable objects admit current-user modify. Null
|
||||
DACLs, unexpected fields, different rights, missing OWNER RIGHTS policy,
|
||||
class/propagation contradictions, count or manifest mismatches, or malformed
|
||||
digests poison and close the returned native token.
|
||||
|
||||
These normalized values deliberately omit raw SIDs, ACEs, masks, and security
|
||||
descriptor buffers. Schema acceptance proves only that the adapter reported the
|
||||
selected policy vocabulary. The native implementation must create the
|
||||
descriptors, read them back from retained handles, canonicalize trustee and
|
||||
rights semantics, and prove the report against real non-administrator Windows
|
||||
access tests. Until that implementation and verifier exist, private-DACL
|
||||
capability remains unavailable.
|
||||
|
||||
The native package remains unimplemented. The owner boundary, packaging shape,
|
||||
and launch binding are accepted; capability availability still depends on the
|
||||
projection and complete native teardown state machine below passing the
|
||||
real-Windows adversarial verifier.
|
||||
|
||||
The initial admission contract accepts only `reparsePolicy: "reject_all"` and
|
||||
can report only `reparseDisposition: "absent"`. A later admitted reparse kind
|
||||
must be added as an explicit normalized policy variant together with its real
|
||||
Windows proof; the generic port does not provide an open-ended allow mode.
|
||||
|
||||
The three implementation slices remain separate:
|
||||
|
||||
1. **Job Object ownership (#288):** create a dedicated Job Object, apply
|
||||
kill-on-close without silent breakaway, and create the launcher already
|
||||
assigned through the process-creation attribute list.
|
||||
2. **Handle-based filesystem authority (#289):** open authority-relevant path
|
||||
components without following reparse points, admit an explicit filesystem
|
||||
and reparse-tag set, retain or revalidate stable identity, and perform
|
||||
sensitive I/O through that authority.
|
||||
3. **Private runtime DACLs (#290):** create the root with a protected DACL,
|
||||
verify owner/trustees/rights/inheritance by handle, and make all runtime
|
||||
children inherit or receive the verified policy.
|
||||
|
||||
#289 owns the shared handle and filesystem-identity primitives used by #290 and
|
||||
the eventual launch binding consumed by #288. Job Object implementation follows
|
||||
#289 and #290; it cannot satisfy or advertise the Windows native authority
|
||||
boundary until the executable, runtime root, and session token are all
|
||||
protected.
|
||||
|
||||
The delivery order is #289, then #290, then #288. This lets process creation
|
||||
consume an already verified executable identity and private runtime root. The
|
||||
P0 limitations remain in force until all three real-Windows gates pass.
|
||||
|
||||
`CreateProcessW` cannot consume the admitted file handle directly. A retained
|
||||
path-component chain opened without write/delete sharing was evaluated and
|
||||
rejected as a complete binding. It prevents later conflicting opens, rename,
|
||||
and ordinary replacement, but it cannot revoke a writable mapped view created
|
||||
before admission. Such a view can mutate the admitted bytes after hashing while
|
||||
the writer's file handle is already closed.
|
||||
|
||||
Protecting only the primary executable is also insufficient for the Windows
|
||||
Ghidra launch. The current launcher includes the command interpreter, a batch
|
||||
script, Java, Ghidra JARs, native libraries, configuration, and DLL search
|
||||
inputs. A share lock on `lpApplicationName` does not bind that runtime closure.
|
||||
|
||||
The selected design projects the complete non-system runtime into the private
|
||||
root before process creation. Immutable-install admission is rejected: a BYO
|
||||
Ghidra or JDK tree normally belongs to the same analyst identity that runs REA,
|
||||
so that identity can replace content or rewrite its ACL. Treating such an
|
||||
installation as immutable would silently narrow the threat boundary.
|
||||
|
||||
The projection contains:
|
||||
|
||||
- the complete Ghidra installation root, including root
|
||||
`application.properties`, the `Ghidra/` module tree, JARs, native libraries,
|
||||
launch properties, and configuration discovered by `GhidraClassLoader`;
|
||||
- the complete admitted JDK 21 root;
|
||||
- the packaged REA Ghidra bridge and session descriptor; and
|
||||
- the digest-bound target snapshot plus private project, home, temporary,
|
||||
cache, configuration, and data directories.
|
||||
|
||||
The native adapter enumerates each admitted tree relative to retained directory
|
||||
handles, rejects every reparse point and unsupported filesystem, admits only
|
||||
regular files and directories, and applies explicit entry, depth, and byte
|
||||
limits. It copies each file through handles into a newly created destination,
|
||||
hashes the bytes while copying, closes and reopens the destination by handle,
|
||||
and verifies the readback digest. A canonical sorted manifest of relative path,
|
||||
file size, and SHA-256 is hashed into one runtime-projection identity. The
|
||||
readback projection, not the mutable source installation, is intended to become
|
||||
the launch authority. Concurrent source mutation may make projection fail or
|
||||
produce a different committed snapshot.
|
||||
|
||||
Read-only projected files and directories use a protected DACL that grants the
|
||||
analyst read/execute and SYSTEM full control while withholding create, write,
|
||||
append, delete, `WRITE_DAC`, and `WRITE_OWNER`. Because a file owner otherwise
|
||||
has implicit DACL-rewrite authority, the descriptor includes an explicit
|
||||
`OWNER RIGHTS` SID entry that withholds those mutation rights. Writable runtime
|
||||
subdirectories use a separate verified policy and never contain executable,
|
||||
classpath, or native-library inputs. Creation and readback of both policies
|
||||
belong to #290.
|
||||
|
||||
Applying that restrictive descriptor only after population is rejected. An
|
||||
access check occurs when a handle is opened; later DACL changes do not revoke a
|
||||
same-user process's previously granted write, delete, mapping, or `WRITE_DAC`
|
||||
rights. The native implementation must therefore apply the final descriptor at
|
||||
object creation or establish another proven construction principal/capability,
|
||||
retain only non-inheritable construction handles, close every mutation-capable
|
||||
handle before commit, and retain only non-mutating authority/cleanup handles.
|
||||
Whether the selected non-administrator Win32 creation APIs can populate the
|
||||
tree under its final descriptor remains an explicit native design/proof gap.
|
||||
Until a real-Windows race proves that pre-open write, delete, `WRITE_DAC`,
|
||||
directory-create, and writable-section handles cannot survive commit, REA does
|
||||
not claim same-user immutability for the projection. #290 can establish privacy
|
||||
against unrelated local principals without silently satisfying that stronger
|
||||
same-user launch-authority requirement.
|
||||
|
||||
Windows launches the projected JDK directly, without `cmd.exe`,
|
||||
`analyzeHeadless.bat`, `launch.bat`, `LaunchSupport`, or another pre-authority
|
||||
process. REA derives and validates the exact Ghidra 12.1.2 foreground headless
|
||||
arguments from the supported launch contract:
|
||||
|
||||
```text
|
||||
<root>\jdk\bin\java.exe <validated-vm-arguments>
|
||||
-cp <root>\ghidra\Ghidra\Framework\Utility\lib\Utility.jar
|
||||
ghidra.Ghidra ghidra.app.util.headless.AnalyzeHeadless <bounded-arguments>
|
||||
```
|
||||
|
||||
`lpApplicationName`, the classpath, script path, target, project, user, cache,
|
||||
and temporary paths all resolve beneath the sealed projection. The current
|
||||
directory is the sealed Ghidra root. The environment removes ambient Java and
|
||||
Ghidra option variables and admits only the projected JDK `bin` plus the
|
||||
canonical System32 directory into `PATH`. Java native-library paths are
|
||||
enumerated from the projection manifest. The process-creation mitigation policy
|
||||
rejects remote and low-integrity image loads and prefers System32 for system
|
||||
images. The verifier must prove the exact supported Windows build honors these
|
||||
mitigations; there is no weaker DLL-search fallback.
|
||||
|
||||
Trusted inputs outside the projection are limited to the Windows kernel,
|
||||
KnownDLLs, and binaries or DLLs resolved from the canonical Windows/System32
|
||||
roots under the supported non-administrator Windows installation. REA records
|
||||
those roots and the applied mitigation policy in the native launch observation.
|
||||
|
||||
The implemented TypeScript port represents the projection lifecycle as type
|
||||
state:
|
||||
|
||||
1. a mutable private-root builder accepts admitted source trees/files and
|
||||
bounded destination-relative names;
|
||||
2. sealing returns an immutable `WindowsRuntimeProjection` observation with the
|
||||
readback tree digest and an opaque projected entry point; and
|
||||
3. `spawnOwnedProcess` accepts only that sealed projection, rejecting a source
|
||||
admission, mutable input, foreign authority, aliased backend token, or
|
||||
closing/closed resource before crossing the native boundary.
|
||||
|
||||
The projection API also commits entry, depth, and byte limits; the exact
|
||||
read-only and writable subtrees; the Ghidra/JDK/bridge/target roles; and the
|
||||
canonical environment and VM-argument inputs. These are provider-neutral
|
||||
resource roles and observations, not Win32 DTOs. #289 owns handle-relative
|
||||
projection and readback, #290 owns sealing and verified DACL state, and #288
|
||||
consumes only the sealed result. The port snapshots every mutable projection
|
||||
and launch container before its first asynchronous boundary, leases all
|
||||
source/projection capabilities across native calls, and poisons/cleans
|
||||
malformed aliased root or process results before returning a contract mismatch.
|
||||
Native package work must implement this sealed-projection contract and may not
|
||||
reintroduce the older admitted-file spawn signature.
|
||||
|
||||
The verifier must include a writable mapping established before admission,
|
||||
hard-link alias writes, ordinary and POSIX-semantics delete/rename, reparse
|
||||
substitution, DLL search substitution, post-admission changes to scripts, JARs,
|
||||
native libraries, and configuration, owner attempts to rewrite the sealed DACL,
|
||||
and attempts to inherit or duplicate REA's retained authority handles. It must
|
||||
also prove that Ghidra starts after the original JDK and Ghidra trees are renamed
|
||||
or modified, demonstrating that no original runtime byte is used after
|
||||
projection. Network and other filesystems whose semantics cannot satisfy the
|
||||
selected proof fail closed. No implementation may fall back to the original
|
||||
unbound pathname or batch launcher.
|
||||
|
||||
## Process I/O and lifetime mechanism
|
||||
|
||||
The proposed process implementation is addon-only. Before the application
|
||||
publishes a `WindowsOwnedProcess`, it strictly parses a normalized ownership
|
||||
report requiring a dedicated unnamed Job Object, atomic
|
||||
`PROC_THREAD_ATTRIBUTE_JOB_LIST` assignment, execution assigned at creation,
|
||||
kill-on-close enabled, breakaway disabled, no admitted nested job, a retained
|
||||
non-inheritable owner handle, exactly one REA-owned Job handle, stdio-only
|
||||
inherited handles, and verified membership. The application derives the policy
|
||||
digest from a versioned ordered projection of every normalized field; it does
|
||||
not accept an opaque backend digest. A missing, malformed, broader, or unknown report
|
||||
poisons the native token and closes the Job rather than degrading to
|
||||
create-then-assign or `taskkill`. This validates only the adapter's report; the
|
||||
native implementation and real-Windows verifier must prove the actual attribute
|
||||
list, queried limits, membership, and descendant behavior.
|
||||
|
||||
The single-owner-handle claim means the addon creates no internal duplicate and
|
||||
retains only its one non-inheritable Job handle. An unnamed Job prevents a new
|
||||
open by name, but it does not stop another process that can obtain
|
||||
`PROCESS_DUP_HANDLE` access to REA from duplicating that handle. Consequently
|
||||
last-handle kill-on-close is claimed only against processes outside that handle-
|
||||
theft boundary. Same-user handle theft remains unproven unless the native design
|
||||
can protect the REA process handle and the verifier demonstrates failed
|
||||
duplication attempts; it is not implied by the normalized report.
|
||||
|
||||
One native resource owns the Job Object, process/thread handles, anonymous-pipe
|
||||
endpoints, wait state, bounded output accumulators, and its reader/wait threads.
|
||||
The launch sequence creates
|
||||
non-inheritable parent pipe ends and supplies only the three intended child
|
||||
stdio handles through `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`. Both the handle-list
|
||||
and job-list attributes are installed in one correctly sized attribute list;
|
||||
`bInheritHandles` is true as required by the handle-list contract, while every
|
||||
unlisted native handle remains non-inheritable.
|
||||
|
||||
The parent closes its duplicates of the child pipe ends immediately after
|
||||
`CreateProcessW` returns. Reader threads exclusively own the remaining parent
|
||||
read handles.
|
||||
|
||||
Dedicated native reader threads continuously drain stdout and stderr regardless
|
||||
of JavaScript consumption. Each accumulator records an overflow-checked total
|
||||
byte count while retaining at most its committed byte limit. `snapshot()` copies
|
||||
the retained prefix into new Node buffers; no external buffer aliases native
|
||||
storage. A native wait thread observes process completion. Promise completion
|
||||
crosses to the main thread through a size-one Node-API `ThreadSafeFunction`
|
||||
using nonblocking calls; an already queued exit notification is coalesced rather
|
||||
than blocking a native thread. Native threads acquire and release the TSFN and
|
||||
never call V8, libuv, or JavaScript directly. `waitForExit` cancellation removes
|
||||
only that waiter and cannot release the Job Object.
|
||||
|
||||
Normal `close` is idempotent: transition once to closing and close the Job Object
|
||||
to trigger kill-on-close. Process termination closes every inherited pipe write
|
||||
end, allowing readers to drain retained tail bytes and receive EOF. If a reader
|
||||
has not exited by the cleanup deadline, call `CancelSynchronousIo` with that
|
||||
reader's thread handle; never close a read handle from underneath a synchronous
|
||||
`ReadFile`. Join each reader before closing its read handle or freeing its
|
||||
accumulator. A timeout keeps native state owned until all threads have exited;
|
||||
it cannot detach or free live reader state.
|
||||
|
||||
Environment teardown first aborts the TSFN. Every reader/wait thread stops
|
||||
calling it on `napi_closing`, releases its acquisition, and exits. An
|
||||
`napi_add_async_cleanup_hook` joins native threads without blocking the
|
||||
JavaScript thread; native state is freed only after both async cleanup and the
|
||||
TSFN finalizer have completed. Pending promises may be abandoned only after
|
||||
JavaScript execution is unavailable. Object finalization requests the same
|
||||
state transition but does not independently free state or silently detach a
|
||||
live job.
|
||||
|
||||
## Packaging and loading contract
|
||||
|
||||
- Support is initially exact to Windows 10 / Windows Server 2016 or newer on
|
||||
x64, and the Node ranges declared by the main package. Runtime loading probes
|
||||
the required process-attribute APIs and fails closed; there is no suspended or
|
||||
post-creation Job assignment fallback.
|
||||
- Release CI builds from source and publishes the platform package before the
|
||||
matching main package version.
|
||||
- The main package pins the optional dependency to the exact same version.
|
||||
- npm package integrity is the installation authenticity boundary. Release CI
|
||||
writes the platform artifact SHA-256 into the version-matched main package's
|
||||
generated metadata before publication. REA must compare that pinned value
|
||||
with the addon bytes immediately before loading, require the exact package,
|
||||
version, contract, Node-API level, and export set, and record the loaded
|
||||
identity for diagnostics. Before first load, the process-global loader rejects
|
||||
an existing `require.cache` entry for the exact canonical resolved addon path.
|
||||
After acceptance it pins the returned module object, canonical path, and
|
||||
identity for the process lifetime: a repeat request for that same tuple is
|
||||
idempotent, while a different observable path or identity is rejected. Alias
|
||||
loads and native images that are not observable through that exact JavaScript
|
||||
loader entry belong to the excluded pre-existing same-user boundary; the
|
||||
loader does not claim process-wide native-image enumeration.
|
||||
- The addon bootstrap has a deliberately narrower threat boundary than the
|
||||
runtime projection it creates. REA loads and validates the addon before it
|
||||
starts Hopper, Ghidra, a target, or another provider-controlled process. npm
|
||||
integrity plus the release-generated digest detects package drift, but a
|
||||
hash-path-`require` handshake cannot prevent an already-running process under
|
||||
the same Windows user from replacing bytes between verification and native
|
||||
image mapping, restoring them afterward, injecting into REA, or preloading a
|
||||
cache entry. REA does not claim resistance to that pre-existing same-user
|
||||
bootstrap attacker. This is consistent with the separately documented
|
||||
unproven same-user Job-handle theft boundary; it is not evidence for runtime
|
||||
projection immutability.
|
||||
- After the bootstrap-accepted addon is loaded, #289/#290 must project every
|
||||
non-system byte that a provider or target can cause REA to execute or read as
|
||||
authority, including Ghidra, the JDK, bridge/session inputs, target snapshot,
|
||||
and runtime configuration. At the composition root, a bootstrap failure must
|
||||
occur before a Hopper/Ghidra/provider client is constructed, a target is
|
||||
opened or admitted, or provider/target code is run. After bootstrap succeeds,
|
||||
no provider or target process may start before the complete projection is
|
||||
sealed. Ordinary side-effect-free JavaScript module evaluation is not part of
|
||||
that execution claim. The addon itself remains bound to the installed npm
|
||||
release and is not recursively required to project itself before loading.
|
||||
- A future claim that includes hostile pre-existing same-user processes needs a
|
||||
different bootstrap, such as a signed separately protected launcher or a
|
||||
native main executable. That stronger boundary is outside #287–#290 and must
|
||||
not be inferred from package hashing or DACL readback.
|
||||
- Runtime selection admits only the expected package, platform, architecture,
|
||||
Node-API version, native contract version, and exported operation set.
|
||||
- Missing optional dependencies, `--omit=optional`, unsupported filesystems,
|
||||
nested-job conflicts, or native load failures produce typed unavailable
|
||||
outcomes. They never trigger node-gyp, a download, PowerShell, `icacls`, or a
|
||||
helper fallback.
|
||||
- The current `taskkill` and pathname implementations remain explicitly limited
|
||||
fallbacks and cannot set the native authority capability flags.
|
||||
|
||||
## Verification
|
||||
|
||||
Package verification must prove:
|
||||
|
||||
- ordinary packed installation selects the exact Windows x64 package;
|
||||
- installation with scripts disabled performs no build or download;
|
||||
- omission or removal of optional dependencies fails closed with actionable
|
||||
doctor output;
|
||||
- registry/package-lock integrity authenticates the installed package set,
|
||||
while the main-package metadata digest independently detects runtime artifact
|
||||
drift and release-pair mismatch without acting as another signature;
|
||||
- the exact main/platform version pair and platform-tarball/addon digest match
|
||||
before native evaluation;
|
||||
- digest, package, version, contract, Node-API, or manifest-declared export-set
|
||||
mismatch prevents addon evaluation and leaves provider construction, target
|
||||
admission, and provider/target launch counts at zero; an actual runtime export
|
||||
or identity mismatch is detected immediately after evaluation and prevents
|
||||
backend use, provider construction, target admission, and provider/target
|
||||
launch;
|
||||
- an exact resolved path already present in `require.cache` is rejected before
|
||||
use, a second load of the pinned tuple is idempotent, and another observable
|
||||
path or identity is rejected;
|
||||
- unsupported or omitted packages never probe provider or target paths and
|
||||
return the typed unavailable outcome;
|
||||
- a successful bootstrap is followed by a sealed complete projection before
|
||||
direct projected-Java launch; and
|
||||
- the published main and platform packages resolve as one release set.
|
||||
|
||||
Neither package verification nor the real proof may claim survival when a
|
||||
same-user attacker can replace both the main metadata and platform package,
|
||||
inject into REA, or preload an alias outside the exact observable cache entry.
|
||||
Real proof output must retain this bootstrap limitation after #289/#290 pass.
|
||||
|
||||
The self-hosted real-Windows verifier must separately prove the acceptance
|
||||
criteria in #288, #289, and #290 while running as a non-administrator. Mocked
|
||||
Node tests may verify TypeScript routing and failure translation but cannot
|
||||
satisfy Job Object, reparse-point, file-identity, or DACL claims.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Releases gain a coordinated Windows platform package and Windows build job.
|
||||
- Installing without optional dependencies keeps the CLI usable but leaves the
|
||||
stronger Windows authority unavailable.
|
||||
- The native surface remains deliberately small and security-specific.
|
||||
- Native reader threads add a small, explicit concurrency surface, but keep
|
||||
output progress independent of JavaScript scheduling and preserve the public
|
||||
Node-API ABI boundary.
|
||||
|
||||
## Research basis
|
||||
|
||||
- Node-API documentation states that ABI stability requires exclusive use of
|
||||
Node-API/node-addon-api and excludes direct V8, libuv, and Node internals.
|
||||
- `node-pty` demonstrates a public Node-API `ThreadSafeFunction` carrying a
|
||||
Windows process-exit observation from a native wait thread. REA adopts the
|
||||
thread-delivery pattern, not its process-tree enumeration or install-time
|
||||
native build/copy workflow.
|
||||
- `sharp` demonstrates platform-specific optional npm packages selected with
|
||||
`os`/`cpu` metadata. REA adopts that distribution shape but not its
|
||||
source-compilation fallback.
|
||||
- `better-sqlite3` demonstrates broad prebuilt coverage, but its
|
||||
prebuild-install-to-node-gyp fallback conflicts with REA's compiler-free
|
||||
installation rule.
|
||||
|
||||
These external repository summaries are discovery evidence. The implementation
|
||||
must still be verified against current Node-API, npm, Win32, repository source,
|
||||
package artifacts, and the real Windows runner.
|
||||
|
|
@ -9,3 +9,4 @@ states its implementation status separately.
|
|||
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Foundation shipped; capability work continues |
|
||||
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Accepted | Linux x86_64 extracted-module replay shipped |
|
||||
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Accepted | PE/CLI triage shipped; deeper contracts planned |
|
||||
| [0004: Windows native authority packaging](0004-windows-native-authority-packaging.md) | Proposed | Design only; P0 limitations remain |
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"client": "2.0.0-beta.4",
|
||||
"core": "2.0.0-beta.4"
|
||||
},
|
||||
"skill_version": "21"
|
||||
"skill_version": "22"
|
||||
},
|
||||
"tools": {
|
||||
"total": 95,
|
||||
|
|
@ -566,10 +566,10 @@
|
|||
"mcp_resource_templates": 8
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "4f19c5773a44980e176978f05626f2d82c95d49e4b86d024ce515c3ff1670bae",
|
||||
"tools_sha256": "28aad2ec69ca46ce9107fa49bff07768d91f194b602f1f32059352e61eb6c19f",
|
||||
"prompts_sha256": "f6da3ad38a1000c11364d1710f122ac4990551f04efd8c53ffb2e819b5cf5707",
|
||||
"resources_sha256": "5dca0fcdad3e78d71a1b6ef7b4f0c12bdcd8e3ec11fcbdd9ccd3a68edadf5c80",
|
||||
"combined_sha256": "b6840d7c79ffdfe8a2ef65fb66c1694f3631bacbc22a0456abbe12aca2d53969"
|
||||
"combined_sha256": "266ea209ddc79eaf85d7ad160e8d3c8ce55cfda6d4b2ca1abb2e4554a914cfd1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@
|
|||
"node": "^22.19.0 || >=24.11.0"
|
||||
},
|
||||
"packageManager": "npm@11.16.0",
|
||||
"rea": {
|
||||
"windowsNativeAuthority": {
|
||||
"packageName": "@rea/windows-x64",
|
||||
"contractVersion": 1,
|
||||
"nodeApiVersion": 9,
|
||||
"artifactSha256": null
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"install.sh",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,16 @@ const versionOf = (name) => {
|
|||
throw new Error(`Missing locked ${name} version`);
|
||||
return value;
|
||||
};
|
||||
const windowsNativeAuthority = packageJson.rea?.windowsNativeAuthority;
|
||||
if (
|
||||
typeof windowsNativeAuthority?.packageName !== "string" ||
|
||||
!Number.isInteger(windowsNativeAuthority.contractVersion) ||
|
||||
!Number.isInteger(windowsNativeAuthority.nodeApiVersion) ||
|
||||
(windowsNativeAuthority.artifactSha256 !== null &&
|
||||
(typeof windowsNativeAuthority.artifactSha256 !== "string" ||
|
||||
!/^[0-9a-f]{64}$/u.test(windowsNativeAuthority.artifactSha256)))
|
||||
)
|
||||
throw new Error("Invalid Windows native authority package metadata");
|
||||
const source = `/** Generated from package.json and package-lock.json; do not edit. */
|
||||
export const PACKAGE_METADATA = {
|
||||
name: ${JSON.stringify(packageJson.name)},
|
||||
|
|
@ -38,6 +48,13 @@ export const PACKAGE_METADATA = {
|
|||
clientSdkVersion: ${JSON.stringify(versionOf("@modelcontextprotocol/client"))},
|
||||
coreSdkVersion: ${JSON.stringify(versionOf("@modelcontextprotocol/core"))},
|
||||
skillVersion: ${JSON.stringify(skillVersion)},
|
||||
windowsNativeAuthority: {
|
||||
packageName: ${JSON.stringify(windowsNativeAuthority.packageName)},
|
||||
packageVersion: ${JSON.stringify(packageJson.version)},
|
||||
contractVersion: ${JSON.stringify(windowsNativeAuthority.contractVersion)},
|
||||
nodeApiVersion: ${JSON.stringify(windowsNativeAuthority.nodeApiVersion)},
|
||||
artifactSha256: ${JSON.stringify(windowsNativeAuthority.artifactSha256)},
|
||||
},
|
||||
} as const;
|
||||
`;
|
||||
const outputPath = join(root, "src/generatedPackageMetadata.ts");
|
||||
|
|
|
|||
|
|
@ -25,6 +25,37 @@ export type ToolAvailabilityReason =
|
|||
| "unsupported_host"
|
||||
| "policy_disabled";
|
||||
|
||||
type ToolKind = (typeof TOOL_CONTRACTS)[number]["kind"];
|
||||
type ProviderDescriptor = {
|
||||
readonly available: boolean;
|
||||
readonly reason: string | null;
|
||||
};
|
||||
type AvailabilityPolicy = {
|
||||
readonly processCaptureEnabled: boolean;
|
||||
readonly evidenceFileRoots: number;
|
||||
readonly browserObservationEnabled?: boolean;
|
||||
readonly electronObservationEnabled?: boolean;
|
||||
readonly javascriptReplayEnabled?: boolean;
|
||||
readonly managedRuntimeEnabled?: boolean;
|
||||
};
|
||||
type Availability = {
|
||||
readonly reason: ToolAvailabilityReason;
|
||||
readonly remediation: string | null;
|
||||
};
|
||||
type AvailabilityContext = {
|
||||
readonly name: string;
|
||||
readonly kind: ToolKind;
|
||||
readonly targetOpen: boolean;
|
||||
readonly targetKind:
|
||||
| "executable"
|
||||
| "database"
|
||||
| "archive"
|
||||
| "artifact"
|
||||
| undefined;
|
||||
readonly descriptors: ReadonlyMap<string, ProviderDescriptor>;
|
||||
readonly policy: AvailabilityPolicy;
|
||||
};
|
||||
|
||||
const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
|
||||
swift_classes: ["list_procedures"],
|
||||
get_objc_classes: ["list_names"],
|
||||
|
|
@ -51,28 +82,21 @@ const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
|
|||
/** Build stable per-operation availability without hiding familiar tools. */
|
||||
export const buildCapabilityInventory = (
|
||||
sessionStatus: JsonValue,
|
||||
policy: {
|
||||
readonly processCaptureEnabled: boolean;
|
||||
readonly evidenceFileRoots: number;
|
||||
readonly browserObservationEnabled?: boolean;
|
||||
readonly electronObservationEnabled?: boolean;
|
||||
readonly javascriptReplayEnabled?: boolean;
|
||||
readonly managedRuntimeEnabled?: boolean;
|
||||
},
|
||||
policy: AvailabilityPolicy,
|
||||
) => {
|
||||
const status = statusSchema.parse(sessionStatus);
|
||||
const descriptors = new Map(
|
||||
status.capabilities.map((descriptor) => [descriptor.operation, descriptor]),
|
||||
);
|
||||
return TOOL_CONTRACTS.map((contract) => {
|
||||
const availability = availabilityFor(
|
||||
contract.name,
|
||||
contract.kind,
|
||||
status.open,
|
||||
status.kind,
|
||||
const availability = availabilityFor({
|
||||
name: contract.name,
|
||||
kind: contract.kind,
|
||||
targetOpen: status.open,
|
||||
targetKind: status.kind,
|
||||
descriptors,
|
||||
policy,
|
||||
);
|
||||
});
|
||||
return {
|
||||
name: contract.name,
|
||||
surface: contract.kind,
|
||||
|
|
@ -90,27 +114,19 @@ export const buildCapabilityInventory = (
|
|||
}).sort((left, right) => left.name.localeCompare(right.name));
|
||||
};
|
||||
|
||||
const availabilityFor = (
|
||||
name: string,
|
||||
kind: (typeof TOOL_CONTRACTS)[number]["kind"],
|
||||
targetOpen: boolean,
|
||||
targetKind: "executable" | "database" | "archive" | "artifact" | undefined,
|
||||
descriptors: ReadonlyMap<
|
||||
string,
|
||||
{ readonly available: boolean; readonly reason: string | null }
|
||||
>,
|
||||
policy: {
|
||||
readonly processCaptureEnabled: boolean;
|
||||
readonly evidenceFileRoots: number;
|
||||
readonly browserObservationEnabled?: boolean;
|
||||
readonly electronObservationEnabled?: boolean;
|
||||
readonly javascriptReplayEnabled?: boolean;
|
||||
readonly managedRuntimeEnabled?: boolean;
|
||||
},
|
||||
): {
|
||||
readonly reason: ToolAvailabilityReason;
|
||||
readonly remediation: string | null;
|
||||
} => {
|
||||
const availabilityFor = (context: AvailabilityContext): Availability => {
|
||||
const policyDecision = policyAvailability(context);
|
||||
if (policyDecision !== null) return policyDecision;
|
||||
const targetDecision = targetAvailability(context);
|
||||
if (targetDecision !== null) return targetDecision;
|
||||
return providerAvailability(context);
|
||||
};
|
||||
|
||||
const policyAvailability = ({
|
||||
name,
|
||||
kind,
|
||||
policy,
|
||||
}: AvailabilityContext): Availability | null => {
|
||||
if (name === "capture_process_scenario" && !policy.processCaptureEnabled)
|
||||
return {
|
||||
reason: "policy_disabled",
|
||||
|
|
@ -159,6 +175,14 @@ const availabilityFor = (
|
|||
"Enable Electron observation and configure a loopback CDP endpoint and canonical file roots.",
|
||||
};
|
||||
if (kind === "session") return { reason: "available", remediation: null };
|
||||
return null;
|
||||
};
|
||||
|
||||
const targetAvailability = ({
|
||||
kind,
|
||||
targetKind,
|
||||
targetOpen,
|
||||
}: AvailabilityContext): Availability | null => {
|
||||
if (!targetOpen)
|
||||
return {
|
||||
reason: "target_required",
|
||||
|
|
@ -175,6 +199,14 @@ const availabilityFor = (
|
|||
remediation:
|
||||
"Inventory or extract a native executable, then call open_binary on that executable.",
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
const providerAvailability = ({
|
||||
descriptors,
|
||||
kind,
|
||||
name,
|
||||
}: AvailabilityContext): Availability => {
|
||||
if (kind === "enhanced") return composedAvailability(name, descriptors);
|
||||
const descriptor = descriptors.get(name);
|
||||
if (descriptor === undefined)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export interface WindowsCapabilityDependencies {
|
|||
probeSymlinkCreation(): Promise<WindowsCapabilityOutcome>;
|
||||
probeNoFollowOpen(): WindowsCapabilityOutcome;
|
||||
probePrivateAcl(): WindowsCapabilityOutcome;
|
||||
probeNativeAuthority(): WindowsCapabilityOutcome;
|
||||
probeUnixDomainSocket(): WindowsCapabilityOutcome;
|
||||
probePty(): Promise<ProcessCaptureCapability>;
|
||||
}
|
||||
|
|
@ -33,6 +34,7 @@ export interface WindowsCapabilityReport {
|
|||
symlink_creation: WindowsCapabilityOutcome;
|
||||
no_follow_open: WindowsCapabilityOutcome;
|
||||
private_acl: WindowsCapabilityOutcome;
|
||||
native_authority: WindowsCapabilityOutcome;
|
||||
unix_domain_socket: WindowsCapabilityOutcome;
|
||||
pty: WindowsCapabilityOutcome;
|
||||
}>;
|
||||
|
|
@ -53,6 +55,7 @@ export const probeWindowsCapabilities = async (
|
|||
symlink_creation: symlinkCreation,
|
||||
no_follow_open: dependencies.probeNoFollowOpen(),
|
||||
private_acl: dependencies.probePrivateAcl(),
|
||||
native_authority: dependencies.probeNativeAuthority(),
|
||||
unix_domain_socket: dependencies.probeUnixDomainSocket(),
|
||||
pty: {
|
||||
available: pty.available,
|
||||
|
|
@ -67,16 +70,16 @@ const systemDependencies = (): WindowsCapabilityDependencies => ({
|
|||
architecture: process.arch,
|
||||
probeSymlinkCreation,
|
||||
probeNoFollowOpen: () =>
|
||||
typeof constants.O_NOFOLLOW === "number"
|
||||
? { available: true, reason: null }
|
||||
: {
|
||||
available: false,
|
||||
reason: "O_NOFOLLOW is unavailable in this Node runtime",
|
||||
},
|
||||
systemNoFollowOpenCapability(process.platform, constants.O_NOFOLLOW),
|
||||
probePrivateAcl: () => ({
|
||||
available: false,
|
||||
reason: "Windows ACL enforcement is not implemented by this REA build",
|
||||
}),
|
||||
probeNativeAuthority: () => ({
|
||||
available: false,
|
||||
reason:
|
||||
"Windows native authority package is not implemented by this REA build",
|
||||
}),
|
||||
probeUnixDomainSocket: () =>
|
||||
process.platform === "win32"
|
||||
? {
|
||||
|
|
@ -87,6 +90,24 @@ const systemDependencies = (): WindowsCapabilityDependencies => ({
|
|||
probePty: probeProcessCaptureCapability,
|
||||
});
|
||||
|
||||
/** Report pathname no-follow support without treating it as Windows authority. */
|
||||
export const systemNoFollowOpenCapability = (
|
||||
platform: NodeJS.Platform,
|
||||
noFollow: number | undefined,
|
||||
): WindowsCapabilityOutcome => {
|
||||
if (platform === "win32")
|
||||
return {
|
||||
available: false,
|
||||
reason: "Windows reparse-safe handle admission is not implemented",
|
||||
};
|
||||
return typeof noFollow === "number"
|
||||
? { available: true, reason: null }
|
||||
: {
|
||||
available: false,
|
||||
reason: "O_NOFOLLOW is unavailable in this Node runtime",
|
||||
};
|
||||
};
|
||||
|
||||
const probeSymlinkCreation = async (): Promise<WindowsCapabilityOutcome> => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-symlink-probe-"));
|
||||
const target = join(root, "target");
|
||||
|
|
@ -106,6 +127,11 @@ const probeSymlinkCreation = async (): Promise<WindowsCapabilityOutcome> => {
|
|||
: "symlink creation failed",
|
||||
};
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
try {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Cleanup failure must not replace the machine-readable probe outcome.
|
||||
// The probe root lives beneath the OS temporary directory and is unique.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,401 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { readFile, realpath } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { win32 } from "node:path";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { PACKAGE_METADATA } from "../generatedPackageMetadata.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
WindowsNativeAuthorityError,
|
||||
type WindowsNativeAuthorityBackend,
|
||||
type WindowsNativeAuthorityIdentity,
|
||||
} from "./WindowsNativeAuthority.js";
|
||||
|
||||
const BACKEND_EXPORTS = [
|
||||
"admitDirectory",
|
||||
"admitFile",
|
||||
"createPrivateRoot",
|
||||
"identity",
|
||||
"spawnOwnedProcess",
|
||||
] as const;
|
||||
|
||||
const identitySchema = z.strictObject({
|
||||
packageName: z.string().min(1),
|
||||
packageVersion: z.string().min(1),
|
||||
contractVersion: z.number().int().positive(),
|
||||
nodeApiVersion: z.number().int().positive(),
|
||||
artifactSha256: z.string().regex(/^[0-9a-f]{64}$/u),
|
||||
});
|
||||
|
||||
const packageManifestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
version: z.string().min(1),
|
||||
os: z.tuple([z.literal("win32")]),
|
||||
cpu: z.tuple([z.literal("x64")]),
|
||||
scripts: z.undefined().optional(),
|
||||
reaNativeAuthority: z.strictObject({
|
||||
contractVersion: z.number().int().positive(),
|
||||
nodeApiVersion: z.number().int().positive(),
|
||||
artifact: z.string().regex(/^[^\\/]+\.node$/u),
|
||||
artifactSha256: z.string().regex(/^[0-9a-f]{64}$/u),
|
||||
exports: z.tuple([
|
||||
z.literal("admitDirectory"),
|
||||
z.literal("admitFile"),
|
||||
z.literal("createPrivateRoot"),
|
||||
z.literal("identity"),
|
||||
z.literal("spawnOwnedProcess"),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
/** Release-generated identity required before the Windows addon can load. */
|
||||
export interface WindowsNativeAuthorityPackageCommitment {
|
||||
readonly packageName: string;
|
||||
readonly packageVersion: string;
|
||||
readonly contractVersion: number;
|
||||
readonly nodeApiVersion: number;
|
||||
readonly artifactSha256: string | null;
|
||||
}
|
||||
|
||||
/** Injectable Node package-loading seam used by the production loader. */
|
||||
export interface WindowsNativeAuthorityLoadHost {
|
||||
readonly platform: NodeJS.Platform;
|
||||
readonly architecture: string;
|
||||
readonly nodeApiVersion: number | null;
|
||||
resolve(packageName: string): string;
|
||||
resolveManifest(packageName: string): string;
|
||||
canonicalize(path: string): Promise<string>;
|
||||
read(path: string): Promise<Uint8Array>;
|
||||
cached(path: string): unknown | undefined;
|
||||
load(path: string): unknown;
|
||||
}
|
||||
|
||||
interface LoadedBackend {
|
||||
readonly backend: LoadedWindowsNativeBackend;
|
||||
readonly canonicalPath: string;
|
||||
readonly identity: WindowsNativeAuthorityIdentity;
|
||||
}
|
||||
|
||||
type LoadedWindowsNativeBackend = WindowsNativeAuthorityBackend & {
|
||||
readonly identity: WindowsNativeAuthorityIdentity;
|
||||
};
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/** Production host backed by Node's exact CommonJS/native-addon loader. */
|
||||
export const systemWindowsNativeAuthorityLoadHost =
|
||||
(): WindowsNativeAuthorityLoadHost => ({
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
nodeApiVersion: parseNodeApiVersion(process.versions.napi),
|
||||
resolve: (packageName) => require.resolve(packageName),
|
||||
resolveManifest: (packageName) =>
|
||||
require.resolve(`${packageName}/package.json`),
|
||||
canonicalize: realpath,
|
||||
read: readFile,
|
||||
cached: (path) => require.cache[path]?.exports,
|
||||
load: (path) => require(path),
|
||||
});
|
||||
|
||||
/** Stateful process-lifetime loader for one pinned native module identity. */
|
||||
export class WindowsNativeAuthorityPackageLoader {
|
||||
private loaded: LoadedBackend | undefined;
|
||||
private loading:
|
||||
| Promise<
|
||||
Result<WindowsNativeAuthorityBackend, WindowsNativeAuthorityError>
|
||||
>
|
||||
| undefined;
|
||||
|
||||
constructor(
|
||||
private readonly host: WindowsNativeAuthorityLoadHost,
|
||||
private readonly commitment: WindowsNativeAuthorityPackageCommitment,
|
||||
) {}
|
||||
|
||||
/** Load once and pin either the backend or its failure for this process. */
|
||||
load(): Promise<
|
||||
Result<WindowsNativeAuthorityBackend, WindowsNativeAuthorityError>
|
||||
> {
|
||||
if (this.loaded !== undefined)
|
||||
return Promise.resolve(ok(this.loaded.backend));
|
||||
this.loading ??= this.loadOnce();
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
private async loadOnce(): Promise<
|
||||
Result<WindowsNativeAuthorityBackend, WindowsNativeAuthorityError>
|
||||
> {
|
||||
const supported = this.checkRuntime();
|
||||
if (!supported.ok) return supported;
|
||||
const manifest = await this.readManifest();
|
||||
if (!manifest.ok) return manifest;
|
||||
let resolvedPath: string;
|
||||
try {
|
||||
resolvedPath = await this.host.canonicalize(
|
||||
this.host.resolve(this.commitment.packageName),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
loadError(
|
||||
"unavailable",
|
||||
"Windows native authority package is not installed or resolvable",
|
||||
cause,
|
||||
),
|
||||
);
|
||||
}
|
||||
let expectedArtifactPath: string;
|
||||
try {
|
||||
expectedArtifactPath = await this.host.canonicalize(
|
||||
win32.resolve(
|
||||
win32.dirname(manifest.value.path),
|
||||
manifest.value.artifact,
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
loadError(
|
||||
"unavailable",
|
||||
"Windows native authority declared addon is unavailable",
|
||||
cause,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (resolvedPath !== expectedArtifactPath)
|
||||
return err(
|
||||
loadError(
|
||||
"native_contract_mismatch",
|
||||
"Windows native authority package resolved an unexpected addon path",
|
||||
),
|
||||
);
|
||||
if (this.host.cached(resolvedPath) !== undefined)
|
||||
return err(
|
||||
loadError(
|
||||
"native_contract_mismatch",
|
||||
"Windows native authority addon was cached before identity verification",
|
||||
),
|
||||
);
|
||||
let artifact: Uint8Array;
|
||||
try {
|
||||
artifact = await this.host.read(resolvedPath);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
loadError(
|
||||
"unavailable",
|
||||
"Windows native authority addon could not be read",
|
||||
cause,
|
||||
),
|
||||
);
|
||||
}
|
||||
const digest = createHash("sha256").update(artifact).digest("hex");
|
||||
if (digest !== this.commitment.artifactSha256)
|
||||
return err(
|
||||
loadError(
|
||||
"identity_drift",
|
||||
"Windows native authority addon digest does not match this REA release",
|
||||
),
|
||||
);
|
||||
let module: unknown;
|
||||
try {
|
||||
module = this.host.load(resolvedPath);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
loadError(
|
||||
"unavailable",
|
||||
"Windows native authority addon could not be loaded",
|
||||
cause,
|
||||
),
|
||||
);
|
||||
}
|
||||
const parsed = parseBackend(module, this.commitment, digest);
|
||||
if (!parsed.ok) return parsed;
|
||||
this.loaded = {
|
||||
backend: parsed.value,
|
||||
canonicalPath: resolvedPath,
|
||||
identity: parsed.value.identity,
|
||||
};
|
||||
return ok(parsed.value);
|
||||
}
|
||||
|
||||
private checkRuntime(): Result<void, WindowsNativeAuthorityError> {
|
||||
if (this.host.platform !== "win32" || this.host.architecture !== "x64")
|
||||
return err(
|
||||
loadError(
|
||||
"unavailable",
|
||||
"Windows native authority requires Windows x64",
|
||||
),
|
||||
);
|
||||
if (this.commitment.artifactSha256 === null)
|
||||
return err(
|
||||
loadError(
|
||||
"unavailable",
|
||||
"This REA build has no committed Windows native authority artifact",
|
||||
),
|
||||
);
|
||||
if (
|
||||
this.host.nodeApiVersion === null ||
|
||||
this.host.nodeApiVersion < this.commitment.nodeApiVersion
|
||||
)
|
||||
return err(
|
||||
loadError(
|
||||
"unavailable",
|
||||
"Windows native authority requires the committed Node-API version",
|
||||
),
|
||||
);
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
private async readManifest(): Promise<
|
||||
Result<
|
||||
{ readonly path: string; readonly artifact: string },
|
||||
WindowsNativeAuthorityError
|
||||
>
|
||||
> {
|
||||
let path: string;
|
||||
let bytes: Uint8Array;
|
||||
try {
|
||||
path = await this.host.canonicalize(
|
||||
this.host.resolveManifest(this.commitment.packageName),
|
||||
);
|
||||
bytes = await this.host.read(path);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
loadError(
|
||||
"unavailable",
|
||||
"Windows native authority package manifest is unavailable",
|
||||
cause,
|
||||
),
|
||||
);
|
||||
}
|
||||
let decoded: unknown;
|
||||
try {
|
||||
decoded = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
loadError(
|
||||
"native_contract_mismatch",
|
||||
"Windows native authority package manifest is not valid JSON",
|
||||
cause,
|
||||
),
|
||||
);
|
||||
}
|
||||
const parsed = packageManifestSchema.safeParse(decoded);
|
||||
if (
|
||||
!parsed.success ||
|
||||
parsed.data.name !== this.commitment.packageName ||
|
||||
parsed.data.version !== this.commitment.packageVersion ||
|
||||
parsed.data.reaNativeAuthority.contractVersion !==
|
||||
this.commitment.contractVersion ||
|
||||
parsed.data.reaNativeAuthority.nodeApiVersion !==
|
||||
this.commitment.nodeApiVersion ||
|
||||
parsed.data.reaNativeAuthority.artifactSha256 !==
|
||||
this.commitment.artifactSha256
|
||||
)
|
||||
return err(
|
||||
loadError(
|
||||
"native_contract_mismatch",
|
||||
"Windows native authority package manifest does not match this REA release",
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
path,
|
||||
artifact: parsed.data.reaNativeAuthority.artifact,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let productionLoader: WindowsNativeAuthorityPackageLoader | undefined;
|
||||
|
||||
/** Return the one process-global loader for release-generated package metadata. */
|
||||
export const createWindowsNativeAuthorityPackageLoader =
|
||||
(): WindowsNativeAuthorityPackageLoader => {
|
||||
productionLoader ??= new WindowsNativeAuthorityPackageLoader(
|
||||
systemWindowsNativeAuthorityLoadHost(),
|
||||
PACKAGE_METADATA.windowsNativeAuthority,
|
||||
);
|
||||
return productionLoader;
|
||||
};
|
||||
|
||||
const parseBackend = (
|
||||
input: unknown,
|
||||
commitment: WindowsNativeAuthorityPackageCommitment,
|
||||
digest: string,
|
||||
): Result<LoadedWindowsNativeBackend, WindowsNativeAuthorityError> => {
|
||||
if (typeof input !== "object" || input === null)
|
||||
return err(
|
||||
loadError(
|
||||
"native_contract_mismatch",
|
||||
"Windows native authority addon exported a non-object contract",
|
||||
),
|
||||
);
|
||||
const keys = Object.keys(input).sort((left, right) =>
|
||||
left.localeCompare(right),
|
||||
);
|
||||
if (!sameStrings(keys, BACKEND_EXPORTS))
|
||||
return err(
|
||||
loadError(
|
||||
"native_contract_mismatch",
|
||||
"Windows native authority addon export set does not match the contract",
|
||||
),
|
||||
);
|
||||
const candidate = input as Record<string, unknown>;
|
||||
const identity = identitySchema.safeParse(candidate.identity);
|
||||
if (!identity.success || !sameIdentity(identity.data, commitment, digest))
|
||||
return err(
|
||||
loadError(
|
||||
"native_contract_mismatch",
|
||||
"Windows native authority addon identity does not match this REA release",
|
||||
),
|
||||
);
|
||||
if (
|
||||
typeof candidate.admitFile !== "function" ||
|
||||
typeof candidate.admitDirectory !== "function" ||
|
||||
typeof candidate.createPrivateRoot !== "function" ||
|
||||
typeof candidate.spawnOwnedProcess !== "function"
|
||||
)
|
||||
return err(
|
||||
loadError(
|
||||
"native_contract_mismatch",
|
||||
"Windows native authority addon operations do not match the contract",
|
||||
),
|
||||
);
|
||||
// SAFETY: the exact export set, identity, and every callable backend operation
|
||||
// were checked above. Operation results remain parsed by WindowsNativeAuthority.
|
||||
return ok(candidate as unknown as LoadedWindowsNativeBackend);
|
||||
};
|
||||
|
||||
const sameIdentity = (
|
||||
identity: WindowsNativeAuthorityIdentity,
|
||||
commitment: WindowsNativeAuthorityPackageCommitment,
|
||||
digest: string,
|
||||
): boolean =>
|
||||
identity.packageName === commitment.packageName &&
|
||||
identity.packageVersion === commitment.packageVersion &&
|
||||
identity.contractVersion === commitment.contractVersion &&
|
||||
identity.nodeApiVersion === commitment.nodeApiVersion &&
|
||||
identity.artifactSha256 === digest;
|
||||
|
||||
const sameStrings = (
|
||||
actual: readonly string[],
|
||||
expected: readonly string[],
|
||||
): boolean =>
|
||||
actual.length === expected.length &&
|
||||
actual.every((value, index) => value === expected[index]);
|
||||
|
||||
const parseNodeApiVersion = (value: string | undefined): number | null => {
|
||||
if (value === undefined || !/^\d+$/u.test(value)) return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
||||
};
|
||||
|
||||
const loadError = (
|
||||
reason: "unavailable" | "identity_drift" | "native_contract_mismatch",
|
||||
message: string,
|
||||
cause?: unknown,
|
||||
): WindowsNativeAuthorityError =>
|
||||
new WindowsNativeAuthorityError(
|
||||
"load_native_authority",
|
||||
reason,
|
||||
message,
|
||||
cause === undefined ? undefined : { cause },
|
||||
);
|
||||
|
|
@ -5,5 +5,12 @@ export const PACKAGE_METADATA = {
|
|||
serverSdkVersion: "2.0.0-beta.4",
|
||||
clientSdkVersion: "2.0.0-beta.4",
|
||||
coreSdkVersion: "2.0.0-beta.4",
|
||||
skillVersion: "21",
|
||||
skillVersion: "22",
|
||||
windowsNativeAuthority: {
|
||||
packageName: "@rea/windows-x64",
|
||||
packageVersion: "2.0.0",
|
||||
contractVersion: 1,
|
||||
nodeApiVersion: 9,
|
||||
artifactSha256: null,
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ export interface PrivateRuntimeRootOptions {
|
|||
}
|
||||
|
||||
/**
|
||||
* Owns one mode-0700 temporary runtime root and removes it idempotently.
|
||||
* Owns one POSIX mode-0700 temporary runtime root and removes it idempotently.
|
||||
*
|
||||
* Protocol adapters decide what belongs inside the directory; this primitive
|
||||
* only establishes a private filesystem boundary and deterministic cleanup.
|
||||
* establishes a private filesystem boundary on POSIX and deterministic cleanup.
|
||||
* Windows privacy requires the separately verified native DACL boundary.
|
||||
*/
|
||||
export class PrivateRuntimeRoot {
|
||||
#closePromise: Promise<void> | undefined;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import {
|
||||
probeWindowsCapabilities,
|
||||
systemNoFollowOpenCapability,
|
||||
type WindowsCapabilityDependencies,
|
||||
} from "../src/application/WindowsCapabilities.js";
|
||||
|
||||
|
|
@ -20,6 +21,10 @@ const dependencies = (
|
|||
available: false,
|
||||
reason: "Windows ACL enforcement is not implemented by this REA build",
|
||||
}),
|
||||
probeNativeAuthority: () => ({
|
||||
available: false,
|
||||
reason: "Windows native authority package is not installed",
|
||||
}),
|
||||
probeUnixDomainSocket: () => ({
|
||||
available: false,
|
||||
reason: "Node path-based IPC uses named pipes on Windows",
|
||||
|
|
@ -34,6 +39,17 @@ const dependencies = (
|
|||
});
|
||||
|
||||
describe("Windows host capability report", () => {
|
||||
it("never treats a numeric POSIX flag as Windows reparse authority", () => {
|
||||
expect(systemNoFollowOpenCapability("win32", 0x20_000)).toEqual({
|
||||
available: false,
|
||||
reason: "Windows reparse-safe handle admission is not implemented",
|
||||
});
|
||||
expect(systemNoFollowOpenCapability("linux", 0x20_000)).toEqual({
|
||||
available: true,
|
||||
reason: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps unavailable security controls explicit", async () => {
|
||||
await expect(probeWindowsCapabilities(dependencies())).resolves.toEqual({
|
||||
platform: "win32",
|
||||
|
|
@ -49,6 +65,10 @@ describe("Windows host capability report", () => {
|
|||
reason:
|
||||
"Windows ACL enforcement is not implemented by this REA build",
|
||||
},
|
||||
native_authority: {
|
||||
available: false,
|
||||
reason: "Windows native authority package is not installed",
|
||||
},
|
||||
unix_domain_socket: {
|
||||
available: false,
|
||||
reason: "Node path-based IPC uses named pipes on Windows",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,273 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
createWindowsNativeAuthorityPackageLoader,
|
||||
WindowsNativeAuthorityPackageLoader,
|
||||
type WindowsNativeAuthorityLoadHost,
|
||||
type WindowsNativeAuthorityPackageCommitment,
|
||||
} from "../src/application/WindowsNativeAuthorityLoader.js";
|
||||
import {
|
||||
unavailableWindowsNativeAuthorityBackend,
|
||||
type WindowsNativeAuthorityBackend,
|
||||
} from "../src/application/WindowsNativeAuthority.js";
|
||||
|
||||
const artifact = Buffer.from("source-owned-windows-native-addon");
|
||||
const digest = createHash("sha256").update(artifact).digest("hex");
|
||||
const packageManifestObject = {
|
||||
name: "@rea/windows-x64",
|
||||
version: "2.0.0",
|
||||
os: ["win32"],
|
||||
cpu: ["x64"],
|
||||
reaNativeAuthority: {
|
||||
contractVersion: 1,
|
||||
nodeApiVersion: 9,
|
||||
artifact: "rea.node",
|
||||
artifactSha256: digest,
|
||||
exports: [
|
||||
"admitDirectory",
|
||||
"admitFile",
|
||||
"createPrivateRoot",
|
||||
"identity",
|
||||
"spawnOwnedProcess",
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
const packageManifest = Buffer.from(JSON.stringify(packageManifestObject));
|
||||
|
||||
const commitment: WindowsNativeAuthorityPackageCommitment = {
|
||||
packageName: "@rea/windows-x64",
|
||||
packageVersion: "2.0.0",
|
||||
contractVersion: 1,
|
||||
nodeApiVersion: 9,
|
||||
artifactSha256: digest,
|
||||
};
|
||||
|
||||
const backend = (): WindowsNativeAuthorityBackend => ({
|
||||
...unavailableWindowsNativeAuthorityBackend("not exercised"),
|
||||
identity: {
|
||||
packageName: commitment.packageName,
|
||||
packageVersion: commitment.packageVersion,
|
||||
contractVersion: commitment.contractVersion,
|
||||
nodeApiVersion: commitment.nodeApiVersion,
|
||||
artifactSha256: digest,
|
||||
},
|
||||
});
|
||||
|
||||
class RecordingLoadHost implements WindowsNativeAuthorityLoadHost {
|
||||
readonly platform: NodeJS.Platform;
|
||||
readonly architecture: string;
|
||||
readonly nodeApiVersion: number | null;
|
||||
readonly events: string[] = [];
|
||||
module: unknown = backend();
|
||||
bytes: Uint8Array = artifact;
|
||||
manifestBytes: Uint8Array = packageManifest;
|
||||
cachedValue: unknown | undefined;
|
||||
resolveFailure: unknown | undefined;
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
readonly platform?: NodeJS.Platform;
|
||||
readonly architecture?: string;
|
||||
readonly nodeApiVersion?: number | null;
|
||||
} = {},
|
||||
) {
|
||||
this.platform = options.platform ?? "win32";
|
||||
this.architecture = options.architecture ?? "x64";
|
||||
this.nodeApiVersion =
|
||||
options.nodeApiVersion === undefined ? 10 : options.nodeApiVersion;
|
||||
}
|
||||
|
||||
resolve(packageName: string): string {
|
||||
this.events.push(`resolve:${packageName}`);
|
||||
if (this.resolveFailure !== undefined) throw this.resolveFailure;
|
||||
return "C:\\rea\\node_modules\\@rea\\windows-x64\\rea.node";
|
||||
}
|
||||
|
||||
resolveManifest(packageName: string): string {
|
||||
this.events.push(`resolve-manifest:${packageName}`);
|
||||
return "C:\\rea\\node_modules\\@rea\\windows-x64\\package.json";
|
||||
}
|
||||
|
||||
async canonicalize(path: string): Promise<string> {
|
||||
this.events.push("canonicalize");
|
||||
return path.toLowerCase();
|
||||
}
|
||||
|
||||
async read(path: string): Promise<Uint8Array> {
|
||||
if (path.endsWith("package.json")) {
|
||||
this.events.push("read-manifest");
|
||||
return this.manifestBytes;
|
||||
}
|
||||
this.events.push("read-addon");
|
||||
return this.bytes;
|
||||
}
|
||||
|
||||
cached(_path: string): unknown | undefined {
|
||||
this.events.push("cached");
|
||||
return this.cachedValue;
|
||||
}
|
||||
|
||||
load(_path: string): unknown {
|
||||
this.events.push("load");
|
||||
return this.module;
|
||||
}
|
||||
}
|
||||
|
||||
describe("Windows native authority package loader", () => {
|
||||
it("owns one production loader for the process lifetime", () => {
|
||||
expect(createWindowsNativeAuthorityPackageLoader()).toBe(
|
||||
createWindowsNativeAuthorityPackageLoader(),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads and pins one exact release tuple idempotently", async () => {
|
||||
const host = new RecordingLoadHost();
|
||||
const loader = new WindowsNativeAuthorityPackageLoader(host, commitment);
|
||||
|
||||
const [first, concurrent] = await Promise.all([
|
||||
loader.load(),
|
||||
loader.load(),
|
||||
]);
|
||||
const second = await loader.load();
|
||||
|
||||
expect(first).toMatchObject({ ok: true });
|
||||
expect(concurrent).toEqual(first);
|
||||
expect(second).toEqual(first);
|
||||
expect(host.events).toEqual([
|
||||
"resolve-manifest:@rea/windows-x64",
|
||||
"canonicalize",
|
||||
"read-manifest",
|
||||
"resolve:@rea/windows-x64",
|
||||
"canonicalize",
|
||||
"canonicalize",
|
||||
"cached",
|
||||
"read-addon",
|
||||
"load",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["wrong platform", { platform: "linux" as const }, commitment],
|
||||
["wrong architecture", { architecture: "arm64" }, commitment],
|
||||
["old Node-API", { nodeApiVersion: 8 }, commitment],
|
||||
["missing Node-API", { nodeApiVersion: null }, commitment],
|
||||
["uncommitted artifact", {}, { ...commitment, artifactSha256: null }],
|
||||
])(
|
||||
"fails before package resolution for %s",
|
||||
async (_name, options, expected) => {
|
||||
const host = new RecordingLoadHost(options);
|
||||
const result = await new WindowsNativeAuthorityPackageLoader(
|
||||
host,
|
||||
expected,
|
||||
).load();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "unavailable" },
|
||||
});
|
||||
expect(host.events).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("fails closed when the optional package is absent", async () => {
|
||||
const host = new RecordingLoadHost();
|
||||
host.resolveFailure = new Error("module details must not escape");
|
||||
|
||||
const result = await new WindowsNativeAuthorityPackageLoader(
|
||||
host,
|
||||
commitment,
|
||||
).load();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
reason: "unavailable",
|
||||
message:
|
||||
"Windows native authority package is not installed or resolvable",
|
||||
},
|
||||
});
|
||||
expect(host.events).toEqual([
|
||||
"resolve-manifest:@rea/windows-x64",
|
||||
"canonicalize",
|
||||
"read-manifest",
|
||||
"resolve:@rea/windows-x64",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects platform manifest drift before addon resolution", async () => {
|
||||
const host = new RecordingLoadHost();
|
||||
host.manifestBytes = Buffer.from(
|
||||
JSON.stringify({
|
||||
...packageManifestObject,
|
||||
version: "1.0.0",
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await new WindowsNativeAuthorityPackageLoader(
|
||||
host,
|
||||
commitment,
|
||||
).load();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "native_contract_mismatch" },
|
||||
});
|
||||
expect(host.events).not.toContain("resolve:@rea/windows-x64");
|
||||
expect(host.events).not.toContain("load");
|
||||
});
|
||||
|
||||
it("rejects cache contamination and digest drift before evaluation", async () => {
|
||||
const cached = new RecordingLoadHost();
|
||||
cached.cachedValue = backend();
|
||||
const cachedResult = await new WindowsNativeAuthorityPackageLoader(
|
||||
cached,
|
||||
commitment,
|
||||
).load();
|
||||
expect(cachedResult).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "native_contract_mismatch" },
|
||||
});
|
||||
expect(cached.events).not.toContain("read-addon");
|
||||
expect(cached.events).not.toContain("load");
|
||||
|
||||
const drifted = new RecordingLoadHost();
|
||||
drifted.bytes = Buffer.from("different-addon");
|
||||
const driftedResult = await new WindowsNativeAuthorityPackageLoader(
|
||||
drifted,
|
||||
commitment,
|
||||
).load();
|
||||
expect(driftedResult).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "identity_drift" },
|
||||
});
|
||||
expect(drifted.events).not.toContain("load");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["non-object", null],
|
||||
["extra export", { ...backend(), unexpected: true }],
|
||||
["missing operation", { ...backend(), admitFile: undefined }],
|
||||
[
|
||||
"wrong package version",
|
||||
{
|
||||
...backend(),
|
||||
identity: { ...backend().identity, packageVersion: "1.0.0" },
|
||||
},
|
||||
],
|
||||
])("rejects a %s native contract", async (_name, module) => {
|
||||
const host = new RecordingLoadHost();
|
||||
host.module = module;
|
||||
|
||||
const result = await new WindowsNativeAuthorityPackageLoader(
|
||||
host,
|
||||
commitment,
|
||||
).load();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "native_contract_mismatch" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue