Repository navigation
Add a Java SDK layer - #26
Merged
Merged
Conversation
Move the worker's output FIFO setup into OutputPipes so a second user can share it, and add carrier.Command, which runs one short command with host-owned FIFOs, a timeout, bounded output, and cancellation and interruption that kill its process tree. The SDK will run Core's public commands through it.
Codec objects map Java values to Jacquard values: integers, double, text, Hash, boolean, List, Optional, unit, tuples, protocol values, records and enums and sealed hierarchies as constructors, and Dynamic by public name. Codecs can be derived from declarations through record components. Every encoding is bounded as it goes by the selected limits, value nodes and text included, so a small shared value cannot cost more than one frame. Signature and identity parsing follow the Rust SDK.
Runtime.prepare checks, hashes, and installs sources with Core's public commands into a verified cache, running each command through the bounded carrier command runner, with one version check and prelude discovery per key in the JVM. Operations binds typed handlers, Program binds checked functions with complete services, and Function calls, invokes, and maps explicit batches, with Invocation keeping the raw evidence. HandlerContext.of makes a context for calling a handler outside an invocation.
Port the Rust 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 invalidation, the full Int range and Real bits, cancellation, output overflow, and result-mapping failures. A further test shows no handler starts once a real invocation stops while a request's arguments are decoded.
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 Go commands. Fulfillment's warehouse commits bookings and outbox entries as one atomic JSON snapshot.
Data.decode now refuses a value with more nodes than max_value_nodes, or nested past the JSON depth, before decoding it, so a shared or deep value built in Java cannot make decoding unbounded. Every value Core sends is within these limits.
Document preparation, codecs and the reason for them, services, errors, batches, and the example projects, and keep the protocol-level host, handler, carrier, and kit sections. The README's Java examples are compiled test sources under readme/, checked by ReadmeTest and run by the live tests.
Update the root and examples READMEs, CONTRIBUTING, LIMITS (Java SDK rows replace the protocol-level-only row), and the changelog. The live CI job now names the Java SDK and runs the four example projects, with duplicate fulfillment and explicit recovery, through the Java command.
The node-budget and decode tests now use a shared value small enough to build in full, so removing either guard fails an assertion rather than exhausting the heap. Test children now outlive the kill checks by minutes, so a command runner that does not kill them fails within ten seconds. Two README sentences are made exact.
Add the Java SDK and projects lane: offline, live, and project test counts on JDK 21 and 25 against jacquard-core-0.3.0, the mutation checks behind each regression, and the unchanged lanes.
JacquardException.batch copied the completed values with List.copyOf, which throws on the nulls Codec.UNIT decodes to, so a failure after a successful List () batch escaped as a NullPointerException.
Each batch is encoded and checked, then discarded, and encoded again just before it runs. Between batches the validation observes the Cancel, the caller's interrupt, and the runtime deadline.
A tuple codec now applies max_collection_items before it encodes any item. A raw Value text is charged like an encoded one: by UTF-8 bytes, refusing a lone surrogate. Document that a codec returning a Value it built itself bypasses the encoder's budget and is measured again by the invoke frame.
Each source must be a regular file (a symbolic link to one is followed) and all sources together at most 16 MiB, checked from their sizes before they are read. The prelude and cache walks read regular files only, at most 256 MiB under one root. Both observe the Cancel and the interrupt between files.
A cache entry now keeps Core's raw check --print-sigs output and the hash output for the source and each prelude file as verified files. Both the cold and the warm path derive names, constructors, signatures, and groups from those outputs, and a load refuses a program.json whose bindings differ. The program schema is now jacquard-java-program-v2.
After a command exits, the host waits up to one second for its output to close: on Linux, until no other process of this user holds either FIFO, found through /proc/<pid>/fd. A holder then fails the command with "its output did not close", as in the Rust SDK, and is killed if it started no earlier than the command. Descendants recorded while the command ran (by pid and start time) are killed when it returns.
On Linux with util-linux setsid, each Core command runs as setsid -- core, which execs without forking, so the command keeps its pid and leads a new process group. The group is killed when the command exits and on every stop: each /proc entry's ProcessHandle is acquired first, then its group id read from /proc/<pid>/stat, and only a match is killed through that handle. The /proc FIFO-holder search and kill are removed: holding the output did not prove a process belonged to the command, and JVM-parented orphans under a subreaper were missed. Elsewhere the recorded-descendant kill remains. A stop during the exit cleanup is reported instead of success.
…dation Rust, Go, Python, and TypeScript bound batch validation by no deadline, and the runtime deadline made the refusal depend on the machine's speed. The Cancel and the interrupt are still observed between batches, and now once more after the last batch is validated, before the first worker starts. Validation returns nothing, so no batch's encoding is kept; the weak- reference test that depended on System.gc() is replaced by exact counts.
A custom codec can return a small shared graph that stands for exponentially many values, and the type check walked it unbounded. Every encoded argument is now measured under one shared node, depth, text, and collection budget before the type check runs, whatever codec built it.
No anchor inside a cache entry resists someone who can rewrite it, so only its owner may: on POSIX, a cache root or entry, or any file or directory in it, that the current user does not own or that its group or others may write is refused on create and load, and new entries are made owner-only. The Core executable is refused over 512 MiB from its size and again while it is read, observing the Cancel and the interrupt between pieces. Tree walks stream each directory with stop checks, refuse more than 100,000 entries (empty files count), and enforce the byte ceiling as they go.
… reads Also test that a descendant that left the group and floods stderr holds neither the caller nor the heap: the FIFOs, not the group kill, cover it.
A signature nesting types deeper than 64 levels, the protocol's value depth, naming more than 4,096 types, or longer than 16,384 tokens is refused before the parser recurses past it. Such a function is recorded as unbound with its reason, so binding it fails with a BINDING exception naming the limit while the program's other functions still bind.
…d cache root A source list over 1,024 is refused from its size before any element is read, and validation observes the Cancel and the interrupt. The failure cleanup is a streamed walk of at most 100,000 entries that ends at a stop, leaving the unpublished staging directory; a later preparation removes staging directories over an hour old within the same bounds. The cache root is resolved to its real directory before it is checked, and the writable-root refusal says how to fix it.
Also tests the signature parser's depth, type-count, and token bounds.
Exit status 126 or 127 with stderr starting "setsid: " means the command never ran. Also states the pid-wraparound window of the group check.
…, and count a record's fields first
…nd keep a file the example store did not create
…nd wrap a throwing codec in Data.encode
… leave the staging path alone once published
…keep a record accessor's
…ning it, cap messages exactly, and refuse an impossible executable name
…reparation failure quoted as an excerpt, and note a message's length once
…lers, and Dynamic values; join a null message safely
…, measure paths as the file system sees them, and distrust an impossible noted length
… flatten, and keep a failed absolute-path lookup a refusal
…olved, by one iterative walk over the case graph
… List and Optional types iteratively within the v0 depth
Merged
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 a Java SDK layer on top of the Java protocol adapter in
java/, matching the Rust, Go, Python and TypeScript SDKs. It addsorg.jacquardlang.host.sdkand acarrier.Commandthat runs Core's commands.Runtime.preparechecks, hashes and installs sources through Core's publiccheck/hash/runcommands. They go into an owner-only, verified cache keyed on the source bytes, the Core executable, every prelude file and the protocol. A changed entry is refused, and bindings are derived again from Core's cached output. Core's--versionand prelude discovery run once per key across instances and threads.call,invokeandmapBatches(every batch validated first, no retries) each have aCancelvariant.Invocationsupports decode, value, render and owner-only save. Every failure is one exception type, and a failed result mapping keeps the raw result.HandlerExceptioncategories map as in Rust, and any other exception becomes completion unknown.ListandOptional, plusDynamic. Messages, names, paths, signature parsing and type nesting are all bounded. Walks over caller-built structures are iterative, so deep input cannot overflow the stack.setsid, and that group is killed when the command ends or is stopped.--inspect,--faultand--deliver-pendingpaths as the Rust command. CI runs them.CI runs the offline suite on Temurin 21 and 25. The live suite runs against the pinned Core with live tests required: 176 tests on each JDK, none skipped.
Java-specific limits are recorded in
docs/LIMITS.mdandjava/README.md: Core command descendants, the cache trust boundary, signature depth, and record component order.docs/EVIDENCE.mdlists a regression test for each hardening fix, each confirmed to fail on the code before it.