From 344fd1fe5a7be40f15f8c670ff6f7484c03e6f86 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Tue, 19 May 2026 13:20:09 +0400 Subject: [PATCH 1/5] feat(sdk): stub net.connect for outbound TCP, pending astrid#745 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `connect(host, port)` to the JS SDK's net module with the exact shape RFC unicity-astrid/rfcs#27 specifies — returns a StreamHandle that flows through the existing recv / tryRecv / send / close surface, same type as the accept path returns. The host fn (`astrid:capsule/net.net-connect-tcp`) hasn't landed yet (tracking unicity-astrid/astrid#745). The body throws SysError.api with the tracking URLs so capsules attempting to use it before the host side ships get a clear, actionable error. Why land the stub now: capsules being built against the final unicity-astrid/wit contract (Unicity / Sphere SDK port, Discord bridges, MQTT, etc.) can import `connect` and write against the final API today. When astrid#745 merges, this commit becomes a one-line swap: replace the `throw SysError.api(...)` with the commented-in `callHost(...)` line above it. No callers change. --- packages/astrid-sdk/src/net.ts | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/astrid-sdk/src/net.ts b/packages/astrid-sdk/src/net.ts index e2460c6..00e1701 100644 --- a/packages/astrid-sdk/src/net.ts +++ b/packages/astrid-sdk/src/net.ts @@ -163,6 +163,46 @@ export function tryAccept(listener: ListenerHandle): StreamHandle | undefined { return id === undefined ? undefined : new StreamHandle(id); } +// --------------------------------------------------------------------------- +// Outbound TCP — STUB pending host fn +// --------------------------------------------------------------------------- + +/** + * Open an outbound TCP connection to `host:port`. + * + * The returned {@link StreamHandle} flows through the same `recv` / + * `tryRecv` / `send` / `close` API as the `accept` path — Astrid's host + * ABI keeps inbound and outbound on one stream-handle type. + * + * **Stubbed today.** The host fn `astrid:capsule/net.net-connect-tcp` + * has not landed yet. See: + * + * - Tracking issue: https://github.com/unicity-astrid/astrid/issues/745 + * - RFC: https://github.com/unicity-astrid/rfcs/pull/27 + * + * Capsules can import this and write against the final API today; the + * call throws a clearly-marked `SysError.api` until the host side lands. + * Once it does, the body becomes a one-line `callHost` matching the + * existing `accept` / `bindUnix` pattern. + * + * @param host Hostname or IP. Must be in the capsule's `net_connect` + * capability allowlist in `Capsule.toml`. + * @param port TCP port (1–65535). Must match the allowlist pattern. + */ +export function connect(host: string, port: number): StreamHandle { + // Real impl, pending astrid#745: + // const id = callHost( + // `net.connect(${JSON.stringify(host)}, ${port})`, + // () => hostConnectTcp(host, port), + // ); + // return new StreamHandle(id); + throw SysError.api( + `net.connect(${JSON.stringify(host)}, ${port}): outbound TCP host fn not yet implemented ` + + `(tracking: https://github.com/unicity-astrid/astrid/issues/745, ` + + `RFC: https://github.com/unicity-astrid/rfcs/pull/27)`, + ); +} + // --------------------------------------------------------------------------- // Sleep shim // --------------------------------------------------------------------------- From 78baaa90cd1063e4ae619d5c2a9e8bb8fb30eafb Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Tue, 19 May 2026 14:24:39 +0400 Subject: [PATCH 2/5] feat(sdk,net): wire connect() to net-connect-tcp host fn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the stub from 5a12d7d with the real impl now that the host side has landed (unicity-astrid/wit#5 + astrid#745 feat/net-connect-tcp). Changes: - packages/astrid-sdk/src/wit-imports.d.ts: declare netConnectTcp in the astrid:capsule/net@0.1.0 ambient module. - packages/astrid-sdk/src/net.ts: connect() now calls hostConnectTcp through the existing callHost wrapper; throws SysError on capability denial, SSRF rejection, DNS failure, or connect timeout. JSDoc lists the three kernel-side checks operators care about (allowlist, SSRF airlock, active-stream cap) so capsule authors don't have to read the host source to understand failure modes. - contracts/: submodule bumped to feat/net-connect-tcp tip (matches the WIT PR head). - packages/astrid-sdk/wit-contracts/astrid-contracts.wit: regenerated by scripts/sync-contracts-wit.sh (no diff vs main beyond canonical updates). Returns the same StreamHandle type as accept() — recv / tryRecv / send / close all Just Work without runtime branching. RFC: unicity-astrid/rfcs#27 WIT: unicity-astrid/wit#5 SDK (rust): unicity-astrid/sdk-rust feat/net-connect-tcp Tracking issue: unicity-astrid/astrid#745 --- contracts | 2 +- packages/astrid-sdk/src/net.ts | 41 ++++++++++++------------ packages/astrid-sdk/src/wit-imports.d.ts | 1 + 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/contracts b/contracts index 65bd8e7..f5039e3 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 65bd8e72539b18d804609f31c35cb96ca42fe71f +Subproject commit f5039e3acfc16bc16891f19050e91d61330ebe57 diff --git a/packages/astrid-sdk/src/net.ts b/packages/astrid-sdk/src/net.ts index 00e1701..2e08865 100644 --- a/packages/astrid-sdk/src/net.ts +++ b/packages/astrid-sdk/src/net.ts @@ -14,6 +14,7 @@ import { netRead as hostRead, netWrite as hostWrite, netCloseStream as hostCloseStream, + netConnectTcp as hostConnectTcp, type NetReadStatus, } from "astrid:capsule/net@0.1.0"; import { clockMs as hostClockMs } from "astrid:capsule/sys@0.1.0"; @@ -174,33 +175,31 @@ export function tryAccept(listener: ListenerHandle): StreamHandle | undefined { * `tryRecv` / `send` / `close` API as the `accept` path — Astrid's host * ABI keeps inbound and outbound on one stream-handle type. * - * **Stubbed today.** The host fn `astrid:capsule/net.net-connect-tcp` - * has not landed yet. See: + * The kernel runs three checks before the TCP syscall: * - * - Tracking issue: https://github.com/unicity-astrid/astrid/issues/745 - * - RFC: https://github.com/unicity-astrid/rfcs/pull/27 + * 1. The capsule's `net_connect` allowlist in `Capsule.toml` must + * contain a pattern matching `"host:port"` (exact) or `"host:*"` + * (any port for the named host). Missing or empty list denies all + * outbound TCP (fail-closed). + * 2. DNS resolution rejects loopback, private, link-local, multicast, + * and unspecified IPs (same airlock as `http.request`). + * 3. Per-capsule active-stream cap (default 8, shared with inbound + * `accept`). * - * Capsules can import this and write against the final API today; the - * call throws a clearly-marked `SysError.api` until the host side lands. - * Once it does, the body becomes a one-line `callHost` matching the - * existing `accept` / `bindUnix` pattern. + * Connect attempts are bounded to ~10s by the host. A stalled DNS or + * TCP handshake surfaces as a `SysError`, not an indefinite hang. * - * @param host Hostname or IP. Must be in the capsule's `net_connect` - * capability allowlist in `Capsule.toml`. - * @param port TCP port (1–65535). Must match the allowlist pattern. + * @param host Hostname or IP, matched against the manifest allowlist. + * @param port TCP port (1–65535). + * + * Tracking issue: https://github.com/unicity-astrid/astrid/issues/745 + * RFC: https://github.com/unicity-astrid/rfcs/pull/27 */ export function connect(host: string, port: number): StreamHandle { - // Real impl, pending astrid#745: - // const id = callHost( - // `net.connect(${JSON.stringify(host)}, ${port})`, - // () => hostConnectTcp(host, port), - // ); - // return new StreamHandle(id); - throw SysError.api( - `net.connect(${JSON.stringify(host)}, ${port}): outbound TCP host fn not yet implemented ` + - `(tracking: https://github.com/unicity-astrid/astrid/issues/745, ` + - `RFC: https://github.com/unicity-astrid/rfcs/pull/27)`, + const id = callHost(`net.connect(${JSON.stringify(host)}, ${port})`, () => + hostConnectTcp(host, port), ); + return new StreamHandle(id); } // --------------------------------------------------------------------------- diff --git a/packages/astrid-sdk/src/wit-imports.d.ts b/packages/astrid-sdk/src/wit-imports.d.ts index 2e09d2e..abd9794 100644 --- a/packages/astrid-sdk/src/wit-imports.d.ts +++ b/packages/astrid-sdk/src/wit-imports.d.ts @@ -135,6 +135,7 @@ declare module "astrid:capsule/net@0.1.0" { export function netRead(streamHandle: bigint): NetReadStatus; export function netWrite(streamHandle: bigint, data: Uint8Array): void; export function netCloseStream(streamHandle: bigint): void; + export function netConnectTcp(host: string, port: number): bigint; } // ---------------------------------------------------------------------- From a50169196d319bd906d28f93dcfa4624b9e2c654 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Tue, 19 May 2026 15:07:59 +0400 Subject: [PATCH 3/5] feat(sdk,net): full std::net::TcpStream surface mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the kernel + sdk-rust expansion in unicity-astrid/sdk-rust feat/net-connect-tcp. Ambient module additions (astrid:capsule/net@0.1.0): - ShutdownHow type ('read' | 'write' | 'both') - 14 new functions: netReadBytes, netWriteBytes, netPeek, netShutdown, netPeerAddr, netLocalAddr, netSetNodelay, netNodelay, netSetReadTimeout, netReadTimeout, netSetWriteTimeout, netWriteTimeout, netSetTtl, netTtl StreamHandle methods: - readBytes(maxBytes), writeBytes(data), peek(maxBytes) - shutdown(how) - peerAddr(), localAddr() - setNodelay(b), nodelay() - setReadTimeout(ms), readTimeout() - setWriteTimeout(ms), writeTimeout() - setTtl(n), ttl() The existing recv / send / close framed surface is unchanged — it's the right shape for the inbound CLI proxy. Outbound TCP capsules use the byte-stream pair. RFC: unicity-astrid/rfcs#27 WIT: unicity-astrid/wit#5 SDK (rust): unicity-astrid/sdk-rust feat/net-connect-tcp Tracking issue: unicity-astrid/astrid#745 --- packages/astrid-sdk/src/net.ts | 124 +++++++++++++++++++++++ packages/astrid-sdk/src/wit-imports.d.ts | 16 +++ 2 files changed, 140 insertions(+) diff --git a/packages/astrid-sdk/src/net.ts b/packages/astrid-sdk/src/net.ts index 2e08865..badfc21 100644 --- a/packages/astrid-sdk/src/net.ts +++ b/packages/astrid-sdk/src/net.ts @@ -15,7 +15,22 @@ import { netWrite as hostWrite, netCloseStream as hostCloseStream, netConnectTcp as hostConnectTcp, + netReadBytes as hostReadBytes, + netWriteBytes as hostWriteBytes, + netPeek as hostPeek, + netShutdown as hostShutdown, + netPeerAddr as hostPeerAddr, + netLocalAddr as hostLocalAddr, + netSetNodelay as hostSetNodelay, + netNodelay as hostNodelay, + netSetReadTimeout as hostSetReadTimeout, + netReadTimeout as hostReadTimeout, + netSetWriteTimeout as hostSetWriteTimeout, + netWriteTimeout as hostWriteTimeout, + netSetTtl as hostSetTtl, + netTtl as hostTtl, type NetReadStatus, + type ShutdownHow, } from "astrid:capsule/net@0.1.0"; import { clockMs as hostClockMs } from "astrid:capsule/sys@0.1.0"; import { SysError, callHost } from "./errors.js"; @@ -111,6 +126,115 @@ export class StreamHandle { } } + // ------------------------------------------------------------------------- + // Byte-stream surface (mirrors std::net::TcpStream / std::io::Read+Write) + // ------------------------------------------------------------------------- + + /** + * Read up to `maxBytes` without length-prefix framing. Mirrors + * `std::net::TcpStream::read`. Returns an empty Uint8Array on EOF. + * Honours any timeout set via {@link setReadTimeout}. + */ + readBytes(maxBytes: number): Uint8Array { + this.#requireOpen(); + return callHost(`net.readBytes(${this.id}, ${maxBytes})`, () => + hostReadBytes(this.id, maxBytes), + ); + } + + /** + * Write `data` without framing. Returns bytes written (may be less than + * `data.length` when the kernel's socket buffer is full). Honours any + * timeout set via {@link setWriteTimeout}. + */ + writeBytes(data: Uint8Array): number { + this.#requireOpen(); + return callHost(`net.writeBytes(${this.id})`, () => hostWriteBytes(this.id, data)); + } + + /** + * Peek up to `maxBytes` without consuming them — the next + * {@link readBytes} returns the same data again. + */ + peek(maxBytes: number): Uint8Array { + this.#requireOpen(); + return callHost(`net.peek(${this.id}, ${maxBytes})`, () => hostPeek(this.id, maxBytes)); + } + + /** Half-close the read side, write side, or both. */ + shutdown(how: ShutdownHow): void { + this.#requireOpen(); + callHost(`net.shutdown(${this.id}, ${how})`, () => hostShutdown(this.id, how)); + } + + /** Remote peer address as `"ip:port"`. */ + peerAddr(): string { + this.#requireOpen(); + return callHost(`net.peerAddr(${this.id})`, () => hostPeerAddr(this.id)); + } + + /** Local socket address as `"ip:port"`. */ + localAddr(): string { + this.#requireOpen(); + return callHost(`net.localAddr(${this.id})`, () => hostLocalAddr(this.id)); + } + + /** Toggle `TCP_NODELAY` (Nagle's algorithm off when `true`). */ + setNodelay(nodelay: boolean): void { + this.#requireOpen(); + callHost(`net.setNodelay(${this.id}, ${nodelay})`, () => hostSetNodelay(this.id, nodelay)); + } + + /** Current `TCP_NODELAY` setting. */ + nodelay(): boolean { + this.#requireOpen(); + return callHost(`net.nodelay(${this.id})`, () => hostNodelay(this.id)); + } + + /** Set the read timeout (milliseconds). `undefined` clears it. */ + setReadTimeout(timeoutMs: number | undefined): void { + this.#requireOpen(); + const ms = timeoutMs === undefined ? undefined : BigInt(timeoutMs); + callHost(`net.setReadTimeout(${this.id}, ${timeoutMs})`, () => + hostSetReadTimeout(this.id, ms), + ); + } + + /** Current read timeout in milliseconds, or `undefined` if unset. */ + readTimeout(): number | undefined { + this.#requireOpen(); + const v = callHost(`net.readTimeout(${this.id})`, () => hostReadTimeout(this.id)); + return v === undefined ? undefined : Number(v); + } + + /** Set the write timeout (milliseconds). `undefined` clears it. */ + setWriteTimeout(timeoutMs: number | undefined): void { + this.#requireOpen(); + const ms = timeoutMs === undefined ? undefined : BigInt(timeoutMs); + callHost(`net.setWriteTimeout(${this.id}, ${timeoutMs})`, () => + hostSetWriteTimeout(this.id, ms), + ); + } + + /** Current write timeout in milliseconds, or `undefined` if unset. */ + writeTimeout(): number | undefined { + this.#requireOpen(); + const v = callHost(`net.writeTimeout(${this.id})`, () => hostWriteTimeout(this.id)); + return v === undefined ? undefined : Number(v); + } + + /** Set the IP `TTL` on outgoing packets. */ + setTtl(ttl: number): void { + this.#requireOpen(); + callHost(`net.setTtl(${this.id}, ${ttl})`, () => hostSetTtl(this.id, ttl)); + } + + /** Current IP `TTL`. */ + ttl(): number { + this.#requireOpen(); + return callHost(`net.ttl(${this.id})`, () => hostTtl(this.id)); + } + close(): void { if (this.#closed) return; this.#closed = true; diff --git a/packages/astrid-sdk/src/wit-imports.d.ts b/packages/astrid-sdk/src/wit-imports.d.ts index abd9794..783f5b4 100644 --- a/packages/astrid-sdk/src/wit-imports.d.ts +++ b/packages/astrid-sdk/src/wit-imports.d.ts @@ -129,6 +129,8 @@ declare module "astrid:capsule/net@0.1.0" { | { tag: "closed" } | { tag: "pending" }; + export type ShutdownHow = "read" | "write" | "both"; + export function netBindUnix(listenerHandle: bigint): bigint; export function netAccept(listenerHandle: bigint): bigint; export function netPollAccept(listenerHandle: bigint): bigint | undefined; @@ -136,6 +138,20 @@ declare module "astrid:capsule/net@0.1.0" { export function netWrite(streamHandle: bigint, data: Uint8Array): void; export function netCloseStream(streamHandle: bigint): void; export function netConnectTcp(host: string, port: number): bigint; + export function netReadBytes(streamHandle: bigint, maxBytes: number): Uint8Array; + export function netWriteBytes(streamHandle: bigint, data: Uint8Array): number; + export function netPeek(streamHandle: bigint, maxBytes: number): Uint8Array; + export function netShutdown(streamHandle: bigint, how: ShutdownHow): void; + export function netPeerAddr(streamHandle: bigint): string; + export function netLocalAddr(streamHandle: bigint): string; + export function netSetNodelay(streamHandle: bigint, nodelay: boolean): void; + export function netNodelay(streamHandle: bigint): boolean; + export function netSetReadTimeout(streamHandle: bigint, timeoutMs: bigint | undefined): void; + export function netReadTimeout(streamHandle: bigint): bigint | undefined; + export function netSetWriteTimeout(streamHandle: bigint, timeoutMs: bigint | undefined): void; + export function netWriteTimeout(streamHandle: bigint): bigint | undefined; + export function netSetTtl(streamHandle: bigint, ttl: number): void; + export function netTtl(streamHandle: bigint): number; } // ---------------------------------------------------------------------- From 53a1a7a0371f7d4be452ea5bd56937bee157b2ea Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Tue, 19 May 2026 16:03:59 +0400 Subject: [PATCH 4/5] fix(sdk,net): apply Gemini review feedback (parallel to sdk-rust#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - toHostTimeout helper validates + converts. Rejects 0, NaN, negative, non-finite — matches std::net::TcpStream::set_{read,write}_timeout semantics where Duration::ZERO is an error. - setReadTimeout / setWriteTimeout both go through the helper. - readBytes / peek docstrings made the EOF-vs-would-block contract explicit (empty Uint8Array = EOF; would-block surfaces as a SysError caller can pattern-match). IPv6 bracket fix from the sdk-rust review doesn't apply here — JS connect(host, port) takes them as separate args. --- packages/astrid-sdk/src/net.ts | 54 +++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/packages/astrid-sdk/src/net.ts b/packages/astrid-sdk/src/net.ts index badfc21..6c21226 100644 --- a/packages/astrid-sdk/src/net.ts +++ b/packages/astrid-sdk/src/net.ts @@ -59,6 +59,26 @@ export class SendError extends Error { } } +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +/** + * Validate + convert a timeout to the host's `bigint | undefined`. + * Mirrors Rust SDK's `to_host_timeout`. Rejects `0` (would be + * ambiguous with "no timeout") matching + * `std::net::TcpStream::set_read_timeout`'s `Duration::ZERO` rule. + */ +function toHostTimeout(timeoutMs: number | undefined): bigint | undefined { + if (timeoutMs === undefined) return undefined; + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { + throw SysError.api( + `timeout must be a positive integer (got ${timeoutMs}); use undefined to clear`, + ); + } + return BigInt(Math.floor(timeoutMs)); +} + // --------------------------------------------------------------------------- // Handles // --------------------------------------------------------------------------- @@ -132,8 +152,15 @@ export class StreamHandle { /** * Read up to `maxBytes` without length-prefix framing. Mirrors - * `std::net::TcpStream::read`. Returns an empty Uint8Array on EOF. - * Honours any timeout set via {@link setReadTimeout}. + * `std::net::TcpStream::read`. + * + * Contract: + * - **Empty Uint8Array = EOF** (peer disconnected). Unambiguous. + * - Non-empty = data read (may be shorter than `maxBytes`). + * - Throws `SysError` with message containing `"would block"` if a + * read timeout was set via {@link setReadTimeout} and expired + * with no data. With no timeout set, blocks until data, EOF, or + * capsule unload. */ readBytes(maxBytes: number): Uint8Array { this.#requireOpen(); @@ -145,7 +172,8 @@ export class StreamHandle { /** * Write `data` without framing. Returns bytes written (may be less than * `data.length` when the kernel's socket buffer is full). Honours any - * timeout set via {@link setWriteTimeout}. + * timeout set via {@link setWriteTimeout}; with no timeout set, blocks + * until the write completes or the peer disconnects. */ writeBytes(data: Uint8Array): number { this.#requireOpen(); @@ -154,7 +182,8 @@ export class StreamHandle { /** * Peek up to `maxBytes` without consuming them — the next - * {@link readBytes} returns the same data again. + * {@link readBytes} returns the same data again. Same EOF / + * would-block semantics as {@link readBytes}. */ peek(maxBytes: number): Uint8Array { this.#requireOpen(); @@ -191,10 +220,15 @@ export class StreamHandle { return callHost(`net.nodelay(${this.id})`, () => hostNodelay(this.id)); } - /** Set the read timeout (milliseconds). `undefined` clears it. */ + /** + * Set the read timeout (milliseconds). `undefined` clears the + * timeout (reads block indefinitely). `0` is rejected — matches + * `std::net::TcpStream::set_read_timeout` which errors on + * `Duration::ZERO`. + */ setReadTimeout(timeoutMs: number | undefined): void { this.#requireOpen(); - const ms = timeoutMs === undefined ? undefined : BigInt(timeoutMs); + const ms = toHostTimeout(timeoutMs); callHost(`net.setReadTimeout(${this.id}, ${timeoutMs})`, () => hostSetReadTimeout(this.id, ms), ); @@ -207,10 +241,14 @@ export class StreamHandle { return v === undefined ? undefined : Number(v); } - /** Set the write timeout (milliseconds). `undefined` clears it. */ + /** + * Set the write timeout (milliseconds). `undefined` clears it; + * `0` is rejected (matches + * `std::net::TcpStream::set_write_timeout`). + */ setWriteTimeout(timeoutMs: number | undefined): void { this.#requireOpen(); - const ms = timeoutMs === undefined ? undefined : BigInt(timeoutMs); + const ms = toHostTimeout(timeoutMs); callHost(`net.setWriteTimeout(${this.id}, ${timeoutMs})`, () => hostSetWriteTimeout(this.id, ms), ); From 4eaff531a0caa9d4ee1f4df8f82631a18ec923db Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Tue, 19 May 2026 16:46:05 +0400 Subject: [PATCH 5/5] chore(contracts): bump submodule to wit main after #5 merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was pinned at f5039e3 — the FIRST commit on the wit PR branch, which added only the single net-connect-tcp fn. The expanded surface (14 more fns + shutdown-how enum) landed in the subsequent commit 215ba1c that this submodule reference had not been updated to. JS-side bindings in wit-imports.d.ts were already declared for the full surface, so this was a documentation lag rather than a runtime mismatch, but the submodule should reference the canonical main SHA (now 9244c74 post squash-merge). --- contracts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts b/contracts index f5039e3..9244c74 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit f5039e3acfc16bc16891f19050e91d61330ebe57 +Subproject commit 9244c74c4160a30a8029d06dbe0a857f4e3b4a2a