Repository navigation
Add a .NET SDK layer - #28
Merged
Merged
Conversation
CoreCommand runs one of Core's public commands to completion on the caller's thread, reading its stdout and stderr only when the adapter's poll(2) reports them ready, bounded by a timeout, output limits, and a CancellationToken. On Linux with setsid at /usr/bin or /bin the command leads its own process group, which is killed with kill(-pgid) when the command exits or is stopped; a reaped leader's pid held by a live process is treated as reuse and nothing is killed. Without setsid, descendants recorded from /proc by pid and start time are killed at the exit, and Process.Kill(true) on a stop. Also add the SDK's one exception type, with its 8,192-char message cap and 64-char excerpts of caller text, Core's hash and signature parsers with depth, type, and token bounds, and the native calls the SDK needs (kill, geteuid, mkdir, statx or stat).
Codec<T> objects map .NET values to Jacquard values: long (Core's 63-bit Int), int, double, string, bool, Hash, Unit, lists, Option<T> (which nests) and nullable codecs, tuples, protocol Values, records and enumerations and type hierarchies as constructors, and Dynamic by public name. Records are mapped through explicit field readers and a constructor function rather than reflection, so mapping works under trimming and ahead-of-time compilation; Codecs.Lazy supports recursive types. There is no decimal mapping: money crosses as integer minor units. Every encoding is bounded as it goes by the selected limits; nesting is checked before each nested encoder exists, raw values, decoded values, nested unions, lazy chains, and Dynamic equality and text are walked iteratively, and caller exceptions from codecs, field readers, and record constructors are wrapped with the exception kept.
Runtime.Prepare checks, hashes, and installs sources with Core's public commands into a verified, owner-only cache keyed on the source bytes, the Core executable's bytes, every prelude file, and the protocol, with one version check and prelude discovery per key in the process. Loading derives every binding again from Core's cached raw output and refuses a changed entry. Source counts, path lengths, source bytes, fingerprinting, tree walks, and the failure cleanup are bounded and observe the token. Operations binds typed handlers with the HandlerContext and its token, PreparedProgram binds checked functions with complete services, and Function calls, invokes, and maps explicit batches (each validated first, none retried), with Invocation keeping the raw evidence.
Port the Java SDK's live suite: quickstart calls and saved inspection files, the 64-row triage oracle in explicit batches, local refusals before any worker, binding completeness, unknown completion with raw evidence, the 25-case planner oracle, cache reuse and corruption, a signature too deep to bind, a redirected name in program.json, the full Int range and Real bits, cancellation, output overflow, unit results kept after a later batch fails, and result-mapping failures. The README examples under Readme/ run here too.
JacquardExamples runs quickstart, planner, triage, and fulfillment from the rules in examples/, with the same --inspect, --fault, and --deliver-pending recovery paths as the Rust and Java commands. Fulfillment's warehouse commits bookings and outbox entries as one atomic JSON snapshot, written to a temporary file it creates exclusively and removes only if it made it and did not rename it.
Once a Core command's leader is reaped, a live process holding its pid means the group id was reused; the group kill must then send nothing. An unrelated group leader stands in for that process.
Document preparation and its bounds, the verified owner-only cache, codecs and the reason for them, money as integer minor units, services, errors, batches, the example projects, and how the SDK runs Core's commands, and keep the protocol-level host, handler, carrier, and kit sections. The README's four C# examples are the compiled sources under Readme/, which DocumentTests checks in order and the live tests run.
Update the root and examples READMEs, CONTRIBUTING, LIMITS (.NET SDK rows replace the protocol-level-only row), EVIDENCE, and the changelog. The live CI job now runs the .NET SDK's live tests with the adapter's, and the four example projects, with duplicate fulfillment and explicit recovery, through the .NET command. The source archive prunes the example command's build output, and the distribution check looks for the SDK and command.
Saving over an existing file kept that file's permissions and owner, and a symbolic link at the target was followed. The record is now written to a file created exclusively in the target's directory, made exactly 0600, and renamed over the target.
…aration The cache root was checked once and then used by its path, so another user who may write its parent could rename it away and put their own directory there. Its device and inode are now pinned at the check and compared again before the staging directory is made, before publishing, and before and after each load. The executable and every prelude file and directory were fingerprinted before Core ran them by path, so a replacement could make artifacts from other bytes under the old fingerprint. Their identities (device, inode, size, mode, owner, modification and status-change times) are now read as they are fingerprinted and must be unchanged once Core's commands finish: inside the version check and prelude discovery, before a cold entry is published, and before a warm entry is loaded. The version check is keyed on the executable's digest and identity, and a run during which anything changed is not kept.
…roup Without setsid, each record of the command's descendants read every /proc entry with no cap and no stop check, and the kills when the command exited walked every recorded process before the stop was checked. A record now reads at most 16,384 entries, 4 MiB of stat files, and 1 KiB of each, keeps at most 16,384 descendants, and ends at the deadline or a cancellation, checked before each entry; the kills at the exit check them before each descendant. On a stop, a last record bounded by the same caps replaces .NET's tree kill, which reads every process on the machine.
A longer name was returned unconverted and then looked up, so its whole length was hashed. Function, effect, operation, public, and constructor names (made, dynamic, or a record codec's) over the limit are now refused first, quoting an excerpt; an encoded constructor name is an invalid argument, the others binding failures.
… them C# chained a standard conversion into the SDK's own, so a ulong or nuint became a Real through double (2^53 + 1 arrived as 2^53), a char an Int, and a float a double. Conversions to Argument and Dynamic.Of overloads for ulong, nuint, char, float, Half, decimal, Int128, UInt128, and BigInteger are now obsolete as errors with the reason, so a use does not compile; reached by reflection, the argument is refused when encoded and Of throws. Exact overloads for the smaller integer types and uint keep them converting to Int without ambiguity.
Lazy<T> in ExecutionAndPublication mode caches the factory's exception, so a lazy codec whose factory failed once could never be used. The factory now runs under a lock and only its codec is kept: a failure keeps nothing, the next use runs it again, and once it succeeds it never runs again. A factory that asks for its own codec is still refused.
Equality walked every path, so two separately built values that share sub-values (a tuple holding the same tuple twice, 64 levels deep) took exponential time. Pairs already compared are skipped.
The executable check accepted any execute bit, so a file only its group or others may execute was resolved and then failed to start with a less clear error. Core and setsid are now checked with access(X_OK); a regular Core executable this user may not run is refused with a message saying so.
PreparedProgram.Function enumerated the caller's bindings dictionary while it held the lock that Provide, Functions, and ToString take, so caller code that blocked held them all. The count and at most one entry past the program's effects are now read first.
Each batch's range was computed from the list's current count, so a list that shrank after validation began made a batch of negative size. The count is read once; a list whose count differs before a later batch is refused.
The README, LIMITS, and CHANGELOG now say how Save replaces its target, what the cache root and Core identity checks cover and what they do not, the bounded descendant records, the name limit, the refused numeric types, the lazy codec retry, and the execute check; the changelog's reflection claim is narrowed to member reflection. EVIDENCE records the new counts and the regressions shown by undoing each fix.
…nup after it Without setsid, a stop now sends Core SIGSTOP (guarded by its start time), makes the last record within 250 ms of the stop, kills Core and every recorded descendant within 500 ms, and waits for the exit until 750 ms. A stop during the kills at Core's own exit lets them finish within the kill budget instead of leaving the rest running.
…he root before each worker start The cache root's path is resolved once with realpath(3); every directory above it must be owned by the user or root and not group- or world-writable unless sticky. A prepared program checks the root's device and inode before each worker starts.
…name every unsafe parent
…e is the one written before its rename
…ckstart remedy; drop review chronology from the .NET evidence
… and read libc's account records only within their buffer The private-group rule now enumerates the password database (getpwent_r, twice, under a process-wide lock, at most 100,000 entries of 64 KiB) and refuses the group when another account has it as its primary group, which gr_mem does not show. A cap, a lookup error, a repeated name, a missing user, or passes that differ count as not private. The pointers getpwuid_r, getgrgid_r, and getpwent_r return are checked against the buffer given to them before they are read.
…may write above them
…mits, and the tests' scratch placement
…bounded and stopped by the token, instead of libc's shared getpwent position
…mits the user, or lists more than 100,000 or malformed entries
…nds, and what it cannot see
…verdict between its directory checks
…ot writable by group or others, and run it by its checked real path
…h the test's own reading of it, private or shared
… a listing over 16 MiB or whose last line lacks a newline
…epare and per Save, and the setsid it runs through
…sing a later check for another account, user id, or group without listing again
…t alters setsid's directory for setsid's check alone
… so Prepare does not start setsid and getent each time
…n, and the new test counts in EVIDENCE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the .NET SDK layer on top of the .NET adapter (#27), with the same scope as the Java SDK (#26) and the Rust SDK. It has no runtime dependencies, targets .NET 8 and is tested on .NET 8 and 10. It includes:
Runtime.Prepare, which checks, hashes and installs sources with Core's publiccheck,hashandruncommands into a verified, owner-only cache;Codecsfor mapping .NET values to Jacquard values, with record, constructor, lazy (recursive) and dynamic by-name forms;Operations, which bind handlers to exact operation identities with typed parameters and the handler'sCancellationToken;Function.Call,InvokeandMapBatches, and anInvocationwith decode, render and owner-only save;--inspect,--faultand--deliver-pendingrecovery paths as the Rust and Java commands.Numbers. Int is a 63-bit
longand Real is adouble. There is no decimal type, so money crosses as integer minor units.decimal,ulong,nuint,char,float,Half,Int128,UInt128andBigIntegerdo not compile as arguments orDynamicvalues, rather than being converted throughdouble.Core commands are bounded to 30 s, 8 MiB stdout and 4 KiB stderr, and are stopped by the token. They read through the adapter's
poll-based pipes. On Linux withsetsid, Core leads its own process group, which is killed when the command ends or stops, with a guard against pid reuse. Withoutsetsid, Core is frozen with SIGSTOP on a stop, its recorded descendants are found by a capped/procscan, and all cleanup after a stop ends within 750 ms.Cache and save trust boundary (POSIX).
getent passwd(run as a separate, bounded process) shows no other account with it as its primary group. This keeps the default cache and work directories usable in umask-002 checkouts on Ubuntu, Fedora and Debian.Invocation.Saveapplies the same directory rule, writes an exclusively created 0600 temp file through one handle, fsyncs it, checks its identity and renames it over the target.docs/LIMITS.mdrecords what remains: same-user replacement, accounts in name services that do not enumerate, and the windows between checks and use.Bounded inputs.
JacquardExceptionwith the cause kept.Tests and CI. 252 tests pass on each runtime with live Core required, none skipped; 224 pass offline. Each guard has a regression test, shown failing on the code before its fix or by mutation;
docs/EVIDENCE.mdlists them. Thedotnetjob runs the offline suite on .NET 8, pinned to the .NET 8 SDK, and on .NET 10.live-examplesruns the SDK live tests and the four projects, after checking that$RUNNER_TEMP's ancestry passes the same directory rule.The README,
examples/README.md, CONTRIBUTING, LIMITS, EVIDENCE and CHANGELOG are updated.dotnet/README.mdis rewritten around the SDK and keeps the protocol-level section.