From c05b791b9d2f77f5165d67c6f5c17941114eb1e3 Mon Sep 17 00:00:00 2001 From: JohnSColeman Date: Mon, 13 Jul 2026 00:58:48 +0700 Subject: [PATCH 01/11] feat(kotlin): SDK project configuration (editorconfig, gitignore, template manifest) --- sdks/kotlin/.editorconfig | 29 +++++++++++++++++++++++++++++ sdks/kotlin/.gitignore | 17 +++++++++++++++++ sdks/kotlin/kotlin-template.yaml | 20 ++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 sdks/kotlin/.editorconfig create mode 100644 sdks/kotlin/.gitignore create mode 100644 sdks/kotlin/kotlin-template.yaml diff --git a/sdks/kotlin/.editorconfig b/sdks/kotlin/.editorconfig new file mode 100644 index 0000000000..d5abb7169d --- /dev/null +++ b/sdks/kotlin/.editorconfig @@ -0,0 +1,29 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{kt,kts}] +# ktlint ruleset: ALL standard style rules enabled EXCEPT identifier naming. +# Every enabled rule is behavior-preserving (no change to compiled output). +# Only naming rules are disabled: renaming classes/functions/properties/ +# packages/files changes identifiers baked into the bytecode and public ABI +# (e.g. top-level declarations in Foo.kt compile to class FooKt, so a file +# rename changes the ABI) and can desync generated code. +ktlint_code_style = intellij_idea + +# Line-width enforcement is off: ktlint cannot auto-break string literals, so a +# long string would become a forced manual edit. We do not want ktlint dictating +# line length; developers keep their own line breaks. +max_line_length = off +ktlint_standard_max-line-length = disabled + +ktlint_standard_class-naming = disabled +ktlint_standard_function-naming = disabled +ktlint_standard_property-naming = disabled +ktlint_standard_backing-property-naming = disabled +ktlint_standard_enum-entry-name-case = disabled +ktlint_standard_package-name = disabled +ktlint_standard_filename = disabled diff --git a/sdks/kotlin/.gitignore b/sdks/kotlin/.gitignore new file mode 100644 index 0000000000..99aeb349da --- /dev/null +++ b/sdks/kotlin/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts and regenerable outputs for the Kotlin SDK toolchain. +# The embedded guest runtime is committed at +# gradle-plugin/src/main/resources/golem/wasm/agent_guest.wasm — NOT under .generated. + +# Regenerable via scripts/generate-agent-guest-wasm.sh +.generated/ + +# Gradle / Kotlin-JS build outputs +build/ +.gradle/ +.kotlin/ +kotlin-js-store/ +node_modules/ + +# Golem build/run scratch +golem-temp/ +.golem/ diff --git a/sdks/kotlin/kotlin-template.yaml b/sdks/kotlin/kotlin-template.yaml new file mode 100644 index 0000000000..10f5406ec8 --- /dev/null +++ b/sdks/kotlin/kotlin-template.yaml @@ -0,0 +1,20 @@ +# Component build template for native Kotlin/Wasm agents (mirrors rust-template.yaml's single- +# command-per-preset shape; unlike the superseded Kotlin/JS path, there is no separate inject/ +# preinit step -- `gradle nativeComponent` alone produces a validated, deployable component). +# +# `golem build` runs `gradle nativeComponent` (the cloud.golem.wasm-component Gradle plugin +# task): Kotlin/Wasm compile -> wasm-tools component embed -> wasm-tools component new --adapt +# (WASI p1->p2) -> wasm-tools validate. No JS, no QuickJS, no bundling. +manifestVersion: 1.6.0 +componentTemplates: + kotlin: + build: + - command: gradle nativeComponent + sources: + - src + - build.gradle.kts + - settings.gradle.kts + targets: + - build/golem/counter-agent.wasm + componentWasm: build/golem/counter-agent.wasm + outputWasm: "{{ golemTempDir }}/agents/{{ component_name | to_snake_case }}.wasm" From de5269a8213b52fb25ee54f2bdbe5f786f1d20d1 Mon Sep 17 00:00:00 2001 From: JohnSColeman Date: Mon, 13 Jul 2026 00:58:48 +0700 Subject: [PATCH 02/11] feat(kotlin): example counter application --- sdks/kotlin/example/.gitignore | 5 + sdks/kotlin/example/build.gradle.kts | 47 ++++ .../example/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + sdks/kotlin/example/gradlew | 248 ++++++++++++++++++ sdks/kotlin/example/gradlew.bat | 93 +++++++ sdks/kotlin/example/settings.gradle.kts | 13 + .../kotlin/counter/CounterAgent.kt | 34 +++ 8 files changed, 447 insertions(+) create mode 100644 sdks/kotlin/example/.gitignore create mode 100644 sdks/kotlin/example/build.gradle.kts create mode 100644 sdks/kotlin/example/gradle/wrapper/gradle-wrapper.jar create mode 100644 sdks/kotlin/example/gradle/wrapper/gradle-wrapper.properties create mode 100755 sdks/kotlin/example/gradlew create mode 100644 sdks/kotlin/example/gradlew.bat create mode 100644 sdks/kotlin/example/settings.gradle.kts create mode 100644 sdks/kotlin/example/src/wasmWasiMain/kotlin/counter/CounterAgent.kt diff --git a/sdks/kotlin/example/.gitignore b/sdks/kotlin/example/.gitignore new file mode 100644 index 0000000000..2865adaa58 --- /dev/null +++ b/sdks/kotlin/example/.gitignore @@ -0,0 +1,5 @@ +/build +/.gradle +/.kotlin +/golem-temp +/.golem diff --git a/sdks/kotlin/example/build.gradle.kts b/sdks/kotlin/example/build.gradle.kts new file mode 100644 index 0000000000..8d5ee5bf2f --- /dev/null +++ b/sdks/kotlin/example/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + kotlin("multiplatform") version "2.4.0" + id("com.google.devtools.ksp") version "2.3.9" + id("cloud.golem.wasm-component") version "0.0.0-SNAPSHOT" + id("org.jlleitschuh.gradle.ktlint") version "14.2.0" +} + +group = "cloud.golem.example" +version = "0.1.0-SNAPSHOT" + +repositories { + mavenLocal() + mavenCentral() +} + +kotlin { + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmWasi { + binaries.executable() + nodejs() + } + + sourceSets { + val wasmWasiMain by getting { + dependencies { + // The SDK is a NORMAL Kotlin/Wasm dependency, compiled directly into this + // agent's own wasm module (no JS bundling, no QuickJS). + implementation("cloud.golem:golem-kotlin-sdk:0.0.0-SNAPSHOT") + } + } + } +} + +dependencies { + // Compile-time-only annotation processor: generates Registration.kt + + // GolemGeneratedGuest.kt (the real @WasmExport golem:agent/guest@2.0.0 functions). + add("kspWasmWasi", "cloud.golem:golem-kotlin-ksp:0.0.0-SNAPSHOT") +} + +wasmComponent { + moduleName.set("counter-agent") + witNativeDir.set(file("../wit-native")) +} + +tasks.withType().configureEach { + exclude { it.file.path.contains("/build/generated/") } +} diff --git a/sdks/kotlin/example/gradle/wrapper/gradle-wrapper.jar b/sdks/kotlin/example/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 zCZTgG2Pr3n8|ec9kIu&uC|d?k4-cQ4#}Z`qDX5Y2mhC(jR1Ms;UG4Ho$DE|+SeJ@{ zJQQhAXj|<)*t3KiOWTuh{Wd^mS{u{&ERV)OpZwiQ%#1->r9p zSK_^*U~=?ywH~4IUxb}{0J!SmL!z2Tzq_PpetoC^_az1JFg0=gMcQADuOP%3=H1hH zH_=dG(PD;d*037Ov5G1924U#Zns?~fs+eh1%-bWqa%ssm3=nio1r3J<4G0IBETtr? zycs~0JIOn;MecYG=~OQsYHIrf?~A5>_ob%8+uOrVA+VCJw}{lygrBBdY1k<8B^wf6 zl|<%N$7)fOZX$%y>4ueco_Gb1H@B%XrKVwrn6hUOecnc^PU0rFuCB5=*2;|u-`o(@ zL*tr4bnQzXYLc4XqFbv5sK0}A)`}`8iM8ehtj#Oc5DrE;0VxbPmL@BUa_BQwa$EW~sU#-LP0?sGmqfUGhGWcciGZ*4(}u3z=@b>Ow9DQe7lcO3K}BG3j(t& zH10>sK!&4Q5-=gN@Nxj6{|*nuyqw7KZJ1?p)NUJ?U0bOigGdsOk}Iz&9PmN_5=W*Z9M zy^pA`&dX0oo6?CSuhE~(pYbLuTPp1a1Fa@e3Lu&mmgd$;D}&g-i=D-{sv?J9kIr9r zrX&Z)aFGK^kNY{LxrotP0}k*;uN12i_2a_JJhKwh zBt{D-JRxC$8U+-`u1xD>gJ^H4lbW;7spI-=H506i=ncdK;xq*L6f7jVz$XGMg5aQk zHRJY&$@g}i_SP##iC?lR?ltnWUTT-UDlq(*BTQaYNkg zNG#sNoo{WmP+Vl}U~?+T?g25b$E-7iwhu=VVgw3JdFXm~ba+LC4p>CP3~rNTiNBl7 zL{RfLLepNPEtZj}yL_#R{(^MqIlG)c0Va}>U|9Pl&B_3tV;Ps{r)WqBznD7FcTlP4 z`JQe2DvGhmeeHGGX39zGyOOxZ3tq~Dft(BQ;mDXwwJi?sBtxo$Gf1SS2w*eQ0p&RVMNVi@d zY8v4J0(n}%6*Rw(g~l@sUuxpiJ*Y}7TzBQyU+>-qWm*InUeGt@)T9g^0J#z4){Lw* zT;69if~U9DXBR9fgVPlYy7aDhJU)gDC?_GHQtwa6QXNaah7-CzA|Fx-lH7d@N9>38 zX(F&fd3w7AkZ+ha8-gKfX%@_~<#HDs?kBg5zW>V3%Xw5jwPs6uni{7r zd`EfPYrA*SU;xDtm@E>5TrJKlg5o=h;NSXk)pt4K)GbpP0xkUg>2o|oG=`UnX7^Un zb&@8d6Fj1cBWW^c(K#Csc8xEBa4KfHY>8Lp^77-lhzgWr9kR9_p+g|-9r?VSv?qA%^1O;cqgke)%AqHlR$B{!Y1Mq zj|)Ecg?{_!>kGDAwGa7%cwSUb{BcayJihkv$}ql+yu=O}jVvAFdC{Hjh$4}u+$mx% z5V$sUiGCX%D3A>bKwY8HR)Gv*lisI4q^3vJ*nDwj|mtr!0r!~+Qoe2cw^jPCXkT7tI*01|w@ z&gPC`?O1w7hQ%=&bcHi7(fqhY3${~JepA7y@^aLwHpew^Yk$;R4v{ASHjXjXtaTc_ zuz5*nXB&PrcyWx#gQ%?HyxawmS+Wu(7ssvB1UMh!1$to&o(mv_f=9~!9@VsJCGxpu z`>g5Sp=xDhpsiCy^y>=fI0DON$&pb7o7^d{@@&hj3!6PUd=vA;G;#7&8ChamsE{`^ zY8pDra8Jntp62Ivi)Y`*XbpM60s06v@Rz^-g)TW_F@B!~y7!4AJ>37mAuz!(!C+xQ zSR61?u!{N|qHWOeR%$RXRL~vpN0SGri7-klNHEJuivbi=0qSbdV4&ghf4i|7?$>z( zI{qH?i}`~a7GyB6|8pZRq982+P*r1+m-t&(%U5#ZWFQd-(CXKLHeN@y(c z;wqq1hzE@q1b$GG0VQ_)`{MeylBlVfy%UHR=;Z98>T3M&;{0i?+0T-Bck?I)AUQrz zeF**_iGu$JlCpLnFv`D9?q6R51jKPM{Rd6!0FF#KP=O|b3iQX*TqXSjO?gXaXAmLr zU#g&%@+XpjVArlGkfaPKk^PUSnMLsjlK<9nH*zxl^V2-jGC$4+HGE%?F3%4|y9>HN z|FJgz*HW$VwU8$RNtuBf(2vdZhW3x;R6%eoJM(|2zvKebxCh$s5J-*fhZ75B_yeUs zFTrToFiB^SNH?gV2>l?G&h!UD>UP%uKh1L;Er59!q&NoZRe$VEf?5Ar^&iUad&2gQ z&WE`E%lTg=_3XQT@gJOjkAi-Hbbqrl{(pA<>_GH4O8+xI^=IAhS#v+$vmgOK=>C!~_xFg-pLM>6kUfy=zL|u~KkNJ< z$L?p*?;%(Ze6w%%M(zjE|4dH&5$)_}mG3z{KUQ6s!Y@_+kInPH;kAC&{T^5HKmqz@ z@+!aA{YNIy&r;uKTz=r6e6v>d-%9<%_4R!+-iN^8H#0N(rQbiu-u&}-|2`q@k1agM zdHkW_1&%VDD_|I;NpK*OZfAjAb z`Ttl8km0{|{F`kWKWltH$^Ech;G2y`{7&N^%H;d0$cGv7Z^oJNOSiwAFaP<=em}wX z<8AA6<}bbeZc_7S=ii6PALi)3nOXL)o&Uj%-OnQ52M&L%(%ZaWiu^(R{b!Bu2WJl< h$Zw`p^gE5e2}ml*LW4$nU|{5+pXG<~Ugg7I{||-5t(pJ; literal 0 HcmV?d00001 diff --git a/sdks/kotlin/example/gradle/wrapper/gradle-wrapper.properties b/sdks/kotlin/example/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..94113f200e --- /dev/null +++ b/sdks/kotlin/example/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/sdks/kotlin/example/gradlew b/sdks/kotlin/example/gradlew new file mode 100755 index 0000000000..739907dfd1 --- /dev/null +++ b/sdks/kotlin/example/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/sdks/kotlin/example/gradlew.bat b/sdks/kotlin/example/gradlew.bat new file mode 100644 index 0000000000..c4bdd3ab8e --- /dev/null +++ b/sdks/kotlin/example/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sdks/kotlin/example/settings.gradle.kts b/sdks/kotlin/example/settings.gradle.kts new file mode 100644 index 0000000000..e81a2d24c2 --- /dev/null +++ b/sdks/kotlin/example/settings.gradle.kts @@ -0,0 +1,13 @@ +rootProject.name = "counter-agent" + +// SDK, KSP processor, and the wasm-component Gradle plugin are resolved as published +// artifacts from mavenLocal (cloud.golem:* at 0.0.0-SNAPSHOT). Publish them first: +// (cd ../sdk && ./gradlew publishToMavenLocal) +// (cd ../ksp && ./gradlew publishToMavenLocal) +// (cd ../gradle-plugin && ./gradlew publishToMavenLocal) +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} diff --git a/sdks/kotlin/example/src/wasmWasiMain/kotlin/counter/CounterAgent.kt b/sdks/kotlin/example/src/wasmWasiMain/kotlin/counter/CounterAgent.kt new file mode 100644 index 0000000000..7b19f99181 --- /dev/null +++ b/sdks/kotlin/example/src/wasmWasiMain/kotlin/counter/CounterAgent.kt @@ -0,0 +1,34 @@ +package counter + +import cloud.golem.BaseAgent +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Description +import cloud.golem.annotations.Endpoint +import cloud.golem.annotations.Prompt + +/** + * CounterAgent: a durable counter scoped to an agent instance. + * + * Mounted at /counters/{name}. Each unique {name} gets its own counter with independent + * persistent state managed by Golem. Compiled natively to Wasm (WasmGC) -- no JS, no QuickJS. + * The KSP processor reads these annotations at compile time and generates the registration + + * the real `@WasmExport("golem:agent/guest@2.0.0#...")` functions. + */ +@Agent(mount = "/counters/{name}", description = "A durable counter agent") +class CounterAgent(val name: String) : BaseAgent() { + + private var value: Int = 0 + + @Prompt("Increase the count by one") + @Description("Increments the counter and returns the new value") + @Endpoint(post = "/increment") + fun increment(): Int { + value++ + return value + } + + @Prompt("Get the current counter value") + @Description("Returns the current value without modifying it") + @Endpoint(get = "/value") + fun getValue(): Int = value +} From f3d550577c24569e69df37aba5ba74cfc8c1feae Mon Sep 17 00:00:00 2001 From: JohnSColeman Date: Mon, 13 Jul 2026 00:58:49 +0700 Subject: [PATCH 03/11] feat(kotlin): core SDK module (runtime, host bindings, codecs) --- sdks/kotlin/sdk/.gitignore | 3 + sdks/kotlin/sdk/build.gradle.kts | 38 + .../sdk/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + sdks/kotlin/sdk/gradlew | 248 ++ sdks/kotlin/sdk/gradlew.bat | 93 + sdks/kotlin/sdk/settings.gradle.kts | 1 + .../kotlin/cloud/golem/BaseAgent.kt | 32 + .../commonMain/kotlin/cloud/golem/Datetime.kt | 7 + .../kotlin/cloud/golem/Principal.kt | 37 + .../kotlin/cloud/golem/Snapshotted.kt | 15 + .../src/commonMain/kotlin/cloud/golem/Uuid.kt | 7 + .../kotlin/cloud/golem/annotations/Agent.kt | 24 + .../cloud/golem/annotations/Description.kt | 5 + .../cloud/golem/annotations/Endpoint.kt | 15 + .../kotlin/cloud/golem/annotations/Prompt.kt | 5 + .../cloud/golem/annotations/ReadOnly.kt | 14 + .../cloud/golem/annotations/RemoteAgent.kt | 12 + .../kotlin/cloud/golem/annotations/Tool.kt | 22 + .../kotlin/cloud/golem/HostActuals.kt | 22 + .../cloud/golem/runtime/AgentTypeModel.kt | 683 ++++++ .../kotlin/cloud/golem/runtime/Checkpoint.kt | 63 + .../kotlin/cloud/golem/runtime/Guards.kt | 123 + .../kotlin/cloud/golem/runtime/Guest.kt | 225 ++ .../kotlin/cloud/golem/runtime/HostApi.kt | 560 +++++ .../golem/runtime/NativeAgentDescriptor.kt | 59 + .../cloud/golem/runtime/NativeAgentRuntime.kt | 42 + .../cloud/golem/runtime/NativeToolRuntime.kt | 26 + .../cloud/golem/runtime/PrincipalBytes.kt | 112 + .../cloud/golem/runtime/PrincipalDecode.kt | 52 + .../kotlin/cloud/golem/runtime/Rpc.kt | 231 ++ .../kotlin/cloud/golem/runtime/SchemaValue.kt | 104 + .../cloud/golem/runtime/SchemaValueBytes.kt | 254 +++ .../cloud/golem/runtime/SnapshotCodec.kt | 12 + .../cloud/golem/runtime/SnapshotEnvelope.kt | 36 + .../cloud/golem/runtime/SnapshotGuest.kt | 139 ++ .../kotlin/cloud/golem/runtime/ToolGuest.kt | 215 ++ .../kotlin/cloud/golem/runtime/ToolHost.kt | 363 +++ .../kotlin/cloud/golem/runtime/ToolModel.kt | 234 ++ .../cloud/golem/runtime/Transactions.kt | 150 ++ .../cloud/golem/runtime/TypedSchemaValue.kt | 230 ++ .../cloud/golem/runtime/config/Config.kt | 16 + .../cloud/golem/runtime/host/ContextApi.kt | 288 +++ .../cloud/golem/runtime/host/DurabilityApi.kt | 215 ++ .../cloud/golem/runtime/host/OplogApi.kt | 726 ++++++ .../cloud/golem/runtime/host/QuotaApi.kt | 174 ++ .../kotlin/cloud/golem/runtime/host/Rdbms.kt | 1996 +++++++++++++++++ .../cloud/golem/runtime/host/RetryApi.kt | 495 ++++ .../cloud/golem/runtime/host/RetryDsl.kt | 397 ++++ .../cloud/golem/runtime/host/SecretApi.kt | 113 + .../cloud/golem/runtime/wasi/Blobstore.kt | 405 ++++ .../kotlin/cloud/golem/runtime/wasi/Config.kt | 84 + .../cloud/golem/runtime/wasi/Environment.kt | 61 + .../cloud/golem/runtime/wasi/KeyValue.kt | 243 ++ .../cloud/golem/runtime/wasi/Logging.kt | 39 + .../kotlin/cloud/golem/wasm/Lift.kt | 328 +++ .../kotlin/cloud/golem/wasm/Lower.kt | 288 +++ .../kotlin/cloud/golem/wasm/Memory.kt | 60 + .../kotlin/cloud/golem/wasm/WitTypeGrammar.kt | 74 + .../runtime/PrincipalBytesRoundTripTest.kt | 24 + .../golem/runtime/PrincipalDecodeTest.kt | 79 + .../golem/runtime/ReadOnlyLoweringTest.kt | 63 + .../golem/runtime/SchemaGraphBuilderTest.kt | 74 + .../golem/runtime/SchemaTypeDecodeTest.kt | 126 ++ .../runtime/SchemaValueBytesRoundTripTest.kt | 42 + .../runtime/SnapshotEnvelopeRoundTripTest.kt | 23 + .../runtime/ToolInvokeResultDecodeTest.kt | 75 + .../runtime/TypedSchemaValueRoundTripTest.kt | 112 + .../cloud/golem/runtime/host/RetryDslTest.kt | 76 + .../runtime/host/SecretErrorDecodeTest.kt | 48 + .../cloud/golem/wasm/ValueRoundTripTest.kt | 112 + 71 files changed, 11376 insertions(+) create mode 100644 sdks/kotlin/sdk/.gitignore create mode 100644 sdks/kotlin/sdk/build.gradle.kts create mode 100644 sdks/kotlin/sdk/gradle/wrapper/gradle-wrapper.jar create mode 100644 sdks/kotlin/sdk/gradle/wrapper/gradle-wrapper.properties create mode 100755 sdks/kotlin/sdk/gradlew create mode 100644 sdks/kotlin/sdk/gradlew.bat create mode 100644 sdks/kotlin/sdk/settings.gradle.kts create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/BaseAgent.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Datetime.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Principal.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Snapshotted.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Uuid.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Agent.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Description.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Endpoint.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Prompt.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/ReadOnly.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/RemoteAgent.kt create mode 100644 sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Tool.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/HostActuals.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/AgentTypeModel.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Checkpoint.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Guards.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Guest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/HostApi.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeAgentDescriptor.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeAgentRuntime.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeToolRuntime.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/PrincipalBytes.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/PrincipalDecode.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Rpc.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SchemaValue.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SchemaValueBytes.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotCodec.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotEnvelope.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotGuest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolGuest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolHost.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolModel.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Transactions.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/TypedSchemaValue.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/config/Config.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/ContextApi.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/DurabilityApi.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/OplogApi.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/QuotaApi.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/Rdbms.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/RetryApi.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/RetryDsl.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/SecretApi.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Blobstore.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Config.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Environment.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/KeyValue.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Logging.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Lift.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Lower.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Memory.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/WitTypeGrammar.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/PrincipalBytesRoundTripTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/PrincipalDecodeTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/ReadOnlyLoweringTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaGraphBuilderTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaTypeDecodeTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaValueBytesRoundTripTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SnapshotEnvelopeRoundTripTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/ToolInvokeResultDecodeTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/TypedSchemaValueRoundTripTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/host/RetryDslTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/host/SecretErrorDecodeTest.kt create mode 100644 sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/wasm/ValueRoundTripTest.kt diff --git a/sdks/kotlin/sdk/.gitignore b/sdks/kotlin/sdk/.gitignore new file mode 100644 index 0000000000..bdfde0f89a --- /dev/null +++ b/sdks/kotlin/sdk/.gitignore @@ -0,0 +1,3 @@ +.gradle/ +build/ +dist/ diff --git a/sdks/kotlin/sdk/build.gradle.kts b/sdks/kotlin/sdk/build.gradle.kts new file mode 100644 index 0000000000..ef1a7d5636 --- /dev/null +++ b/sdks/kotlin/sdk/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + kotlin("multiplatform") version "2.4.0" + `maven-publish` + id("org.jlleitschuh.gradle.ktlint") version "14.2.0" +} + +group = "cloud.golem" +version = "0.0.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +kotlin { + // Native path: compile Kotlin/Wasm (WasmGC) directly to a Wasm Component (no JS/QuickJS). + // The agent compiles + links the SDK, which is componentized via wasm-tools. + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmWasi { + nodejs() + binaries.executable() + } + + sourceSets { + val commonMain by getting + val wasmWasiMain by getting + val wasmWasiTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} + +// Published to mavenLocal at 0.0.0-SNAPSHOT for local/dev/CI so agent projects resolve +// `cloud.golem:golem-kotlin-sdk` (wasmWasi klib) from mavenLocal. +publishing { + repositories { mavenLocal() } +} diff --git a/sdks/kotlin/sdk/gradle/wrapper/gradle-wrapper.jar b/sdks/kotlin/sdk/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/sdks/kotlin/sdk/gradle/wrapper/gradle-wrapper.properties b/sdks/kotlin/sdk/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..94113f200e --- /dev/null +++ b/sdks/kotlin/sdk/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/sdks/kotlin/sdk/gradlew b/sdks/kotlin/sdk/gradlew new file mode 100755 index 0000000000..739907dfd1 --- /dev/null +++ b/sdks/kotlin/sdk/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/sdks/kotlin/sdk/gradlew.bat b/sdks/kotlin/sdk/gradlew.bat new file mode 100644 index 0000000000..c4bdd3ab8e --- /dev/null +++ b/sdks/kotlin/sdk/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sdks/kotlin/sdk/settings.gradle.kts b/sdks/kotlin/sdk/settings.gradle.kts new file mode 100644 index 0000000000..356e76ca21 --- /dev/null +++ b/sdks/kotlin/sdk/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "golem-kotlin-sdk" diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/BaseAgent.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/BaseAgent.kt new file mode 100644 index 0000000000..6f31c67def --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/BaseAgent.kt @@ -0,0 +1,32 @@ +package cloud.golem + +/** + * Base class for all Golem agent implementations. + * Extend this in your agent class annotated with @Agent. + * + * Provides the agent's self-identity, read from the Golem host at call time + * (mirrors the Scala SDK's BaseAgent.agentId / agentType / agentName). These are + * host-backed: they return real values only inside the Golem wasm runtime. + */ +abstract class BaseAgent { + /** Canonical string agent ID: component + agent type + constructor parameters. */ + val agentId: String get() = currentAgentId() + + /** Agent type name (best-effort — see the SDK host bindings). */ + val agentType: String get() = currentAgentType() + + /** Agent name / primary constructor parameter (best-effort). */ + val agentName: String get() = currentAgentName() + + /** + * The authenticated identity of the caller of the *current* invocation (the `principal` the + * host passes to `initialize`/`invoke`). [Principal.Anonymous] outside an invocation or when + * the call carried no authenticated identity. + */ + val principal: Principal get() = currentPrincipal() +} + +internal expect fun currentAgentId(): String +internal expect fun currentAgentType(): String +internal expect fun currentAgentName(): String +internal expect fun currentPrincipal(): Principal diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Datetime.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Datetime.kt new file mode 100644 index 0000000000..4d04c6b29e --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Datetime.kt @@ -0,0 +1,7 @@ +package cloud.golem + +/** + * A point in time as whole [seconds] plus [nanoseconds], matching WIT's `datetime` value. Usable + * directly as an agent method parameter or return type (maps to the `datetime` WIT type). + */ +data class Datetime(val seconds: Long, val nanoseconds: Int) diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Principal.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Principal.kt new file mode 100644 index 0000000000..24f4f18f56 --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Principal.kt @@ -0,0 +1,37 @@ +package cloud.golem + +/** + * The authenticated identity of whoever invoked the current agent method — delivered by the Golem + * host on every `initialize`/`invoke` call. Read it inside an agent via [BaseAgent.principal]. + * + * Mirrors `golem:agent/common`'s `principal` variant. [Anonymous] means the call carried no + * authenticated identity. + */ +sealed class Principal { + + /** An OpenID Connect identity (a human or service authenticated via OIDC). */ + data class Oidc( + /** The `sub` claim — the subject's stable identifier. */ + val sub: String, + /** The token issuer (`iss`). */ + val issuer: String, + val email: String?, + val name: String?, + val emailVerified: Boolean?, + val givenName: String?, + val familyName: String?, + val picture: String?, + val preferredUsername: String?, + /** The raw claims blob (JSON) as provided by the issuer. */ + val claims: String, + ) : Principal() + + /** Another Golem agent, identified by its canonical string agent-id. */ + data class Agent(val agentId: String) : Principal() + + /** A Golem user account, identified by its account UUID. */ + data class GolemUser(val accountId: Uuid) : Principal() + + /** No authenticated caller. */ + object Anonymous : Principal() +} diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Snapshotted.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Snapshotted.kt new file mode 100644 index 0000000000..c161eb460a --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Snapshotted.kt @@ -0,0 +1,15 @@ +package cloud.golem + +/** + * Opt into snapshot-based updates by mixing this in alongside [BaseAgent] with your state type: + * `class MyAgent(...) : BaseAgent(), Snapshotted { override var state = MyState(...) }`. + * + * [state] is auto-serialized by the runtime (KSP derives the codec from `S` at compile time) and + * wrapped in a principal-carrying envelope, so a manual (snapshot-based) update restores both your + * state and the caller identity captured at initialize. `S` MUST be a WIT-mappable type (data + * class, list, map, pair/triple, enum, sealed class, primitive, Datetime, Either); a non-mappable + * `S` is a compile-time error. An agent that does not mix this in produces an empty snapshot. + */ +interface Snapshotted { + var state: S +} diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Uuid.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Uuid.kt new file mode 100644 index 0000000000..9ec82c71d7 --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/Uuid.kt @@ -0,0 +1,7 @@ +package cloud.golem + +/** + * A 128-bit UUID as two 64-bit halves, matching `golem:core/types`' `uuid` record + * (`{ high-bits: u64, low-bits: u64 }`). + */ +data class Uuid(val highBits: ULong, val lowBits: ULong) diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Agent.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Agent.kt new file mode 100644 index 0000000000..6e22d9cf7e --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Agent.kt @@ -0,0 +1,24 @@ +package cloud.golem.annotations + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Agent( + val mount: String = "", + val description: String = "", + /** If true, the mount's `http-mount-details.auth-details` requires authentication. */ + val auth: Boolean = false, + /** Allowed CORS origin patterns for the mount, e.g. `["*"]`. Empty = no CORS headers. */ + val cors: Array = [], + /** + * `"durable"` (default) or `"ephemeral"` -- mirrors `golem:agent/common@2.0.0`'s + * `agent-mode` enum and Scala's `@agentDefinition(mode = DurabilityMode....)`. + */ + val mode: String = "durable", + /** + * Snapshotting cadence, using the same DSL as Scala's `@agentDefinition(snapshotting = ...)`: + * `"disabled"` (default), `"enabled"` (server default cadence), `"periodic()"` + * (periodic snapshots every `` nanoseconds), or `"every()"` (every `` + * invocations, `` must fit a u16: 0..65535). + */ + val snapshotting: String = "disabled", +) diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Description.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Description.kt new file mode 100644 index 0000000000..d168406a2f --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Description.kt @@ -0,0 +1,5 @@ +package cloud.golem.annotations + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Description(val text: String = "") diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Endpoint.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Endpoint.kt new file mode 100644 index 0000000000..dd15ef35f2 --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Endpoint.kt @@ -0,0 +1,15 @@ +package cloud.golem.annotations + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Endpoint( + val post: String = "", + val get: String = "", + val put: String = "", + val delete: String = "", + val path: String = "", + /** If true, the endpoint's `http-endpoint-details.auth-details` requires authentication. */ + val auth: Boolean = false, + /** Allowed CORS origin patterns for the endpoint, e.g. `["*"]`. Empty = no CORS headers. */ + val cors: Array = [], +) diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Prompt.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Prompt.kt new file mode 100644 index 0000000000..8b49d77867 --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Prompt.kt @@ -0,0 +1,5 @@ +package cloud.golem.annotations + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Prompt(val hint: String = "") diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/ReadOnly.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/ReadOnly.kt new file mode 100644 index 0000000000..dace1d5497 --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/ReadOnly.kt @@ -0,0 +1,14 @@ +package cloud.golem.annotations + +/** + * Marks an [Endpoint]/agent method as **read-only**: it does not mutate agent state, so Golem may + * cache its result according to [cache]. Mirrors the Scala SDK's `@readOnly`. + * + * [cache] is a DSL string (the same style as `@Agent(snapshotting = ...)`): + * - `"until-write"` (default) — cache the result until the next state-mutating call; + * - `"no-cache"` — never cache; + * - `"ttl()"` — cache for a fixed duration, in nanoseconds (e.g. `"ttl(5000000000)"`). + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class ReadOnly(val cache: String = "until-write") diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/RemoteAgent.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/RemoteAgent.kt new file mode 100644 index 0000000000..ae6efc5512 --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/RemoteAgent.kt @@ -0,0 +1,12 @@ +package cloud.golem.annotations + +/** + * Marks a Kotlin interface as a typed client for a remote agent (agent-to-agent RPC). The + * interface's abstract methods mirror the remote agent's methods; KSP generates a `Rpc` + * implementation that encodes each call's arguments to a `schema-value-tree`, invokes the remote + * agent via `golem:agent/host`'s wasm-rpc, and decodes the result back to the method's Kotlin + * return type. [typeName] is the remote agent's registered type name. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class RemoteAgent(val typeName: String) diff --git a/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Tool.kt b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Tool.kt new file mode 100644 index 0000000000..4b0af1e076 --- /dev/null +++ b/sdks/kotlin/sdk/src/commonMain/kotlin/cloud/golem/annotations/Tool.kt @@ -0,0 +1,22 @@ +package cloud.golem.annotations + +/** + * Marks a method as a `golem:tool@0.1.0` tool. The tool's identity is its root command name + * (`name`); it is exported alongside the agent's `golem:agent/guest@2.0.0` surface. + * + * Scope: one tool = one root command (no subcommands), whose positionals are derived 1:1 + * from the annotated method's parameters, in declaration order. + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Tool( + val name: String, + val description: String = "", +) + +/** Documents a single positional parameter of a [Tool]-annotated method. */ +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class Command( + val description: String = "", +) diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/HostActuals.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/HostActuals.kt new file mode 100644 index 0000000000..e1783c2a1d --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/HostActuals.kt @@ -0,0 +1,22 @@ +package cloud.golem + +import cloud.golem.runtime.HostApi +import cloud.golem.runtime.ParseAgentIdResult + +// Native (wasmWasi) actuals for BaseAgent's host-backed identity. agentId is wired to the real +// host via golem:api/host@1.5.0's get-self-metadata. agentType is +// wired via golem:agent/host@2.0.0's parse-agent-id. agentName remains unwired: +// see HostApi.ParsedAgentId's doc comment -- there is no well-defined, host-documented way to +// derive it from the WIT-level API (it would require lifting an arbitrary schema-graph). +internal actual fun currentAgentId(): String = HostApi.getSelfMetadata().agentId.agentId + +internal actual fun currentAgentType(): String = when (val r = HostApi.parseAgentId(currentAgentId())) { + is ParseAgentIdResult.Ok -> r.value.agentTypeName + is ParseAgentIdResult.Err -> "" +} + +internal actual fun currentAgentName(): String = "" + +// The principal is per-invocation (not host-queryable like agentId): Guest.kt decodes it from the +// `initialize`/`invoke` args and stashes it on NativeAgentRuntime before dispatch. +internal actual fun currentPrincipal(): Principal = cloud.golem.runtime.NativeAgentRuntime.currentPrincipal diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/AgentTypeModel.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/AgentTypeModel.kt new file mode 100644 index 0000000000..c823a9b9a0 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/AgentTypeModel.kt @@ -0,0 +1,683 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.childWitTypes +import cloud.golem.wasm.enumCases +import cloud.golem.wasm.innerOf +import cloud.golem.wasm.recordFields +import cloud.golem.wasm.splitTopLevelCommas +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt +import cloud.golem.wasm.storeLong +import cloud.golem.wasm.storeShort +import cloud.golem.wasm.variantCases +import cloud.golem.wasm.writeEmptyListField +import cloud.golem.wasm.writeListField +import cloud.golem.wasm.writeOptionNone +import cloud.golem.wasm.writeStringField + +/** + * Canonical-ABI layout constants for `golem:agent/common@2.0.0` and `golem:core/types@2.0.0` + * (the types `agent-type` transitively touches). Computed via `wit-parser::SizeAlign` -- the + * same size/align/offset algorithm wasmtime and wit-bindgen use internally -- run against the + * real WIT under `wit/deps/golem-agent/common.wit` and `wit/deps/golem-core-v2/golem-core-v2.wit` + * on 2026-06-30 (see docs/spikes/compile-to-wasm-poc for the dump tool). These are NOT + * hand-derived: hand-deriving canonical-ABI offsets for a deep/wide WIT type (schema-type-body + * alone has 36 variant cases) risks silent misalignment of every subsequent field. + * + * All variant/enum discriminants in this schema are a single byte (u8) -- case counts are all + * <= 256, so `tag_size` is always 1 in the dump. + */ +private object Layout { + // record agent-type: size=176 align=8 + const val AGENT_TYPE_SIZE = 176 + const val AGENT_TYPE_ALIGN = 8 + const val AT_TYPE_NAME = 0 + const val AT_DESCRIPTION = 8 + const val AT_SOURCE_LANGUAGE = 16 + const val AT_SCHEMA = 24 + const val AT_CONSTRUCTOR = 44 + const val AT_METHODS = 88 + const val AT_DEPENDENCIES = 96 + const val AT_MODE = 104 + const val AT_HTTP_MOUNT = 108 + const val AT_SNAPSHOTTING = 144 + const val AT_CONFIG = 168 + + // record agent-constructor: size=44 align=4 + const val AGENT_CONSTRUCTOR_SIZE = 44 + const val AGENT_CONSTRUCTOR_ALIGN = 4 + const val AC_NAME = 0 + const val AC_DESCRIPTION = 12 + const val AC_PROMPT_HINT = 20 + const val AC_INPUT_SCHEMA = 32 + + // record agent-method: size=88 align=8 + const val AGENT_METHOD_SIZE = 88 + const val AGENT_METHOD_ALIGN = 8 + const val AM_NAME = 0 + const val AM_DESCRIPTION = 8 + const val AM_HTTP_ENDPOINT = 16 + const val AM_PROMPT_HINT = 24 + const val AM_INPUT_SCHEMA = 36 + const val AM_OUTPUT_SCHEMA = 48 + const val AM_READ_ONLY = 56 + + // option at AM_READ_ONLY: option is align 8, so its payload (the + // read-only-config) sits at +8. read-only-config: size=24 align=8 { cache-policy @0 (16B), + // uses-principal: bool @16 }. cache-policy: variant size=16 align=8 { no-cache, until-write, + // ttl(duration) } -- tag@0, ttl's u64 duration @8. (all verified via abi-dump.) + const val READ_ONLY_OPTION_PAYLOAD_OFFSET = 8 + const val RO_USES_PRINCIPAL = 16 + const val CACHE_POLICY_PAYLOAD_OFFSET = 8 + const val CP_NO_CACHE = 0 + const val CP_UNTIL_WRITE = 1 + const val CP_TTL = 2 + + // record named-field: size=72 align=4 + const val NAMED_FIELD_SIZE = 72 + const val NAMED_FIELD_ALIGN = 4 + const val NF_NAME = 0 + const val NF_SOURCE = 8 + const val NF_SCHEMA = 12 + const val NF_METADATA = 16 + + // record http-mount-details: size=28 align=4 + const val HMD_PATH_PREFIX = 0 + const val HMD_AUTH_DETAILS = 8 + const val HMD_PHANTOM_AGENT = 10 + const val HMD_CORS_OPTIONS = 12 + const val HMD_WEBHOOK_SUFFIX = 20 + + // option: payload_offset = align_to(1, align=4) = 4 + const val HTTP_MOUNT_OPTION_PAYLOAD_OFFSET = 4 + + // record auth-details: size=1 align=1 { required: bool } + // option: size=2 align=1, payload_offset = align_to(1, align=1) = 1 + const val AUTH_DETAILS_OPTION_PAYLOAD_OFFSET = 1 + + // record http-endpoint-details: size=48 align=4 + const val HTTP_ENDPOINT_DETAILS_SIZE = 48 + const val HTTP_ENDPOINT_DETAILS_ALIGN = 4 + const val HED_HTTP_METHOD = 0 + const val HED_PATH_SUFFIX = 12 + const val HED_HEADER_VARS = 20 + const val HED_QUERY_VARS = 28 + const val HED_AUTH_DETAILS = 36 + const val HED_CORS_OPTIONS = 40 + + // record cors-options: size=8 align=4 + const val CO_ALLOWED_PATTERNS = 0 + + // variant path-segment: size=12 align=4, tag_size=1, payload_offset=4 + const val PATH_SEGMENT_SIZE = 12 + const val PATH_SEGMENT_ALIGN = 4 + const val PATH_SEGMENT_PAYLOAD_OFFSET = 4 + const val PS_LITERAL = 0 + const val PS_PATH_VARIABLE = 2 + const val PS_REMAINING_PATH_VARIABLE = 3 + + // variant http-method: size=12 align=4, tag_size=1, payload_offset=4 + const val HM_GET = 0 + const val HM_HEAD = 1 + const val HM_POST = 2 + const val HM_PUT = 3 + const val HM_DELETE = 4 + const val HM_CONNECT = 5 + const val HM_OPTIONS = 6 + const val HM_TRACE = 7 + const val HM_PATCH = 8 + + // variant input-schema: size=12 align=4, tag_size=1, payload_offset=4 + const val IS_PARAMETERS = 0 + const val INPUT_SCHEMA_PAYLOAD_OFFSET = 4 + + // variant output-schema: size=8 align=4, tag_size=1, payload_offset=4 + const val OS_UNIT = 0 + const val OS_SINGLE = 1 + const val OUTPUT_SCHEMA_PAYLOAD_OFFSET = 4 + + // variant field-source: size=2 align=1, tag_size=1, payload_offset=1 + const val FS_USER_SUPPLIED = 0 + + // variant snapshotting: size=24 align=8, tag_size=1, payload_offset=8 + const val SNAP_DISABLED = 0 + const val SNAP_ENABLED = 1 + const val SNAPSHOTTING_PAYLOAD_OFFSET = 8 + + // variant snapshotting-config: size=16 align=8, tag_size=1, payload_offset=8 -- nested inside + // `snapshotting`'s `enabled` payload, i.e. at (snapshotting_base + SNAPSHOTTING_PAYLOAD_OFFSET). + const val SNAPCFG_DEFAULT = 0 + const val SNAPCFG_PERIODIC = 1 + const val SNAPCFG_EVERY_N_INVOCATION = 2 + const val SNAPSHOTTING_CONFIG_PAYLOAD_OFFSET = 8 + + // enum agent-mode: size=1 align=1 + const val AGENT_MODE_DURABLE = 0 + const val AGENT_MODE_EPHEMERAL = 1 + + // record schema-graph: size=20 align=4 + const val SCHEMA_GRAPH_SIZE = 20 + const val SCHEMA_GRAPH_ALIGN = 4 + const val SG_TYPE_NODES = 0 + const val SG_DEFS = 8 + const val SG_ROOT = 16 + + // record schema-type-node: size=144 align=8 + const val SCHEMA_TYPE_NODE_SIZE = 144 + const val SCHEMA_TYPE_NODE_ALIGN = 8 + const val STN_BODY = 0 + const val STN_METADATA = 88 + + // record metadata-envelope: size=56 align=4 + const val ME_DOC = 0 + const val ME_ALIASES = 12 + const val ME_EXAMPLES = 20 + const val ME_DEPRECATED = 28 + const val ME_ROLE = 40 + + // variant schema-type-body: size=88 align=8, tag_size=1, payload_offset=8 + // (payload_offset=8 because the largest case -- quantity-type, 80 bytes -- needs 8-byte + // alignment; the type-level size is fixed by ALL 36 cases, hence computing it via the tool + // rather than by hand.) Primitive case indices verified via abi-dump against the real WIT + // (schema-type-body case ordering); the numeric cases (s8..f64) each carry + // option, always lowered as `none`; bool/char/string are tag-only. + const val STB_BOOL_TYPE = 1 + const val STB_S8_TYPE = 2 + const val STB_S16_TYPE = 3 + const val STB_S32_TYPE = 4 + const val STB_S64_TYPE = 5 + const val STB_U8_TYPE = 6 + const val STB_U16_TYPE = 7 + const val STB_U32_TYPE = 8 + const val STB_U64_TYPE = 9 + const val STB_F32_TYPE = 10 + const val STB_F64_TYPE = 11 + const val STB_CHAR_TYPE = 12 + const val STB_STRING_TYPE = 13 + + // Composite case tags (verified via abi-dump against schema-type-body's case list). + const val STB_RECORD_TYPE = 14 + const val STB_VARIANT_TYPE = 15 + const val STB_ENUM_TYPE = 16 + const val STB_TUPLE_TYPE = 18 + const val STB_LIST_TYPE = 19 + const val STB_MAP_TYPE = 21 + const val STB_OPTION_TYPE = 22 + const val STB_RESULT_TYPE = 23 + const val STB_DATETIME_TYPE = 28 // tag-only (verified via schema-type-body case ordering) + const val SCHEMA_TYPE_BODY_PAYLOAD_OFFSET = 8 + + // named-field-type: size=68 align=4 { name@0, body(type-node-index)@8, metadata@12 (56B) } + const val NAMED_FIELD_TYPE_SIZE = 68 + const val NAMED_FIELD_TYPE_ALIGN = 4 + const val NFT_NAME = 0 + const val NFT_BODY = 8 + const val NFT_METADATA = 12 + + // variant-case-type: size=72 align=4 { name@0, payload(option)@8, metadata@16 (56B) } + const val VARIANT_CASE_TYPE_SIZE = 72 + const val VARIANT_CASE_TYPE_ALIGN = 4 + const val VCT_NAME = 0 + const val VCT_PAYLOAD = 8 + const val VCT_METADATA = 16 + + // type-node-index = s32 (4B); option = 8B (tag@0, s32@4). + const val TYPE_NODE_INDEX_SIZE = 4 + + // map-spec: size=8 { key(idx)@0, value(idx)@4 } + const val MS_KEY = 0 + const val MS_VALUE = 4 + + // result-spec: size=16 { ok(option)@0, err(option)@8 } + const val RS_OK = 0 + const val RS_ERR = 8 +} + +/** + * Lowers a [NativeAgentDescriptor] to the canonical-ABI `agent-type` record + * (golem:agent/common@2.0.0), returning a pointer to it. + */ +fun lowerAgentType(descriptor: NativeAgentDescriptor): Int { + // Build the merged schema-graph: one type-node per distinct WIT type (transitively) referenced + // by the constructor/method params and outputs. `collectTypeNodes` walks each root witType + // recursively, registering child type-nodes (record fields, list/option/tuple/map/variant/ + // result element types) deduped by WIT type string. + val roots = buildList { + descriptor.constructorParams.forEach { add(it.witType) } + descriptor.methods.forEach { m -> + m.inputParams.forEach { add(it.witType) } + if (m.outputWitType != "()") add(m.outputWitType) + } + } + val typeIndex = collectTypeNodes(roots) + + val agentType = alloc(Layout.AGENT_TYPE_SIZE, Layout.AGENT_TYPE_ALIGN) + + writeStringField(agentType, Layout.AT_TYPE_NAME, descriptor.typeName) + writeStringField(agentType, Layout.AT_DESCRIPTION, descriptor.description) + writeStringField(agentType, Layout.AT_SOURCE_LANGUAGE, "kotlin") + + lowerSchemaGraphInto(agentType, Layout.AT_SCHEMA, typeIndex) + lowerConstructorInto(agentType, Layout.AT_CONSTRUCTOR, descriptor.description, descriptor.constructorParams, typeIndex) + lowerMethodsInto(agentType, Layout.AT_METHODS, descriptor.methods, typeIndex) + + writeEmptyListField(agentType, Layout.AT_DEPENDENCIES) + lowerAgentModeInto(agentType, Layout.AT_MODE, descriptor.mode) + + if (descriptor.mountPath.isNotEmpty()) { + lowerHttpMountSome(agentType, Layout.AT_HTTP_MOUNT, descriptor.mountPath, descriptor.mountAuth, descriptor.mountCors) + } else { + writeOptionNone(agentType, Layout.AT_HTTP_MOUNT) + } + + lowerSnapshottingInto(agentType, Layout.AT_SNAPSHOTTING, descriptor.snapshotting) + writeEmptyListField(agentType, Layout.AT_CONFIG) + + return agentType +} + +/** + * Recursively registers [roots] and every type they transitively reference (record fields, + * list/option/tuple/map/variant/result element types) into a deduped `witType -> type-node-index` + * map. Children are registered before their parent, so a composite body can always look its child + * indices up. An empty root set falls back to a single `s32` node (the schema-graph needs >=1). + */ +internal fun collectTypeNodes(roots: List): LinkedHashMap { + val index = LinkedHashMap() + fun register(wit: String) { + if (wit in index) return + childWitTypes(wit).forEach { register(it) } + index[wit] = index.size + } + (roots.ifEmpty { listOf("s32") }).forEach { register(it) } + return index +} + +internal fun lowerSchemaGraphInto(base: Int, offset: Int, typeIndex: Map) { + val ordered = typeIndex.entries.sortedBy { it.value }.map { it.key } + val graphBase = base + offset + writeListField( + graphBase, + Layout.SG_TYPE_NODES, + ordered.size, + Layout.SCHEMA_TYPE_NODE_SIZE, + Layout.SCHEMA_TYPE_NODE_ALIGN, + ) { i, nodePtr -> lowerSchemaTypeNodeInto(nodePtr, ordered[i], typeIndex) } + writeEmptyListField(graphBase, Layout.SG_DEFS) + storeInt(graphBase + Layout.SG_ROOT, 0) // structural placeholder per the WIT doc comment +} + +internal fun lowerSchemaTypeNodeInto(base: Int, witType: String, typeIndex: Map) { + lowerSchemaTypeBodyInto(base, Layout.STN_BODY, witType, typeIndex) + lowerEmptyMetadataInto(base, Layout.STN_METADATA) +} + +/** Writes `option` (8B: tag@0, s32@4). */ +private fun lowerOptionIndexInto(base: Int, index: Int?) { + if (index == null) { + storeByte(base, 0) + } else { + storeByte(base, 1) + storeInt(base + 4, index) + } +} + +internal fun lowerSchemaTypeBodyInto(base: Int, offset: Int, witType: String, typeIndex: Map) { + val varBase = base + offset + val payload = varBase + Layout.SCHEMA_TYPE_BODY_PAYLOAD_OFFSET + + // Numeric primitives carry option, always lowered `none` (a single 0 + // byte at the payload offset). bool/char/string are tag-only. Composite bodies build their + // payload referencing child type-node indices (looked up from [typeIndex]). + fun tagOnly(tag: Int) = storeByte(varBase, tag.toByte()) + fun numeric(tag: Int) { + storeByte(varBase, tag.toByte()) + storeByte(payload, 0) // option = none + } + when { + witType == "bool" -> tagOnly(Layout.STB_BOOL_TYPE) + witType == "char" -> tagOnly(Layout.STB_CHAR_TYPE) + witType == "string" -> tagOnly(Layout.STB_STRING_TYPE) + witType == "datetime" -> tagOnly(Layout.STB_DATETIME_TYPE) + witType == "s8" -> numeric(Layout.STB_S8_TYPE) + witType == "s16" -> numeric(Layout.STB_S16_TYPE) + witType == "s32" -> numeric(Layout.STB_S32_TYPE) + witType == "s64" -> numeric(Layout.STB_S64_TYPE) + witType == "u8" -> numeric(Layout.STB_U8_TYPE) + witType == "u16" -> numeric(Layout.STB_U16_TYPE) + witType == "u32" -> numeric(Layout.STB_U32_TYPE) + witType == "u64" -> numeric(Layout.STB_U64_TYPE) + witType == "f32" -> numeric(Layout.STB_F32_TYPE) + witType == "f64" -> numeric(Layout.STB_F64_TYPE) + + witType == "enum" || (witType.startsWith("enum<") && witType.endsWith(">")) -> { + tagOnly(Layout.STB_ENUM_TYPE) // enum-type(list) + val cases = enumCases(witType) + writeListField(varBase, Layout.SCHEMA_TYPE_BODY_PAYLOAD_OFFSET, cases.size, 8, 4) { i, ep -> + writeStringField(ep, 0, cases[i]) + } + } + witType.startsWith("record<") && witType.endsWith(">") -> { + tagOnly(Layout.STB_RECORD_TYPE) // record-type(list) + val fields = recordFields(witType) + writeListField( + varBase, + Layout.SCHEMA_TYPE_BODY_PAYLOAD_OFFSET, + fields.size, + Layout.NAMED_FIELD_TYPE_SIZE, + Layout.NAMED_FIELD_TYPE_ALIGN, + ) { i, fp -> + writeStringField(fp, Layout.NFT_NAME, fields[i].first) + storeInt(fp + Layout.NFT_BODY, typeIndex.getValue(fields[i].second)) + lowerEmptyMetadataInto(fp, Layout.NFT_METADATA) + } + } + witType.startsWith("variant<") && witType.endsWith(">") -> { + tagOnly(Layout.STB_VARIANT_TYPE) // variant-type(list) + val cases = variantCases(witType) + writeListField( + varBase, + Layout.SCHEMA_TYPE_BODY_PAYLOAD_OFFSET, + cases.size, + Layout.VARIANT_CASE_TYPE_SIZE, + Layout.VARIANT_CASE_TYPE_ALIGN, + ) { i, cp -> + writeStringField(cp, Layout.VCT_NAME, cases[i].first) + lowerOptionIndexInto(cp + Layout.VCT_PAYLOAD, cases[i].second?.let { typeIndex.getValue(it) }) + lowerEmptyMetadataInto(cp, Layout.VCT_METADATA) + } + } + witType.startsWith("list<") && witType.endsWith(">") -> { + tagOnly(Layout.STB_LIST_TYPE) // list-type(type-node-index) + storeInt(payload, typeIndex.getValue(innerOf(witType, "list<"))) + } + witType.startsWith("option<") && witType.endsWith(">") -> { + tagOnly(Layout.STB_OPTION_TYPE) // option-type(type-node-index) + storeInt(payload, typeIndex.getValue(innerOf(witType, "option<"))) + } + witType.startsWith("tuple<") && witType.endsWith(">") -> { + tagOnly(Layout.STB_TUPLE_TYPE) // tuple-type(list) + val elems = splitTopLevelCommas(innerOf(witType, "tuple<")) + writeListField(varBase, Layout.SCHEMA_TYPE_BODY_PAYLOAD_OFFSET, elems.size, Layout.TYPE_NODE_INDEX_SIZE, 4) { i, ep -> + storeInt(ep, typeIndex.getValue(elems[i])) + } + } + witType.startsWith("map<") && witType.endsWith(">") -> { + tagOnly(Layout.STB_MAP_TYPE) // map-type(map-spec { key, value }) + val (k, v) = splitTopLevelCommas(innerOf(witType, "map<")).let { it[0] to it[1] } + storeInt(payload + Layout.MS_KEY, typeIndex.getValue(k)) + storeInt(payload + Layout.MS_VALUE, typeIndex.getValue(v)) + } + witType.startsWith("result<") && witType.endsWith(">") -> { + tagOnly(Layout.STB_RESULT_TYPE) // result-type(result-spec { ok, err }) + val (ok, err) = splitTopLevelCommas(innerOf(witType, "result<")).let { it[0] to it[1] } + lowerOptionIndexInto(payload + Layout.RS_OK, if (ok == "_") null else typeIndex.getValue(ok)) + lowerOptionIndexInto(payload + Layout.RS_ERR, if (err == "_") null else typeIndex.getValue(err)) + } + else -> error("native lowerSchemaTypeBody: unsupported wit type $witType") + } +} + +internal fun lowerEmptyMetadataInto(base: Int, offset: Int) { + val metaBase = base + offset + writeOptionNone(metaBase, Layout.ME_DOC) + writeEmptyListField(metaBase, Layout.ME_ALIASES) + writeEmptyListField(metaBase, Layout.ME_EXAMPLES) + writeOptionNone(metaBase, Layout.ME_DEPRECATED) + writeOptionNone(metaBase, Layout.ME_ROLE) +} + +private fun lowerConstructorInto( + base: Int, + offset: Int, + agentDescription: String, + params: List, + typeIndex: Map, +) { + val ctorBase = base + offset + writeOptionNone(ctorBase, Layout.AC_NAME) // JS path: constructor.name = undefined + writeStringField(ctorBase, Layout.AC_DESCRIPTION, agentDescription) // JS path reuses the agent's description + writeOptionNone(ctorBase, Layout.AC_PROMPT_HINT) + lowerInputSchemaInto(ctorBase, Layout.AC_INPUT_SCHEMA, params, typeIndex) +} + +private fun lowerInputSchemaInto(base: Int, offset: Int, params: List, typeIndex: Map) { + val varBase = base + offset + storeByte(varBase, Layout.IS_PARAMETERS.toByte()) + writeListField( + varBase, + Layout.INPUT_SCHEMA_PAYLOAD_OFFSET, + params.size, + Layout.NAMED_FIELD_SIZE, + Layout.NAMED_FIELD_ALIGN, + ) { i, fieldPtr -> + val p = params[i] + writeStringField(fieldPtr, Layout.NF_NAME, p.name) + storeByte(fieldPtr + Layout.NF_SOURCE, Layout.FS_USER_SUPPLIED.toByte()) + storeInt(fieldPtr + Layout.NF_SCHEMA, typeIndex.getValue(p.witType)) + lowerEmptyMetadataInto(fieldPtr, Layout.NF_METADATA) + } +} + +private fun lowerOutputSchemaInto(base: Int, offset: Int, outputWitType: String, typeIndex: Map) { + val varBase = base + offset + if (outputWitType == "()") { + storeByte(varBase, Layout.OS_UNIT.toByte()) + } else { + storeByte(varBase, Layout.OS_SINGLE.toByte()) + storeInt(varBase + Layout.OUTPUT_SCHEMA_PAYLOAD_OFFSET, typeIndex.getValue(outputWitType)) + } +} + +/** Writes `option` at [base]+[offset]: `none` when [value] is empty, else `some(value)`. */ +internal fun lowerOptionStringInto(base: Int, offset: Int, value: String) { + if (value.isEmpty()) { + writeOptionNone(base, offset) + } else { + storeByte(base + offset, 1) // some + writeStringField(base, offset + 4, value) // string payload @ option+4 + } +} + +/** + * Writes `option` at [base]+[offset] from the `@ReadOnly(cache=...)` DSL string + * [cache] (`null` => `none`). read-only-config = { cache-policy, uses-principal: bool }; the cache + * policy parses like `@Agent(snapshotting=...)`: `"no-cache"`, `"until-write"`, or `"ttl()"`. + * `uses-principal` is always `false` (the SDK has no Principal-typed params yet). + */ +internal fun lowerReadOnlyInto(base: Int, offset: Int, cache: String?) { + if (cache == null) { + writeOptionNone(base, offset) + return + } + val optBase = base + offset + storeByte(optBase, 1) // some + val cfg = optBase + Layout.READ_ONLY_OPTION_PAYLOAD_OFFSET + when { + cache == "no-cache" -> storeByte(cfg, Layout.CP_NO_CACHE.toByte()) + cache == "until-write" -> storeByte(cfg, Layout.CP_UNTIL_WRITE.toByte()) + cache.startsWith("ttl(") && cache.endsWith(")") -> { + val nanos = cache.substring("ttl(".length, cache.length - 1).toLongOrNull() + ?: error("native lowerReadOnly: ttl(...) argument must be an integer nanosecond count, got \"$cache\"") + storeByte(cfg, Layout.CP_TTL.toByte()) + storeLong(cfg + Layout.CACHE_POLICY_PAYLOAD_OFFSET, nanos) + } + else -> error("native lowerReadOnly: unsupported @ReadOnly(cache=\"$cache\") -- expected \"no-cache\", \"until-write\", or \"ttl()\"") + } + storeByte(cfg + Layout.RO_USES_PRINCIPAL, 0) // uses-principal = false +} + +private fun lowerMethodsInto(base: Int, offset: Int, methods: List, typeIndex: Map) { + writeListField( + base, + offset, + methods.size, + Layout.AGENT_METHOD_SIZE, + Layout.AGENT_METHOD_ALIGN, + ) { i, methodPtr -> + val m = methods[i] + writeStringField(methodPtr, Layout.AM_NAME, m.name) + writeStringField(methodPtr, Layout.AM_DESCRIPTION, "") // JS path: method.description = "" + lowerHttpEndpointsInto(methodPtr, Layout.AM_HTTP_ENDPOINT, m.httpEndpoints) + lowerOptionStringInto(methodPtr, Layout.AM_PROMPT_HINT, m.promptHint) // from @Prompt(hint=...) + lowerInputSchemaInto(methodPtr, Layout.AM_INPUT_SCHEMA, m.inputParams, typeIndex) + lowerOutputSchemaInto(methodPtr, Layout.AM_OUTPUT_SCHEMA, m.outputWitType, typeIndex) + lowerReadOnlyInto(methodPtr, Layout.AM_READ_ONLY, m.readOnlyCache) // from @ReadOnly(cache=...) + } +} + +private fun lowerHttpEndpointsInto(base: Int, offset: Int, endpoints: List) { + writeListField( + base, + offset, + endpoints.size, + Layout.HTTP_ENDPOINT_DETAILS_SIZE, + Layout.HTTP_ENDPOINT_DETAILS_ALIGN, + ) { i, epPtr -> + val ep = endpoints[i] + lowerHttpMethodInto(epPtr, Layout.HED_HTTP_METHOD, ep.verb) + lowerPathSegmentsInto(epPtr, Layout.HED_PATH_SUFFIX, ep.path) + writeEmptyListField(epPtr, Layout.HED_HEADER_VARS) + writeEmptyListField(epPtr, Layout.HED_QUERY_VARS) + lowerAuthDetailsInto(epPtr, Layout.HED_AUTH_DETAILS, ep.auth) + lowerCorsInto(epPtr, Layout.HED_CORS_OPTIONS, ep.cors) + } +} + +private fun lowerHttpMethodInto(base: Int, offset: Int, verb: String) { + val varBase = base + offset + val caseIndex = when (verb.uppercase()) { + "GET" -> Layout.HM_GET + "HEAD" -> Layout.HM_HEAD + "POST" -> Layout.HM_POST + "PUT" -> Layout.HM_PUT + "DELETE" -> Layout.HM_DELETE + "CONNECT" -> Layout.HM_CONNECT + "OPTIONS" -> Layout.HM_OPTIONS + "TRACE" -> Layout.HM_TRACE + "PATCH" -> Layout.HM_PATCH + else -> error("native lowerHttpMethod: unsupported verb $verb (custom verbs are not supported)") + } + storeByte(varBase, caseIndex.toByte()) // all supported cases are tag-only, no payload to write +} + +/** + * Lowers `list`, porting the JS-path `parsePathSegments` splitting logic: + * "{name}" -> path-variable, "{+rest}" -> remaining-path-variable, else literal. The + * `path-variable` record's only field (variable-name: string) sits at its own offset 0, so its + * payload is byte-identical to writing a bare string at the case's payload offset. + */ +private fun lowerPathSegmentsInto(base: Int, offset: Int, path: String) { + val segments = path.split("/").filter { it.isNotEmpty() } + writeListField( + base, + offset, + segments.size, + Layout.PATH_SEGMENT_SIZE, + Layout.PATH_SEGMENT_ALIGN, + ) { i, segPtr -> + val seg = segments[i] + if (seg.startsWith("{") && seg.endsWith("}")) { + val inner = seg.substring(1, seg.length - 1) + if (inner.startsWith("+")) { + storeByte(segPtr, Layout.PS_REMAINING_PATH_VARIABLE.toByte()) + writeStringField(segPtr, Layout.PATH_SEGMENT_PAYLOAD_OFFSET, inner.substring(1)) + } else { + storeByte(segPtr, Layout.PS_PATH_VARIABLE.toByte()) + writeStringField(segPtr, Layout.PATH_SEGMENT_PAYLOAD_OFFSET, inner) + } + } else { + storeByte(segPtr, Layout.PS_LITERAL.toByte()) + writeStringField(segPtr, Layout.PATH_SEGMENT_PAYLOAD_OFFSET, seg) + } + } +} + +private fun lowerCorsInto(base: Int, offset: Int, patterns: List) { + val corsBase = base + offset + if (patterns.isEmpty()) { + writeEmptyListField(corsBase, Layout.CO_ALLOWED_PATTERNS) + } else { + writeListField(corsBase, Layout.CO_ALLOWED_PATTERNS, patterns.size, 8, 4) { i, elemPtr -> + writeStringField(elemPtr, 0, patterns[i]) + } + } +} + +/** Lowers `option`: `none` when [auth] is false, `some({required: true})` otherwise. */ +private fun lowerAuthDetailsInto(base: Int, offset: Int, auth: Boolean) { + val optBase = base + offset + if (auth) { + storeByte(optBase, 1) // some + storeByte(optBase + Layout.AUTH_DETAILS_OPTION_PAYLOAD_OFFSET, 1) // auth-details.required = true + } else { + writeOptionNone(optBase, 0) + } +} + +private fun lowerHttpMountSome(base: Int, offset: Int, mountPath: String, mountAuth: Boolean, mountCors: List) { + val optBase = base + offset + storeByte(optBase, 1) // some + val mountBase = optBase + Layout.HTTP_MOUNT_OPTION_PAYLOAD_OFFSET + lowerPathSegmentsInto(mountBase, Layout.HMD_PATH_PREFIX, mountPath) + lowerAuthDetailsInto(mountBase, Layout.HMD_AUTH_DETAILS, mountAuth) + storeByte(mountBase + Layout.HMD_PHANTOM_AGENT, 0) // false + lowerCorsInto(mountBase, Layout.HMD_CORS_OPTIONS, mountCors) + writeEmptyListField(mountBase, Layout.HMD_WEBHOOK_SUFFIX) +} + +/** Lowers `enum agent-mode` from `@Agent(mode=...)`'s `"durable"`/`"ephemeral"` string. */ +internal fun lowerAgentModeInto(base: Int, offset: Int, mode: String) { + val caseIndex = when (mode) { + "durable" -> Layout.AGENT_MODE_DURABLE + "ephemeral" -> Layout.AGENT_MODE_EPHEMERAL + else -> error("native lowerAgentMode: unsupported mode \"$mode\" (expected \"durable\" or \"ephemeral\")") + } + storeByte(base + offset, caseIndex.toByte()) +} + +/** + * Lowers the `snapshotting` variant from the DSL string accepted by `@Agent(snapshotting=...)` + * -- the same DSL as Scala's `Snapshotting.parse`: `"disabled"`, `"enabled"`, + * `"periodic()"`, or `"every()"` (count must fit u16: 0..65535). No `Regex` use -- + * Kotlin/Wasm's stdlib regex support is not exercised anywhere else in this SDK, so this parses + * with plain string ops to stay consistent with the rest of the native runtime. + */ +internal fun lowerSnapshottingInto(base: Int, offset: Int, snapshotting: String) { + val varBase = base + offset + when { + snapshotting == "disabled" -> storeByte(varBase, Layout.SNAP_DISABLED.toByte()) + + snapshotting == "enabled" -> { + storeByte(varBase, Layout.SNAP_ENABLED.toByte()) + storeByte(varBase + Layout.SNAPSHOTTING_PAYLOAD_OFFSET, Layout.SNAPCFG_DEFAULT.toByte()) + } + + snapshotting.startsWith("periodic(") && snapshotting.endsWith(")") -> { + val nanos = snapshotting.substring("periodic(".length, snapshotting.length - 1).toLongOrNull() + ?: error("native lowerSnapshotting: \"$snapshotting\" -- periodic(...) argument must be an integer nanosecond count") + storeByte(varBase, Layout.SNAP_ENABLED.toByte()) + val cfgBase = varBase + Layout.SNAPSHOTTING_PAYLOAD_OFFSET + storeByte(cfgBase, Layout.SNAPCFG_PERIODIC.toByte()) + storeLong(cfgBase + Layout.SNAPSHOTTING_CONFIG_PAYLOAD_OFFSET, nanos) + } + + snapshotting.startsWith("every(") && snapshotting.endsWith(")") -> { + val count = snapshotting.substring("every(".length, snapshotting.length - 1).toIntOrNull() + ?: error("native lowerSnapshotting: \"$snapshotting\" -- every(...) argument must be an integer invocation count") + require(count in 0..65535) { "native lowerSnapshotting: every() must fit u16, got $count" } + storeByte(varBase, Layout.SNAP_ENABLED.toByte()) + val cfgBase = varBase + Layout.SNAPSHOTTING_PAYLOAD_OFFSET + storeByte(cfgBase, Layout.SNAPCFG_EVERY_N_INVOCATION.toByte()) + storeShort(cfgBase + Layout.SNAPSHOTTING_CONFIG_PAYLOAD_OFFSET, count.toShort()) + } + + else -> error( + "native lowerSnapshotting: unsupported snapshotting DSL \"$snapshotting\" " + + "(expected \"disabled\", \"enabled\", \"periodic()\", or \"every()\")", + ) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Checkpoint.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Checkpoint.kt new file mode 100644 index 0000000000..e4ab8ce0c3 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Checkpoint.kt @@ -0,0 +1,63 @@ +package cloud.golem.runtime + +// Native port of Scala's Checkpoint.scala. Same situation as Transactions.kt: pure application +// logic built entirely on HostApi.getOplogIndex/setOplogIndex (no new WIT surface, no new host +// imports), ported synchronously rather than Future-based -- the underlying host calls have no +// async boundary, so Scala's Future wrapping (a Scala.js environment artifact) doesn't carry +// over. Reuses Transactions.kt's Either (same package, first and so-far-only other consumer). + +/** + * Captures the current oplog index and can revert execution to that point. Create one with + * [Checkpoint.invoke] (mirrors Scala's `Checkpoint()` factory call), or use + * [Checkpoint.withCheckpoint] / [Checkpoint.withCheckpointTry] to run a block with automatic + * revert on failure. + */ +class Checkpoint private constructor(private val oplogIndex: Long) { + /** Reverts execution to the oplog index captured when this checkpoint was created. Never returns normally. */ + fun revert(): Nothing { + HostApi.setOplogIndex(oplogIndex) + error("Unreachable: reverted to checkpoint") + } + + /** Returns the successful value, or reverts to the checkpoint if [result] is a [Either.Left]. */ + fun unwrapOrRevert(result: Either<*, T>): T = when (result) { + is Either.Right -> result.value + is Either.Left -> revert() + } + + /** Runs [fn], reverting to the checkpoint if it returns a [Either.Left]. */ + fun runOrRevert(fn: () -> Either<*, T>): T = unwrapOrRevert(fn()) + + /** Runs [fn], reverting to the checkpoint if it throws. */ + fun tryOrRevert(fn: () -> T): T = try { + fn() + } catch (e: Throwable) { + revert() + } + + /** Reverts to the checkpoint if [condition] is false. */ + fun assertOrRevert(condition: Boolean) { + if (!condition) revert() + } + + companion object { + /** Creates a new checkpoint at the current oplog index. */ + operator fun invoke(): Checkpoint = Checkpoint(HostApi.getOplogIndex()) + + /** Creates a checkpoint and runs [fn]; reverts if it returns a [Either.Left]. */ + fun withCheckpoint(fn: (Checkpoint) -> Either<*, T>): T { + val cp = Checkpoint() + return cp.unwrapOrRevert(fn(cp)) + } + + /** Creates a checkpoint and runs [fn]; reverts if it throws. */ + fun withCheckpointTry(fn: (Checkpoint) -> T): T { + val cp = Checkpoint() + return try { + fn(cp) + } catch (e: Throwable) { + cp.revert() + } + } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Guards.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Guards.kt new file mode 100644 index 0000000000..8fa32fd88a --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Guards.kt @@ -0,0 +1,123 @@ +package cloud.golem.runtime + +import cloud.golem.runtime.host.NamedPolicy +import cloud.golem.runtime.host.NamedRetryPolicy +import cloud.golem.runtime.host.RetryApi +import cloud.golem.runtime.host.toNamedRetryPolicy + +// Native port of Scala's Guards.scala. Unlike the Scala.js version (which wraps every guard in +// a Future -- an artifact of Scala.js's single-threaded, callback-based environment), this SDK's +// underlying HostApi calls are all plain synchronous functions with no async host boundary at +// all, so the native port is synchronous throughout: no coroutines, no suspend. Matches +// CLAUDE.md's own guidance ("map suspend -> Golem durable suspension where the host model +// allows") -- here the host model doesn't involve async at all, so a synchronous translation is +// the faithful one, not a simplification. +// +// `useRetryPolicy`/`withRetryPolicy` mirror Scala's Guards: they register a named retry policy for +// the duration of a scope and restore the policy previously registered under the same name +// afterward (removing it if none existed). They build directly on `RetryApi` (the host binding) +// and the Retry DSL's `NamedPolicy` -- both fully ported -- so no blocker remains. + +/** Scoped runtime controls. Each `use*` applies a change and returns a [Guard] that restores + * the previous value when [Guard.drop] (or [Guard.close]) is called; each `with*` applies the + * change for the duration of [block] and guarantees restoration afterward. */ +object Guards { + fun usePersistenceLevel(level: HostApi.PersistenceLevel): PersistenceLevelGuard { + val original = HostApi.getOplogPersistenceLevel() + HostApi.setOplogPersistenceLevel(level) + return PersistenceLevelGuard { HostApi.setOplogPersistenceLevel(original) } + } + + fun withPersistenceLevel(level: HostApi.PersistenceLevel, block: () -> A): A { + val guard = usePersistenceLevel(level) + try { + return block() + } finally { + guard.drop() + } + } + + fun useIdempotenceMode(flag: Boolean): IdempotenceModeGuard { + val original = HostApi.getIdempotenceMode() + HostApi.setIdempotenceMode(flag) + return IdempotenceModeGuard { HostApi.setIdempotenceMode(original) } + } + + fun withIdempotenceMode(flag: Boolean, block: () -> A): A { + val guard = useIdempotenceMode(flag) + try { + return block() + } finally { + guard.drop() + } + } + + fun markAtomicOperation(): AtomicOperationGuard { + val begin = HostApi.markBeginOperation() + return AtomicOperationGuard { HostApi.markEndOperation(begin) } + } + + /** + * Executes [block] atomically. On success the atomic region is committed via + * `markEndOperation`. On failure, calls the host `trap` function, which surfaces as an + * uncatchable wasm trap so caller code cannot observe the failure via `try`/`catch`. The + * atomic region is intentionally left open -- the existing replay-time fallback in + * `markBeginOperation` deletes the partial inner side effects and re-executes the block. + */ + fun atomically(block: () -> A): A { + val guard = markAtomicOperation() + return try { + val result = block() + guard.drop() + result + } catch (e: Throwable) { + HostApi.trap("atomic block failed: ${e.stackTraceToString()}") + } + } + + /** + * Registers [policy] as the current agent's retry policy, returning a [RetryPolicyGuard] that + * on [Guard.drop] (or [Guard.close]) restores the policy previously registered under the same + * name -- or removes it if none existed. Faithful port of Scala's `Guards.useRetryPolicy`. + */ + fun useRetryPolicy(policy: NamedRetryPolicy): RetryPolicyGuard { + val previous = RetryApi.getRetryPolicyByName(policy.name) + val name = policy.name + RetryApi.setRetryPolicy(policy) + return RetryPolicyGuard { + if (previous != null) RetryApi.setRetryPolicy(previous) else RetryApi.removeRetryPolicy(name) + } + } + + /** DSL overload of [useRetryPolicy] taking a [NamedPolicy] built with the Retry DSL. */ + fun useRetryPolicy(policy: NamedPolicy): RetryPolicyGuard = useRetryPolicy(policy.toNamedRetryPolicy()) + + /** Registers [policy] for the duration of [block], restoring the previous policy afterward. */ + fun withRetryPolicy(policy: NamedRetryPolicy, block: () -> A): A { + val guard = useRetryPolicy(policy) + try { + return block() + } finally { + guard.drop() + } + } + + /** DSL overload of [withRetryPolicy] taking a [NamedPolicy] built with the Retry DSL. */ + fun withRetryPolicy(policy: NamedPolicy, block: () -> A): A = withRetryPolicy(policy.toNamedRetryPolicy(), block) + + sealed class Guard(private val release: () -> Unit) : AutoCloseable { + private var active = true + final override fun close() = drop() + fun drop() { + if (active) { + active = false + release() + } + } + } + + class PersistenceLevelGuard internal constructor(release: () -> Unit) : Guard(release) + class IdempotenceModeGuard internal constructor(release: () -> Unit) : Guard(release) + class AtomicOperationGuard internal constructor(release: () -> Unit) : Guard(release) + class RetryPolicyGuard internal constructor(release: () -> Unit) : Guard(release) +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Guest.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Guest.kt new file mode 100644 index 0000000000..96b6036c93 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Guest.kt @@ -0,0 +1,225 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.buildSchemaValueTree +import cloud.golem.wasm.liftParamRecord +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.resetHeap +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt +import cloud.golem.wasm.writeListField +import cloud.golem.wasm.writeStringField + +/** + * Canonical-ABI implementations for `golem:agent/guest@2.0.0`'s four functions. Layout verified + * via `wit-parser::SizeAlign` + `Resolve::wasm_signature(GuestExport)` against the real WIT (see + * docs/spikes/compile-to-wasm-poc for the verification tool): + * + * initialize(agent-type: string, input: schema-value-tree, principal: principal) + * -> result<_, agent-error> + * invoke(method-name: string, input: schema-value-tree, principal: principal) + * -> result, agent-error> + * get-definition() -> agent-type + * discover-agent-types() -> result, agent-error> + * + * `initialize`/`invoke` each take 3 logical params too complex to flatten into scalar wasm + * params, so the canonical ABI bundles them behind a SINGLE indirect pointer (`indirect_params`) + * -- an anonymous record of the 3 params in declaration order, size=136 align=8. Both results are + * likewise too complex for scalar registers (`retptr`), so each function returns a single i32 + * pointer to its own result area, which the guest allocates. + * + * These 8 functions (the 4 above + their `cabi_post_*` companions) are deliberately NOT + * `@WasmExport`-annotated here. A Kotlin/Wasm reactor component (invoked via exported interface + * functions, never via `main()`) does not run top-level property initializers automatically at + * instantiation, and dead-code elimination strips anything unreachable from an actual export -- + * so agent registration (which lives in KSP-generated, per-project code the SDK can't see ahead + * of time) needs a guaranteed-reachable trigger. The KSP-generated registration file + * is the thing that carries the real `@WasmExport` annotations: it declares the four exports, + * calls its generated `registerAllAgents()` once (idempotently) at the top of each, and then + * delegates to the plain functions below. This makes registration a `@WasmExport`-native problem + * (the host guarantees the wrapper runs) rather than a top-level-initialization-timing problem. + */ +private object GuestLayout { + // args-struct (initialize/invoke): size=136 align=8 + const val ARGS_STRING_PARAM = 0 // string (agent-type name / method-name), size 8 + const val ARGS_INPUT = 8 // schema-value-tree, size 12 + const val ARGS_PRINCIPAL = 24 // principal (size 112, align 8) -- the authenticated caller + + // result<_, agent-error> / result, agent-error> / + // result, agent-error>: all size=40 align=4, payload_offset=4 (tag_size=1 + // rounded up to the max case align of 4, since agent-error's own align is 4). + const val RESULT_SIZE = 40 + const val RESULT_ALIGN = 4 + const val RESULT_PAYLOAD_OFFSET = 4 + const val RESULT_OK = 0 + const val RESULT_ERR = 1 + + // option: size=16 align=4, payload_offset=4 + const val OPTION_SVT_PAYLOAD_OFFSET = 4 + + // variant agent-error: size=36 align=4, payload_offset=4 -- all cases used here + // (invalid-input/invalid-method/invalid-type/invalid-agent-id) carry a single string message. + const val AGENT_ERROR_PAYLOAD_OFFSET = 4 + const val AE_INVALID_INPUT = 0 + const val AE_INVALID_METHOD = 1 + const val AE_INVALID_TYPE = 2 + const val AE_INVALID_AGENT_ID = 3 + + // record agent-type: size=176 (from AgentTypeModel.Layout; duplicated here as a plain Int + // since that Layout object is private to AgentTypeModel.kt). + const val AGENT_TYPE_SIZE = 176 + const val AGENT_TYPE_ALIGN = 8 +} + +private fun agentErrorCaseIndex(tag: String): Int = when (tag) { + "invalid-input" -> GuestLayout.AE_INVALID_INPUT + "invalid-method" -> GuestLayout.AE_INVALID_METHOD + "invalid-type" -> GuestLayout.AE_INVALID_TYPE + "invalid-agent-id" -> GuestLayout.AE_INVALID_AGENT_ID + else -> GuestLayout.AE_INVALID_INPUT +} + +/** Write `result<..., agent-error> = err(agent-error)` into a fresh result area; return its pointer. */ +private fun lowerErrResult(tag: String, message: String): Int { + val base = alloc(GuestLayout.RESULT_SIZE, GuestLayout.RESULT_ALIGN) + storeByte(base, GuestLayout.RESULT_ERR.toByte()) + val errBase = base + GuestLayout.RESULT_PAYLOAD_OFFSET + storeByte(errBase, agentErrorCaseIndex(tag).toByte()) + writeStringField(errBase, GuestLayout.AGENT_ERROR_PAYLOAD_OFFSET, message) + return base +} + +/** Write `result<_, agent-error> = ok(_)` (unit ok payload -- nothing else to write). */ +private fun lowerUnitOkResult(): Int { + val base = alloc(GuestLayout.RESULT_SIZE, GuestLayout.RESULT_ALIGN) + storeByte(base, GuestLayout.RESULT_OK.toByte()) + return base +} + +/** Write `result, agent-error> = ok(option)` (some or none). */ +private fun lowerInvokeOkResult(output: SchemaValue?): Int { + val base = alloc(GuestLayout.RESULT_SIZE, GuestLayout.RESULT_ALIGN) + storeByte(base, GuestLayout.RESULT_OK.toByte()) + val optBase = base + GuestLayout.RESULT_PAYLOAD_OFFSET + if (output == null) { + storeByte(optBase, 0) // none + } else { + storeByte(optBase, 1) // some + val treePtr = buildSchemaValueTree(output) + // Copy the 12-byte schema-value-tree {value-nodes.ptr, value-nodes.len, root} inline + // into the option's payload region (a record-typed option payload is stored inline, not + // via a further pointer indirection). + val svtBase = optBase + GuestLayout.OPTION_SVT_PAYLOAD_OFFSET + storeInt(svtBase, loadInt(treePtr)) + storeInt(svtBase + 4, loadInt(treePtr + 4)) + storeInt(svtBase + 8, loadInt(treePtr + 8)) + } + return base +} + +/** Write `result, agent-error> = ok(list)`. */ +private fun lowerDiscoverOkResult(descriptors: List): Int { + val base = alloc(GuestLayout.RESULT_SIZE, GuestLayout.RESULT_ALIGN) + storeByte(base, GuestLayout.RESULT_OK.toByte()) + val listFieldBase = base + GuestLayout.RESULT_PAYLOAD_OFFSET + writeListField( + listFieldBase, + 0, + descriptors.size, + GuestLayout.AGENT_TYPE_SIZE, + GuestLayout.AGENT_TYPE_ALIGN, + ) { i, elemPtr -> + // lowerAgentType allocates its own 176-byte block; list elements must be one contiguous + // buffer, so copy that block's bytes into this element's slot. + val agentTypePtr = lowerAgentType(descriptors[i]) + for (b in 0 until GuestLayout.AGENT_TYPE_SIZE) storeByte(elemPtr + b, loadByte(agentTypePtr + b)) + } + return base +} + +fun initialize(argsPtr: Int): Int { + val agentTypeName = liftString( + loadInt(argsPtr + GuestLayout.ARGS_STRING_PARAM), + loadInt(argsPtr + GuestLayout.ARGS_STRING_PARAM + 4), + ) + val inputTreePtr = argsPtr + GuestLayout.ARGS_INPUT + NativeAgentRuntime.currentPrincipal = liftPrincipal(argsPtr + GuestLayout.ARGS_PRINCIPAL) + NativeAgentRuntime.initializationPrincipal = NativeAgentRuntime.currentPrincipal + + if (NativeAgentRuntime.current != null) { + return lowerErrResult("invalid-input", "Agent already initialized") + } + val descriptor = NativeAgentRuntime.lookup(agentTypeName) + ?: return lowerErrResult("invalid-type", "Unknown agent type: $agentTypeName") + + return try { + val params = liftParamRecord(inputTreePtr, descriptor.constructorParams.map { it.witType }) + NativeAgentRuntime.current = descriptor.factory(params) + NativeAgentRuntime.currentDescriptor = descriptor + lowerUnitOkResult() + } catch (e: AgentException) { + lowerErrResult(e.tag, e.message ?: "initialization failed") + } catch (e: Throwable) { + lowerErrResult("invalid-input", e.message ?: "initialization failed") + } +} + +fun cabiPostInitialize(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} + +fun invoke(argsPtr: Int): Int { + val methodName = liftString( + loadInt(argsPtr + GuestLayout.ARGS_STRING_PARAM), + loadInt(argsPtr + GuestLayout.ARGS_STRING_PARAM + 4), + ) + val inputTreePtr = argsPtr + GuestLayout.ARGS_INPUT + NativeAgentRuntime.currentPrincipal = liftPrincipal(argsPtr + GuestLayout.ARGS_PRINCIPAL) + + val agent = NativeAgentRuntime.current + ?: return lowerErrResult("invalid-input", "Agent not initialized -- call initialize first") + val descriptor = NativeAgentRuntime.currentDescriptor + ?: return lowerErrResult("invalid-input", "No agent descriptor -- call initialize first") + val method = descriptor.methods.find { it.name == methodName } + ?: return lowerErrResult("invalid-method", "Unknown method: $methodName") + + return try { + val params = liftParamRecord(inputTreePtr, method.inputParams.map { it.witType }) + val result = method.handler(agent, params) + val output = if (result is SchemaValue.Unit_) null else result + lowerInvokeOkResult(output) + } catch (e: AgentException) { + lowerErrResult(e.tag, e.message ?: "invocation failed") + } catch (e: Throwable) { + lowerErrResult("invalid-input", e.message ?: "invocation failed") + } +} + +fun cabiPostInvoke(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} + +fun getDefinition(): Int { + val descriptor = NativeAgentRuntime.currentDescriptor ?: NativeAgentRuntime.all().firstOrNull() + return lowerAgentType( + descriptor ?: NativeAgentDescriptor("unknown", "", "", emptyList(), emptyList(), factory = { Unit }), + ) +} + +fun cabiPostGetDefinition(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} + +fun discoverAgentTypes(): Int = try { + lowerDiscoverOkResult(NativeAgentRuntime.all()) +} catch (e: Throwable) { + lowerErrResult("invalid-input", e.message ?: "discovery failed") +} + +fun cabiPostDiscoverAgentTypes(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/HostApi.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/HostApi.kt new file mode 100644 index 0000000000..7abf339db9 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/HostApi.kt @@ -0,0 +1,560 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong +import cloud.golem.wasm.storeByte + +// Raw canonical-ABI import bindings to golem:api/host@1.5.0. Signatures verified via +// wit-parser::Resolve::wasm_signature(AbiVariant::GuestImport) against +// wit-native/deps/golem-1.x/golem-host.wit (proven end-to-end with a temp spike: @WasmImport's +// (module, name) pair matches wit-bindgen's own naming convention exactly -- wasm-tools +// component embed resolves it as a real `import golem:api/host@1.5.0;`, not a leftover raw +// import). IMPORTANT: for imports (unlike exports), `retptr=true` means the GUEST allocates +// the result area and passes its pointer as an EXTRA PARAMETER (the host writes into it and +// returns nothing) -- the opposite of the export convention this SDK's Guest.kt/ToolGuest.kt +// use, where the guest allocates and RETURNS the pointer. Confirmed against wit-parser's own +// abi.rs source comment ("Imports take a return pointer to write into and exports return a +// pointer they wrote into"), not assumed by analogy -- an earlier draft of this file got +// exactly this backwards for generate-idempotency-key, caught only by re-deriving the ground +// truth per-function rather than trusting a first pass. +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "get-oplog-index") +private external fun hostGetOplogIndex(): Long + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "set-oplog-index") +private external fun hostSetOplogIndex(idx: Long) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "mark-begin-operation") +private external fun hostMarkBeginOperation(): Long + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "mark-end-operation") +private external fun hostMarkEndOperation(begin: Long) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "oplog-commit") +private external fun hostOplogCommit(replicas: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "trap") +private external fun hostTrap(reasonPtr: Int, reasonLen: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "get-oplog-persistence-level") +private external fun hostGetOplogPersistenceLevel(): Int + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "set-oplog-persistence-level") +private external fun hostSetOplogPersistenceLevel(level: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "get-idempotence-mode") +private external fun hostGetIdempotenceMode(): Int + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "set-idempotence-mode") +private external fun hostSetIdempotenceMode(flag: Int) + +// generate-idempotency-key(): uuid -- an import with retptr=true, so its core signature is +// (ptr: i32) -> () (the guest allocates a 16-byte `uuid` {high-bits: u64, low-bits: u64} area +// and passes its address; the host writes the result there and returns nothing). +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "generate-idempotency-key") +private external fun hostGenerateIdempotencyKey(retPtr: Int) + +// Agent metadata/registry imports. All `agent-id` parameters flatten to 4 core +// words (component-id.uuid.{high,low}: I64, I64 + the agent-id string: Pointer, Length) -- +// verified via wit-parser::wasm_signature(GuestImport) against wit-native/deps/golem-1.x/ +// golem-host.wit, matching every call site below exactly (e.g. fork-agent's 9 params = two +// flattened agent-ids + one oplog-index). +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "get-self-metadata") +private external fun hostGetSelfMetadata(retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "get-agent-metadata") +private external fun hostGetAgentMetadata(compHigh: Long, compLow: Long, idPtr: Int, idLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "resolve-component-id") +private external fun hostResolveComponentId(refPtr: Int, refLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "resolve-agent-id") +private external fun hostResolveAgentId(refPtr: Int, refLen: Int, namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "resolve-agent-id-strict") +private external fun hostResolveAgentIdStrict(refPtr: Int, refLen: Int, namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "update-agent") +private external fun hostUpdateAgent(compHigh: Long, compLow: Long, idPtr: Int, idLen: Int, targetRevision: Long, mode: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "fork-agent") +private external fun hostForkAgent( + srcCompHigh: Long, + srcCompLow: Long, + srcIdPtr: Int, + srcIdLen: Int, + tgtCompHigh: Long, + tgtCompLow: Long, + tgtIdPtr: Int, + tgtIdLen: Int, + cutOff: Long, +) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "revert-agent") +private external fun hostRevertAgent(compHigh: Long, compLow: Long, idPtr: Int, idLen: Int, targetTag: Int, targetPayload: Long) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "fork") +private external fun hostFork(retPtr: Int) + +// parse-agent-id lives on a DIFFERENT WIT interface than everything above: golem:agent/host@2.0.0 +// (package golem:agent@2.0.0, interface "host"), not golem:api/host@1.5.0. Unlike that interface, +// agent-guest's own world does NOT import it transitively (verified in +// wit/deps/golem-agent/guest.wit -- agent-guest only imports golem:api/host@1.5.0 and `common`), +// so wit-native/main.wit's kotlin-agent world needed an explicit `import golem:agent/host@2.0.0;` +// added for this one function. +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "parse-agent-id") +private external fun hostParseAgentId(idPtr: Int, idLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "get-all-agent-types") +private external fun hostGetAllAgentTypes(retPtr: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "get-agent-type") +private external fun hostGetAgentType(namePtr: Int, nameLen: Int, retPtr: Int) + +// ----- Resource-handle canonical ABI (first use in this SDK; unblocks the previously-deferred +// tool-rpc/secret-value/quota-token-handle/lazy-initialized-pollable/get-promise-result/oplog +// work too, one at a time). A WIT `resource` compiles to three kinds of raw core imports, none +// of them declared in the WIT source text itself -- they're canonical-ABI intrinsics wit-parser +// synthesizes per resource: +// - `[constructor]`: a normal function import; RESULT is the handle (i32). +// - `[method].`: a normal function import; FIRST param is the +// self handle (i32). +// - `[resource-drop]`: releases the guest's handle-table entry on the host +// side. NOT part of `iface.functions` (so abi-dump's `sig`/generic dump modes never see +// it) -- its name is synthesized by `Resolve::wasm_import_name` (verified by reading +// wit-parser's resolve.rs directly, since no existing tool surfaced it): for an imported +// resource under the Legacy/sync ABI (what this whole SDK uses), the raw name is exactly +// `[resource-drop]` with an EMPTY prefix (the empty prefix comes from +// `LiftLowerAbi::Sync::import_prefix()` returning "" -- confirmed in wit-parser's lib.rs, +// not assumed). Skipping the drop call would leak the handle on the host side until the +// whole component instance tears down -- callers of `GetAgentsHandle` MUST call `close()`. +// Constructor/method signatures are still fetched the normal way (abi-dump's `sig` mode, same +// as every other import in this file) since those two ARE normal `iface.functions` entries. +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "[constructor]get-agents") +private external fun hostGetAgentsConstructor(compHigh: Long, compLow: Long, hasFilter: Int, filterPtr: Int, filterLen: Int, precise: Int): Int + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "[method]get-agents.get-next") +private external fun hostGetAgentsGetNext(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/host@1.5.0", "[resource-drop]get-agents") +private external fun hostGetAgentsDrop(handle: Int) + +/** + * A handle to an in-progress agent enumeration (`golem:api/host@1.5.0`'s `get-agents` + * resource). Filtering (`agent-any-filter`) is not yet supported -- this always enumerates + * every agent of every agent type in the given component; the filter builders are their own + * follow-up (the agent filter builders remain deferred). + * + * MUST be [close]d when done: this wraps a raw component-model resource handle, which is not + * tied to Kotlin/Wasm's own GC -- an unclosed handle stays live in the host's resource table + * until the whole component instance tears down. + */ +class GetAgentsHandle internal constructor(private val handle: Int) { + private var closed = false + + /** The next batch of agent metadata, or `null` when the enumeration is exhausted. */ + fun getNext(): List? { + check(!closed) { "GetAgentsHandle already closed" } + val retPtr = alloc(12, 4) // option>: tag@0(1,1), payload@4(8,4) -- NOT + // the align-8 shape liftOption assumes (list is only align-4), so this is hand-rolled. + hostGetAgentsGetNext(handle, retPtr) + if (loadByte(retPtr).toInt() == 0) return null + val listBase = retPtr + 4 + val dataPtr = loadInt(listBase) + val len = loadInt(listBase + 4) + return (0 until len).map { i -> liftAgentMetadata(dataPtr + i * 88) } + } + + fun close() { + if (!closed) { + hostGetAgentsDrop(handle) + closed = true + } + } +} + +/** A Golem UUID (two u64 halves, matching `golem:core/types@2.0.0`'s `uuid` record). */ +data class Uuid(val highBits: Long, val lowBits: Long) + +/** Matches `golem:core/types@2.0.0`'s `component-id` record: `{uuid: uuid}` (16 bytes, align 8). */ +data class ComponentId(val uuid: Uuid) + +/** Matches `golem:core/types@2.0.0`'s `environment-id` record: `{uuid: uuid}` (16 bytes, align 8). */ +data class EnvironmentId(val uuid: Uuid) + +/** + * Matches `golem:core/types@2.0.0`'s `agent-id` record: `{component-id: component-id, + * agent-id: string}` (24 bytes, align 8) -- the canonical string form of the agent's identity + * (component + agent type + constructor parameters), the same string `BaseAgent.agentId` + * surfaces. + */ +data class AgentId(val componentId: ComponentId, val agentId: String) + +/** Matches `golem:api/host@1.5.0`'s `agent-status` enum case order exactly. */ +enum class AgentStatus { RUNNING, IDLE, SUSPENDED, INTERRUPTED, RETRYING, FAILED, EXITED } + +/** Matches `golem:api/host@1.5.0`'s `update-mode` enum case order exactly. */ +enum class UpdateMode { AUTOMATIC, SNAPSHOT_BASED } + +/** Matches `golem:api/host@1.5.0`'s `revert-agent-target` variant (payload always `oplog-index`/`u64`). */ +sealed class RevertAgentTarget { + data class RevertToOplogIndex(val oplogIndex: Long) : RevertAgentTarget() + data class RevertLastInvocations(val count: Long) : RevertAgentTarget() +} + +/** Matches `golem:api/host@1.5.0`'s `fork-result` variant (payload always `fork-details { forked-phantom-id: uuid }`). */ +sealed class ForkResult { + data class Original(val forkedPhantomId: Uuid) : ForkResult() + data class Forked(val forkedPhantomId: Uuid) : ForkResult() +} + +/** Matches `golem:api/host@1.5.0`'s `agent-metadata` record (88 bytes, align 8) field-for-field. */ +data class AgentMetadata( + val agentId: AgentId, + val args: List, + val env: List>, + val config: List>, + val status: AgentStatus, + val componentRevision: Long, + val retryCount: Long, + val environmentId: EnvironmentId, +) + +// ----- Fixed-schema record lift/lower for the agent-id/agent-metadata family -------------- +// These are hand-rolled (not routed through the generic schema-value-tree machinery in +// Lift.kt/Lower.kt): the shapes here are compile-time-known host-API records, not runtime +// agent payloads, so a direct byte-offset decode is simpler and matches the pattern already +// used for `uuid` in `generateIdempotencyKey`. All offsets verified via +// wit-parser::SizeAlign against wit-native/deps/golem-1.x/golem-host.wit and +// wit-native/deps/golem-core-v2/golem-core-v2.wit, not hand-derived. + +internal fun liftComponentId(base: Int): ComponentId = ComponentId(Uuid(loadLong(base), loadLong(base + 8))) + +private fun liftEnvironmentId(base: Int): EnvironmentId = EnvironmentId(Uuid(loadLong(base), loadLong(base + 8))) + +// agent-id: {component-id: offset=0 (16,8), agent-id: offset=16 (string, 8,4)} +private fun liftAgentId(base: Int): AgentId = AgentId(liftComponentId(base), liftString(loadInt(base + 16), loadInt(base + 20))) + +private fun liftListOfString(base: Int): List { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> + val elemPtr = dataPtr + i * 8 + liftString(loadInt(elemPtr), loadInt(elemPtr + 4)) + } +} + +// list>: each element is 16 bytes (two 8-byte strings back to back). +private fun liftListOfStringPair(base: Int): List> { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> + val elemPtr = dataPtr + i * 16 + liftString(loadInt(elemPtr), loadInt(elemPtr + 4)) to liftString(loadInt(elemPtr + 8), loadInt(elemPtr + 12)) + } +} + +// agent-metadata: size=88 align=8 -- agent-id@0(24,8), args@24(8,4), env@32(8,4), config@40(8,4), +// status@48(1,1), component-revision@56(8,8), retry-count@64(8,8), environment-id@72(16,8). +private fun liftAgentMetadata(base: Int): AgentMetadata = AgentMetadata( + agentId = liftAgentId(base), + args = liftListOfString(base + 24), + env = liftListOfStringPair(base + 32), + config = liftListOfStringPair(base + 40), + status = AgentStatus.entries[loadByte(base + 48).toInt() and 0xFF], + componentRevision = loadLong(base + 56), + retryCount = loadLong(base + 64), + environmentId = liftEnvironmentId(base + 72), +) + +// option: tag byte @0, payload @ align_to(1, align(T)) -- 8 for every T used here (all align-8). +internal fun liftOption(base: Int, liftPayload: (Int) -> T): T? = if (loadByte(base).toInt() == 0) null else liftPayload(base + 8) + +/** + * `golem:agent@2.0.0`'s `agent-error` variant. The four string-payload cases are fully + * decoded; `custom-error`'s payload is a `typed-schema-value` (constructor-parameter shaped: + * a `schema-graph` + `schema-value-tree` pair) which this SDK does not yet lift generically -- + * see `ParsedAgentId`'s doc comment for why that's out of scope here. + */ +sealed class AgentError { + data class InvalidInput(val message: String) : AgentError() + data class InvalidMethod(val message: String) : AgentError() + data class InvalidType(val message: String) : AgentError() + data class InvalidAgentId(val message: String) : AgentError() + object CustomError : AgentError() +} + +// agent-error: size=36 align=4, tag_size=1, payload_offset=4 (max of the 4 string payloads +// [8 bytes] and custom-error's typed-schema-value [32 bytes]). +private fun liftAgentError(base: Int): AgentError { + val payload = base + 4 + return when (val tag = loadByte(base).toInt() and 0xFF) { + 0 -> AgentError.InvalidInput(liftString(loadInt(payload), loadInt(payload + 4))) + 1 -> AgentError.InvalidMethod(liftString(loadInt(payload), loadInt(payload + 4))) + 2 -> AgentError.InvalidType(liftString(loadInt(payload), loadInt(payload + 4))) + 3 -> AgentError.InvalidAgentId(liftString(loadInt(payload), loadInt(payload + 4))) + 4 -> AgentError.CustomError + else -> error("unknown agent-error tag: $tag") + } +} + +/** + * The result of `parseAgentId`: the agent's type name (from `@agentDefinition`) and its + * optional phantom UUID. + * + * Deliberately does NOT expose the constructor parameters (`typed-schema-value` in the WIT + * result) that `parse-agent-id` also returns: decoding it generically requires lifting an + * arbitrary `schema-graph` (a full recursive type-description structure, comparable in size + * to the whole `schema-value-tree` value model), and + * even Scala's own `HostApi.parseAgentId` drops it from its public API for the same reason + * (`AgentIdParts` only carries `agentTypeName`/`phantom`). This is why `BaseAgent.agentName` + * remains unwired: there is no well-defined, host-documented way to derive "the agent's name" + * from the WIT-level API alone (Scala's own `agentName` comes from a JS-shim-only field with + * no WIT equivalent -- not a mechanism this native path can reuse). + */ +data class ParsedAgentId(val agentTypeName: String, val phantom: Uuid?) + +sealed class ParseAgentIdResult { + data class Ok(val value: ParsedAgentId) : ParseAgentIdResult() + data class Err(val error: AgentError) : ParseAgentIdResult() +} + +/** + * The agent-type name plus a raw pointer to the constructor-parameters `schema-value-tree`, from + * [HostApi.parseAgentIdConstructorParams]. Internal because [paramsValueTreePtr] is a live scratch + * pointer that must be lifted (via `liftParamRecord`) before the next `resetHeap`. + */ +internal class ConstructorParamsRef(val agentTypeName: String, val paramsValueTreePtr: Int) + +/** + * A registered agent type as reported by the Golem host registry: just the type name and the + * component that implements it. Mirrors Scala's public `RegisteredAgentType` exactly (see + * `HostApi.scala`'s `fromHostRegisteredAgentType`) -- Scala's own wrapper *also* projects only + * these two fields out of the full `registered-agent-type` (`{agent-type: agent-type, + * implemented-by: component-id}`, 192 bytes), discarding `agent-type`'s much richer + * schema/constructor/methods/http-mount details (176 bytes on its own). This SDK follows the + * same projection rather than lifting `agent-type` in full: a full lift would need to handle + * every `schema-type-body` variant case (36 of them) for an ARBITRARY registered agent, not + * just this component's own narrow, self-generated set (the export-side lowering in + * `AgentTypeModel.kt` only ever writes 2 of the 36 cases, because it only has to describe + * types this SDK itself produces). + */ +data class RegisteredAgentType(val typeName: String, val implementedBy: ComponentId) + +// registered-agent-type: size=192 align=8 { agent-type: offset=0 (176,8), implemented-by: +// offset=176 (16,8) }. Only agent-type's own first field (type-name: string @ offset 0) is +// read -- see RegisteredAgentType's doc comment. +private fun liftRegisteredAgentType(base: Int): RegisteredAgentType = RegisteredAgentType(liftString(loadInt(base), loadInt(base + 4)), liftComponentId(base + 176)) + +internal fun lowerStringToPtrLen(s: String): Pair { + val bytes = s.encodeToByteArray() + val ptr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + return ptr to bytes.size +} + +/** + * Native SDK access to Golem's runtime host API (`golem:api/host@1.5.0`). It covers + * the core oplog/atomic-region primitives, persistence level, idempotence mode, + * and idempotency-key generation -- the foundation the future durability/transaction/guard + * machinery builds on. Mirrors the Scala SDK's `HostApi` object + * (`sdks/scala/core/js/src/main/scala/golem/HostApi.scala`) for this subset; the rest of that + * file's surface (agent metadata/registry, fork/revert/update, promises, webhooks) is later + * increments of this same task. + */ +object HostApi { + /** Matches `golem:api/host@1.5.0`'s `persistence-level` variant case order exactly. */ + enum class PersistenceLevel { PERSIST_NOTHING, PERSIST_REMOTE_SIDE_EFFECTS, SMART } + + fun getOplogIndex(): Long = hostGetOplogIndex() + fun setOplogIndex(index: Long) = hostSetOplogIndex(index) + fun markBeginOperation(): Long = hostMarkBeginOperation() + fun markEndOperation(begin: Long) = hostMarkEndOperation(begin) + fun oplogCommit(replicas: Int) = hostOplogCommit(replicas) + + /** + * Unconditionally traps the current invocation with the given reason. This call never + * returns: it surfaces as an uncatchable wasm trap on the host side and the worker enters + * the standard trap-recovery flow (mirrors the Scala SDK's `trap`, including the impossible + * fallback `error(...)` in case the host call ever returned). + */ + fun trap(reason: String): Nothing { + val bytes = reason.encodeToByteArray() + val ptr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + hostTrap(ptr, bytes.size) + error("trap host call returned unexpectedly: $reason") + } + + fun getOplogPersistenceLevel(): PersistenceLevel = PersistenceLevel.entries[hostGetOplogPersistenceLevel()] + fun setOplogPersistenceLevel(level: PersistenceLevel) = hostSetOplogPersistenceLevel(level.ordinal) + + fun getIdempotenceMode(): Boolean = hostGetIdempotenceMode() != 0 + fun setIdempotenceMode(flag: Boolean) = hostSetIdempotenceMode(if (flag) 1 else 0) + + fun generateIdempotencyKey(): Uuid { + val ptr = alloc(16, 8) // uuid: {high-bits: u64 @0, low-bits: u64 @8} + hostGenerateIdempotencyKey(ptr) + return Uuid(loadLong(ptr), loadLong(ptr + 8)) + } + + // ----- Agent metadata / registry ---------------------------------------- + + /** The current agent's own metadata, including its full `agent-id` string. */ + fun getSelfMetadata(): AgentMetadata { + val ptr = alloc(88, 8) + hostGetSelfMetadata(ptr) + return liftAgentMetadata(ptr) + } + + fun getAgentMetadata(agentId: AgentId): AgentMetadata? { + val (idPtr, idLen) = lowerStringToPtrLen(agentId.agentId) + val retPtr = alloc(96, 8) // option: tag@0(1,1), payload@8(88,8) + hostGetAgentMetadata(agentId.componentId.uuid.highBits, agentId.componentId.uuid.lowBits, idPtr, idLen, retPtr) + return liftOption(retPtr) { liftAgentMetadata(it) } + } + + fun resolveComponentId(componentReference: String): ComponentId? { + val (refPtr, refLen) = lowerStringToPtrLen(componentReference) + val retPtr = alloc(24, 8) // option: tag@0(1,1), payload@8(16,8) + hostResolveComponentId(refPtr, refLen, retPtr) + return liftOption(retPtr) { liftComponentId(it) } + } + + fun resolveAgentId(componentReference: String, agentName: String): AgentId? { + val (refPtr, refLen) = lowerStringToPtrLen(componentReference) + val (namePtr, nameLen) = lowerStringToPtrLen(agentName) + val retPtr = alloc(32, 8) // option: tag@0(1,1), payload@8(24,8) + hostResolveAgentId(refPtr, refLen, namePtr, nameLen, retPtr) + return liftOption(retPtr) { liftAgentId(it) } + } + + fun resolveAgentIdStrict(componentReference: String, agentName: String): AgentId? { + val (refPtr, refLen) = lowerStringToPtrLen(componentReference) + val (namePtr, nameLen) = lowerStringToPtrLen(agentName) + val retPtr = alloc(32, 8) + hostResolveAgentIdStrict(refPtr, refLen, namePtr, nameLen, retPtr) + return liftOption(retPtr) { liftAgentId(it) } + } + + fun updateAgent(agentId: AgentId, targetRevision: Long, mode: UpdateMode) { + val (idPtr, idLen) = lowerStringToPtrLen(agentId.agentId) + hostUpdateAgent(agentId.componentId.uuid.highBits, agentId.componentId.uuid.lowBits, idPtr, idLen, targetRevision, mode.ordinal) + } + + fun forkAgent(sourceAgentId: AgentId, targetAgentId: AgentId, cutOff: Long) { + val (srcPtr, srcLen) = lowerStringToPtrLen(sourceAgentId.agentId) + val (tgtPtr, tgtLen) = lowerStringToPtrLen(targetAgentId.agentId) + hostForkAgent( + sourceAgentId.componentId.uuid.highBits, sourceAgentId.componentId.uuid.lowBits, srcPtr, srcLen, + targetAgentId.componentId.uuid.highBits, targetAgentId.componentId.uuid.lowBits, tgtPtr, tgtLen, + cutOff, + ) + } + + fun revertAgent(agentId: AgentId, target: RevertAgentTarget) { + val (idPtr, idLen) = lowerStringToPtrLen(agentId.agentId) + val (tag, payload) = when (target) { + is RevertAgentTarget.RevertToOplogIndex -> 0 to target.oplogIndex + is RevertAgentTarget.RevertLastInvocations -> 1 to target.count + } + hostRevertAgent(agentId.componentId.uuid.highBits, agentId.componentId.uuid.lowBits, idPtr, idLen, tag, payload) + } + + /** Forks the current agent at the current execution point (see `fork-result`'s doc comment in `golem-host.wit`). */ + fun fork(): ForkResult { + val retPtr = alloc(24, 8) // fork-result: tag@0(1,1), payload@8(fork-details{uuid}, 16,8) + hostFork(retPtr) + val phantomId = Uuid(loadLong(retPtr + 8), loadLong(retPtr + 16)) + return when (loadByte(retPtr).toInt()) { + 0 -> ForkResult.Original(phantomId) + 1 -> ForkResult.Forked(phantomId) + else -> error("unknown fork-result tag: ${loadByte(retPtr)}") + } + } + + // ----- Agent-id parsing -------------------------------------------------- + + /** + * Parses an agent-id string (as returned by `getSelfMetadata().agentId.agentId` or + * `resolveAgentId`) into its agent-type name and optional phantom UUID. See + * [ParsedAgentId]'s doc comment for why the constructor parameters are not exposed. + */ + fun parseAgentId(agentId: String): ParseAgentIdResult { + val (idPtr, idLen) = lowerStringToPtrLen(agentId) + // result>, agent-error>: tag@0(1,1), + // payload@8 (max(tuple 64 bytes/align 8, agent-error 36 bytes/align 4) = 64) -> 72 total. + val retPtr = alloc(72, 8) + hostParseAgentId(idPtr, idLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) { + // ok payload: tuple> @ retPtr+8. + // string @ +0 (8,4); typed-schema-value @ +8 (32,4, skipped); option @ +40 (24,8). + val tupleBase = retPtr + 8 + val agentTypeName = liftString(loadInt(tupleBase), loadInt(tupleBase + 4)) + val phantomBase = tupleBase + 40 + val phantom = if (loadByte(phantomBase).toInt() == 0) null else Uuid(loadLong(phantomBase + 8), loadLong(phantomBase + 16)) + ParseAgentIdResult.Ok(ParsedAgentId(agentTypeName, phantom)) + } else { + ParseAgentIdResult.Err(liftAgentError(retPtr + 8)) + } + } + + /** + * Like [parseAgentId], but also exposes a pointer to the constructor-parameters value tree so a + * caller can lift them with the agent's declared parameter WIT types (the same `record-value` + * root [liftParamRecord] reads for `initialize`'s input). Used by snapshot recovery, where the + * host calls `load-snapshot` on a fresh instance WITHOUT a preceding `initialize`, so the guest + * must reconstruct the agent from its own id. Returns null on parse error. + * + * The returned [paramsValueTreePtr] points into this call's bump-allocated scratch region; it + * stays valid until the next `resetHeap`, so the caller must lift the params before yielding. + * The tuple's `option` phantom is intentionally dropped: reconstruction targets the + * agent's own non-phantom id (a phantom id only arises for forked agents). + */ + internal fun parseAgentIdConstructorParams(agentId: String): ConstructorParamsRef? { + val (idPtr, idLen) = lowerStringToPtrLen(agentId) + val retPtr = alloc(72, 8) + hostParseAgentId(idPtr, idLen, retPtr) + if (loadByte(retPtr).toInt() != 0) return null + // ok payload tuple @ retPtr+8: string @ +0; typed-schema-value @ +8 (graph 20B, value @ +20); + // so the value's schema-value-tree {nodes.ptr, nodes.len, root} is inline at tupleBase+28. + val tupleBase = retPtr + 8 + val agentTypeName = liftString(loadInt(tupleBase), loadInt(tupleBase + 4)) + return ConstructorParamsRef(agentTypeName, tupleBase + 28) + } + + // ----- Agent type registry ------------------------------------------------ + + /** All agent types currently registered with the Golem host. */ + fun getAllAgentTypes(): List { + val retPtr = alloc(8, 4) // list: {ptr: i32, len: i32} + hostGetAllAgentTypes(retPtr) + val dataPtr = loadInt(retPtr) + val len = loadInt(retPtr + 4) + return (0 until len).map { i -> liftRegisteredAgentType(dataPtr + i * 192) } + } + + /** Looks up a single registered agent type by name. */ + fun registeredAgentType(typeName: String): RegisteredAgentType? { + val (namePtr, nameLen) = lowerStringToPtrLen(typeName) + val retPtr = alloc(200, 8) // option: tag@0(1,1), payload@8(192,8) + hostGetAgentType(namePtr, nameLen, retPtr) + return liftOption(retPtr) { liftRegisteredAgentType(it) } + } + + // ----- Resource-handle canonical ABI: first use --------------------------- + + /** + * Starts enumerating every agent of every agent type in the given component. Always + * unfiltered and `precise=false` for now -- see [GetAgentsHandle]'s doc comment. The + * returned handle MUST be [GetAgentsHandle.close]d when done. + */ + fun getAgents(componentId: ComponentId): GetAgentsHandle { + val handle = hostGetAgentsConstructor(componentId.uuid.highBits, componentId.uuid.lowBits, 0, 0, 0, 0) + return GetAgentsHandle(handle) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeAgentDescriptor.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeAgentDescriptor.kt new file mode 100644 index 0000000000..8a42f3e2f1 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeAgentDescriptor.kt @@ -0,0 +1,59 @@ +package cloud.golem.runtime + +/** A named, WIT-typed parameter -- used to build constructor/method input schemas. */ +data class NativeParamSchema(val name: String, val witType: String) + +/** + * An HTTP endpoint for a method: verb (e.g. "POST") + path suffix (e.g. "/increment"), plus + * the auth/CORS metadata from `@Endpoint(auth=..., cors=...)`. + */ +data class NativeHttpEndpoint( + val verb: String, + val path: String, + val auth: Boolean = false, + val cors: List = emptyList(), +) + +/** + * Describes a single agent method: its name, return type, input parameters, and a handler + * that takes the agent instance plus the lifted parameter list and returns the lowered result. + * This is the native (canonical-ABI, `dynamic`-free) equivalent of the JS-path + * `MethodDescriptor` -- it exchanges `SchemaValue`, not `dynamic`. + */ +data class NativeMethodDescriptor( + val name: String, + /** WIT return type of the method, e.g. "s32", "string", "()" -- used to build the output schema. */ + val outputWitType: String, + /** Method parameters (name + WIT type) -- used to build the input schema. */ + val inputParams: List, + /** HTTP endpoints exposing this method (from @Endpoint) -- used to build http-endpoint metadata. */ + val httpEndpoints: List, + /** From `@Prompt(hint=...)` -- the method's `agent-method.prompt-hint` (empty = none). */ + val promptHint: String = "", + /** From `@ReadOnly(cache=...)` -- the `agent-method.read-only` config's cache policy (null = not read-only). */ + val readOnlyCache: String? = null, + val handler: (instance: Any, input: List) -> SchemaValue, +) + +/** + * Everything the native runtime needs to know about one agent type: how to construct it, what + * methods it has, and metadata for get-definition(). Native equivalent of the JS-path + * `AgentDescriptor`. + */ +data class NativeAgentDescriptor( + val typeName: String, + val description: String, + val mountPath: String, + /** Constructor parameters (name + WIT type) -- used to build the constructor input schema. */ + val constructorParams: List, + val methods: List, + val factory: (input: List) -> Any, + /** From `@Agent(auth=..., cors=...)` -- the mount's `http-mount-details.auth-details`/`cors-options`. */ + val mountAuth: Boolean = false, + val mountCors: List = emptyList(), + /** From `@Agent(mode=...)` -- `"durable"` or `"ephemeral"`. */ + val mode: String = "durable", + /** From `@Agent(snapshotting=...)` -- the Scala-DSL string, parsed in `AgentTypeModel.kt`. */ + val snapshotting: String = "disabled", + val snapshotCodec: SnapshotCodec? = null, +) diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeAgentRuntime.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeAgentRuntime.kt new file mode 100644 index 0000000000..3d635c80e4 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeAgentRuntime.kt @@ -0,0 +1,42 @@ +package cloud.golem.runtime + +/** + * The native (canonical-ABI) agent registry + the currently-active agent instance. Mirrors the + * JS-path `AgentRuntime`'s registry role, but dispatches on [SchemaValue] rather than `dynamic`. + * `Guest.kt`'s `initialize`/`invoke`/`get-definition`/`discover-agent-types` shims read/write + * this object; the KSP-generated registration populates it at module load. + */ +object NativeAgentRuntime { + private val registry = LinkedHashMap() + + /** The agent instance created by the most recent successful `initialize` call. */ + var current: Any? = null + + /** The descriptor of the currently active agent type. */ + var currentDescriptor: NativeAgentDescriptor? = null + + /** + * The authenticated identity of the caller of the in-flight `initialize`/`invoke`. Set by + * `Guest.kt` from the `principal` argument before dispatching; read via [BaseAgent.principal]. + * Invocations are single-threaded per agent, so a plain field is safe. + */ + var currentPrincipal: cloud.golem.Principal = cloud.golem.Principal.Anonymous + + /** Caller principal captured at `initialize` (not overwritten by later invokes) — used by snapshot save/load. */ + var initializationPrincipal: cloud.golem.Principal = cloud.golem.Principal.Anonymous + + fun registerAgent(descriptor: NativeAgentDescriptor) { + registry[descriptor.typeName] = descriptor + } + + fun lookup(typeName: String): NativeAgentDescriptor? = registry[typeName] + + fun all(): List = registry.values.toList() +} + +/** + * Thrown by agent factories/handlers to signal a WIT `agent-error`. `tag` must be one of + * "invalid-input" | "invalid-method" | "invalid-type" | "invalid-agent-id" (the cases `Guest.kt` + * lowers); any other tag falls back to "invalid-input". + */ +class AgentException(val tag: String, message: String) : RuntimeException(message) diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeToolRuntime.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeToolRuntime.kt new file mode 100644 index 0000000000..12c07cbf12 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/NativeToolRuntime.kt @@ -0,0 +1,26 @@ +package cloud.golem.runtime + +/** + * The native `golem:tool@0.1.0` registry. Tools are stateless from the host's perspective (per + * the WIT doc comment on `guest`), so unlike [NativeAgentRuntime] there is no "current instance" + * -- a tool's [NativeToolDescriptor.handler] is a standalone function, invoked fresh each call. + */ +object NativeToolRuntime { + private val registry = LinkedHashMap() + + fun registerTool(descriptor: NativeToolDescriptor) { + registry[descriptor.name] = descriptor + } + + fun lookup(name: String): NativeToolDescriptor? = registry[name] + + fun all(): List = registry.values.toList() +} + +/** + * Thrown by tool handlers to signal a WIT `tool-error`. `tag` must be one of "invalid-tool-name" + * | "invalid-command-path" | "invalid-input" | "constraint-violation" | "invalid-result" (the + * cases `ToolGuest.kt` lowers with a plain string payload); any other tag falls back to + * "invalid-input". + */ +class ToolException(val tag: String, message: String) : RuntimeException(message) diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/PrincipalBytes.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/PrincipalBytes.kt new file mode 100644 index 0000000000..edf3ec1246 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/PrincipalBytes.kt @@ -0,0 +1,112 @@ +package cloud.golem.runtime + +import cloud.golem.Principal +import cloud.golem.Uuid + +/** + * Self-contained tagged binary codec for [Principal]. Independent of the host's principal variant + * record ([liftPrincipal] reads that from a live pointer); snapshots control both ends, so this + * uses a compact, dependency-free framing. Format v1: + * tag: u8 (0=Oidc, 1=Agent, 2=GolemUser, 3=Anonymous) + * s := [len: u32 BE][utf8]; opt-string := [present: u8][s?]; opt-bool := [present: u8][u8?] + * Oidc := 0, s(sub), s(issuer), opt-string(email), opt-string(name), opt-bool(emailVerified), + * opt-string(givenName), opt-string(familyName), opt-string(picture), + * opt-string(preferredUsername), s(claims) + * Agent := 1, s(agentId); GolemUser := 2, u64 BE(high), u64 BE(low); Anonymous := 3 + */ +internal object PrincipalBytes { + fun encode(p: Principal): ByteArray { + val out = ArrayList(32) + when (p) { + is Principal.Oidc -> { + out.add(0) + putStr(out, p.sub) + putStr(out, p.issuer) + putOptStr(out, p.email) + putOptStr(out, p.name) + putOptBool(out, p.emailVerified) + putOptStr(out, p.givenName) + putOptStr(out, p.familyName) + putOptStr(out, p.picture) + putOptStr(out, p.preferredUsername) + putStr(out, p.claims) + } + is Principal.Agent -> { + out.add(1) + putStr(out, p.agentId) + } + is Principal.GolemUser -> { + out.add(2) + putU64(out, p.accountId.highBits) + putU64(out, p.accountId.lowBits) + } + Principal.Anonymous -> out.add(3) + } + return out.toByteArray() + } + + fun decode(bytes: ByteArray): Principal { + val r = Reader(bytes) + return when (val tag = r.u8()) { + 0 -> Principal.Oidc(r.str(), r.str(), r.optStr(), r.optStr(), r.optBool(), r.optStr(), r.optStr(), r.optStr(), r.optStr(), r.str()) + 1 -> Principal.Agent(r.str()) + 2 -> Principal.GolemUser(Uuid(r.u64(), r.u64())) + 3 -> Principal.Anonymous + else -> error("PrincipalBytes.decode: unknown tag $tag") + } + } + + private fun putStr(out: ArrayList, s: String) { + val b = s.encodeToByteArray() + putU32(out, b.size) + b.forEach { out.add(it) } + } + private fun putOptStr(out: ArrayList, s: String?) { + if (s == null) { + out.add(0) + } else { + out.add(1) + putStr(out, s) + } + } + private fun putOptBool(out: ArrayList, b: Boolean?) { + if (b == null) { + out.add(0) + } else { + out.add(1) + out.add(if (b) 1 else 0) + } + } + private fun putU32(out: ArrayList, v: Int) { + out.add((v ushr 24).toByte()) + out.add((v ushr 16).toByte()) + out.add((v ushr 8).toByte()) + out.add(v.toByte()) + } + private fun putU64(out: ArrayList, v: ULong) { + for (s in 56 downTo 0 step 8) out.add((v shr s).toByte()) + } + + private class Reader(val b: ByteArray) { + var i = 0 + fun u8(): Int = b[i++].toInt() and 0xFF + fun u32(): Int { + var v = 0 + repeat(4) { v = (v shl 8) or u8() } + return v + } + fun u64(): ULong { + var v = 0uL + repeat(8) { v = (v shl 8) or u8().toULong() } + return v + } + fun str(): String { + val n = u32() + val s = b.decodeToString(i, i + n) + i += n + return s + } + fun optStr(): String? = if (u8() == 0) null else str() + fun optBool(): Boolean? = if (u8() == 0) null else (u8() != 0) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/PrincipalDecode.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/PrincipalDecode.kt new file mode 100644 index 0000000000..53361dc7ff --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/PrincipalDecode.kt @@ -0,0 +1,52 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.runtime + +import cloud.golem.Principal +import cloud.golem.Uuid +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong + +// Decodes the `principal` variant (golem:agent/common@2.0.0) the host passes to initialize/invoke. +// All offsets verified via abi-dump. principal: size=112 align=8, tag@0, payload@8. Cases: +// 0 oidc(oidc-principal), 1 agent(agent-principal), 2 golem-user(golem-user-principal), 3 anonymous. + +/** Reads a canonical-ABI `string` (ptr@off, len@off+4) at [base]+[off]. */ +private fun str(base: Int, off: Int): String = liftString(loadInt(base + off), loadInt(base + off + 4)) + +/** Reads `option` (tag@off, string@off+4) at [base]+[off]. */ +private fun optStr(base: Int, off: Int): String? = if (loadByte(base + off).toInt() and 0xFF == 0) null else str(base, off + 4) + +// oidc-principal: size=100 align=4. sub@0, issuer@8, email@16 (opt), name@28 (opt), +// email-verified@40 (opt: tag@40,bool@41), given-name@44 (opt), family-name@56 (opt), +// picture@68 (opt), preferred-username@80 (opt), claims@92. +private fun liftOidcPrincipal(b: Int): Principal.Oidc = Principal.Oidc( + sub = str(b, 0), + issuer = str(b, 8), + email = optStr(b, 16), + name = optStr(b, 28), + emailVerified = if (loadByte(b + 40).toInt() and 0xFF == 0) null else (loadByte(b + 41).toInt() != 0), + givenName = optStr(b, 44), + familyName = optStr(b, 56), + picture = optStr(b, 68), + preferredUsername = optStr(b, 80), + claims = str(b, 92), +) + +// account-id { uuid } (16B); uuid { high-bits: u64 @0, low-bits: u64 @8 }. +private fun liftUuid(b: Int): Uuid = Uuid(loadLong(b).toULong(), loadLong(b + 8).toULong()) + +/** Lifts the `principal` variant at [base] (the 112-byte record). */ +internal fun liftPrincipal(base: Int): Principal { + val payload = base + 8 + return when (loadByte(base).toInt() and 0xFF) { + 0 -> liftOidcPrincipal(payload) + // agent-principal { agent-id } @0; agent-id { component-id @0 (16B), agent-id: string @16 }. + 1 -> Principal.Agent(str(payload, 16)) + // golem-user-principal { account-id } @0; account-id { uuid } @0. + 2 -> Principal.GolemUser(liftUuid(payload)) + else -> Principal.Anonymous + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Rpc.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Rpc.kt new file mode 100644 index 0000000000..a112e861f1 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Rpc.kt @@ -0,0 +1,231 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.buildSchemaValueTree +import cloud.golem.wasm.liftSingleValue +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt + +// Agent-to-agent RPC via golem:agent/host@2.0.0's `wasm-rpc` resource. Values ride +// `schema-value-tree` (built/lifted with buildSchemaValueTree/liftSingleValue), so the composite +// type machinery ([TypedSchemaValue]/WitType grammar) applies directly. The resource uses the +// proven resource-handle canonical ABI. golem:agent/host@2.0.0 is already imported in +// wit-native/main.wit (for parse-agent-id), so no new WIT import is needed. +// +// Constructor param flattening (verified via abi-dump `sig`): +// [constructor]wasm-rpc(agent-type-name: string, constructor: schema-value-tree, +// phantom-id: option, agent-config: list) -> handle +// flattens to [nameP, nameL, svtNodesP, svtNodesL, svtRoot, phantomTag(i32), phantomHigh(i64), +// phantomLow(i64), cfgP, cfgL]. phantom-id `none` = (0,0,0); agent-config empty = (0,0). + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[constructor]wasm-rpc") +private external fun hostWasmRpcNew( + nameP: Int, + nameL: Int, + svtNodesP: Int, + svtNodesL: Int, + svtRoot: Int, + phantomTag: Int, + phantomHigh: Long, + phantomLow: Long, + cfgP: Int, + cfgL: Int, +): Int + +// invoke[-and-await](method-name: string, input: schema-value-tree). retptr=true. input flattens +// to (nodes.ptr, nodes.len, root). +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]wasm-rpc.invoke-and-await") +private external fun hostWasmRpcInvokeAndAwait(self: Int, mP: Int, mL: Int, inNodesP: Int, inNodesL: Int, inRoot: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]wasm-rpc.invoke") +private external fun hostWasmRpcInvoke(self: Int, mP: Int, mL: Int, inNodesP: Int, inNodesL: Int, inRoot: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[resource-drop]wasm-rpc") +private external fun hostWasmRpcDrop(handle: Int) + +// async-invoke-and-await(method-name, input) -> future-invoke-result handle. Same input flattening +// as invoke; no retptr, returns the resource handle. +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]wasm-rpc.async-invoke-and-await") +private external fun hostWasmRpcAsyncInvokeAndAwait(self: Int, mP: Int, mL: Int, inNodesP: Int, inNodesL: Int, inRoot: Int): Int + +// schedule[-cancelable]-invocation(scheduled-time: datetime, method-name, input). datetime flattens +// to (seconds: i64, nanoseconds: i32); params [self, seconds, nanos, methodP, methodL, inNodesP, +// inNodesL, inRoot]. The cancelable variant returns a cancellation-token handle. +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]wasm-rpc.schedule-invocation") +private external fun hostWasmRpcScheduleInvocation(self: Int, seconds: Long, nanos: Int, mP: Int, mL: Int, inNodesP: Int, inNodesL: Int, inRoot: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]wasm-rpc.schedule-cancelable-invocation") +private external fun hostWasmRpcScheduleCancelableInvocation(self: Int, seconds: Long, nanos: Int, mP: Int, mL: Int, inNodesP: Int, inNodesL: Int, inRoot: Int): Int + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]future-invoke-result.subscribe") +private external fun hostFutureSubscribe(self: Int): Int + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]future-invoke-result.get") +private external fun hostFutureGet(self: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]future-invoke-result.cancel") +private external fun hostFutureCancel(self: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[resource-drop]future-invoke-result") +private external fun hostFutureDrop(handle: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[method]cancellation-token.cancel") +private external fun hostCancellationTokenCancel(self: Int) + +@kotlin.wasm.WasmImport("golem:agent/host@2.0.0", "[resource-drop]cancellation-token") +private external fun hostCancellationTokenDrop(handle: Int) + +/** The error arm of a wasm-rpc call's `result<_, rpc-error>` (golem:agent/host@2.0.0). */ +sealed class RpcError { + data class ProtocolError(val message: String) : RpcError() + data class Denied(val message: String) : RpcError() + data class NotFound(val message: String) : RpcError() + data class RemoteInternalError(val message: String) : RpcError() + + /** `remote-agent-error(agent-error)` -- the nested agent-error payload is not yet decoded. */ + object RemoteAgentError : RpcError() +} + +/** The result of a blocking RPC call. */ +sealed class RpcResult { + /** Success; [value] is null when the remote method returns unit / no value. */ + data class Ok(val value: SchemaValue?) : RpcResult() + data class Err(val error: RpcError) : RpcResult() +} + +/** Thrown by a KSP-generated typed RPC client when the remote call returns an [RpcError]. */ +class RpcException(val error: RpcError) : RuntimeException(error.toString()) + +// rpc-error: variant size=40 align=4, tag@base, payload@4; cases 0..3 carry a string (@payload+0, +// i.e. base+4), case 4 (remote-agent-error) carries agent-error (left undecoded for now). +internal fun liftRpcError(base: Int): RpcError { + val tag = loadByte(base).toInt() and 0xFF + val msg = if (tag <= 3) liftString(loadInt(base + 4), loadInt(base + 8)) else "" + return when (tag) { + 0 -> RpcError.ProtocolError(msg) + 1 -> RpcError.Denied(msg) + 2 -> RpcError.NotFound(msg) + 3 -> RpcError.RemoteInternalError(msg) + else -> RpcError.RemoteAgentError + } +} + +/** + * The pending result of an [WasmRpc.asyncInvokeAndAwait] call (golem:agent/host's + * `future-invoke-result`). Poll [get] until it returns non-null, or wait on [subscribe]'s pollable. + * [close] when done. + */ +class FutureInvokeResult internal constructor(private val handle: Int, private val resultWitType: String) { + /** A `wasi:io/poll` pollable handle that becomes ready when the invocation completes (caller owns it). */ + fun subscribe(): Int = hostFutureSubscribe(handle) + + /** The result if the invocation has completed, or null if it is still pending. */ + fun get(): RpcResult? { + val ret = alloc(48, 4) // option, rpc-error>> + hostFutureGet(handle, ret) + if (loadByte(ret).toInt() == 0) return null // not ready yet + val res = ret + 4 // result, rpc-error>: tag@0, payload@4 + if (loadByte(res).toInt() != 0) return RpcResult.Err(liftRpcError(res + 4)) + val value = if (loadByte(res + 4).toInt() == 0 || resultWitType == "()") { + null + } else { + liftSingleValue(res + 8, resultWitType) + } + return RpcResult.Ok(value) + } + + fun cancel() = hostFutureCancel(handle) + + /** Releases the future-invoke-result handle's guest-side handle-table entry. */ + fun close() = hostFutureDrop(handle) +} + +/** A handle to cancel a [WasmRpc.scheduleCancelableInvocation] before it fires. [close] when done. */ +class CancellationToken internal constructor(private val handle: Int) { + fun cancel() = hostCancellationTokenCancel(handle) + + /** Releases the cancellation-token handle's guest-side handle-table entry. */ + fun close() = hostCancellationTokenDrop(handle) +} + +/** + * A client for invoking methods on another agent (golem:agent/host's `wasm-rpc`). Construct with + * the target agent's type name + its constructor arguments (a [SchemaValue] -- typically a + * `Record` of the target constructor's params); call [invokeAndAwait]/[invoke]; [close] when done + * (the handle is not tied to Kotlin/Wasm GC). Higher-level typed clients (KSP-generated) build the + * arg [SchemaValue]s and decode results automatically. + */ +class WasmRpc(agentTypeName: String, constructorArgs: SchemaValue) { + private val handle: Int + + init { + val (nameP, nameL) = lowerStringToPtrLen(agentTypeName) + val tree = buildSchemaValueTree(constructorArgs) + handle = hostWasmRpcNew( + nameP, nameL, loadInt(tree), loadInt(tree + 4), loadInt(tree + 8), + 0, 0L, 0L, // phantom-id = none + 0, 0, // agent-config = empty list + ) + } + + /** + * Invokes [methodName] with [input] (a schema-value-tree, typically a `Record` of the method's + * args), blocking for the result. [resultWitType] is the method's WIT return type used to + * decode the returned value ("()" for a unit return). + */ + fun invokeAndAwait(methodName: String, input: SchemaValue, resultWitType: String): RpcResult { + val (mP, mL) = lowerStringToPtrLen(methodName) + val tree = buildSchemaValueTree(input) + val ret = alloc(44, 4) // result, rpc-error>: tag@0, payload@4 + hostWasmRpcInvokeAndAwait(handle, mP, mL, loadInt(tree), loadInt(tree + 4), loadInt(tree + 8), ret) + if (loadByte(ret).toInt() != 0) return RpcResult.Err(liftRpcError(ret + 4)) + // ok = option @ 4: opt tag@4, svt inline@8 (nodes.ptr@8/len@12/root@16). + val value = if (loadByte(ret + 4).toInt() == 0 || resultWitType == "()") { + null + } else { + liftSingleValue(ret + 8, resultWitType) + } + return RpcResult.Ok(value) + } + + /** Fire-and-forget invoke: returns null on success, or the [RpcError] the host reported. */ + fun invoke(methodName: String, input: SchemaValue): RpcError? { + val (mP, mL) = lowerStringToPtrLen(methodName) + val tree = buildSchemaValueTree(input) + val ret = alloc(44, 4) // result<_, rpc-error> + hostWasmRpcInvoke(handle, mP, mL, loadInt(tree), loadInt(tree + 4), loadInt(tree + 8), ret) + return if (loadByte(ret).toInt() == 0) null else liftRpcError(ret + 4) + } + + /** + * Invokes [methodName] with [input] asynchronously, returning a [FutureInvokeResult] to poll + * for the outcome. [resultWitType] is the method's WIT return type ("()" for unit). + */ + fun asyncInvokeAndAwait(methodName: String, input: SchemaValue, resultWitType: String): FutureInvokeResult { + val (mP, mL) = lowerStringToPtrLen(methodName) + val tree = buildSchemaValueTree(input) + val h = hostWasmRpcAsyncInvokeAndAwait(handle, mP, mL, loadInt(tree), loadInt(tree + 4), loadInt(tree + 8)) + return FutureInvokeResult(h, resultWitType) + } + + /** Schedules [methodName]([input]) to run at [scheduledSeconds].[scheduledNanoseconds] (Unix time). */ + fun scheduleInvocation(scheduledSeconds: Long, scheduledNanoseconds: Int, methodName: String, input: SchemaValue) { + val (mP, mL) = lowerStringToPtrLen(methodName) + val tree = buildSchemaValueTree(input) + hostWasmRpcScheduleInvocation(handle, scheduledSeconds, scheduledNanoseconds, mP, mL, loadInt(tree), loadInt(tree + 4), loadInt(tree + 8)) + } + + /** Like [scheduleInvocation], but returns a [CancellationToken] to cancel it before it fires. */ + fun scheduleCancelableInvocation(scheduledSeconds: Long, scheduledNanoseconds: Int, methodName: String, input: SchemaValue): CancellationToken { + val (mP, mL) = lowerStringToPtrLen(methodName) + val tree = buildSchemaValueTree(input) + val h = hostWasmRpcScheduleCancelableInvocation(handle, scheduledSeconds, scheduledNanoseconds, mP, mL, loadInt(tree), loadInt(tree + 4), loadInt(tree + 8)) + return CancellationToken(h) + } + + /** Releases the wasm-rpc handle's guest-side handle-table entry. */ + fun close() = hostWasmRpcDrop(handle) +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SchemaValue.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SchemaValue.kt new file mode 100644 index 0000000000..974ff5a1e0 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SchemaValue.kt @@ -0,0 +1,104 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime + +// The Kotlin data model for the `schema-value-tree` value model (golem:core/types@2.0.0). +// Covers the numeric/bool/char primitives, record/list/option/tuple/result, +// variant/enum/flags/map, the rich nodes text/binary/path/ +// url/datetime/duration/quantity/union, plus the original core cases +// (s32, string, record, unit). Case/field NAMES are never carried at the value level +// (only case INDICES / a flat field-order list) -- names live in the schema-graph, which the +// value tree deliberately doesn't redundantly repeat (see the WIT doc comment on +// schema-value-tree). +// +// `secret-value`/`quota-token-handle` (this file's last increment) are `own` handles +// (capability nodes, not data) rather than a nested schema-value-tree structure -- unlike every +// other case above, no lift/lower recursion is needed: an owned resource handle appearing +// inside memory (not as a direct flattened function argument) is just a plain i32 at that +// offset, per the canonical ABI. This turned out to be MUCH simpler than the resource-handle +// canonical ABI work done for `HostApi.getAgents` (constructor + method dispatch): `secret`/ +// `quota-token` (golem:core/types@2.0.0) declare NO methods at all ("An unforgeable handle to +// sensitive material held by the runtime... reveal it only through capability-gated host +// interfaces" -- those interfaces are not built yet), so consuming one here is +// only ever read-the-handle-and-hold-it (or drop it) -- there's no `[method]secret.*` import to +// write. `dropSecret`/`dropQuotaToken` below are exposed for exactly that "hold or drop" case. + +@kotlin.wasm.WasmImport("golem:core/types@2.0.0", "[resource-drop]secret") +private external fun hostSecretDrop(handle: Int) + +@kotlin.wasm.WasmImport("golem:core/types@2.0.0", "[resource-drop]quota-token") +private external fun hostQuotaTokenDrop(handle: Int) + +/** Releases a `secret` handle's guest-side handle-table entry. See [SchemaValue.SecretVal]. */ +fun dropSecret(handle: Int) = hostSecretDrop(handle) + +/** Releases a `quota-token` handle's guest-side handle-table entry. See [SchemaValue.QuotaTokenVal]. */ +fun dropQuotaToken(handle: Int) = hostQuotaTokenDrop(handle) +sealed class SchemaValue { + data class Bool(val v: Boolean) : SchemaValue() + data class S8(val v: Byte) : SchemaValue() + data class S16(val v: Short) : SchemaValue() + data class S32(val v: Int) : SchemaValue() + data class S64(val v: Long) : SchemaValue() + data class U8(val v: UByte) : SchemaValue() + data class U16(val v: UShort) : SchemaValue() + data class U32(val v: UInt) : SchemaValue() + data class U64(val v: ULong) : SchemaValue() + data class F32(val v: Float) : SchemaValue() + data class F64(val v: Double) : SchemaValue() + + // NOTE: WIT `char` is a full Unicode scalar value (up to U+10FFFF); Kotlin's Char is a + // 16-bit UTF-16 code unit (max U+FFFF). Values outside the Basic Multilingual Plane are not + // representable here -- a known gap, not yet hit by any agent this SDK has (a future + // increment would need to widen this to Int and expose codePointAt-style accessors). + data class Chr(val v: Char) : SchemaValue() + data class Str(val v: String) : SchemaValue() + data class Record(val fields: List) : SchemaValue() + data class ListVal(val items: List) : SchemaValue() + data class TupleVal(val items: List) : SchemaValue() + data class OptionVal(val inner: SchemaValue?) : SchemaValue() + data class ResultVal(val ok: Boolean, val inner: SchemaValue?) : SchemaValue() + + /** `payload` is null for a case with no payload (or one that's present-but-unset). */ + data class VariantVal(val caseIndex: Int, val payload: SchemaValue?) : SchemaValue() + data class EnumVal(val caseIndex: Int) : SchemaValue() + + /** One entry per flag NAME declared in the schema, in schema order -- names aren't repeated here. */ + data class FlagsVal(val flags: List) : SchemaValue() + data class MapVal(val entries: List>) : SchemaValue() + data class TextVal(val text: String, val language: String?) : SchemaValue() + + // `List`, not ByteArray -- Kotlin data class equals() uses reference equality for + // array-typed properties, which would break structural comparison (and this SDK's tests). + data class BinaryVal(val bytes: List, val mimeType: String?) : SchemaValue() + data class PathVal(val v: String) : SchemaValue() + data class UrlVal(val v: String) : SchemaValue() + data class DatetimeVal(val seconds: Long, val nanoseconds: Int) : SchemaValue() + data class DurationVal(val nanoseconds: Long) : SchemaValue() + data class QuantityVal(val mantissa: Long, val scale: Int, val unit: String) : SchemaValue() + data class UnionVal(val tag: String, val body: SchemaValue) : SchemaValue() + + /** An owned `secret` resource handle (an opaque i32 token). Call [dropSecret] when done. */ + data class SecretVal(val handle: Int) : SchemaValue() + + /** An owned `quota-token` resource handle (an opaque i32 token). Call [dropQuotaToken] when done. */ + data class QuotaTokenVal(val handle: Int) : SchemaValue() + object Unit_ : SchemaValue() +} + +// Typed accessors for KSP-generated handler/factory lambdas (the native equivalent of the +// JS-path SDK's extractString/extractInt helpers): extract a raw Kotlin value out of a lifted +// SchemaValue, so generated code reads `input[i].asString()` instead of pattern-matching. +fun SchemaValue.asString(): String = (this as SchemaValue.Str).v +fun SchemaValue.asInt(): Int = (this as SchemaValue.S32).v +fun SchemaValue.asBoolean(): Boolean = (this as SchemaValue.Bool).v +fun SchemaValue.asByte(): Byte = (this as SchemaValue.S8).v +fun SchemaValue.asShort(): Short = (this as SchemaValue.S16).v +fun SchemaValue.asLong(): Long = (this as SchemaValue.S64).v +fun SchemaValue.asUByte(): UByte = (this as SchemaValue.U8).v +fun SchemaValue.asUShort(): UShort = (this as SchemaValue.U16).v +fun SchemaValue.asUInt(): UInt = (this as SchemaValue.U32).v +fun SchemaValue.asULong(): ULong = (this as SchemaValue.U64).v +fun SchemaValue.asFloat(): Float = (this as SchemaValue.F32).v +fun SchemaValue.asDouble(): Double = (this as SchemaValue.F64).v +fun SchemaValue.asChar(): Char = (this as SchemaValue.Chr).v diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SchemaValueBytes.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SchemaValueBytes.kt new file mode 100644 index 0000000000..a58a8986d7 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SchemaValueBytes.kt @@ -0,0 +1,254 @@ +package cloud.golem.runtime + +/** + * Self-contained recursive codec: [SchemaValue] <-> ByteArray. Needed because the linear-memory + * schema-value-tree is position-dependent (internal pointers) and can't be detached as portable + * bytes. Scope: exactly the cases KSP's ConverterCodegen emits for a mapped agent-state type + * (primitives, record, list, tuple, option, result, variant, enum, map, datetime, unit). Cases + * outside that set (flags/text/binary/path/url/duration/quantity/union/secret/quota) are never + * produced for user state and are rejected. Node := [tag: u8][payload]; ints are big-endian. + */ +object SchemaValueBytes { + private const val BOOL = 1 + private const val S8 = 2 + private const val S16 = 3 + private const val S32 = 4 + private const val S64 = 5 + private const val U8 = 6 + private const val U16 = 7 + private const val U32 = 8 + private const val U64 = 9 + private const val F32 = 10 + private const val F64 = 11 + private const val CHR = 12 + private const val STR = 13 + private const val RECORD = 14 + private const val LIST = 15 + private const val TUPLE = 16 + private const val OPTION = 17 + private const val RESULT = 18 + private const val VARIANT = 19 + private const val ENUM = 20 + private const val MAP = 21 + private const val DATETIME = 22 + private const val UNIT = 23 + + fun encode(v: SchemaValue): ByteArray { + val w = Writer() + put(w, v) + return w.toByteArray() + } + fun decode(bytes: ByteArray): SchemaValue = get(Reader(bytes)) + + private fun put(w: Writer, v: SchemaValue) { + when (v) { + is SchemaValue.Bool -> { + w.u8(BOOL) + w.u8(if (v.v) 1 else 0) + } + is SchemaValue.S8 -> { + w.u8(S8) + w.i32(v.v.toInt()) + } + is SchemaValue.S16 -> { + w.u8(S16) + w.i32(v.v.toInt()) + } + is SchemaValue.S32 -> { + w.u8(S32) + w.i32(v.v) + } + is SchemaValue.S64 -> { + w.u8(S64) + w.i64(v.v) + } + is SchemaValue.U8 -> { + w.u8(U8) + w.i32(v.v.toInt()) + } + is SchemaValue.U16 -> { + w.u8(U16) + w.i32(v.v.toInt()) + } + is SchemaValue.U32 -> { + w.u8(U32) + w.i32(v.v.toInt()) + } + is SchemaValue.U64 -> { + w.u8(U64) + w.i64(v.v.toLong()) + } + is SchemaValue.F32 -> { + w.u8(F32) + w.i32(v.v.toBits()) + } + is SchemaValue.F64 -> { + w.u8(F64) + w.i64(v.v.toBits()) + } + is SchemaValue.Chr -> { + w.u8(CHR) + w.i32(v.v.code) + } + is SchemaValue.Str -> { + w.u8(STR) + w.str(v.v) + } + is SchemaValue.Record -> { + w.u8(RECORD) + w.i32(v.fields.size) + v.fields.forEach { put(w, it) } + } + is SchemaValue.ListVal -> { + w.u8(LIST) + w.i32(v.items.size) + v.items.forEach { put(w, it) } + } + is SchemaValue.TupleVal -> { + w.u8(TUPLE) + w.i32(v.items.size) + v.items.forEach { put(w, it) } + } + is SchemaValue.OptionVal -> { + w.u8(OPTION) + val i = v.inner + if (i == null) { + w.u8(0) + } else { + w.u8(1) + put(w, i) + } + } + is SchemaValue.ResultVal -> { + w.u8(RESULT) + w.u8(if (v.ok) 1 else 0) + val i = v.inner + if (i == null) { + w.u8(0) + } else { + w.u8(1) + put(w, i) + } + } + is SchemaValue.VariantVal -> { + w.u8(VARIANT) + w.i32(v.caseIndex) + val p = v.payload + if (p == null) { + w.u8(0) + } else { + w.u8(1) + put(w, p) + } + } + is SchemaValue.EnumVal -> { + w.u8(ENUM) + w.i32(v.caseIndex) + } + is SchemaValue.MapVal -> { + w.u8(MAP) + w.i32(v.entries.size) + v.entries.forEach { (k, vv) -> + put(w, k) + put(w, vv) + } + } + is SchemaValue.DatetimeVal -> { + w.u8(DATETIME) + w.i64(v.seconds) + w.i32(v.nanoseconds) + } + SchemaValue.Unit_ -> w.u8(UNIT) + else -> error("SchemaValueBytes: unsupported case ${v::class.simpleName} (not produced for mapped agent state)") + } + } + + private fun get(r: Reader): SchemaValue = when (val tag = r.u8()) { + BOOL -> SchemaValue.Bool(r.u8() != 0) + S8 -> SchemaValue.S8(r.i32().toByte()) + S16 -> SchemaValue.S16(r.i32().toShort()) + S32 -> SchemaValue.S32(r.i32()) + S64 -> SchemaValue.S64(r.i64()) + U8 -> SchemaValue.U8(r.i32().toUByte()) + U16 -> SchemaValue.U16(r.i32().toUShort()) + U32 -> SchemaValue.U32(r.i32().toUInt()) + U64 -> SchemaValue.U64(r.i64().toULong()) + F32 -> SchemaValue.F32(Float.fromBits(r.i32())) + F64 -> SchemaValue.F64(Double.fromBits(r.i64())) + CHR -> SchemaValue.Chr(r.i32().toChar()) + STR -> SchemaValue.Str(r.str()) + RECORD -> { + val n = r.i32() + SchemaValue.Record((0 until n).map { get(r) }) + } + LIST -> { + val n = r.i32() + SchemaValue.ListVal((0 until n).map { get(r) }) + } + TUPLE -> { + val n = r.i32() + SchemaValue.TupleVal((0 until n).map { get(r) }) + } + OPTION -> SchemaValue.OptionVal(if (r.u8() == 0) null else get(r)) + RESULT -> { + val ok = r.u8() != 0 + val inner = if (r.u8() == 0) null else get(r) + SchemaValue.ResultVal(ok, inner) + } + VARIANT -> { + val ci = r.i32() + val p = if (r.u8() == 0) null else get(r) + SchemaValue.VariantVal(ci, p) + } + ENUM -> SchemaValue.EnumVal(r.i32()) + MAP -> { + val n = r.i32() + SchemaValue.MapVal((0 until n).map { get(r) to get(r) }) + } + DATETIME -> SchemaValue.DatetimeVal(r.i64(), r.i32()) + UNIT -> SchemaValue.Unit_ + else -> error("SchemaValueBytes.decode: unknown tag $tag") + } + + private class Writer { + private val b = ArrayList(64) + fun u8(v: Int) { + b.add(v.toByte()) + } + fun i32(v: Int) { + b.add((v ushr 24).toByte()) + b.add((v ushr 16).toByte()) + b.add((v ushr 8).toByte()) + b.add(v.toByte()) + } + fun i64(v: Long) { + for (s in 56 downTo 0 step 8) b.add((v ushr s).toByte()) + } + fun str(s: String) { + val e = s.encodeToByteArray() + i32(e.size) + e.forEach { b.add(it) } + } + fun toByteArray() = b.toByteArray() + } + private class Reader(val b: ByteArray) { + var i = 0 + fun u8(): Int = b[i++].toInt() and 0xFF + fun i32(): Int { + var v = 0 + repeat(4) { v = (v shl 8) or u8() } + return v + } + fun i64(): Long { + var v = 0L + repeat(8) { v = (v shl 8) or u8().toLong() } + return v + } + fun str(): String { + val n = i32() + val s = b.decodeToString(i, i + n) + i += n + return s + } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotCodec.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotCodec.kt new file mode 100644 index 0000000000..66545768e8 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotCodec.kt @@ -0,0 +1,12 @@ +package cloud.golem.runtime + +/** + * Byte-level snapshot codec for one agent type, generated by KSP when the agent mixes in + * `Snapshotted`. [save]/[load] operate on the live agent instance (cast to the concrete class + * inside the generated bodies). Null on the descriptor ⇒ the agent didn't opt in (empty snapshot). + * Mirrors Scala's `SnapshotCodec`. + */ +class SnapshotCodec( + val save: (Any) -> ByteArray, + val load: (Any, ByteArray) -> Unit, +) diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotEnvelope.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotEnvelope.kt new file mode 100644 index 0000000000..9d119c61b2 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotEnvelope.kt @@ -0,0 +1,36 @@ +package cloud.golem.runtime + +/** + * Snapshot wire envelope, mirroring Scala's v2 binary framing: + * [version: u8 = 2][principalLen: u32 BE][principalBytes][stateBytes] + * State is everything after the principal region. Principal recovery on load is the reason the + * envelope exists: a snapshot-based update must restore the identity captured at initialize. + */ +internal object SnapshotEnvelope { + const val VERSION_BINARY = 2 + + data class Decoded(val principal: ByteArray, val state: ByteArray) + + fun encode(principal: ByteArray, state: ByteArray): ByteArray { + val out = ByteArray(1 + 4 + principal.size + state.size) + out[0] = VERSION_BINARY.toByte() + out[1] = (principal.size ushr 24).toByte() + out[2] = (principal.size ushr 16).toByte() + out[3] = (principal.size ushr 8).toByte() + out[4] = principal.size.toByte() + principal.copyInto(out, 5) + state.copyInto(out, 5 + principal.size) + return out + } + + fun decode(bytes: ByteArray): Decoded { + check(bytes.size >= 5) { "SnapshotEnvelope.decode: truncated header (need >= 5 bytes)" } + val version = bytes[0].toInt() and 0xFF + check(version == VERSION_BINARY) { "SnapshotEnvelope.decode: unsupported version $version" } + val len = ((bytes[1].toInt() and 0xFF) shl 24) or ((bytes[2].toInt() and 0xFF) shl 16) or + ((bytes[3].toInt() and 0xFF) shl 8) or (bytes[4].toInt() and 0xFF) + val pEnd = 5 + len + check(pEnd <= bytes.size) { "SnapshotEnvelope.decode: principal length $len exceeds buffer" } + return Decoded(bytes.copyOfRange(5, pEnd), bytes.copyOfRange(pEnd, bytes.size)) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotGuest.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotGuest.kt new file mode 100644 index 0000000000..d95e58c341 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/SnapshotGuest.kt @@ -0,0 +1,139 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftParamRecord +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.resetHeap +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt + +// snapshot record { payload: list (ptr@0,len@4), mime-type: string (ptr@8,len@12) }, size 16. +// result<_, string> lowered { tag@0 (0=ok,1=err), errPtr@4, errLen@8 }, size 12. +private const val SNAP_SIZE = 16 +private const val SNAP_ALIGN = 4 +private const val SNAP_PAYLOAD_PTR = 0 +private const val SNAP_PAYLOAD_LEN = 4 +private const val SNAP_MIME_PTR = 8 +private const val SNAP_MIME_LEN = 12 +private const val RES_SIZE = 12 +private const val RES_ALIGN = 4 +private const val MIME = "application/octet-stream" + +private fun writeBytesField(recordBase: Int, ptrOff: Int, lenOff: Int, bytes: ByteArray) { + val p = if (bytes.isEmpty()) 0 else alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(p + i, bytes[i]) + storeInt(recordBase + ptrOff, p) + storeInt(recordBase + lenOff, bytes.size) +} + +private fun readBytesField(base: Int, ptrOff: Int, lenOff: Int): ByteArray { + val p = loadInt(base + ptrOff) + val n = loadInt(base + lenOff) + return ByteArray(n) { loadByte(p + it) } +} + +/** + * `save: func() -> snapshot`. Empty payload if the live agent didn't opt in (no snapshotCodec); + * otherwise the principal-carrying envelope around the auto-serialized state. Never throws. + */ +fun saveSnapshot(): Int { + val codec = NativeAgentRuntime.currentDescriptor?.snapshotCodec + val instance = NativeAgentRuntime.current + val payload = if (codec == null || instance == null) { + ByteArray(0) + } else { + val state = codec.save(instance) + SnapshotEnvelope.encode(PrincipalBytes.encode(NativeAgentRuntime.initializationPrincipal), state) + } + val rec = alloc(SNAP_SIZE, SNAP_ALIGN) + writeBytesField(rec, SNAP_PAYLOAD_PTR, SNAP_PAYLOAD_LEN, payload) + writeBytesField(rec, SNAP_MIME_PTR, SNAP_MIME_LEN, MIME.encodeToByteArray()) + return rec +} + +// Reclaim the bump heap after the host has consumed the returned record. The canonical ABI runs +// cabi_post AFTER the caller reads the return value, so resetting here is safe -- and necessary: +// the periodic/"every(n)" snapshot cadences invoke save-snapshot repeatedly, and without this each +// call would permanently advance the bump pointer (mirrors cabiPostInvoke/cabiPostInitialize). +fun cabiPostSaveSnapshot(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} + +/** + * On a snapshot-based (manual) update, the host recovers the worker by calling `load-snapshot` + * on a FRESH wasm instance WITHOUT a preceding `initialize` (worker-executor skips the original + * initialize oplog entry -- it is inside the deleted region up to the snapshot index). So unlike + * `invoke`, `load` cannot assume the agent already exists: it must reconstruct it. We rebuild it + * exactly as `initialize` would -- construct via the descriptor factory from the constructor + * parameters carried in the agent's own id (obtained via get-self-metadata + parse-agent-id) -- + * and only then restore the saved state onto it. + */ +private fun ensureAgentReconstructed() { + if (NativeAgentRuntime.current != null) return + val agentId = HostApi.getSelfMetadata().agentId.agentId + val ref = HostApi.parseAgentIdConstructorParams(agentId) + ?: error("load-snapshot: cannot parse self agent-id '$agentId' to reconstruct the agent") + val descriptor = NativeAgentRuntime.lookup(ref.agentTypeName) + ?: error("load-snapshot: unknown agent type '${ref.agentTypeName}' for agent-id '$agentId'") + val params = liftParamRecord(ref.paramsValueTreePtr, descriptor.constructorParams.map { it.witType }) + NativeAgentRuntime.current = descriptor.factory(params) + NativeAgentRuntime.currentDescriptor = descriptor +} + +/** + * `load: func(snapshot) -> result<_, string>`. Restores the initialization principal from the + * envelope, reconstructs the agent instance if the host didn't `initialize` first (snapshot + * recovery), then hands the state bytes to the descriptor's codec (load). Empty payload = no-op Ok. + */ +fun loadSnapshot(argsPtr: Int): Int { + val res = alloc(RES_SIZE, RES_ALIGN) + try { + val payload = readBytesField(argsPtr, SNAP_PAYLOAD_PTR, SNAP_PAYLOAD_LEN) + if (payload.isNotEmpty()) { + val env = SnapshotEnvelope.decode(payload) + val principal = PrincipalBytes.decode(env.principal) + NativeAgentRuntime.initializationPrincipal = principal + NativeAgentRuntime.currentPrincipal = principal + ensureAgentReconstructed() + val codec = NativeAgentRuntime.currentDescriptor?.snapshotCodec + val instance = NativeAgentRuntime.current + if (codec != null && instance != null) codec.load(instance, env.state) + } + storeByte(res, 0) + } catch (e: Throwable) { + storeByte(res, 1) + val msg = (e.message ?: "snapshot load failed").encodeToByteArray() + val p = alloc(msg.size, 1) + for (i in msg.indices) storeByte(p + i, msg[i]) + storeInt(res + 4, p) + storeInt(res + 8, msg.size) + } + return res +} + +fun cabiPostLoadSnapshot(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} + +/** + * Canonical-ABI flat adapter for `load: func(snapshot) -> result<_, string>`. + * + * The Wasm Canonical ABI flattens `record { payload: list, mime-type: string }` into four + * I32 parameters (payloadPtr, payloadLen, mimeTypePtr, mimeTypeLen) rather than passing a + * single pointer to an in-memory record. The `@WasmExport` generated by KSP uses this + * function signature so that wasm-tools component embed sees `[I32, I32, I32, I32] -> [I32]`, + * which matches the WIT-derived type for the `golem:api/load-snapshot@1.5.0#load` export. + */ +fun loadSnapshotFlat(payloadPtr: Int, payloadLen: Int, mimeTypePtr: Int, mimeTypeLen: Int): Int { + // Assemble the snapshot record in our linear-memory heap so the existing + // loadSnapshot(argsPtr) helper (which reads fields at fixed offsets) can process it. + val rec = alloc(SNAP_SIZE, SNAP_ALIGN) + storeInt(rec + SNAP_PAYLOAD_PTR, payloadPtr) + storeInt(rec + SNAP_PAYLOAD_LEN, payloadLen) + storeInt(rec + SNAP_MIME_PTR, mimeTypePtr) + storeInt(rec + SNAP_MIME_LEN, mimeTypeLen) + return loadSnapshot(rec) +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolGuest.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolGuest.kt new file mode 100644 index 0000000000..aebdbf7295 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolGuest.kt @@ -0,0 +1,215 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.buildSchemaValueTree +import cloud.golem.wasm.liftParamRecord +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.resetHeap +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt +import cloud.golem.wasm.writeListField +import cloud.golem.wasm.writeOptionNone +import cloud.golem.wasm.writeStringField + +/** + * Canonical-ABI implementations for `golem:tool@0.1.0`'s `guest` interface. Layout verified via + * `wit-parser::SizeAlign` + `Resolve::wasm_signature(GuestExport)` against the real WIT under + * `wit-native/deps/golem-tool/{common,guest}.wit` on 2026-07-07 (same tool-verified discipline + * as `Guest.kt`/`AgentTypeModel.kt`/`ToolModel.kt`): + * + * discover-tools() -> result, tool-error> + * get-tool(name: string) -> result + * invoke(tool-name: string, command-path: list, input: typed-schema-value, + * stdin: option, principal: principal) -> result + * + * `discover-tools` takes no params (single guest-allocated retptr result). `get-tool`'s single + * string param flattens directly (2 core params: ptr, len) -- unlike `initialize`/`invoke` in + * `Guest.kt`, ONE non-scalar param does not trigger indirect bundling. `invoke`'s 5 params DO + * get bundled behind a single indirect pointer (`indirect_params=true`), size=168 align=8. + * + * Scope: tools have a root command only (no subcommands), so `command-path` + * is expected empty and `stdin`/`principal` are read but unused, matching `Guest.kt`'s existing + * "principal intentionally unread" precedent. Like `Guest.kt`'s exports, these are deliberately + * NOT `@WasmExport`-annotated -- KSP generates the actual export declarations in the + * agent's own module (see `Guest.kt`'s header comment for why). + */ +private object ToolGuestLayout { + // invoke's bundled args: size=168 align=8 + const val INVOKE_ARGS_TOOL_NAME = 0 // string, size 8 + const val INVOKE_ARGS_INPUT = 16 // typed-schema-value, size 32 + // command-path @8 (list), stdin @48 (option), principal @56 (112 + // bytes) intentionally unread -- unused here (root command only, no streams). + + // typed-schema-value: size=32 align=4 = { graph: schema-graph@0(20), value: schema-value-tree@20(12) } + const val TSV_VALUE_OFFSET = 20 + + // result, tool-error> / result: size=40 align=4 payload_offset=4 + const val RESULT40_SIZE = 40 + const val RESULT40_ALIGN = 4 + const val RESULT40_PAYLOAD_OFFSET = 4 + + // result: size=48 align=4 payload_offset=4 + const val RESULT48_SIZE = 48 + const val RESULT48_ALIGN = 4 + const val RESULT48_PAYLOAD_OFFSET = 4 + + const val RESULT_OK = 0 + const val RESULT_ERR = 1 + + // record tool: size=36 align=4 (from ToolModel.kt's ToolLayout, duplicated as a plain Int + // since that object is private to ToolModel.kt). + const val TOOL_SIZE = 36 + const val TOOL_ALIGN = 4 + + // record invocation-result: size=44 align=4 + const val IR_RESULT = 0 // option, size 36 + const val IR_STDOUT = 36 // option, size 8 + + // option payload_offset = align_to(1, 4) = 4 + const val IR_RESULT_OPTION_PAYLOAD_OFFSET = 4 + + // variant tool-error: size=36 align=4, payload_offset=4. All cases used here carry a plain + // string message except custom-error (unused here). + const val TE_INVALID_TOOL_NAME = 0 + const val TE_INVALID_COMMAND_PATH = 1 + const val TE_INVALID_INPUT = 2 + const val TOOL_ERROR_PAYLOAD_OFFSET = 4 +} + +private fun toolErrorCaseIndex(tag: String): Int = when (tag) { + "invalid-tool-name" -> ToolGuestLayout.TE_INVALID_TOOL_NAME + "invalid-command-path" -> ToolGuestLayout.TE_INVALID_COMMAND_PATH + "invalid-input" -> ToolGuestLayout.TE_INVALID_INPUT + else -> ToolGuestLayout.TE_INVALID_INPUT +} + +private fun lowerToolErrResult(size: Int, align: Int, payloadOffset: Int, tag: String, message: String): Int { + val base = alloc(size, align) + storeByte(base, ToolGuestLayout.RESULT_ERR.toByte()) + val errBase = base + payloadOffset + storeByte(errBase, toolErrorCaseIndex(tag).toByte()) + writeStringField(errBase, ToolGuestLayout.TOOL_ERROR_PAYLOAD_OFFSET, message) + return base +} + +/** Copies a freshly-`lowerTool`-allocated 36-byte `tool` record's bytes into `dest`. */ +private fun copyToolInto(dest: Int, descriptor: NativeToolDescriptor) { + val src = lowerTool(descriptor) + for (b in 0 until ToolGuestLayout.TOOL_SIZE) storeByte(dest + b, loadByte(src + b)) +} + +fun discoverTools(): Int { + val descriptors = NativeToolRuntime.all() + val base = alloc(ToolGuestLayout.RESULT40_SIZE, ToolGuestLayout.RESULT40_ALIGN) + storeByte(base, ToolGuestLayout.RESULT_OK.toByte()) + val listFieldBase = base + ToolGuestLayout.RESULT40_PAYLOAD_OFFSET + writeListField( + listFieldBase, + 0, + descriptors.size, + ToolGuestLayout.TOOL_SIZE, + ToolGuestLayout.TOOL_ALIGN, + ) { i, elemPtr -> copyToolInto(elemPtr, descriptors[i]) } + return base +} + +fun cabiPostDiscoverTools(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} + +fun getTool(namePtr: Int, nameLen: Int): Int { + val name = liftString(namePtr, nameLen) + val descriptor = NativeToolRuntime.lookup(name) + ?: return lowerToolErrResult( + ToolGuestLayout.RESULT40_SIZE, + ToolGuestLayout.RESULT40_ALIGN, + ToolGuestLayout.RESULT40_PAYLOAD_OFFSET, + "invalid-tool-name", + "Unknown tool: $name", + ) + val base = alloc(ToolGuestLayout.RESULT40_SIZE, ToolGuestLayout.RESULT40_ALIGN) + storeByte(base, ToolGuestLayout.RESULT_OK.toByte()) + copyToolInto(base + ToolGuestLayout.RESULT40_PAYLOAD_OFFSET, descriptor) + return base +} + +fun cabiPostGetTool(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} + +fun invokeTool(argsPtr: Int): Int { + val toolName = liftString( + loadInt(argsPtr + ToolGuestLayout.INVOKE_ARGS_TOOL_NAME), + loadInt(argsPtr + ToolGuestLayout.INVOKE_ARGS_TOOL_NAME + 4), + ) + val inputTreePtr = argsPtr + ToolGuestLayout.INVOKE_ARGS_INPUT + ToolGuestLayout.TSV_VALUE_OFFSET + + val descriptor = NativeToolRuntime.lookup(toolName) + ?: return lowerToolErrResult( + ToolGuestLayout.RESULT48_SIZE, + ToolGuestLayout.RESULT48_ALIGN, + ToolGuestLayout.RESULT48_PAYLOAD_OFFSET, + "invalid-tool-name", + "Unknown tool: $toolName", + ) + + return try { + val params = liftParamRecord(inputTreePtr, descriptor.params.map { it.witType }) + val result = descriptor.handler(params) + lowerInvokeOkResult(result, descriptor.outputWitType) + } catch (e: ToolException) { + lowerToolErrResult( + ToolGuestLayout.RESULT48_SIZE, + ToolGuestLayout.RESULT48_ALIGN, + ToolGuestLayout.RESULT48_PAYLOAD_OFFSET, + e.tag, + e.message ?: "invocation failed", + ) + } catch (e: Throwable) { + lowerToolErrResult( + ToolGuestLayout.RESULT48_SIZE, + ToolGuestLayout.RESULT48_ALIGN, + ToolGuestLayout.RESULT48_PAYLOAD_OFFSET, + "invalid-input", + e.message ?: "invocation failed", + ) + } +} + +/** Write `result = ok(invocation-result)`. */ +private fun lowerInvokeOkResult(output: SchemaValue, outputWitType: String): Int { + val base = alloc(ToolGuestLayout.RESULT48_SIZE, ToolGuestLayout.RESULT48_ALIGN) + storeByte(base, ToolGuestLayout.RESULT_OK.toByte()) + val irBase = base + ToolGuestLayout.RESULT48_PAYLOAD_OFFSET + + val resultOptBase = irBase + ToolGuestLayout.IR_RESULT + if (output is SchemaValue.Unit_) { + writeOptionNone(resultOptBase, 0) + } else { + storeByte(resultOptBase, 1) // some + lowerTypedSchemaValueInto(resultOptBase + ToolGuestLayout.IR_RESULT_OPTION_PAYLOAD_OFFSET, output, outputWitType) + } + writeOptionNone(irBase, ToolGuestLayout.IR_STDOUT) + return base +} + +/** + * Lowers a single-value `typed-schema-value` (a minimal 1-node schema-graph describing + * [outputWitType], paired with the value tree `buildSchemaValueTree` already knows how to + * build) into `dest` (32 bytes: graph@0(20), value@20(12)). + */ +private fun lowerTypedSchemaValueInto(dest: Int, value: SchemaValue, outputWitType: String) { + lowerSchemaGraphInto(dest, 0, collectTypeNodes(listOf(outputWitType))) + val treePtr = buildSchemaValueTree(value) + storeInt(dest + 20, loadInt(treePtr)) + storeInt(dest + 24, loadInt(treePtr + 4)) + storeInt(dest + 28, loadInt(treePtr + 8)) +} + +fun cabiPostInvokeTool(@Suppress("UNUSED_PARAMETER") resultPtr: Int) { + resetHeap() +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolHost.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolHost.kt new file mode 100644 index 0000000000..7977820c51 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolHost.kt @@ -0,0 +1,363 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.storeInt + +// Raw canonical-ABI import bindings to golem:tool/host@0.1.0 (consuming tools +// registered by other components). Not pulled in by an explicit wit-native/main.wit edit: +// this world already `include`s golem:tool/tool-guest@0.1.0 (for exporting OUR OWN tools), +// and that world's own definition (wit/deps/golem-tool/guest.wit) already +// `import`s the local `host` interface -- i.e. golem:tool/host@0.1.0 -- transitively. +// Verified structurally the same way as every other import in this file: declared-but-absent +// in the componentized output until actually called. +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "get-all-tools") +private external fun hostGetAllTools(retPtr: Int) + +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "get-tool") +private external fun hostGetTool(namePtr: Int, nameLen: Int, retPtr: Int) + +// tool-rpc resource: [constructor](tool-name) -> handle; [method]invoke; [resource-drop]. +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[constructor]tool-rpc") +private external fun hostToolRpcNew(namePtr: Int, nameLen: Int): Int + +// invoke(self, command-path: list, input: typed-schema-value, stdin: option) +// -> result<_, rpc-error>. Fully flattened (indirect_params=false), retptr=true. Param order +// verified via abi-dump `sig`: [self, cmd.ptr, cmd.len, graph.type-nodes.ptr/len, graph.defs.ptr/len, +// graph.root, value.value-nodes.ptr/len, value.root, stdin.tag, stdin.handle, retptr]. The 8 +// typed-schema-value words are exactly the 8 i32s of the in-memory 32-byte typed-schema-value +// record (graph@0..19, value@20..31), so we build it with lowerTypedSchemaValue and read them out. +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[method]tool-rpc.invoke") +private external fun hostToolRpcInvoke( + self: Int, + cmdPtr: Int, + cmdLen: Int, + gTypeNodesPtr: Int, + gTypeNodesLen: Int, + gDefsPtr: Int, + gDefsLen: Int, + gRoot: Int, + vNodesPtr: Int, + vNodesLen: Int, + vRoot: Int, + stdinTag: Int, + stdinHandle: Int, + retPtr: Int, +) + +// invoke-and-await: same flattened param order as invoke (verified via abi-dump `sig`), retptr=true. +// Result `result`: size=48 align=4, tag@0, payload@4. +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[method]tool-rpc.invoke-and-await") +private external fun hostToolRpcInvokeAndAwait( + self: Int, + cmdPtr: Int, + cmdLen: Int, + gTypeNodesPtr: Int, + gTypeNodesLen: Int, + gDefsPtr: Int, + gDefsLen: Int, + gRoot: Int, + vNodesPtr: Int, + vNodesLen: Int, + vRoot: Int, + stdinTag: Int, + stdinHandle: Int, + retPtr: Int, +) + +// async-invoke-and-await: same params minus the retptr; returns a future-invoke-result handle (I32). +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[method]tool-rpc.async-invoke-and-await") +private external fun hostToolRpcAsyncInvokeAndAwait( + self: Int, + cmdPtr: Int, + cmdLen: Int, + gTypeNodesPtr: Int, + gTypeNodesLen: Int, + gDefsPtr: Int, + gDefsLen: Int, + gRoot: Int, + vNodesPtr: Int, + vNodesLen: Int, + vRoot: Int, + stdinTag: Int, + stdinHandle: Int, +): Int + +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[resource-drop]tool-rpc") +private external fun hostToolRpcDrop(handle: Int) + +// future-invoke-result resource: subscribe()->pollable(i32); get(self, retptr) -> +// option> (52B, opt tag@0, inner result@4); cancel(self); drop. +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[method]future-invoke-result.subscribe") +private external fun hostToolFutureSubscribe(self: Int): Int + +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[method]future-invoke-result.get") +private external fun hostToolFutureGet(self: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[method]future-invoke-result.cancel") +private external fun hostToolFutureCancel(self: Int) + +@kotlin.wasm.WasmImport("golem:tool/host@0.1.0", "[resource-drop]future-invoke-result") +private external fun hostToolFutureDrop(handle: Int) + +/** + * A tool registered by another component in the environment, projected down to what this SDK + * can currently read without decoding `tool.schema`/`tool.commands`' full CLI-command-tree + * structure (`tool.commands.nodes[1..]`, `tool.commands.nodes[*].body`/`globals`, etc. -- + * options/flags/positionals/constraints, deep recursive data comparable in shape to + * `golem:agent/common`'s `agent-type`). + * + * Per the WIT's own construction invariant ("The tool's identity is its root command name + * (`commands.nodes[0].name`)"), [name] IS the tool's real identity -- this is not a lossy + * substitute the way it would be for an arbitrary field, it's the one field the spec itself + * treats as canonical. [version] is read too since it costs nothing extra (both are flat + * fields on `tool` / `tool.commands.nodes[0]`, no deeper traversal needed). Mirrors + * `RegisteredAgentType`, which projects `registered-agent-type` down to + * `typeName`/`implementedBy` for the identical reason: the reference SDK's own public surface + * (where one exists) or the WIT spec's own stated identity field, not the full record. + */ +data class RegisteredTool(val name: String, val version: String, val implementedBy: ComponentId) + +// registered-tool: size=56 align=8 { definition: offset=0 (tool, 36,4), implemented-by: +// offset=40 (component-id, 16,8) }. +// tool: size=36 align=4 { version: offset=0 (string,8,4), commands: offset=8 +// (command-tree,8,4), schema: offset=16 (schema-graph,20,4, not read) }. +// command-tree: size=8 align=4 { nodes: offset=0 (list,8,4) }. +// command-node's first field is `name: string` at its own offset 0 -- only node[0] is read, +// so command-node's total size/stride is never needed. +private fun liftRegisteredTool(base: Int): RegisteredTool { + val toolBase = base + val version = liftString(loadInt(toolBase), loadInt(toolBase + 4)) + val nodesListBase = toolBase + 8 + val nodesDataPtr = loadInt(nodesListBase) + val node0 = nodesDataPtr // node[0], offset 0 into the array + val name = liftString(loadInt(node0), loadInt(node0 + 4)) + val implementedBy = liftComponentId(base + 40) + return RegisteredTool(name, version, implementedBy) +} + +/** + * Native SDK access to `golem:tool/host@0.1.0`: discovering tools registered by other + * components in the environment. The `Tools` wrapper for the discovery + * half of golem:tool/host. + * + * Actual tool invocation lives on the [ToolRpc] resource (below), which encodes its input via + * [TypedSchemaValue] (composite payloads supported). + */ +object ToolHost { + /** Every tool the calling agent has access to in the current environment. */ + fun getAllTools(): List { + val retPtr = alloc(8, 4) // list: {ptr: i32, len: i32} + hostGetAllTools(retPtr) + val dataPtr = loadInt(retPtr) + val len = loadInt(retPtr + 4) + return (0 until len).map { i -> liftRegisteredTool(dataPtr + i * 56) } + } + + /** Looks up a single registered tool by name, iff the calling agent has access to it. */ + fun getTool(name: String): RegisteredTool? { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val retPtr = alloc(64, 8) // option: tag@0(1,1), payload@8(56,8) + hostGetTool(namePtr, nameLen, retPtr) + return liftOption(retPtr) { liftRegisteredTool(it) } + } +} + +/** The error arm of tool-rpc's `result<..., rpc-error>` (golem:tool/host@0.1.0). */ +sealed class ToolRpcError { + data class ProtocolError(val message: String) : ToolRpcError() + data class Denied(val message: String) : ToolRpcError() + data class NotFound(val message: String) : ToolRpcError() + data class RemoteInternalError(val message: String) : ToolRpcError() + + /** `remote-tool-error(tool-error)` -- the nested `tool-error` payload is not yet decoded. */ + object RemoteToolError : ToolRpcError() +} + +/** + * The success payload of an awaited tool invocation: `invocation-result` + * (golem:tool/host@0.1.0) = `{ result: option, stdout: option }`. + */ +data class ToolInvocationResult( + /** The tool's returned value, fully decoded (self-describing), or `null` if it produced none. */ + val result: TypedSchemaValue?, + /** + * An opaque `wasi:io` output-stream handle carrying the tool's stdout, or `null`. The caller + * owns it; stream reads are not yet wrapped, so drop it via `wasi:io` when done with it. + */ + val stdoutHandle: Int?, +) + +/** The result of an awaited tool invocation: either a [ToolInvocationResult] or a [ToolRpcError]. */ +sealed class ToolInvokeResult { + data class Ok(val value: ToolInvocationResult) : ToolInvokeResult() + data class Err(val error: ToolRpcError) : ToolInvokeResult() +} + +// invocation-result layout (44B align 4, verified via abi-dump): result: option +// @0 (opt tag@0, tsv@4..35); stdout: option @36 (opt tag@36, handle@40). +private const val IR_RESULT_TSV_OFFSET = 4 +private const val IR_STDOUT_TAG_OFFSET = 36 +private const val IR_STDOUT_HANDLE_OFFSET = 40 + +/** Decodes an `rpc-error` (tag@0; cases 0..3 carry a string @ payload+4/+8; case 4 = tool-error). */ +private fun liftToolRpcError(base: Int): ToolRpcError { + val tag = loadByte(base).toInt() and 0xFF + val msg = if (tag <= 3) liftString(loadInt(base + 4), loadInt(base + 8)) else "" + return when (tag) { + 0 -> ToolRpcError.ProtocolError(msg) + 1 -> ToolRpcError.Denied(msg) + 2 -> ToolRpcError.NotFound(msg) + 3 -> ToolRpcError.RemoteInternalError(msg) + else -> ToolRpcError.RemoteToolError + } +} + +/** Decodes an `invocation-result` record at [base]. */ +internal fun liftInvocationResult(base: Int): ToolInvocationResult { + val result = if (loadByte(base).toInt() and 0xFF == 0) { + null + } else { + liftTypedSchemaValue(base + IR_RESULT_TSV_OFFSET) + } + val stdout = if (loadByte(base + IR_STDOUT_TAG_OFFSET).toInt() and 0xFF == 0) { + null + } else { + loadInt(base + IR_STDOUT_HANDLE_OFFSET) + } + return ToolInvocationResult(result, stdout) +} + +/** Decodes a `result` at [base] (tag@0, payload@4). */ +internal fun liftToolInvokeResult(base: Int): ToolInvokeResult = if (loadByte(base).toInt() and 0xFF == 0) { + ToolInvokeResult.Ok(liftInvocationResult(base + 4)) +} else { + ToolInvokeResult.Err(liftToolRpcError(base + 4)) +} + +// Lowers a list into a fresh array of {ptr,len} string records; returns (dataPtr, len). +private fun lowerStringList(items: List): Pair { + if (items.isEmpty()) return 0 to 0 + val arr = alloc(items.size * 8, 4) // each element: string {ptr@0, len@4} + items.forEachIndexed { i, s -> + val (p, l) = lowerStringToPtrLen(s) + storeInt(arr + i * 8, p) + storeInt(arr + i * 8 + 4, l) + } + return arr to items.size +} + +/** + * A handle to a tool registered elsewhere in the environment, for invoking it (golem:tool/host's + * `tool-rpc` resource). Obtain via the constructor with the tool's name, then call: + * - [invoke] -- fire-and-forget (`result<_, rpc-error>`); + * - [invokeAndAwait] -- blocking, returns the tool's [ToolInvokeResult]; + * - [asyncInvokeAndAwait] -- returns a [ToolFutureInvokeResult] to poll/await. + * + * [close] when done (the handle is not tied to Kotlin/Wasm GC). `stdin` is always `none` on every + * variant (streamed stdin, a `wasi:io/streams` input-stream, is the remaining follow-up). + */ +class ToolRpc(toolName: String) { + private val handle: Int + + init { + val (p, l) = lowerStringToPtrLen(toolName) + handle = hostToolRpcNew(p, l) + } + + // The 8 i32 words of the in-memory 32-byte typed-schema-value record (graph@0..19, value@20..31) + // are exactly the flattened typed-schema-value params, in order. + private fun tsvWords(tsv: Int): IntArray = intArrayOf( + loadInt(tsv), + loadInt(tsv + 4), + loadInt(tsv + 8), + loadInt(tsv + 12), + loadInt(tsv + 16), + loadInt(tsv + 20), + loadInt(tsv + 24), + loadInt(tsv + 28), + ) + + /** + * Invokes the tool at [commandPath] with [input] (fire-and-forget). Returns `null` on success + * or the [ToolRpcError] the host reported. `stdin` is always `none`. [input] may be any + * composite [TypedSchemaValue]. + */ + fun invoke(commandPath: List, input: TypedSchemaValue): ToolRpcError? { + val (cmdPtr, cmdLen) = lowerStringList(commandPath) + val w = tsvWords(lowerTypedSchemaValue(input)) + val ret = alloc(44, 4) // result<_, rpc-error>: tag@0, payload(rpc-error)@4 + hostToolRpcInvoke( + handle, cmdPtr, cmdLen, + w[0], w[1], w[2], w[3], w[4], w[5], w[6], w[7], + 0, 0, // stdin = none + ret, + ) + return if (loadByte(ret).toInt() and 0xFF == 0) null else liftToolRpcError(ret + 4) + } + + /** + * Invokes the tool at [commandPath] with [input] and blocks for the result, returning the + * tool's [ToolInvokeResult] (its self-describing return value + optional stdout stream, or an + * error). `stdin` is always `none`. [input] may be any composite [TypedSchemaValue]. + */ + fun invokeAndAwait(commandPath: List, input: TypedSchemaValue): ToolInvokeResult { + val (cmdPtr, cmdLen) = lowerStringList(commandPath) + val w = tsvWords(lowerTypedSchemaValue(input)) + val ret = alloc(48, 4) // result: tag@0, payload@4 + hostToolRpcInvokeAndAwait( + handle, cmdPtr, cmdLen, + w[0], w[1], w[2], w[3], w[4], w[5], w[6], w[7], + 0, 0, // stdin = none + ret, + ) + return liftToolInvokeResult(ret) + } + + /** + * Starts an asynchronous invocation of the tool at [commandPath] with [input], returning a + * [ToolFutureInvokeResult] to poll ([ToolFutureInvokeResult.get]) or wait on + * ([ToolFutureInvokeResult.subscribe]). `stdin` is always `none`. + */ + fun asyncInvokeAndAwait(commandPath: List, input: TypedSchemaValue): ToolFutureInvokeResult { + val (cmdPtr, cmdLen) = lowerStringList(commandPath) + val w = tsvWords(lowerTypedSchemaValue(input)) + val futureHandle = hostToolRpcAsyncInvokeAndAwait( + handle, cmdPtr, cmdLen, + w[0], w[1], w[2], w[3], w[4], w[5], w[6], w[7], + 0, 0, // stdin = none + ) + return ToolFutureInvokeResult(futureHandle) + } + + /** Releases the tool-rpc handle's guest-side handle-table entry. */ + fun close() = hostToolRpcDrop(handle) +} + +/** + * A pending asynchronous tool invocation (golem:tool/host's `future-invoke-result` resource). + * Poll [get] until it returns non-`null`, or wait on [subscribe]'s pollable. [cancel] to abort; + * [close] to release the handle (not tied to Kotlin/Wasm GC). + */ +class ToolFutureInvokeResult internal constructor(private val handle: Int) { + /** A `wasi:io/poll` pollable handle that becomes ready when the invocation completes (caller owns it). */ + fun subscribe(): Int = hostToolFutureSubscribe(handle) + + /** Returns `null` while the invocation is still pending, else its [ToolInvokeResult]. */ + fun get(): ToolInvokeResult? { + val ret = alloc(52, 4) // option>: opt tag@0, inner result@4 + hostToolFutureGet(handle, ret) + if (loadByte(ret).toInt() and 0xFF == 0) return null // still pending + return liftToolInvokeResult(ret + 4) + } + + /** Requests cancellation of the in-flight invocation. */ + fun cancel() = hostToolFutureCancel(handle) + + /** Releases the future-invoke-result handle. */ + fun close() = hostToolFutureDrop(handle) +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolModel.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolModel.kt new file mode 100644 index 0000000000..8ae88b65b9 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/ToolModel.kt @@ -0,0 +1,234 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt +import cloud.golem.wasm.writeEmptyListField +import cloud.golem.wasm.writeListField +import cloud.golem.wasm.writeOptionNone +import cloud.golem.wasm.writeStringField + +/** + * A single tool parameter: mirrors [NativeParamSchema], but named separately since a tool's + * parameters lower to `positional`s, not `named-field`s in an input-schema `record`. + */ +class NativeToolParamSchema(val name: String, val witType: String) + +/** + * A native `golem:tool@0.1.0` tool, scoped to one root command (no subcommands, no + * options/flags/constraints/streams/errors). Parameters lower 1:1 to fixed positionals, in + * declaration order; `outputWitType == "()"` means no `result-spec`. + */ +class NativeToolDescriptor( + val name: String, + val description: String, + val params: List, + val outputWitType: String, + val handler: (List) -> SchemaValue, +) + +/** + * Canonical-ABI layout constants for `golem:tool@0.1.0`'s `common` interface, computed via + * `wit-parser::SizeAlign` against `wit-native/deps/golem-tool/common.wit` on 2026-07-07 (see + * `AgentTypeModel.kt`'s `Layout` doc comment for why these are tool-verified, not hand-derived). + * `schema-graph`/`schema-type-node`/`schema-type-body`/`metadata-envelope` are shared with + * `golem:agent/common@2.0.0` (both packages reference the same `golem:core/types@2.0.0`), so + * this reuses `AgentTypeModel.kt`'s `lowerSchemaGraphInto`/`lowerSchemaTypeNodeInto`/ + * `lowerSchemaTypeBodyInto`/`lowerEmptyMetadataInto` rather than duplicating them. + */ +private object ToolLayout { + // record doc: size=24 align=4 + const val DOC_SIZE = 24 + const val DOC_ALIGN = 4 + const val DOC_SUMMARY = 0 + const val DOC_DESCRIPTION = 8 + const val DOC_EXAMPLES = 16 + + // record positional: size=68 align=4 + const val POSITIONAL_SIZE = 68 + const val POSITIONAL_ALIGN = 4 + const val POS_NAME = 0 + const val POS_DOC = 8 + const val POS_VALUE_NAME = 32 + const val POS_TYPE = 44 + const val POS_DEFAULT = 48 + const val POS_REQUIRED = 64 + const val POS_ACCEPTS_STDIO = 65 + + // record positionals: size=88 align=4 + const val POSITIONALS_FIXED = 0 + const val POSITIONALS_TAIL = 8 + + // record globals: size=16 align=4 + const val GLOBALS_OPTIONS = 0 + const val GLOBALS_FLAGS = 8 + + // record result-spec: size=44 align=4 + const val RESULT_SPEC_SIZE = 44 + const val RS_TYPE = 0 + const val RS_DOC = 4 + const val RS_FORMATTERS = 28 + const val RS_DEFAULT_FORMATTER = 36 + + // record formatter: size=32 align=4 + const val FORMATTER_SIZE = 32 + const val FORMATTER_ALIGN = 4 + const val FMT_NAME = 0 + const val FMT_DOC = 8 + + // option payload_offset = align_to(1, 4) = 4 + const val RESULT_SPEC_OPTION_PAYLOAD_OFFSET = 4 + + // record command-body: size=256 align=4 + const val COMMAND_BODY_SIZE = 256 + const val COMMAND_BODY_ALIGN = 4 + const val CB_POSITIONALS = 0 + const val CB_OPTIONS = 88 + const val CB_FLAGS = 96 + const val CB_CONSTRAINTS = 104 + const val CB_STDIN = 112 + const val CB_STDOUT = 152 + const val CB_RESULT = 192 + const val CB_ERRORS = 240 + const val CB_ANNOTATIONS = 248 + + // record command-node: size=324 align=4 + const val COMMAND_NODE_SIZE = 324 + const val COMMAND_NODE_ALIGN = 4 + const val CN_NAME = 0 + const val CN_ALIASES = 8 + const val CN_DOC = 16 + const val CN_GLOBALS = 40 + const val CN_SUBCOMMANDS = 56 + const val CN_BODY = 64 + + // option payload_offset = align_to(1, 4) = 4 + const val COMMAND_BODY_OPTION_PAYLOAD_OFFSET = 4 + + // record command-tree: size=8 align=4 (inline record: nodes: list) + const val COMMAND_TREE_SIZE = 8 + const val CT_NODES = 0 + + // record tool: size=36 align=4 + const val TOOL_SIZE = 36 + const val TOOL_ALIGN = 4 + const val T_VERSION = 0 + const val T_COMMANDS = 8 + const val T_SCHEMA = 16 +} + +/** + * Lowers a [NativeToolDescriptor] to the canonical-ABI `tool` record (golem:tool@0.1.0#common), + * returning a pointer to it. One command-node (the root), fixed positionals only. + */ +fun lowerTool(descriptor: NativeToolDescriptor): Int { + val roots = buildList { + descriptor.params.forEach { add(it.witType) } + if (descriptor.outputWitType != "()") add(descriptor.outputWitType) + } + val typeIndex = collectTypeNodes(roots) + + val tool = alloc(ToolLayout.TOOL_SIZE, ToolLayout.TOOL_ALIGN) + writeStringField(tool, ToolLayout.T_VERSION, "0.1.0") + lowerCommandTreeInto(tool, ToolLayout.T_COMMANDS, descriptor, typeIndex) + lowerSchemaGraphInto(tool, ToolLayout.T_SCHEMA, typeIndex) + return tool +} + +private fun lowerCommandTreeInto(base: Int, offset: Int, d: NativeToolDescriptor, typeIndex: Map) { + val treeBase = base + offset + writeListField( + treeBase, + ToolLayout.CT_NODES, + 1, + ToolLayout.COMMAND_NODE_SIZE, + ToolLayout.COMMAND_NODE_ALIGN, + ) { _, nodePtr -> lowerRootCommandNodeInto(nodePtr, d, typeIndex) } +} + +private fun lowerRootCommandNodeInto(base: Int, d: NativeToolDescriptor, typeIndex: Map) { + writeStringField(base, ToolLayout.CN_NAME, d.name) + writeEmptyListField(base, ToolLayout.CN_ALIASES) + lowerDocInto(base, ToolLayout.CN_DOC, d.description) + lowerEmptyGlobalsInto(base, ToolLayout.CN_GLOBALS) + writeEmptyListField(base, ToolLayout.CN_SUBCOMMANDS) + lowerCommandBodySomeInto(base, ToolLayout.CN_BODY, d, typeIndex) +} + +private fun lowerDocInto(base: Int, offset: Int, summary: String) { + val docBase = base + offset + writeStringField(docBase, ToolLayout.DOC_SUMMARY, summary) + writeStringField(docBase, ToolLayout.DOC_DESCRIPTION, "") + writeEmptyListField(docBase, ToolLayout.DOC_EXAMPLES) +} + +private fun lowerEmptyGlobalsInto(base: Int, offset: Int) { + val globalsBase = base + offset + writeEmptyListField(globalsBase, ToolLayout.GLOBALS_OPTIONS) + writeEmptyListField(globalsBase, ToolLayout.GLOBALS_FLAGS) +} + +private fun lowerCommandBodySomeInto(base: Int, offset: Int, d: NativeToolDescriptor, typeIndex: Map) { + val optBase = base + offset + storeByte(optBase, 1) // some + val bodyBase = optBase + ToolLayout.COMMAND_BODY_OPTION_PAYLOAD_OFFSET + + lowerPositionalsInto(bodyBase, ToolLayout.CB_POSITIONALS, d.params, typeIndex) + writeEmptyListField(bodyBase, ToolLayout.CB_OPTIONS) + writeEmptyListField(bodyBase, ToolLayout.CB_FLAGS) + writeEmptyListField(bodyBase, ToolLayout.CB_CONSTRAINTS) + writeOptionNone(bodyBase, ToolLayout.CB_STDIN) + writeOptionNone(bodyBase, ToolLayout.CB_STDOUT) + if (d.outputWitType == "()") { + writeOptionNone(bodyBase, ToolLayout.CB_RESULT) + } else { + lowerResultSpecSomeInto(bodyBase, ToolLayout.CB_RESULT, typeIndex.getValue(d.outputWitType)) + } + writeEmptyListField(bodyBase, ToolLayout.CB_ERRORS) + writeOptionNone(bodyBase, ToolLayout.CB_ANNOTATIONS) +} + +private fun lowerPositionalsInto(base: Int, offset: Int, params: List, typeIndex: Map) { + val posBase = base + offset + writeListField( + posBase, + ToolLayout.POSITIONALS_FIXED, + params.size, + ToolLayout.POSITIONAL_SIZE, + ToolLayout.POSITIONAL_ALIGN, + ) { i, p -> lowerPositionalInto(p, params[i], typeIndex) } + writeOptionNone(posBase, ToolLayout.POSITIONALS_TAIL) +} + +private fun lowerPositionalInto(base: Int, p: NativeToolParamSchema, typeIndex: Map) { + writeStringField(base, ToolLayout.POS_NAME, p.name) + lowerDocInto(base, ToolLayout.POS_DOC, "") + writeOptionNone(base, ToolLayout.POS_VALUE_NAME) + storeInt(base + ToolLayout.POS_TYPE, typeIndex.getValue(p.witType)) + writeOptionNone(base, ToolLayout.POS_DEFAULT) + storeByte(base + ToolLayout.POS_REQUIRED, 1) // true: no optional positionals + storeByte(base + ToolLayout.POS_ACCEPTS_STDIO, 0) // false +} + +private fun lowerResultSpecSomeInto(base: Int, offset: Int, outputTypeIndex: Int) { + val optBase = base + offset + storeByte(optBase, 1) // some + val rsBase = optBase + ToolLayout.RESULT_SPEC_OPTION_PAYLOAD_OFFSET + storeInt(rsBase + ToolLayout.RS_TYPE, outputTypeIndex) + lowerDocInto(rsBase, ToolLayout.RS_DOC, "") + // default-formatter must resolve to a name in formatters (construction invariant) -- a + // single "default" formatter with no special rendering hint satisfies it. + writeListField( + rsBase, + ToolLayout.RS_FORMATTERS, + 1, + ToolLayout.FORMATTER_SIZE, + ToolLayout.FORMATTER_ALIGN, + ) { _, fmtPtr -> + writeStringField(fmtPtr, ToolLayout.FMT_NAME, "default") + lowerDocInto(fmtPtr, ToolLayout.FMT_DOC, "") + } + writeStringField(rsBase, ToolLayout.RS_DEFAULT_FORMATTER, "default") +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Transactions.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Transactions.kt new file mode 100644 index 0000000000..1174af8692 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/Transactions.kt @@ -0,0 +1,150 @@ +package cloud.golem.runtime + +// Native port of Scala's Transactions.scala: compensating-transaction (saga) helpers built +// entirely on already-existing HostApi/Guards primitives (getOplogIndex/setOplogIndex, +// markAtomicOperation, trap) -- no new WIT surface, no new host imports. Synchronous throughout +// for the same reason as Guards.kt: none of the underlying host calls are actually async, so +// Scala's Future-based structure (a Scala.js environment artifact) doesn't carry over; this SDK +// has no coroutines dependency yet and doesn't need one for this. +// +// ==Infallible Transactions== +// Use [infallibleTransaction] when operations must eventually succeed. On failure, +// compensations run and the transaction retries automatically. +// +// ==Fallible Transactions== +// Use [fallibleTransaction] when you want explicit error handling instead of automatic retry. + +/** Minimal local Either -- this SDK has no Arrow/stdlib Either dependency and this is its first use. */ +sealed class Either { + data class Left(val value: L) : Either() + data class Right(val value: R) : Either() +} + +/** Describes how a fallible transaction failed. */ +sealed class TransactionFailure { + data class FailedAndRolledBackCompletely(val error: Err) : TransactionFailure() + data class FailedAndRolledBackPartially(val error: Err, val compensationFailure: Err) : TransactionFailure() +} + +/** An atomic operation with execute and compensate steps. */ +class Operation( + private val run: (In) -> Either, + private val compensateFn: (In, Out) -> Either, +) { + fun execute(input: In): Either = run(input) + fun compensate(input: In, output: Out): Either = compensateFn(input, output) +} + +object Transactions { + /** Creates an [Operation] from execute and compensate functions. */ + fun operation( + run: (In) -> Either, + compensate: (In, Out) -> Either, + ): Operation = Operation(run, compensate) + + // The signal InfallibleTransaction.rollback() throws to request a retry from the + // infallibleTransaction loop below, distinguished by identity from any other (unexpected) + // exception -- those trap instead of retrying, matching the Scala source exactly. + private object RetrySignal : RuntimeException() + + /** + * Runs a transaction that retries on failure. When any operation fails: all registered + * compensations run in reverse order, the oplog index resets to the transaction start, and + * the entire transaction body re-executes. Keeps retrying until all operations succeed. + */ + fun infallibleTransaction(body: (InfallibleTransaction) -> A): A { + while (true) { + val guard = Guards.markAtomicOperation() + val begin = HostApi.getOplogIndex() + val tx = InfallibleTransaction() + try { + val result = body(tx) + guard.drop() + return result + } catch (e: Throwable) { + if (e === RetrySignal) { + HostApi.setOplogIndex(begin) + guard.drop() + continue + } + HostApi.trap("infallibleTransaction failed: ${e.stackTraceToString()}") + } + } + } + + /** + * Runs a transaction that returns errors instead of retrying. When an operation fails: + * registered compensations run in reverse order (best-effort), and the failure is returned + * with rollback status. + */ + fun fallibleTransaction(body: (FallibleTransaction) -> Either): Either, A> { + val guard = Guards.markAtomicOperation() + val tx = FallibleTransaction() + return try { + when (val result = body(tx)) { + is Either.Right -> { + guard.drop() + Either.Right(result.value) + } + is Either.Left -> { + val failure = tx.onFailure(result.value) + guard.drop() + Either.Left(failure) + } + } + } catch (e: Throwable) { + HostApi.trap("fallibleTransaction failed: ${e.stackTraceToString()}") + } + } + + /** Transaction context for infallible transactions: operations that fail trigger automatic rollback and retry. */ + class InfallibleTransaction internal constructor() { + private val compensations = mutableListOf<() -> Unit>() + + /** Executes [operation] within the transaction. On success, registers the compensation + * for potential rollback. On failure, runs all compensations and signals retry. */ + fun execute(operation: Operation, input: In): Out = when (val result = operation.execute(input)) { + is Either.Right -> { + compensations.add(0) { + when (val c = operation.compensate(input, result.value)) { + is Either.Right -> Unit + is Either.Left -> error("Infallible compensation failed: ${c.value}") + } + } + result.value + } + is Either.Left -> rollback() + } + + private fun rollback(): Nothing { + for (c in compensations) c() + throw RetrySignal + } + } + + /** Transaction context for fallible transactions: operations that fail trigger best-effort rollback without retry. */ + class FallibleTransaction internal constructor() { + private val compensations = mutableListOf<() -> Either>() + + /** Executes [operation] within the transaction. On success, registers the compensation + * for potential rollback. On failure, returns the error (call [onFailure] to roll back). */ + fun execute(operation: Operation, input: In): Either = when (val result = operation.execute(input)) { + is Either.Right -> { + compensations.add(0) { operation.compensate(input, result.value) } + result + } + is Either.Left -> result + } + + /** Triggers rollback after a failure: runs all registered compensations in reverse order. */ + fun onFailure(error: Err): TransactionFailure { + for (c in compensations) { + when (val r = c()) { + is Either.Left -> return TransactionFailure.FailedAndRolledBackPartially(error, r.value) + is Either.Right -> {} + } + } + return TransactionFailure.FailedAndRolledBackCompletely(error) + } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/TypedSchemaValue.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/TypedSchemaValue.kt new file mode 100644 index 0000000000..739accf0f0 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/TypedSchemaValue.kt @@ -0,0 +1,230 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.buildSchemaValueTree +import cloud.golem.wasm.liftSingleValue +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.storeInt + +/** + * A `typed-schema-value` (golem:core/types@2.0.0): a self-describing value = a schema-graph + * describing the type, paired with a schema-value-tree holding the value. This is the wire + * carrier that durable-function persistence (`DurabilityApi`), tool-rpc invocation (`ToolHost`), + * and the oplog all traffic in. + * + * [witType] is a rich witType-string (the same grammar the agent-surface type mapping uses): a + * primitive (`bool`/`s8`..`u64`/`f32`/`f64`/`char`/`string`) or an arbitrarily-nested composite + * (`record<...>`, `variant<...>`, `enum<...>`, `list`, `option`, `tuple<...>`, `map`, + * `result`). [value] must be the matching [SchemaValue] for [witType]. Both encode + * ([lowerTypedSchemaValue]) and decode ([liftTypedSchemaValue]) support the full composite grammar: + * encode serializes the type via the shared recursive schema-graph builder, and decode walks that + * graph back to a witType-string, then lifts the value tree positionally against it. + */ +data class TypedSchemaValue(val witType: String, val value: SchemaValue) + +// typed-schema-value record layout (verified via abi-dump against wit-native, 2026-07-10): +// size=32 align=4; graph @ 0 (schema-graph, 20B), value @ 20 (schema-value-tree, 12B). +internal const val TSV_SIZE = 32 +internal const val TSV_ALIGN = 4 +private const val TSV_GRAPH_OFFSET = 0 +private const val TSV_VALUE_OFFSET = 20 + +/** Allocates and lowers a [TypedSchemaValue], returning the pointer to the 32-byte record. */ +fun lowerTypedSchemaValue(tsv: TypedSchemaValue): Int { + val base = alloc(TSV_SIZE, TSV_ALIGN) + lowerTypedSchemaValueInto(base, tsv) + return base +} + +/** + * Lowers a [TypedSchemaValue] into an already-allocated 32-byte slot at [base] -- used when the + * typed-schema-value is a field of a larger bundle (e.g. persist's args), stored inline rather + * than via a pointer. + */ +fun lowerTypedSchemaValueInto(base: Int, tsv: TypedSchemaValue) { + // graph @ 0: a schema-graph describing the value's type. Reuses AgentTypeModel's recursive + // schema-graph builder. Unlike the agent-type schema (where graph.root is a structural + // placeholder left at 0), a typed-schema-value's graph.root is SEMANTIC -- it must point at the + // value's own type node. collectTypeNodes registers the root type LAST (children first), so its + // index is typeIndex[witType]; overwrite the builder's 0 placeholder with it. (For a primitive + // that index is already 0, so this is a no-op there.) + val typeIndex = collectTypeNodes(listOf(tsv.witType)) + lowerSchemaGraphInto(base, TSV_GRAPH_OFFSET, typeIndex) + storeInt(base + TSV_GRAPH_OFFSET + SG_ROOT, typeIndex.getValue(tsv.witType)) + // value @ 20: the 12-byte schema-value-tree {value-nodes.ptr, value-nodes.len, root}, + // copied inline (a record-typed field is stored inline, not via a further indirection -- + // same pattern as Guest.kt's invoke-result lowering). + val treePtr = buildSchemaValueTree(tsv.value) + val valBase = base + TSV_VALUE_OFFSET + storeInt(valBase, loadInt(treePtr)) + storeInt(valBase + 4, loadInt(treePtr + 4)) + storeInt(valBase + 8, loadInt(treePtr + 8)) +} + +// --- DECODE (exact inverse of the encode above) ------------------------------------------- + +// schema-graph layout (verified via abi-dump, mirrors AgentTypeModel.Layout.SG_*/SCHEMA_TYPE_NODE_* +// and the sub-record strides in lowerSchemaTypeBodyInto): +// schema-graph: type-nodes list @0 (ptr@0,len@4), defs @8, root: type-node-index @16. +// schema-type-node: 144B; its schema-type-body variant starts at node offset 0 (tag@0, payload@8). +private const val SG_TYPE_NODES_PTR = 0 +private const val SG_DEFS_PTR = 8 +private const val SG_ROOT = 16 +private const val SCHEMA_TYPE_NODE_SIZE = 144 +private const val STB_PAYLOAD_OFFSET = 8 + +// schema-type-def { id@0 (type-id, 8B), name@8 (option, 12B), body(type-node-index)@20 }: size 24. +private const val SCHEMA_TYPE_DEF_SIZE = 24 +private const val STD_BODY = 20 + +// union-branch { tag@0 (string, 8B), body(type-node-index)@8, discriminator@12, metadata@36 }: size 92. +private const val UNION_BRANCH_SIZE = 92 +private const val UB_BODY = 8 + +// named-field-type { name@0 (string), body(type-node-index)@8, metadata@12 }: size 68. +private const val NAMED_FIELD_TYPE_SIZE = 68 +private const val NFT_BODY = 8 + +// variant-case-type { name@0 (string), payload(option)@8, metadata@16 }: size 72. +private const val VARIANT_CASE_TYPE_SIZE = 72 +private const val VCT_PAYLOAD = 8 +private const val ENUM_CASE_SIZE = 8 // list element: ptr@0, len@4 +private const val TYPE_NODE_INDEX_SIZE = 4 // list / tuple element: s32 +// map-spec { key(idx)@0, value(idx)@4 }; result-spec { ok(option)@0, err(option)@8 }. + +/** Inverse of [lowerSchemaTypeBodyInto]'s primitive cases: schema-type-body tag -> primitive WIT type. */ +private fun primitiveWitTypeForBodyTag(tag: Int): String = when (tag) { + 1 -> "bool" + 2 -> "s8" + 3 -> "s16" + 4 -> "s32" + 5 -> "s64" + 6 -> "u8" + 7 -> "u16" + 8 -> "u32" + 9 -> "u64" + 10 -> "f32" + 11 -> "f64" + 12 -> "char" + 13 -> "string" + else -> error("native liftTypedSchemaValue: unexpected primitive schema-type-body tag=$tag") +} + +/** Reads a canonical-ABI `string` field (ptr@offset, len@offset+4) at [ptr]+[offset]. */ +private fun readStringField(ptr: Int, offset: Int): String = liftString(loadInt(ptr + offset), loadInt(ptr + offset + 4)) + +/** Reads `option` (tag@0 byte, s32@4) at [ptr], returning the index or `null`. */ +private fun readOptionIndex(ptr: Int): Int? = if (loadByte(ptr).toInt() and 0xFF == 0) null else loadInt(ptr + 4) + +/** + * Reconstructs the witType-string of the schema-graph type-node at [nodeIndex] in the type-nodes + * pool at [typeNodesPtr], recursing into children for composites. Exact inverse of + * [lowerSchemaTypeBodyInto]: the string it yields is the same grammar [liftSingleValue] parses, so + * the value tree lifts positionally against it. Field/case names are recovered for fidelity (the + * value lift ignores them). + * + * Handles ALL 36 `schema-type-body` cases. The SDK's own encoder only emits the primitive + + * record/variant/enum/tuple/list/map/option/result bodies, but a live host graph (oplog / durable + * persist / tool-rpc) can carry any of the rest: `ref-type` (named-definition indirection into the + * graph's `defs` at [defsPtr]), the rich semantic scalars (flags/text/binary/path/url/duration/ + * quantity/secret/quota-token -- reconstructed by fixed name, since their value lift ignores the + * inline restrictions), `fixed-list`, `union` (tagged branches), and the WASI-P3 `future`/`stream` + * stubs (type-parseable; no constructible values). + * + * [visitingDefs] is the set of `def-index`es currently being expanded on this path. A `ref-type` + * back to one of them is a recursive type, which a FLAT witType-string fundamentally cannot + * represent -- so we error cleanly rather than loop forever. Acyclic sharing (the common `ref` + * dedup case, incl. diamonds) expands inline and is unaffected. + */ +internal fun schemaNodeToWitType(typeNodesPtr: Int, defsPtr: Int, nodeIndex: Int, visitingDefs: Set = emptySet()): String { + val nodeBase = typeNodesPtr + nodeIndex * SCHEMA_TYPE_NODE_SIZE + val tag = loadByte(nodeBase).toInt() and 0xFF // schema-type-body tag (node body starts at offset 0) + val payload = nodeBase + STB_PAYLOAD_OFFSET + fun child(idx: Int) = schemaNodeToWitType(typeNodesPtr, defsPtr, idx, visitingDefs) + fun listHeader() = loadInt(payload) to loadInt(payload + 4) // (ptr, len) + return when (tag) { + 0 -> { // ref-type(def-index): expand the referenced named definition's body inline. + val defIdx = loadInt(payload) + if (defIdx in visitingDefs) { + error("native liftTypedSchemaValue: recursive schema type (ref cycle at def=$defIdx) cannot be reconstructed as a flat witType") + } + val bodyNode = loadInt(defsPtr + defIdx * SCHEMA_TYPE_DEF_SIZE + STD_BODY) + schemaNodeToWitType(typeNodesPtr, defsPtr, bodyNode, visitingDefs + defIdx) + } + in 1..13 -> primitiveWitTypeForBodyTag(tag) + 14 -> { // record-type(list) + val (ptr, len) = listHeader() + (0 until len).joinToString(",", "record<", ">") { i -> + val fp = ptr + i * NAMED_FIELD_TYPE_SIZE + "${readStringField(fp, 0)}:${child(loadInt(fp + NFT_BODY))}" + } + } + 15 -> { // variant-type(list) + val (ptr, len) = listHeader() + (0 until len).joinToString(",", "variant<", ">") { i -> + val cp = ptr + i * VARIANT_CASE_TYPE_SIZE + val payloadIdx = readOptionIndex(cp + VCT_PAYLOAD) + "${readStringField(cp, 0)}:${payloadIdx?.let { child(it) } ?: "_"}" + } + } + 16 -> { // enum-type(list) + val (ptr, len) = listHeader() + if (len == 0) { + "enum" + } else { + (0 until len).joinToString(",", "enum<", ">") { i -> readStringField(ptr + i * ENUM_CASE_SIZE, 0) } + } + } + 17 -> "flags" // flags-type(list): value lift is positional over list, names dropped + 18 -> { // tuple-type(list) + val (ptr, len) = listHeader() + (0 until len).joinToString(",", "tuple<", ">") { i -> child(loadInt(ptr + i * TYPE_NODE_INDEX_SIZE)) } + } + 19 -> "list<${child(loadInt(payload))}>" // list-type(type-node-index) + 20 -> "fixed-list<${child(loadInt(payload))}>" // fixed-list-spec { element@0, length@4 } -- length not needed by the value lift + 21 -> "map<${child(loadInt(payload))},${child(loadInt(payload + 4))}>" // map-spec { key@0, value@4 } + 22 -> "option<${child(loadInt(payload))}>" // option-type(type-node-index) + 23 -> { // result-spec { ok(option)@0, err(option)@8 } + val ok = readOptionIndex(payload) + val err = readOptionIndex(payload + 8) + "result<${ok?.let { child(it) } ?: "_"},${err?.let { child(it) } ?: "_"}>" + } + 24 -> "text" // text-type(text-restrictions) -- value = { text, option } + 25 -> "binary" // binary-type(binary-restrictions) -- value = { bytes, option } + 26 -> "path" // path-type(path-spec) -- value = string + 27 -> "url" // url-type(url-restrictions) -- value = string + 28 -> "datetime" // datetime-type (tag-only) + 29 -> "duration" // duration-type (tag-only) + 30 -> "quantity" // quantity-type(quantity-spec) -- value = { mantissa, scale, unit } + 31 -> { // union-type(union-spec { branches: list }); value carries the matched branch tag + val (ptr, len) = listHeader() + (0 until len).joinToString(",", "union<", ">") { i -> + val bp = ptr + i * UNION_BRANCH_SIZE + "${readStringField(bp, 0)}:${child(loadInt(bp + UB_BODY))}" + } + } + 32 -> "secret" // secret-type(secret-spec) -- value = own handle + 33 -> "quota-token" // quota-token-type(quota-token-spec) -- value = own handle + 34 -> readOptionIndex(payload)?.let { "future<${child(it)}>" } ?: "future" // future-type(option): WASI-P3 stub + 35 -> readOptionIndex(payload)?.let { "stream<${child(it)}>" } ?: "stream" // stream-type(option): WASI-P3 stub + else -> error("native liftTypedSchemaValue: unsupported schema-type-body tag=$tag") + } +} + +/** + * Lifts a `typed-schema-value` from the 32-byte record at [base] (graph@0, value@20). Reconstructs + * the full (possibly composite) WIT type from the graph's semantic root node, then lifts the value + * tree's root against it. + */ +fun liftTypedSchemaValue(base: Int): TypedSchemaValue { + val typeNodesPtr = loadInt(base + TSV_GRAPH_OFFSET + SG_TYPE_NODES_PTR) + val defsPtr = loadInt(base + TSV_GRAPH_OFFSET + SG_DEFS_PTR) + val root = loadInt(base + TSV_GRAPH_OFFSET + SG_ROOT) + val witType = schemaNodeToWitType(typeNodesPtr, defsPtr, root) + val value = liftSingleValue(base + TSV_VALUE_OFFSET, witType) + return TypedSchemaValue(witType, value) +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/config/Config.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/config/Config.kt new file mode 100644 index 0000000000..f9bd6eb323 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/config/Config.kt @@ -0,0 +1,16 @@ +package cloud.golem.runtime.config + +// Native port of Scala's golem.config.Config[T] (sdks/scala/model/src/main/scala/golem/config/ +// Config.scala): a tiny lazy-loaded config value wrapper, package-private in Scala +// (`private[golem]`) -- infrastructure for future annotation-driven config plumbing (Task +// the extended annotations), not directly user-facing. Kept equally minimal here. + +/** A lazily-loaded configuration value. */ +class Config internal constructor(private val loadFn: () -> T) { + val value: T get() = loadFn() + + companion object { + internal fun of(loadFn: () -> T): Config = Config(loadFn) + internal fun eager(value: T): Config = Config { value } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/ContextApi.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/ContextApi.kt new file mode 100644 index 0000000000..77c842164f --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/ContextApi.kt @@ -0,0 +1,288 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.host + +import cloud.golem.runtime.lowerStringToPtrLen +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt + +// Raw canonical-ABI bindings to golem:api/context@1.5.0. Unlike golem:api/host@1.5.0's +// get-agents (this SDK's first resource, see HostApi.kt), `span`/`invocation-context` have NO +// `constructor` in their WIT bodies -- the only way to obtain one is the plain top-level +// functions start-span/current-context, which return the handle as an ordinary flattened i32 +// result (not a `[constructor]` intrinsic). Signatures verified via abi-dump's `sig` mode +// against wit-native/deps/golem-1.x/golem-context.wit, same as every other import in this SDK. +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "start-span") +private external fun hostStartSpan(namePtr: Int, nameLen: Int): Int + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "current-context") +private external fun hostCurrentContext(): Int + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "allow-forwarding-trace-context-headers") +private external fun hostAllowForwardingTraceContextHeaders(allow: Int): Int + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]span.started-at") +private external fun hostSpanStartedAt(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]span.set-attribute") +private external fun hostSpanSetAttribute(handle: Int, namePtr: Int, nameLen: Int, valueTag: Int, valuePtr: Int, valueLen: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]span.set-attributes") +private external fun hostSpanSetAttributes(handle: Int, listPtr: Int, listLen: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]span.finish") +private external fun hostSpanFinish(handle: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[resource-drop]span") +private external fun hostSpanDrop(handle: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]invocation-context.trace-id") +private external fun hostInvocationContextTraceId(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]invocation-context.span-id") +private external fun hostInvocationContextSpanId(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]invocation-context.parent") +private external fun hostInvocationContextParent(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]invocation-context.get-attribute") +private external fun hostInvocationContextGetAttribute(handle: Int, keyPtr: Int, keyLen: Int, inherited: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]invocation-context.get-attributes") +private external fun hostInvocationContextGetAttributes(handle: Int, inherited: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]invocation-context.get-attribute-chain") +private external fun hostInvocationContextGetAttributeChain(handle: Int, keyPtr: Int, keyLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]invocation-context.get-attribute-chains") +private external fun hostInvocationContextGetAttributeChains(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[method]invocation-context.trace-context-headers") +private external fun hostInvocationContextTraceContextHeaders(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/context@1.5.0", "[resource-drop]invocation-context") +private external fun hostInvocationContextDrop(handle: Int) + +/** Matches `wasi:clocks/wall-clock@0.2.3`'s `datetime` record (16 bytes, align 8) -- confirmed via wit-parser, not assumed to match `golem:core/types@2.0.0`'s own `datetime` despite the same field names. */ +data class ContextDateTime(val seconds: Long, val nanoseconds: Int) + +/** Matches `golem:api/context@1.5.0`'s `attribute-value` variant (currently one case: a string). */ +sealed class AttributeValue { + data class StringValue(val value: String) : AttributeValue() +} + +data class Attribute(val key: String, val value: AttributeValue) +data class AttributeChain(val key: String, val values: List) + +private fun liftDateTime(base: Int): ContextDateTime = ContextDateTime(loadLong(base), loadInt(base + 8)) + +// attribute-value: size=12 align=4, tag_size=1, payload_offset=4. Single case (string). +private fun liftAttributeValue(base: Int): AttributeValue { + val tag = loadByte(base).toInt() and 0xFF + require(tag == 0) { "unknown attribute-value tag: $tag" } + return AttributeValue.StringValue(liftString(loadInt(base + 4), loadInt(base + 8))) +} + +private fun lowerAttributeValueParams(value: AttributeValue): Triple = when (value) { + is AttributeValue.StringValue -> { + val (ptr, len) = lowerStringToPtrLen(value.value) + Triple(0, ptr, len) + } +} + +// attribute: size=20 align=4 { key: offset=0 (string,8,4), value: offset=8 (attribute-value,12,4) }. +private fun liftAttribute(base: Int): Attribute = Attribute(liftString(loadInt(base), loadInt(base + 4)), liftAttributeValue(base + 8)) + +private fun writeAttribute(base: Int, attr: Attribute) { + val (namePtr, nameLen) = lowerStringToPtrLen(attr.key) + storeInt(base, namePtr) + storeInt(base + 4, nameLen) + val valueBase = base + 8 + when (attr.value) { + is AttributeValue.StringValue -> { + storeByte(valueBase, 0) + val (ptr, len) = lowerStringToPtrLen(attr.value.value) + storeInt(valueBase + 4, ptr) + storeInt(valueBase + 8, len) + } + } +} + +// attribute-chain: size=16 align=4 { key: offset=0 (string,8,4), values: offset=8 (list,8,4) }. +private fun liftAttributeChain(base: Int): AttributeChain { + val key = liftString(loadInt(base), loadInt(base + 4)) + val listBase = base + 8 + val dataPtr = loadInt(listBase) + val len = loadInt(listBase + 4) + return AttributeChain(key, (0 until len).map { i -> liftAttributeValue(dataPtr + i * 12) }) +} + +private fun liftListOfAttribute(base: Int): List { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> liftAttribute(dataPtr + i * 20) } +} + +private fun liftListOfAttributeValue(base: Int): List { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> liftAttributeValue(dataPtr + i * 12) } +} + +private fun liftListOfAttributeChain(base: Int): List { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> liftAttributeChain(dataPtr + i * 16) } +} + +private fun liftListOfStringPair(base: Int): List> { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> + val elemPtr = dataPtr + i * 16 + liftString(loadInt(elemPtr), loadInt(elemPtr + 4)) to liftString(loadInt(elemPtr + 8), loadInt(elemPtr + 12)) + } +} + +/** + * Represents a unit of work or operation (`golem:api/context@1.5.0`'s `span` resource). MUST + * be [close]d when done -- per the WIT doc comment, dropping without calling [finish] first is + * the NORMAL lifecycle (the host finishes the span automatically at drop time), not a bug; + * [finish] is only for an EARLY finish. + */ +class Span internal constructor(private val handle: Int) { + private var closed = false + + fun startedAt(): ContextDateTime { + check(!closed) { "Span already closed" } + val retPtr = alloc(16, 8) + hostSpanStartedAt(handle, retPtr) + return liftDateTime(retPtr) + } + + fun setAttribute(name: String, value: AttributeValue) { + check(!closed) { "Span already closed" } + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val (tag, valuePtr, valueLen) = lowerAttributeValueParams(value) + hostSpanSetAttribute(handle, namePtr, nameLen, tag, valuePtr, valueLen) + } + + fun setAttributes(attributes: List) { + check(!closed) { "Span already closed" } + val arr = alloc(attributes.size * 20, 4) + attributes.forEachIndexed { i, a -> writeAttribute(arr + i * 20, a) } + hostSpanSetAttributes(handle, arr, attributes.size) + } + + /** Early-finishes the span; otherwise it finishes automatically when [close]d. */ + fun finish() { + check(!closed) { "Span already closed" } + hostSpanFinish(handle) + } + + fun close() { + if (!closed) { + hostSpanDrop(handle) + closed = true + } + } +} + +/** + * Allows querying the stack of attributes created by automatic and user-defined spans + * (`golem:api/context@1.5.0`'s `invocation-context` resource). MUST be [close]d when done. + */ +class InvocationContext internal constructor(private val handle: Int) { + private var closed = false + + fun traceId(): String { + check(!closed) { "InvocationContext already closed" } + val retPtr = alloc(8, 4) + hostInvocationContextTraceId(handle, retPtr) + return liftString(loadInt(retPtr), loadInt(retPtr + 4)) + } + + fun spanId(): String { + check(!closed) { "InvocationContext already closed" } + val retPtr = alloc(8, 4) + hostInvocationContextSpanId(handle, retPtr) + return liftString(loadInt(retPtr), loadInt(retPtr + 4)) + } + + /** The parent context, if any. The returned [InvocationContext] MUST also be [close]d when done. */ + fun parent(): InvocationContext? { + check(!closed) { "InvocationContext already closed" } + val retPtr = alloc(8, 4) // option: tag@0(1,1), payload@4(i32 handle,4,4) + hostInvocationContextParent(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) null else InvocationContext(loadInt(retPtr + 4)) + } + + fun getAttribute(key: String, inherited: Boolean): AttributeValue? { + check(!closed) { "InvocationContext already closed" } + val (keyPtr, keyLen) = lowerStringToPtrLen(key) + val retPtr = alloc(16, 4) // option: tag@0(1,1), payload@4(12,4) + hostInvocationContextGetAttribute(handle, keyPtr, keyLen, if (inherited) 1 else 0, retPtr) + return if (loadByte(retPtr).toInt() == 0) null else liftAttributeValue(retPtr + 4) + } + + fun getAttributes(inherited: Boolean): List { + check(!closed) { "InvocationContext already closed" } + val retPtr = alloc(8, 4) + hostInvocationContextGetAttributes(handle, if (inherited) 1 else 0, retPtr) + return liftListOfAttribute(retPtr) + } + + fun getAttributeChain(key: String): List { + check(!closed) { "InvocationContext already closed" } + val (keyPtr, keyLen) = lowerStringToPtrLen(key) + val retPtr = alloc(8, 4) + hostInvocationContextGetAttributeChain(handle, keyPtr, keyLen, retPtr) + return liftListOfAttributeValue(retPtr) + } + + fun getAttributeChains(): List { + check(!closed) { "InvocationContext already closed" } + val retPtr = alloc(8, 4) + hostInvocationContextGetAttributeChains(handle, retPtr) + return liftListOfAttributeChain(retPtr) + } + + fun traceContextHeaders(): List> { + check(!closed) { "InvocationContext already closed" } + val retPtr = alloc(8, 4) + hostInvocationContextTraceContextHeaders(handle, retPtr) + return liftListOfStringPair(retPtr) + } + + fun close() { + if (!closed) { + hostInvocationContextDrop(handle) + closed = true + } + } +} + +/** + * Native SDK access to `golem:api/context@1.5.0`: invocation context / tracing spans. Mirrors + * the Scala SDK's `ContextApi` object (`sdks/scala/core/js/src/main/scala/golem/host/ContextApi.scala`). + * Entirely resource-based, so this was gated on [[cloud.golem.runtime.HostApi.getAgents]]'s + * resource-handle canonical ABI work, now proven. + */ +object ContextApi { + /** Starts a new span with the given name, as a child of the current invocation context. */ + fun startSpan(name: String): Span { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + return Span(hostStartSpan(namePtr, nameLen)) + } + + /** The current invocation context. */ + fun currentContext(): InvocationContext = InvocationContext(hostCurrentContext()) + + /** Allows or disallows forwarding of trace context headers in outgoing HTTP requests; returns the previous value. */ + fun allowForwardingTraceContextHeaders(allow: Boolean): Boolean = hostAllowForwardingTraceContextHeaders(if (allow) 1 else 0) != 0 +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/DurabilityApi.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/DurabilityApi.kt new file mode 100644 index 0000000000..4999322d3a --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/DurabilityApi.kt @@ -0,0 +1,215 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.host + +import cloud.golem.runtime.HostApi +import cloud.golem.runtime.TypedSchemaValue +import cloud.golem.runtime.liftTypedSchemaValue +import cloud.golem.runtime.lowerTypedSchemaValueInto +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeLong +import cloud.golem.wasm.writeStringField + +// Raw canonical-ABI import bindings to golem:durability/durability@1.5.0 (package +// golem:durability@1.5.0, interface "durability"). Signatures verified via +// wit-parser::Resolve::wasm_signature(AbiVariant::GuestImport) against +// wit-native/deps/golem-durability/golem-durability.wit. This interface is not pulled in +// transitively by anything else in wit-native/main.wit's world, so it needed an explicit +// `import golem:durability/durability@1.5.0;` there (same situation as the golem:agent/host@2.0.0 import). +@kotlin.wasm.WasmImport("golem:durability/durability@1.5.0", "observe-function-call") +private external fun hostObserveFunctionCall(ifacePtr: Int, ifaceLen: Int, funcPtr: Int, funcLen: Int) + +// begin-durable-function(function-type: durable-function-type) -> oplog-index. durable-function-type +// (an alias of `wrapped-function-type`, a 6-case variant whose only payload shape is +// `option` i.e. `option`) flattens to 3 core words: the outer tag, then the +// union of each case's flattened payload -- here just the inner option's [tag, payload] +// pair -- confirmed via abi-dump's `sig` mode against the real signature, not assumed from the +// WIT shape alone. +@kotlin.wasm.WasmImport("golem:durability/durability@1.5.0", "begin-durable-function") +private external fun hostBeginDurableFunction(tag: Int, hasBegin: Int, begin: Long): Long + +@kotlin.wasm.WasmImport("golem:durability/durability@1.5.0", "end-durable-function") +private external fun hostEndDurableFunction(tag: Int, hasBegin: Int, begin: Long, beginIndex: Long, forcedCommit: Int) + +@kotlin.wasm.WasmImport("golem:durability/durability@1.5.0", "current-durable-execution-state") +private external fun hostCurrentDurableExecutionState(retPtr: Int) + +// persist-durable-function-invocation(function-name: string, request: typed-schema-value, +// response: typed-schema-value, function-type: durable-function-type). indirect_params=true +// (the flattened param set exceeds the ABI's core-arg limit), so all four args are bundled into +// one 96-byte, 8-aligned memory block passed by pointer -- layout verified via abi-dump's +// `funcargs` mode against the real WIT (2026-07-10): function-name@0, request@8, response@40, +// function-type@72. +@kotlin.wasm.WasmImport("golem:durability/durability@1.5.0", "persist-durable-function-invocation") +private external fun hostPersistDurableFunctionInvocation(argsPtr: Int) + +// read-persisted-durable-function-invocation() -> persisted-durable-function-invocation. retptr=true: +// the guest allocates the 88-byte/align-8 result record and passes its pointer; the host writes +// into it. Result layout verified via abi-dump `resulttype` (2026-07-10): timestamp@0 (datetime, +// 16B), function-name@16 (string), response@24 (typed-schema-value, 32B), function-type@56 +// (durable-function-type in-memory variant, 24B), entry-version@80 (enum, 1B). +@kotlin.wasm.WasmImport("golem:durability/durability@1.5.0", "read-persisted-durable-function-invocation") +private external fun hostReadPersistedDurableFunctionInvocation(retPtr: Int) + +/** + * Matches `golem:api/oplog@1.5.0`'s `wrapped-function-type` variant (aliased as + * `durable-function-type` in `golem:durability@1.5.0`) case order and payload shape exactly. + */ +sealed class DurableFunctionType { + object ReadLocal : DurableFunctionType() + object WriteLocal : DurableFunctionType() + object ReadRemote : DurableFunctionType() + object WriteRemote : DurableFunctionType() + data class WriteRemoteBatched(val begin: Long?) : DurableFunctionType() + data class WriteRemoteTransaction(val begin: Long?) : DurableFunctionType() +} + +// Lowers a DurableFunctionType into the 3 flattened core words wit-bindgen expects for this +// variant: (outer tag, inner option tag, inner option payload). +private fun lowerDurableFunctionType(type: DurableFunctionType): Triple = when (type) { + DurableFunctionType.ReadLocal -> Triple(0, 0, 0L) + DurableFunctionType.WriteLocal -> Triple(1, 0, 0L) + DurableFunctionType.ReadRemote -> Triple(2, 0, 0L) + DurableFunctionType.WriteRemote -> Triple(3, 0, 0L) + is DurableFunctionType.WriteRemoteBatched -> Triple(4, if (type.begin != null) 1 else 0, type.begin ?: 0L) + is DurableFunctionType.WriteRemoteTransaction -> Triple(5, if (type.begin != null) 1 else 0, type.begin ?: 0L) +} + +// Writes a durable-function-type as an IN-MEMORY variant (24 bytes, align 8) at [base], for use +// inside a bundled args block (as opposed to the flattened form above used for direct call +// args). Layout verified via abi-dump: tag @ 0 (u8), payload @ 8 = option i.e. +// option whose own tag is @ 8 and u64 value @ 16. +@OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) +private fun writeDurableFunctionTypeInMemory(base: Int, type: DurableFunctionType) { + val (tag, hasBegin, begin) = lowerDurableFunctionType(type) + storeByte(base, tag.toByte()) + storeByte(base + 8, hasBegin.toByte()) // option discriminant + if (hasBegin == 1) storeLong(base + 16, begin) // oplog-index (u64) +} + +// Inverse of writeDurableFunctionTypeInMemory: reads a 24-byte in-memory durable-function-type. +@OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) +internal fun readDurableFunctionTypeInMemory(base: Int): DurableFunctionType { + val begin: Long? = if (loadByte(base + 8).toInt() != 0) loadLong(base + 16) else null + return when (loadByte(base).toInt() and 0xFF) { + 0 -> DurableFunctionType.ReadLocal + 1 -> DurableFunctionType.WriteLocal + 2 -> DurableFunctionType.ReadRemote + 3 -> DurableFunctionType.WriteRemote + 4 -> DurableFunctionType.WriteRemoteBatched(begin) + 5 -> DurableFunctionType.WriteRemoteTransaction(begin) + else -> error("native readDurableFunctionType: unknown tag") + } +} + +/** WIT `oplog-entry-version` enum (golem:durability@1.5.0). */ +enum class OplogEntryVersion { V1, V2 } + +/** A persisted durable function invocation, read back during replay. */ +data class PersistedDurableFunctionInvocation( + /** Timestamp seconds since the Unix epoch. */ + val timestampSeconds: Long, + /** Sub-second nanoseconds of the timestamp. */ + val timestampNanoseconds: Int, + val functionName: String, + val response: TypedSchemaValue, + val functionType: DurableFunctionType, + val entryVersion: OplogEntryVersion, +) + +/** Matches `golem:durability@1.5.0`'s `durable-execution-state` record (2 bytes, align 1) field-for-field. */ +data class DurableExecutionState(val isLive: Boolean, val persistenceLevel: HostApi.PersistenceLevel) + +/** + * Native SDK access to `golem:durability/durability@1.5.0`. It provides the + * core oplog-observation/durable-function-region primitives, plus the current durable + * execution state. Mirrors the Scala SDK's `DurabilityApi` object + * (`sdks/scala/core/js/src/main/scala/golem/host/DurabilityApi.scala`) for this subset. + * + * `persistDurableFunctionInvocation`/`readPersistedDurableFunctionInvocation` lower/lift the + * request/response `typed-schema-value`s via `TypedSchemaValue` encode/decode, which supports the + * full composite grammar (records/variants/enums/lists/options/tuples/maps/results, nested). Still + * deferred: `lazy-initialized-pollable` (a WIT `resource` for async pollable wiring, with no + * consumer in the SDK's synchronous model -- part of the deferred wasi:io/poll workstream). + */ +object DurabilityApi { + fun observeFunctionCall(iface: String, function: String) { + val ifaceBytes = iface.encodeToByteArray() + val ifacePtr = alloc(ifaceBytes.size, 1) + for (i in ifaceBytes.indices) storeByte(ifacePtr + i, ifaceBytes[i]) + val funcBytes = function.encodeToByteArray() + val funcPtr = alloc(funcBytes.size, 1) + for (i in funcBytes.indices) storeByte(funcPtr + i, funcBytes[i]) + hostObserveFunctionCall(ifacePtr, ifaceBytes.size, funcPtr, funcBytes.size) + } + + fun beginDurableFunction(functionType: DurableFunctionType): Long { + val (tag, hasBegin, begin) = lowerDurableFunctionType(functionType) + return hostBeginDurableFunction(tag, hasBegin, begin) + } + + fun endDurableFunction(functionType: DurableFunctionType, beginIndex: Long, forcedCommit: Boolean) { + val (tag, hasBegin, begin) = lowerDurableFunctionType(functionType) + hostEndDurableFunction(tag, hasBegin, begin, beginIndex, if (forcedCommit) 1 else 0) + } + + fun currentDurableExecutionState(): DurableExecutionState { + val ptr = alloc(2, 1) + hostCurrentDurableExecutionState(ptr) + val isLive = loadByte(ptr).toInt() != 0 + val persistenceLevel = HostApi.PersistenceLevel.entries[loadByte(ptr + 1).toInt() and 0xFF] + return DurableExecutionState(isLive, persistenceLevel) + } + + /** + * Writes a durable-function-invocation record to the agent's oplog. [request]/[response] are + * self-describing `typed-schema-value`s (schema graph + value); any composite [TypedSchemaValue] + * is supported. Bundles the four args into the 96-byte block the host's indirect-params ABI + * expects and calls the import. + */ + fun persistDurableFunctionInvocation( + functionName: String, + request: TypedSchemaValue, + response: TypedSchemaValue, + functionType: DurableFunctionType, + ) { + val args = alloc(96, 8) + writeStringField(args, 0, functionName) // function-name @ 0 (string, 8B) + lowerTypedSchemaValueInto(args + 8, request) // request @ 8 (typed-schema-value, 32B) + lowerTypedSchemaValueInto(args + 40, response) // response @ 40 (typed-schema-value, 32B) + writeDurableFunctionTypeInMemory(args + 72, functionType) // function-type @ 72 (24B) + hostPersistDurableFunctionInvocation(args) + } + + /** + * Reads the next persisted durable-function invocation from the oplog during replay. The + * [PersistedDurableFunctionInvocation.response] is decoded via the typed-schema-value DECODE + * path, which supports composite payloads (see [TypedSchemaValue]). + */ + fun readPersistedDurableFunctionInvocation(): PersistedDurableFunctionInvocation { + val ret = alloc(88, 8) + hostReadPersistedDurableFunctionInvocation(ret) + val timestampSeconds = loadLong(ret) // datetime.seconds @ 0 (u64) + val timestampNanos = loadInt(ret + 8) // datetime.nanoseconds @ 8 (u32) + val functionName = liftString(loadInt(ret + 16), loadInt(ret + 20)) // string @ 16 + val response = liftTypedSchemaValue(ret + 24) // typed-schema-value @ 24 (32B) + val functionType = readDurableFunctionTypeInMemory(ret + 56) // durable-function-type @ 56 (24B) + val entryVersion = when (loadByte(ret + 80).toInt() and 0xFF) { // enum @ 80 + 0 -> OplogEntryVersion.V1 + else -> OplogEntryVersion.V2 + } + return PersistedDurableFunctionInvocation( + timestampSeconds, + timestampNanos, + functionName, + response, + functionType, + entryVersion, + ) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/OplogApi.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/OplogApi.kt new file mode 100644 index 0000000000..ec1a7e0cc4 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/OplogApi.kt @@ -0,0 +1,726 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.host + +import cloud.golem.runtime.AgentId +import cloud.golem.runtime.ComponentId +import cloud.golem.runtime.EnvironmentId +import cloud.golem.runtime.HostApi +import cloud.golem.runtime.TypedSchemaValue +import cloud.golem.runtime.Uuid +import cloud.golem.runtime.liftTypedSchemaValue +import cloud.golem.runtime.lowerStringToPtrLen +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong + +// Native SDK access to golem:api/oplog@1.5.0's read surface -- the `get-oplog` and `search-oplog` +// resources, mirroring the Scala SDK's `OplogApi.GetOplog`/`SearchOplog`. Both yield +// `public-oplog-entry`, a 46-case variant (size=208, align=8, tag@0, payload_offset=8 -- verified +// via abi-dump against wit-native/deps/golem-1.x/golem-oplog.wit). +// +// FULL faithful port: ALL 46 public-oplog-entry cases are now given a typed decoding -- from the +// timestamp-only cases through the flat scalar/string records, typed-schema-value payloads, +// retry-policy/state trees, spans, agent-invocation / agent-invocation-result / update-description +// variants, plugin descriptors, cards, and the 14-field `create` record. [PublicOplogEntry.Unsupported] +// remains only as a defensive fallback for an unexpected/future variant tag. Every field offset, +// variant tag, and payload offset was verified via abi-dump against golem-oplog.wit, not +// hand-derived. + +@kotlin.wasm.WasmImport("golem:api/oplog@1.5.0", "[constructor]get-oplog") +private external fun hostGetOplogNew(uuidHigh: Long, uuidLow: Long, idPtr: Int, idLen: Int, start: Long): Int + +@kotlin.wasm.WasmImport("golem:api/oplog@1.5.0", "[method]get-oplog.get-next") +private external fun hostGetOplogGetNext(self: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/oplog@1.5.0", "[resource-drop]get-oplog") +private external fun hostGetOplogDrop(handle: Int) + +@kotlin.wasm.WasmImport("golem:api/oplog@1.5.0", "[constructor]search-oplog") +private external fun hostSearchOplogNew(uuidHigh: Long, uuidLow: Long, idPtr: Int, idLen: Int, textPtr: Int, textLen: Int): Int + +@kotlin.wasm.WasmImport("golem:api/oplog@1.5.0", "[method]search-oplog.get-next") +private external fun hostSearchOplogGetNext(self: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/oplog@1.5.0", "[resource-drop]search-oplog") +private external fun hostSearchOplogDrop(handle: Int) + +/** WIT `timestamp` (wall-clock `datetime`, 16B): seconds since the Unix epoch + sub-second nanos. */ +data class Timestamp(val seconds: Long, val nanoseconds: Int) + +/** WIT `oplog-region`: an inclusive `[start, end]` range of oplog indices. */ +data class OplogRegion(val start: Long, val end: Long) + +/** WIT `log-level`. */ +enum class LogLevel { STDOUT, STDERR, TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL } + +// `Attribute` / `AttributeValue` (span attribute types) are shared with ContextApi.kt (same +// package, identical golem:api/context definitions) -- reused here rather than redeclared. + +/** WIT `state-node`: a node in a persisted `retry-policy-state` tree. Indices reference the node list. */ +sealed class StateNode { + /** Counter-based state (periodic/exponential/fibonacci). */ + data class Counter(val value: UInt) : StateNode() + + /** Terminal state -- the policy has given up. */ + object Terminal : StateNode() + + /** Wrapper state delegating to the inner node. */ + data class Wrapper(val inner: Int) : StateNode() + + /** Count-box state tracking [attempts] over the inner node. */ + data class CountBox(val attempts: UInt, val inner: Int) : StateNode() + + /** And-then sequential-composition state. */ + data class AndThen(val left: Int, val right: Int, val onRight: Boolean) : StateNode() + + /** Pair state for union/intersect composition. */ + data class Pair(val left: Int, val right: Int) : StateNode() +} + +/** WIT `retry-policy-state`: the persisted state of an active semantic retry policy (root = `nodes[0]`). */ +data class RetryPolicyState(val nodes: List) + +/** WIT `plugin-installation-description`. [grantId] is the environment-plugin-grant-id's uuid. */ +data class PluginInstallationDescription( + val grantId: Uuid, + val priority: Int, + val name: String, + val version: String, + val parameters: List>, +) + +/** WIT `snapshot-data`: a worker-state snapshot blob + its MIME type. */ +data class SnapshotData(val data: List, val mimeType: String) + +/** WIT `queued-card-event`: a durable-queue entry for pending permission-card work. */ +sealed class QueuedCardEvent { + data class Install(val cardId: Uuid) : QueuedCardEvent() + data class Revoke(val cardId: Uuid) : QueuedCardEvent() +} + +/** WIT `card-install-failure`. */ +enum class CardInstallFailure { CARD_REVOKED, NOT_FOUND, RECIPIENT_MISMATCH, NOT_PERMITTED } + +/** WIT `agent-mode`. */ +enum class AgentMode { DURABLE, EPHEMERAL } + +/** WIT `local-agent-config-entry`: a config path and its typed value. */ +data class LocalAgentConfigEntry(val path: List, val value: TypedSchemaValue) + +/** WIT `update-description`: how a pending update will be applied. */ +sealed class UpdateDescription { + /** Automatic update by replaying the oplog on the new version. */ + object AutoUpdate : UpdateDescription() + + /** Custom update by loading [snapshot] on the new version. */ + data class SnapshotBased(val snapshot: SnapshotData) : UpdateDescription() +} + +/** WIT `span-data`: a captured invocation-context span. */ +sealed class SpanData { + data class LocalSpan( + val spanId: String, + val start: Timestamp, + val parent: String?, + val linkedContext: Long?, + val attributes: List, + val inherited: Boolean, + ) : SpanData() + + data class ExternalSpan(val spanId: String) : SpanData() +} + +/** WIT `agent-invocation`: the kind of invocation recorded for an agent. */ +sealed class AgentInvocation { + data class AgentInitialization( + val idempotencyKey: String, + val constructorParameters: TypedSchemaValue, + val traceId: String, + val traceStates: List, + val invocationContext: List>, + ) : AgentInvocation() + + data class AgentMethodInvocation( + val idempotencyKey: String, + val methodName: String, + val functionInput: TypedSchemaValue, + val traceId: String, + val traceStates: List, + val invocationContext: List>, + ) : AgentInvocation() + + object SaveSnapshot : AgentInvocation() + data class LoadSnapshot(val snapshot: SnapshotData) : AgentInvocation() + data class ProcessOplogEntries(val idempotencyKey: String) : AgentInvocation() + data class ManualUpdate(val targetRevision: Long) : AgentInvocation() +} + +/** WIT `agent-invocation-result`: the outcome of an [AgentInvocation]. */ +sealed class AgentInvocationResult { + data class AgentInitialization(val output: TypedSchemaValue) : AgentInvocationResult() + data class AgentMethod(val output: TypedSchemaValue) : AgentInvocationResult() + object ManualUpdate : AgentInvocationResult() + + /** `fallible-result`: [error] is null on success. */ + data class LoadSnapshot(val error: String?) : AgentInvocationResult() + data class SaveSnapshot(val snapshot: SnapshotData) : AgentInvocationResult() + + /** `fallible-result`: [error] is null on success. */ + data class ProcessOplogEntries(val error: String?) : AgentInvocationResult() +} + +/** + * A public oplog entry (golem:api/oplog@1.5.0 `public-oplog-entry`). One data class per variant + * case. Cases not yet given a typed decoding surface as [Unsupported] with the raw tag; they are + * filled in over subsequent increments. + */ +sealed class PublicOplogEntry { + /** Agent suspended. */ + data class Suspend(val timestamp: Timestamp) : PublicOplogEntry() + + /** Marker added when get-oplog-index is called, to make jumping predictable. */ + data class NoOp(val timestamp: Timestamp) : PublicOplogEntry() + + /** The agent was interrupted at this point. */ + data class Interrupted(val timestamp: Timestamp) : PublicOplogEntry() + + /** The agent exited via WASI's exit function. */ + data class Exited(val timestamp: Timestamp) : PublicOplogEntry() + + /** Begins an atomic region. */ + data class BeginAtomicRegion(val timestamp: Timestamp) : PublicOplogEntry() + + /** The agent was restarted, forgetting its history. */ + data class Restart(val timestamp: Timestamp) : PublicOplogEntry() + + /** Recover up to [jump]'s end, then continue from its start (ignoring operations in between). */ + data class Jump(val timestamp: Timestamp, val jump: OplogRegion) : PublicOplogEntry() + + /** Ends an atomic region begun at [beginIndex]. */ + data class EndAtomicRegion(val timestamp: Timestamp, val beginIndex: Long) : PublicOplogEntry() + + /** Increased total linear memory size by [delta] bytes. */ + data class GrowMemory(val timestamp: Timestamp, val delta: Long) : PublicOplogEntry() + + /** Filesystem usage changed by the signed [delta]. */ + data class FilesystemStorageUsageUpdate(val timestamp: Timestamp, val delta: Long) : PublicOplogEntry() + + /** Created a resource instance. */ + data class CreateResource(val timestamp: Timestamp, val id: Long, val name: String, val owner: String) : PublicOplogEntry() + + /** Dropped a resource instance. */ + data class DropResource(val timestamp: Timestamp, val id: Long, val name: String, val owner: String) : PublicOplogEntry() + + /** Changed the current persistence level. */ + data class ChangePersistenceLevel(val timestamp: Timestamp, val persistenceLevel: HostApi.PersistenceLevel) : PublicOplogEntry() + + /** Removed a named retry policy. */ + data class RemoveRetryPolicy(val timestamp: Timestamp, val name: String) : PublicOplogEntry() + + /** Begins a remote transaction. */ + data class BeginRemoteTransaction(val timestamp: Timestamp, val transactionId: String) : PublicOplogEntry() + + /** Pre-commit of the remote transaction begun at [beginIndex]. */ + data class PreCommitRemoteTransaction(val timestamp: Timestamp, val beginIndex: Long) : PublicOplogEntry() + + /** Pre-rollback of the remote transaction begun at [beginIndex]. */ + data class PreRollbackRemoteTransaction(val timestamp: Timestamp, val beginIndex: Long) : PublicOplogEntry() + + /** The remote transaction begun at [beginIndex] committed. */ + data class CommittedRemoteTransaction(val timestamp: Timestamp, val beginIndex: Long) : PublicOplogEntry() + + /** The remote transaction begun at [beginIndex] rolled back. */ + data class RolledBackRemoteTransaction(val timestamp: Timestamp, val beginIndex: Long) : PublicOplogEntry() + + /** Successful completion of the durable host call started at [startIndex]; [response] is its result. */ + data class End(val timestamp: Timestamp, val startIndex: Long, val response: TypedSchemaValue?, val forcedCommit: Boolean) : PublicOplogEntry() + + /** The durable host call started at [startIndex] was cancelled; [partial] is any partial result. */ + data class Cancelled(val timestamp: Timestamp, val startIndex: Long, val partial: TypedSchemaValue?) : PublicOplogEntry() + + /** Sets or overwrites a named retry policy. */ + data class SetRetryPolicy(val timestamp: Timestamp, val policy: NamedRetryPolicy) : PublicOplogEntry() + + /** The agent failed; [retryPolicyState] is the persisted retry state when a semantic policy is active. */ + data class Error( + val timestamp: Timestamp, + val error: String, + val retryFrom: Long, + val insideAtomicRegion: Boolean, + val retryPolicyState: RetryPolicyState?, + ) : PublicOplogEntry() + + /** The agent emitted a log message. */ + data class Log(val timestamp: Timestamp, val level: LogLevel, val context: String, val message: String) : PublicOplogEntry() + + /** Started a new span in the invocation context. */ + data class StartSpan( + val timestamp: Timestamp, + val spanId: String, + val parent: String?, + val linkedContextId: String?, + val attributes: List, + ) : PublicOplogEntry() + + /** Finished an open span. */ + data class FinishSpan(val timestamp: Timestamp, val spanId: String) : PublicOplogEntry() + + /** Set an attribute on an open span. */ + data class SetSpanAttribute(val timestamp: Timestamp, val spanId: String, val key: String, val value: AttributeValue) : PublicOplogEntry() + + /** Reverted the agent, dropping [droppedRegion] from the oplog. */ + data class Revert(val timestamp: Timestamp, val droppedRegion: OplogRegion) : PublicOplogEntry() + + /** Cancelled a pending invocation identified by [idempotencyKey]. */ + data class CancelPendingInvocation(val timestamp: Timestamp, val idempotencyKey: String) : PublicOplogEntry() + + /** A snapshot of the worker's state. */ + data class Snapshot(val timestamp: Timestamp, val data: SnapshotData) : PublicOplogEntry() + + /** Checkpoint for oplog-processor-plugin delivery tracking. */ + data class OplogProcessorCheckpoint( + val timestamp: Timestamp, + val plugin: PluginInstallationDescription, + val targetAgentId: AgentId, + val confirmedUpTo: Long, + val sendingUpTo: Long, + val lastBatchStart: Long, + ) : PublicOplogEntry() + + /** Activated a plugin. */ + data class ActivatePlugin(val timestamp: Timestamp, val plugin: PluginInstallationDescription) : PublicOplogEntry() + + /** Deactivated a plugin. */ + data class DeactivatePlugin(val timestamp: Timestamp, val plugin: PluginInstallationDescription) : PublicOplogEntry() + + /** Durable-queue entry for pending permission-card work. */ + data class CardEventQueued(val timestamp: Timestamp, val event: QueuedCardEvent) : PublicOplogEntry() + + /** A permission card was installed into the agent wallet. */ + data class CardInstalled(val timestamp: Timestamp, val queuedEventIndex: Long?, val cardId: Uuid) : PublicOplogEntry() + + /** A permission-card installation failed. */ + data class CardInstallFailed(val timestamp: Timestamp, val queuedEventIndex: Long, val cardId: Uuid, val reason: CardInstallFailure) : PublicOplogEntry() + + /** A permission card used by the agent was revoked. */ + data class CardRevoked(val timestamp: Timestamp, val queuedEventIndex: Long, val cardId: Uuid) : PublicOplogEntry() + + /** A permission card used by the agent expired. */ + data class CardExpired(val timestamp: Timestamp, val cardId: Uuid) : PublicOplogEntry() + + /** Marks the start of a durable host call (or scope). [request] is the call's input. */ + data class Start( + val timestamp: Timestamp, + val parentStartIndex: Long?, + val functionName: String, + val request: TypedSchemaValue?, + val durableFunctionType: DurableFunctionType, + ) : PublicOplogEntry() + + /** An update to [targetRevision] arrived and will be applied when the agent restarts. */ + data class PendingUpdate(val timestamp: Timestamp, val targetRevision: Long, val description: UpdateDescription) : PublicOplogEntry() + + /** An update to [targetRevision] was successfully applied. */ + data class SuccessfulUpdate( + val timestamp: Timestamp, + val targetRevision: Long, + val newComponentSize: Long, + val newActivePlugins: List, + ) : PublicOplogEntry() + + /** An update to [targetRevision] failed to apply; [details] is the reason, if any. */ + data class FailedUpdate(val timestamp: Timestamp, val targetRevision: Long, val details: String?) : PublicOplogEntry() + + /** The agent was invoked. */ + data class AgentInvocationStarted(val timestamp: Timestamp, val invocation: AgentInvocation) : PublicOplogEntry() + + /** The agent completed an invocation. */ + data class AgentInvocationFinished( + val timestamp: Timestamp, + val result: AgentInvocationResult, + val methodName: String?, + val consumedFuel: Long, + val componentRevision: Long, + ) : PublicOplogEntry() + + /** An invocation request arrived while the agent was busy. */ + data class PendingAgentInvocation(val timestamp: Timestamp, val invocation: AgentInvocation) : PublicOplogEntry() + + /** The initial agent oplog entry, capturing the agent's full creation context. */ + data class Create( + val timestamp: Timestamp, + val agentId: AgentId, + val agentMode: AgentMode, + val componentRevision: Long, + val env: List>, + val createdBy: Uuid, + val environmentId: EnvironmentId, + val parent: AgentId?, + val componentSize: Long, + val initialTotalLinearMemorySize: Long, + val initialActivePlugins: List, + val localAgentConfig: List, + val originalPhantomId: Uuid?, + val instanceId: Uuid, + ) : PublicOplogEntry() + + /** + * A variant case not yet given a typed decoding. With all 46 known cases now typed this is a + * defensive fallback for an unexpected/future tag only; [tag] is the raw case index. + */ + data class Unsupported(val tag: Int) : PublicOplogEntry() +} + +// public-oplog-entry: variant size=208 align=8, tag@0 (u8), payload_offset=8. +private const val PUBLIC_OPLOG_ENTRY_SIZE = 208 +private const val PUBLIC_OPLOG_ENTRY_PAYLOAD = 8 + +/** timestamp (datetime): seconds u64@0, nanoseconds u32@8. */ +private fun liftTimestamp(base: Int): Timestamp = Timestamp(loadLong(base), loadInt(base + 8)) + +private fun liftStringAt(base: Int): String = liftString(loadInt(base), loadInt(base + 4)) + +/** option: tag@base(1B), typed-schema-value@base+4 (32B) when `some`. */ +private fun liftOptionTypedSchemaValue(base: Int): TypedSchemaValue? = if (loadByte(base).toInt() == 0) null else liftTypedSchemaValue(base + 4) + +/** option: tag@base(1B), string{ptr@base+4, len@base+8} when `some`. */ +private fun liftOptionStringAt(base: Int): String? = if (loadByte(base).toInt() == 0) null else liftStringAt(base + 4) + +/** attribute-value: variant tag@0, payload@4; the only case is `string(string)`. */ +private fun liftAttributeValue(base: Int): AttributeValue = AttributeValue.StringValue(liftStringAt(base + 4)) + +/** list: element = attribute (20B: key string@0, value attribute-value@8/12B). */ +private fun liftAttributes(listBase: Int): List { + val ptr = loadInt(listBase) + val len = loadInt(listBase + 4) + return (0 until len).map { + val e = ptr + it * 20 + Attribute(liftStringAt(e), liftAttributeValue(e + 8)) + } +} + +/** state-node: variant size=16 align=4, tag@0, payload@4. */ +private fun liftStateNode(nodePtr: Int): StateNode { + val pay = nodePtr + 4 + return when (loadByte(nodePtr).toInt() and 0xFF) { + 0 -> StateNode.Counter(loadInt(pay).toUInt()) + 1 -> StateNode.Terminal + 2 -> StateNode.Wrapper(loadInt(pay)) + 3 -> StateNode.CountBox(loadInt(pay).toUInt(), loadInt(pay + 4)) + 4 -> StateNode.AndThen(loadInt(pay), loadInt(pay + 4), loadByte(pay + 8).toInt() != 0) + else -> StateNode.Pair(loadInt(pay), loadInt(pay + 4)) + } +} + +/** retry-policy-state = {nodes: list}; [listBase] points at the list's (ptr,len). */ +private fun liftRetryPolicyState(listBase: Int): RetryPolicyState { + val ptr = loadInt(listBase) + val len = loadInt(listBase + 4) + return RetryPolicyState((0 until len).map { liftStateNode(ptr + it * 16) }) +} + +/** option: tag@base(1B), retry-policy-state{nodes list @base+4} when `some`. */ +private fun liftOptionRetryPolicyState(base: Int): RetryPolicyState? = if (loadByte(base).toInt() == 0) null else liftRetryPolicyState(base + 4) + +/** A 16-byte id (uuid / card-id / grant-id): high u64@0, low u64@8. */ +private fun liftUuid(base: Int): Uuid = Uuid(loadLong(base), loadLong(base + 8)) + +/** agent-id (24B): uuid.high@0, uuid.low@8, agent-id string@16. */ +private fun liftAgentId(base: Int): AgentId = AgentId(ComponentId(liftUuid(base)), liftStringAt(base + 16)) + +/** option (16B): tag@base(1B), u64@base+8 when `some`. */ +private fun liftOptionOplogIndex(base: Int): Long? = if (loadByte(base).toInt() == 0) null else loadLong(base + 8) + +/** plugin-installation-description (48B). */ +private fun liftPluginInstallationDescription(base: Int): PluginInstallationDescription = PluginInstallationDescription( + grantId = liftUuid(base), // environment-plugin-grant-id.uuid @0 (16B) + priority = loadInt(base + 16), // plugin-priority (s32) + name = liftStringAt(base + 20), + version = liftStringAt(base + 28), + parameters = liftStringPairs(base + 36), // list> @36 +) + +/** snapshot-data (16B): data list@0, mime-type string@8. */ +private fun liftSnapshotData(base: Int): SnapshotData { + val dataPtr = loadInt(base) + val dataLen = loadInt(base + 4) + return SnapshotData((0 until dataLen).map { loadByte(dataPtr + it).toUByte() }, liftStringAt(base + 8)) +} + +/** queued-card-event (24B): variant tag@0, card-id payload@8 (16B). */ +private fun liftQueuedCardEvent(base: Int): QueuedCardEvent { + val cardId = liftUuid(base + 8) + return if (loadByte(base).toInt() == 0) QueuedCardEvent.Install(cardId) else QueuedCardEvent.Revoke(cardId) +} + +/** list: element 48B. */ +private fun liftPluginList(listBase: Int): List { + val ptr = loadInt(listBase) + val len = loadInt(listBase + 4) + return (0 until len).map { liftPluginInstallationDescription(ptr + it * 48) } +} + +/** list>: element 16B (two strings). */ +private fun liftStringPairs(listBase: Int): List> { + val ptr = loadInt(listBase) + val len = loadInt(listBase + 4) + return (0 until len).map { + val e = ptr + it * 16 + liftStringAt(e) to liftStringAt(e + 8) + } +} + +/** list: element 40B (path list@0, value tsv@8). */ +private fun liftLocalAgentConfig(listBase: Int): List { + val ptr = loadInt(listBase) + val len = loadInt(listBase + 4) + return (0 until len).map { + val e = ptr + it * 40 + LocalAgentConfigEntry(liftStringList(e), liftTypedSchemaValue(e + 8)) + } +} + +/** option (32B): tag@base, agent-id@base+8 (24B) when `some`. */ +private fun liftOptionAgentId(base: Int): AgentId? = if (loadByte(base).toInt() == 0) null else liftAgentId(base + 8) + +/** option (24B): tag@base, uuid@base+8 (16B) when `some`. */ +private fun liftOptionUuid(base: Int): Uuid? = if (loadByte(base).toInt() == 0) null else liftUuid(base + 8) + +/** update-description (20B): variant tag@0, payload@4; snapshot-based carries snapshot-data. */ +private fun liftUpdateDescription(base: Int): UpdateDescription = if (loadByte(base).toInt() == 0) { + UpdateDescription.AutoUpdate +} else { + UpdateDescription.SnapshotBased(liftSnapshotData(base + 4)) +} + +/** list: element = string (8B). */ +private fun liftStringList(listBase: Int): List { + val ptr = loadInt(listBase) + val len = loadInt(listBase + 4) + return (0 until len).map { liftStringAt(ptr + it * 8) } +} + +/** span-data (80B): variant tag@0, payload@8. local-span(local-span-data 72B) / external-span(8B). */ +private fun liftSpanData(base: Int): SpanData { + val pay = base + 8 + return if (loadByte(base).toInt() == 0) { + SpanData.LocalSpan( + spanId = liftStringAt(pay), // span-id@0 + start = liftTimestamp(pay + 8), // start datetime@8 + parent = liftOptionStringAt(pay + 24), // parent opt@24 + linkedContext = if (loadByte(pay + 40).toInt() == 0) null else loadLong(pay + 48), // linked-context opt@40 + attributes = liftAttributes(pay + 56), // attributes@56 + inherited = loadByte(pay + 64).toInt() != 0, // inherited@64 + ) + } else { + SpanData.ExternalSpan(liftStringAt(pay)) // external-span-data.span-id@0 + } +} + +/** invocation-context = list>: outer element = list (8B), inner element = span-data (80B). */ +private fun liftInvocationContext(listBase: Int): List> { + val ptr = loadInt(listBase) + val len = loadInt(listBase + 4) + return (0 until len).map { i -> + val inner = ptr + i * 8 + val innerPtr = loadInt(inner) + val innerLen = loadInt(inner + 4) + (0 until innerLen).map { j -> liftSpanData(innerPtr + j * 80) } + } +} + +/** agent-invocation (80B): variant tag@0, payload@8. */ +private fun liftAgentInvocation(base: Int): AgentInvocation { + val pay = base + 8 + return when (loadByte(base).toInt() and 0xFF) { + // agent-initialization-parameters(64B): idempotency-key@0, constructor-parameters(tsv)@8, + // trace-id@40, trace-states@48, invocation-context@56. + 0 -> AgentInvocation.AgentInitialization(liftStringAt(pay), liftTypedSchemaValue(pay + 8), liftStringAt(pay + 40), liftStringList(pay + 48), liftInvocationContext(pay + 56)) + // agent-method-invocation-parameters(72B): idempotency-key@0, method-name@8, function-input(tsv)@16, + // trace-id@48, trace-states@56, invocation-context@64. + 1 -> AgentInvocation.AgentMethodInvocation(liftStringAt(pay), liftStringAt(pay + 8), liftTypedSchemaValue(pay + 16), liftStringAt(pay + 48), liftStringList(pay + 56), liftInvocationContext(pay + 64)) + 2 -> AgentInvocation.SaveSnapshot + 3 -> AgentInvocation.LoadSnapshot(liftSnapshotData(pay)) // load-snapshot-parameters.snapshot@0 + 4 -> AgentInvocation.ProcessOplogEntries(liftStringAt(pay)) // idempotency-key@0 + else -> AgentInvocation.ManualUpdate(loadLong(pay)) // target-revision@0 + } +} + +/** agent-invocation-result (36B): variant tag@0, payload@4. */ +private fun liftAgentInvocationResult(base: Int): AgentInvocationResult { + val pay = base + 4 + return when (loadByte(base).toInt() and 0xFF) { + 0 -> AgentInvocationResult.AgentInitialization(liftTypedSchemaValue(pay)) // output@0 + 1 -> AgentInvocationResult.AgentMethod(liftTypedSchemaValue(pay)) + 2 -> AgentInvocationResult.ManualUpdate + 3 -> AgentInvocationResult.LoadSnapshot(liftOptionStringAt(pay)) // fallible-result.error(opt)@0 + 4 -> AgentInvocationResult.SaveSnapshot(liftSnapshotData(pay)) // save-snapshot-result.snapshot@0 + else -> AgentInvocationResult.ProcessOplogEntries(liftOptionStringAt(pay)) + } +} + +private fun liftPublicOplogEntry(base: Int): PublicOplogEntry { + val tag = loadByte(base).toInt() and 0xFF + val pay = base + PUBLIC_OPLOG_ENTRY_PAYLOAD + // All these -parameters records start with `timestamp: datetime` @0 (relative to `pay`). + return when (tag) { + 6 -> PublicOplogEntry.Suspend(liftTimestamp(pay)) + 8 -> PublicOplogEntry.NoOp(liftTimestamp(pay)) + 10 -> PublicOplogEntry.Interrupted(liftTimestamp(pay)) + 11 -> PublicOplogEntry.Exited(liftTimestamp(pay)) + 12 -> PublicOplogEntry.BeginAtomicRegion(liftTimestamp(pay)) + 23 -> PublicOplogEntry.Restart(liftTimestamp(pay)) + 9 -> PublicOplogEntry.Jump(liftTimestamp(pay), OplogRegion(loadLong(pay + 16), loadLong(pay + 24))) + 13 -> PublicOplogEntry.EndAtomicRegion(liftTimestamp(pay), loadLong(pay + 16)) + 18 -> PublicOplogEntry.GrowMemory(liftTimestamp(pay), loadLong(pay + 16)) + 19 -> PublicOplogEntry.FilesystemStorageUsageUpdate(liftTimestamp(pay), loadLong(pay + 16)) + 20 -> PublicOplogEntry.CreateResource(liftTimestamp(pay), loadLong(pay + 16), liftStringAt(pay + 24), liftStringAt(pay + 32)) + 21 -> PublicOplogEntry.DropResource(liftTimestamp(pay), loadLong(pay + 16), liftStringAt(pay + 24), liftStringAt(pay + 32)) + 31 -> PublicOplogEntry.ChangePersistenceLevel(liftTimestamp(pay), HostApi.PersistenceLevel.entries[loadByte(pay + 16).toInt() and 0xFF]) + 40 -> PublicOplogEntry.RemoveRetryPolicy(liftTimestamp(pay), liftStringAt(pay + 16)) + 32 -> PublicOplogEntry.BeginRemoteTransaction(liftTimestamp(pay), liftStringAt(pay + 16)) + 33 -> PublicOplogEntry.PreCommitRemoteTransaction(liftTimestamp(pay), loadLong(pay + 16)) + 34 -> PublicOplogEntry.PreRollbackRemoteTransaction(liftTimestamp(pay), loadLong(pay + 16)) + 35 -> PublicOplogEntry.CommittedRemoteTransaction(liftTimestamp(pay), loadLong(pay + 16)) + 36 -> PublicOplogEntry.RolledBackRemoteTransaction(liftTimestamp(pay), loadLong(pay + 16)) + // end/cancelled: response/partial = option @24; end also forced-commit @60. + 2 -> PublicOplogEntry.End(liftTimestamp(pay), loadLong(pay + 16), liftOptionTypedSchemaValue(pay + 24), loadByte(pay + 60).toInt() != 0) + 3 -> PublicOplogEntry.Cancelled(liftTimestamp(pay), loadLong(pay + 16), liftOptionTypedSchemaValue(pay + 24)) + 39 -> PublicOplogEntry.SetRetryPolicy(liftTimestamp(pay), liftNamedRetryPolicy(pay + 16)) // named-retry-policy @16 + // error: error@16, retry-from@24, inside-atomic-region@32, retry-policy-state(option)@36. + 7 -> PublicOplogEntry.Error(liftTimestamp(pay), liftStringAt(pay + 16), loadLong(pay + 24), loadByte(pay + 32).toInt() != 0, liftOptionRetryPolicyState(pay + 36)) + // log: level@16 (enum u8), context@20, message@28. + 22 -> PublicOplogEntry.Log(liftTimestamp(pay), LogLevel.entries[loadByte(pay + 16).toInt() and 0xFF], liftStringAt(pay + 20), liftStringAt(pay + 28)) + // start-span: span-id@16, parent(opt)@24, linked-context-id(opt)@36, attributes@48. + 28 -> PublicOplogEntry.StartSpan(liftTimestamp(pay), liftStringAt(pay + 16), liftOptionStringAt(pay + 24), liftOptionStringAt(pay + 36), liftAttributes(pay + 48)) + 29 -> PublicOplogEntry.FinishSpan(liftTimestamp(pay), liftStringAt(pay + 16)) + // set-span-attribute: span-id@16, key@24, value(attribute-value)@32. + 30 -> PublicOplogEntry.SetSpanAttribute(liftTimestamp(pay), liftStringAt(pay + 16), liftStringAt(pay + 24), liftAttributeValue(pay + 32)) + 26 -> PublicOplogEntry.Revert(liftTimestamp(pay), OplogRegion(loadLong(pay + 16), loadLong(pay + 24))) + 27 -> PublicOplogEntry.CancelPendingInvocation(liftTimestamp(pay), liftStringAt(pay + 16)) + 37 -> PublicOplogEntry.Snapshot(liftTimestamp(pay), liftSnapshotData(pay + 16)) // snapshot-data @16 + 38 -> PublicOplogEntry.OplogProcessorCheckpoint( + liftTimestamp(pay), + liftPluginInstallationDescription(pay + 16), + liftAgentId(pay + 64), + loadLong(pay + 88), + loadLong(pay + 96), + loadLong(pay + 104), + ) + 24 -> PublicOplogEntry.ActivatePlugin(liftTimestamp(pay), liftPluginInstallationDescription(pay + 16)) + 25 -> PublicOplogEntry.DeactivatePlugin(liftTimestamp(pay), liftPluginInstallationDescription(pay + 16)) + 41 -> PublicOplogEntry.CardEventQueued(liftTimestamp(pay), liftQueuedCardEvent(pay + 16)) // queued-card-event @16 + // card-installed: queued-event-index(option)@16 (16B), card-id@32. + 42 -> PublicOplogEntry.CardInstalled(liftTimestamp(pay), liftOptionOplogIndex(pay + 16), liftUuid(pay + 32)) + // card-install-failed: queued-event-index(oplog-index)@16, card-id@24, reason(enum)@40. + 43 -> PublicOplogEntry.CardInstallFailed(liftTimestamp(pay), loadLong(pay + 16), liftUuid(pay + 24), CardInstallFailure.entries[loadByte(pay + 40).toInt() and 0xFF]) + 44 -> PublicOplogEntry.CardRevoked(liftTimestamp(pay), loadLong(pay + 16), liftUuid(pay + 24)) + 45 -> PublicOplogEntry.CardExpired(liftTimestamp(pay), liftUuid(pay + 16)) + // start: parent-start-index(opt)@16, function-name@32, request(opt)@40, + // durable-function-type(wrapped-function-type in-memory, reused from DurabilityApi)@80. + 1 -> PublicOplogEntry.Start(liftTimestamp(pay), liftOptionOplogIndex(pay + 16), liftStringAt(pay + 32), liftOptionTypedSchemaValue(pay + 40), readDurableFunctionTypeInMemory(pay + 80)) + // pending/successful/failed-update: target-revision@16 then the update-specific fields. + 15 -> PublicOplogEntry.PendingUpdate(liftTimestamp(pay), loadLong(pay + 16), liftUpdateDescription(pay + 24)) + 16 -> PublicOplogEntry.SuccessfulUpdate(liftTimestamp(pay), loadLong(pay + 16), loadLong(pay + 24), liftPluginList(pay + 32)) + 17 -> PublicOplogEntry.FailedUpdate(liftTimestamp(pay), loadLong(pay + 16), liftOptionStringAt(pay + 24)) + // agent-invocation-started/pending-agent-invocation: invocation(agent-invocation 80B)@16. + 4 -> PublicOplogEntry.AgentInvocationStarted(liftTimestamp(pay), liftAgentInvocation(pay + 16)) + 14 -> PublicOplogEntry.PendingAgentInvocation(liftTimestamp(pay), liftAgentInvocation(pay + 16)) + // agent-invocation-finished: result@16, method-name(opt)@52, consumed-fuel@64, component-revision@72. + 5 -> PublicOplogEntry.AgentInvocationFinished(liftTimestamp(pay), liftAgentInvocationResult(pay + 16), liftOptionStringAt(pay + 52), loadLong(pay + 64), loadLong(pay + 72)) + // create: the largest record -- agent-id@16, agent-mode@40, component-revision@48, env@56, + // created-by@64, environment-id@80, parent(opt)@96, component-size@128, + // initial-total-linear-memory-size@136, initial-active-plugins@144, local-agent-config@152, + // original-phantom-id(opt)@160, instance-id@184. + 0 -> PublicOplogEntry.Create( + liftTimestamp(pay), + liftAgentId(pay + 16), + if (loadByte(pay + 40).toInt() == 0) AgentMode.DURABLE else AgentMode.EPHEMERAL, + loadLong(pay + 48), + liftStringPairs(pay + 56), + liftUuid(pay + 64), + EnvironmentId(liftUuid(pay + 80)), + liftOptionAgentId(pay + 96), + loadLong(pay + 128), + loadLong(pay + 136), + liftPluginList(pay + 144), + liftLocalAgentConfig(pay + 152), + liftOptionUuid(pay + 160), + liftUuid(pay + 184), + ) + else -> PublicOplogEntry.Unsupported(tag) + } +} + +/** + * Reads an agent's oplog forward from a starting index. Mirrors the Scala SDK's + * `OplogApi.GetOplog`. [close] when done -- the handle is not tied to Kotlin/Wasm GC. + */ +class GetOplog(agentId: AgentId, start: Long) { + private val handle: Int + + init { + val (idPtr, idLen) = lowerStringToPtrLen(agentId.agentId) + handle = hostGetOplogNew( + agentId.componentId.uuid.highBits, + agentId.componentId.uuid.lowBits, + idPtr, + idLen, + start, + ) + } + + /** The next batch of entries, or null once the oplog is exhausted. */ + fun getNext(): List? { + val ret = alloc(12, 4) // option>: tag@0, list{ptr@4, len@8} + hostGetOplogGetNext(handle, ret) + if (loadByte(ret).toInt() == 0) return null + val listPtr = loadInt(ret + 4) + val len = loadInt(ret + 8) + return (0 until len).map { liftPublicOplogEntry(listPtr + it * PUBLIC_OPLOG_ENTRY_SIZE) } + } + + /** Releases the get-oplog handle's guest-side handle-table entry. */ + fun close() = hostGetOplogDrop(handle) +} + +/** + * Full-text search over an agent's oplog. Mirrors the Scala SDK's `OplogApi.SearchOplog`. Each + * result pairs the matching entry's oplog index with the entry. [close] when done. + */ +class SearchOplog(agentId: AgentId, text: String) { + private val handle: Int + + init { + val (idPtr, idLen) = lowerStringToPtrLen(agentId.agentId) + val (textPtr, textLen) = lowerStringToPtrLen(text) + handle = hostSearchOplogNew( + agentId.componentId.uuid.highBits, + agentId.componentId.uuid.lowBits, + idPtr, + idLen, + textPtr, + textLen, + ) + } + + /** The next batch of (oplog-index, entry) matches, or null once exhausted. */ + fun getNext(): List>? { + val ret = alloc(12, 4) // option>> + hostSearchOplogGetNext(handle, ret) + if (loadByte(ret).toInt() == 0) return null + val listPtr = loadInt(ret + 4) + val len = loadInt(ret + 8) + // tuple: size 216, align 8. + return (0 until len).map { i -> + val elem = listPtr + i * 216 + loadLong(elem) to liftPublicOplogEntry(elem + 8) + } + } + + /** Releases the search-oplog handle's guest-side handle-table entry. */ + fun close() = hostSearchOplogDrop(handle) +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/QuotaApi.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/QuotaApi.kt new file mode 100644 index 0000000000..8870716c7d --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/QuotaApi.kt @@ -0,0 +1,174 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.host + +import cloud.golem.runtime.Either +import cloud.golem.wasm.alloc +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong +import cloud.golem.wasm.storeByte + +// Raw canonical-ABI bindings to golem:quota/types@1.5.0. The `quota-token` capability itself is +// golem:core/types@2.0.0's `quota-token` resource -- the SAME resource +// (SchemaValue.kt's SecretVal/QuotaTokenVal) already established has NO methods of its own; this +// interface only exposes free functions that act on a `quota-token` handle, plus the +// `reservation` resource. Signatures verified via abi-dump's `sig`/`resulttype` modes against +// wit-native/deps/golem-quota/types.wit. +// +// New wrinkle vs everything built so far: `reservation.commit` is declared +// `static func(this: reservation, used: u64)` -- a static function that takes the resource by +// VALUE (owned, not borrowed) as an explicit `this` parameter, not a `[method]`. Calling it +// consumes the reservation (the host releases the handle as part of the call); there is no +// separate [resource-drop]reservation call needed after a successful commit. Same +// `[static].` raw name shape as `[static]bucket.open-bucket`, just with +// the resource threaded as a normal parameter instead of being implicit. +@kotlin.wasm.WasmImport("golem:quota/types@1.5.0", "new-token") +private external fun hostNewToken(namePtr: Int, nameLen: Int, expectedUse: Long): Int + +@kotlin.wasm.WasmImport("golem:quota/types@1.5.0", "reserve") +private external fun hostReserve(tokenHandle: Int, amount: Long, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:quota/types@1.5.0", "split") +private external fun hostSplit(tokenHandle: Int, childExpectedUse: Long): Int + +@kotlin.wasm.WasmImport("golem:quota/types@1.5.0", "merge") +private external fun hostMerge(tokenHandle: Int, otherHandle: Int) + +@kotlin.wasm.WasmImport("golem:quota/types@1.5.0", "[static]reservation.commit") +private external fun hostReservationCommit(reservationHandle: Int, used: Long) + +private const val TOKEN_CONSUMED = + "quota token has already been transferred and can no longer be used; split the token first " + + "if you need to both keep and send a capability" + +private fun lowerStringToPtrLen(s: String): Pair { + val bytes = s.encodeToByteArray() + val ptr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + return ptr to bytes.size +} + +/** Returned when a reservation cannot be granted (enforcement policy = `reject`). */ +data class FailedReservation(val estimatedWaitNanos: Long?) + +/** + * A short-lived capability representing a pending resource consumption. Dropping without + * calling [commit] is equivalent to committing zero usage -- this SDK doesn't expose a manual + * drop for it (matching the Scala reference), only [commit], which itself consumes the handle. + */ +class Reservation internal constructor(handle: Int) { + private var handle: Int? = handle + + /** + * Commit actual usage. `used` < reserved returns unused capacity to the pool; `used` > + * reserved deducts the excess as "debt" from the token's allocation. Consumes this + * reservation -- calling [commit] twice fails. + */ + fun commit(used: Long) { + val h = handle ?: error("reservation already committed") + handle = null + hostReservationCommit(h, used) + } +} + +/** + * An unforgeable capability granting the right to consume a named resource. Holds only an + * opaque, affine handle to the owned host resource: it carries no readable content and can be + * transferred to exactly one destination ([merge]'s `other` argument, or wherever this SDK's + * schema-value-tree machinery sends a [cloud.golem.runtime.SchemaValue.QuotaTokenVal]). Once + * transferred, the token can no longer be used -- [split] first if you need to both keep and + * send a capability. + */ +class QuotaToken internal constructor(handle: Int) { + private var handle: Int? = handle + + private fun withHandle(block: (Int) -> T): T = block(handle ?: error(TOKEN_CONSUMED)) + + /** Takes ownership of this token's handle for a one-time transfer (e.g. into [merge]'s `other`). Returns null if already consumed. */ + internal fun take(): Int? { + val h = handle + handle = null + return h + } + + /** + * Reserve `amount` units from the local allocation. Blocks internally until capacity is + * available or the resource's enforcement action fires. Returns `Right(reservation)` on + * success, or `Left(FailedReservation)` when the enforcement policy is `reject`. Traps (via + * [error]) if this token has already been transferred. + */ + fun reserve(amount: Long): Either = withHandle { h -> + // result: tag@0(1,1), payload@8 (max(reservation i32/4, + // failed-reservation{option} 16/8) = 16) -> 24 total, align8. + val retPtr = alloc(24, 8) + hostReserve(h, amount, retPtr) + if (loadByte(retPtr).toInt() == 0) { + Either.Right(Reservation(loadInt(retPtr + 8))) + } else { + val errBase = retPtr + 8 // failed-reservation: {estimated-wait-nanos: option} @0 + val hasWait = loadByte(errBase).toInt() != 0 + Either.Left(FailedReservation(if (hasWait) loadLong(errBase + 8) else null)) + } + } + + /** + * Split off a child token with `childExpectedUse` units of expected-use. The parent's + * expected-use is reduced by `childExpectedUse`; credits are divided proportionally. Traps + * if `childExpectedUse` exceeds the parent's current expected-use, or if this token has + * already been transferred. + */ + fun split(childExpectedUse: Long): QuotaToken = withHandle { h -> QuotaToken(hostSplit(h, childExpectedUse)) } + + /** + * Merge `other` back into this token: combines expected-use and credits. `other` is + * consumed by this call and must not be used afterwards; this token remains usable. Traps + * if the tokens refer to different resources, or if either token has already been + * transferred. + */ + fun merge(other: QuotaToken) { + require(other !== this) { "cannot merge a quota token with itself" } + withHandle { thisHandle -> + val otherHandle = other.take() ?: error(TOKEN_CONSUMED) + hostMerge(thisHandle, otherHandle) + } + } + + /** Reserve `amount` units, run [block], then commit the actual usage [block] returns. Commits zero usage and rethrows on failure. */ + fun withReservation(amount: Long, block: (Reservation) -> Pair): Either = QuotaApi.withReservation(this, amount, block) + + companion object { + /** + * Request a quota capability for the named resource. + * + * @param resourceName the resource name as declared in the manifest. + * @param expectedUse expected units per reservation; used to derive the credit rate and max-credit for fair scheduling. + */ + fun create(resourceName: String, expectedUse: Long): QuotaToken { + val (namePtr, nameLen) = lowerStringToPtrLen(resourceName) + return QuotaToken(hostNewToken(namePtr, nameLen, expectedUse)) + } + } +} + +object QuotaApi { + /** + * Reserve `amount` units from `token`, run [block], then commit the actual usage [block] + * returns. Commits zero usage and rethrows on failure, ensuring unused capacity is always + * returned to the pool. + */ + fun withReservation(token: QuotaToken, amount: Long, block: (Reservation) -> Pair): Either = when (val r = token.reserve(amount)) { + is Either.Left -> Either.Left(r.value) + is Either.Right -> { + val reservation = r.value + try { + val (used, value) = block(reservation) + reservation.commit(used) + Either.Right(value) + } catch (e: Throwable) { + reservation.commit(0L) + throw e + } + } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/Rdbms.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/Rdbms.kt new file mode 100644 index 0000000000..b118939be7 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/Rdbms.kt @@ -0,0 +1,1996 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.host + +import cloud.golem.runtime.Either +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadDouble +import cloud.golem.wasm.loadFloat +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong +import cloud.golem.wasm.loadShort +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeDouble +import cloud.golem.wasm.storeFloat +import cloud.golem.wasm.storeInt +import cloud.golem.wasm.storeLong +import cloud.golem.wasm.storeShort + +// Raw canonical-ABI bindings to golem:rdbms/postgres@1.5.0 (Postgres only, primitive db-value cases only; see PostgresDbValue's doc comment +// for exactly what's deferred and why). golem:rdbms/mysql@1.5.0 and golem:rdbms/ignite2@1.5.0 +// are entirely out of scope for this increment; they mirror the same shape (their own +// large db-value variant + Connection/Transaction resources) and are future increments of +// their own, matching how this whole SDK stages large per-backend/per-file surfaces. +// +// db-connection.open and db-transaction's methods are all ordinary resource-ABI shapes already +// proven elsewhere in this SDK ([static]. for open, [method] for the rest). +// Signatures verified via abi-dump's `sig`/`resulttype` modes against +// wit-native/deps/golem-rdbms/{types,postgres}.wit. + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[static]db-connection.open") +private external fun hostConnectionOpen(addrPtr: Int, addrLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[method]db-connection.query") +private external fun hostConnectionQuery(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[method]db-connection.execute") +private external fun hostConnectionExecute(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[method]db-connection.begin-transaction") +private external fun hostConnectionBeginTransaction(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[resource-drop]db-connection") +private external fun hostConnectionDrop(handle: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[method]db-transaction.query") +private external fun hostTransactionQuery(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[method]db-transaction.execute") +private external fun hostTransactionExecute(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[method]db-transaction.commit") +private external fun hostTransactionCommit(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[method]db-transaction.rollback") +private external fun hostTransactionRollback(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[resource-drop]db-transaction") +private external fun hostTransactionDrop(handle: Int) + +// lazy-db-value: the recursion db-value's composite/domain/array/range cases go through. +// [constructor]lazy-db-value has indirect_params=true (confirmed via abi-dump's funcargs mode) +// -- its single `value: db-value` param doesn't fit the flat-param threshold, so the guest +// builds the FULL db-value in memory (same layout lowerDbValue already writes) and passes a +// POINTER to it as the function's sole argument, with the resource handle returned directly +// (no retptr). This is why db-value can have a bounded size (56 bytes) despite being +// conceptually recursive: the recursion is INDIRECT, through resource handles in the host's +// own table, not inlined in memory -- `list` at the wire level is just +// `list` (a list of handles), and reading a nested value means calling .get() on each +// handle, not walking inline bytes. +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[constructor]lazy-db-value") +private external fun hostLazyDbValueConstructor(valuePtr: Int): Int + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[method]lazy-db-value.get") +private external fun hostLazyDbValueGet(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/postgres@1.5.0", "[resource-drop]lazy-db-value") +private external fun hostLazyDbValueDrop(handle: Int) + +private fun lowerStringToPtrLen(s: String): Pair { + val bytes = s.encodeToByteArray() + val ptr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + return ptr to bytes.size +} + +/** Matches `golem:rdbms/types@1.5.0`'s `date` record (8 bytes, align 4). */ +data class DbDate(val year: Int, val month: UByte, val day: UByte) + +/** Matches `golem:rdbms/types@1.5.0`'s `time` record (8 bytes, align 4). */ +data class DbTime(val hour: UByte, val minute: UByte, val second: UByte, val nanosecond: UInt) + +/** Matches `golem:rdbms/types@1.5.0`'s `timestamp` record (16 bytes, align 4). */ +data class DbTimestamp(val date: DbDate, val time: DbTime) + +/** Matches `golem:rdbms/types@1.5.0`'s `timestamptz` record (20 bytes, align 4). */ +data class DbTimestampTz(val timestamp: DbTimestamp, val offset: Int) + +/** Matches `golem:rdbms/types@1.5.0`'s `timetz` record (12 bytes, align 4). */ +data class DbTimeTz(val time: DbTime, val offset: Int) + +/** Matches `golem:rdbms/postgres@1.5.0`'s `interval` record (16 bytes, align 8). */ +data class DbInterval(val months: Int, val days: Int, val microseconds: Long) + +private fun liftDbDate(base: Int): DbDate = DbDate(loadInt(base), loadByte(base + 4).toUByte(), loadByte(base + 5).toUByte()) +private fun liftDbTime(base: Int): DbTime = DbTime(loadByte(base).toUByte(), loadByte(base + 1).toUByte(), loadByte(base + 2).toUByte(), loadInt(base + 4).toUInt()) +private fun liftDbTimestamp(base: Int): DbTimestamp = DbTimestamp(liftDbDate(base), liftDbTime(base + 8)) +private fun liftDbTimestampTz(base: Int): DbTimestampTz = DbTimestampTz(liftDbTimestamp(base), loadInt(base + 16)) +private fun liftDbTimeTz(base: Int): DbTimeTz = DbTimeTz(liftDbTime(base), loadInt(base + 8)) +private fun liftDbInterval(base: Int): DbInterval = DbInterval(loadInt(base), loadInt(base + 4), loadLong(base + 8)) + +private fun storeDbDate(base: Int, d: DbDate) { + storeInt(base, d.year) + storeByte(base + 4, d.month.toByte()) + storeByte(base + 5, d.day.toByte()) +} +private fun storeDbTime(base: Int, t: DbTime) { + storeByte(base, t.hour.toByte()) + storeByte(base + 1, t.minute.toByte()) + storeByte(base + 2, t.second.toByte()) + storeInt(base + 4, t.nanosecond.toInt()) +} +private fun storeDbTimestamp(base: Int, ts: DbTimestamp) { + storeDbDate(base, ts.date) + storeDbTime(base + 8, ts.time) +} +private fun storeDbTimestampTz(base: Int, tstz: DbTimestampTz) { + storeDbTimestamp(base, tstz.timestamp) + storeInt(base + 16, tstz.offset) +} +private fun storeDbTimeTz(base: Int, ttz: DbTimeTz) { + storeDbTime(base, ttz.time) + storeInt(base + 8, ttz.offset) +} +private fun storeDbInterval(base: Int, iv: DbInterval) { + storeInt(base, iv.months) + storeInt(base + 4, iv.days) + storeLong(base + 8, iv.microseconds) +} + +/** Matches `golem:rdbms/types@1.5.0`'s `ip-address` variant (18 bytes, align 2). */ +sealed class IpAddress { + data class Ipv4(val a: UByte, val b: UByte, val c: UByte, val d: UByte) : IpAddress() + data class Ipv6( + val a: UShort, + val b: UShort, + val c: UShort, + val d: UShort, + val e: UShort, + val f: UShort, + val g: UShort, + val h: UShort, + ) : IpAddress() +} + +/** Matches `golem:rdbms/types@1.5.0`'s `mac-address` record (6 bytes, align 1). */ +data class MacAddress(val a: UByte, val b: UByte, val c: UByte, val d: UByte, val e: UByte, val f: UByte) + +// ip-address: size=18 align=2, tag_size=1, payload_offset=2. +private fun liftIpAddress(base: Int): IpAddress { + val tag = loadByte(base).toInt() and 0xFF + val p = base + 2 + return when (tag) { + 0 -> IpAddress.Ipv4(loadByte(p).toUByte(), loadByte(p + 1).toUByte(), loadByte(p + 2).toUByte(), loadByte(p + 3).toUByte()) + 1 -> IpAddress.Ipv6( + loadShort(p).toUShort(), + loadShort(p + 2).toUShort(), + loadShort(p + 4).toUShort(), + loadShort(p + 6).toUShort(), + loadShort(p + 8).toUShort(), + loadShort(p + 10).toUShort(), + loadShort(p + 12).toUShort(), + loadShort(p + 14).toUShort(), + ) + else -> error("unknown ip-address tag $tag") + } +} + +private fun storeIpAddress(base: Int, ip: IpAddress) { + val p = base + 2 + when (ip) { + is IpAddress.Ipv4 -> { + storeByte(base, 0) + storeByte(p, ip.a.toByte()) + storeByte(p + 1, ip.b.toByte()) + storeByte(p + 2, ip.c.toByte()) + storeByte(p + 3, ip.d.toByte()) + } + is IpAddress.Ipv6 -> { + storeByte(base, 1) + storeShort(p, ip.a.toShort()) + storeShort(p + 2, ip.b.toShort()) + storeShort(p + 4, ip.c.toShort()) + storeShort(p + 6, ip.d.toShort()) + storeShort(p + 8, ip.e.toShort()) + storeShort(p + 10, ip.f.toShort()) + storeShort(p + 12, ip.g.toShort()) + storeShort(p + 14, ip.h.toShort()) + } + } +} + +private fun liftMacAddress(base: Int): MacAddress = MacAddress( + loadByte(base).toUByte(), + loadByte(base + 1).toUByte(), + loadByte(base + 2).toUByte(), + loadByte(base + 3).toUByte(), + loadByte(base + 4).toUByte(), + loadByte(base + 5).toUByte(), +) + +private fun storeMacAddress(base: Int, m: MacAddress) { + storeByte(base, m.a.toByte()) + storeByte(base + 1, m.b.toByte()) + storeByte(base + 2, m.c.toByte()) + storeByte(base + 3, m.d.toByte()) + storeByte(base + 4, m.e.toByte()) + storeByte(base + 5, m.f.toByte()) +} + +private fun liftListOfBool(base: Int): List { + val ptr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> loadByte(ptr + i).toInt() != 0 } +} + +private fun lowerListOfBool(base: Int, bits: List) { + val arr = alloc(bits.size, 1) + bits.forEachIndexed { i, b -> storeByte(arr + i, if (b) 1 else 0) } + storeInt(base, arr) + storeInt(base + 4, bits.size) +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `int4bound` variant (8 bytes, align 4). */ +sealed class Int4Bound { + data class Included(val value: Int) : Int4Bound() + data class Excluded(val value: Int) : Int4Bound() + object Unbounded : Int4Bound() +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `int8bound` variant (16 bytes, align 8). */ +sealed class Int8Bound { + data class Included(val value: Long) : Int8Bound() + data class Excluded(val value: Long) : Int8Bound() + object Unbounded : Int8Bound() +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `numbound` variant (12 bytes, align 4). */ +sealed class NumBound { + data class Included(val value: String) : NumBound() + data class Excluded(val value: String) : NumBound() + object Unbounded : NumBound() +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `tsbound` variant (20 bytes, align 4). */ +sealed class TsBound { + data class Included(val value: DbTimestamp) : TsBound() + data class Excluded(val value: DbTimestamp) : TsBound() + object Unbounded : TsBound() +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `tstzbound` variant (24 bytes, align 4). */ +sealed class TsTzBound { + data class Included(val value: DbTimestampTz) : TsTzBound() + data class Excluded(val value: DbTimestampTz) : TsTzBound() + object Unbounded : TsTzBound() +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `datebound` variant (12 bytes, align 4). */ +sealed class DateBound { + data class Included(val value: DbDate) : DateBound() + data class Excluded(val value: DbDate) : DateBound() + object Unbounded : DateBound() +} + +data class Int4Range(val start: Int4Bound, val end: Int4Bound) +data class Int8Range(val start: Int8Bound, val end: Int8Bound) +data class NumRange(val start: NumBound, val end: NumBound) +data class TsRange(val start: TsBound, val end: TsBound) +data class TsTzRange(val start: TsTzBound, val end: TsTzBound) +data class DateRange(val start: DateBound, val end: DateBound) + +// All 6 bound variants: tag@0(1,1), payload@payload_offset. unbounded (tag=2) has no payload. +private fun liftInt4Bound(base: Int): Int4Bound = when (loadByte(base).toInt() and 0xFF) { + 0 -> Int4Bound.Included(loadInt(base + 4)) + 1 -> Int4Bound.Excluded(loadInt(base + 4)) + else -> Int4Bound.Unbounded +} +private fun storeInt4Bound(base: Int, b: Int4Bound) { + when (b) { + is Int4Bound.Included -> { + storeByte(base, 0) + storeInt(base + 4, b.value) + } + is Int4Bound.Excluded -> { + storeByte(base, 1) + storeInt(base + 4, b.value) + } + Int4Bound.Unbounded -> storeByte(base, 2) + } +} + +private fun liftInt8Bound(base: Int): Int8Bound = when (loadByte(base).toInt() and 0xFF) { + 0 -> Int8Bound.Included(loadLong(base + 8)) + 1 -> Int8Bound.Excluded(loadLong(base + 8)) + else -> Int8Bound.Unbounded +} +private fun storeInt8Bound(base: Int, b: Int8Bound) { + when (b) { + is Int8Bound.Included -> { + storeByte(base, 0) + storeLong(base + 8, b.value) + } + is Int8Bound.Excluded -> { + storeByte(base, 1) + storeLong(base + 8, b.value) + } + Int8Bound.Unbounded -> storeByte(base, 2) + } +} + +private fun liftNumBound(base: Int): NumBound = when (loadByte(base).toInt() and 0xFF) { + 0 -> NumBound.Included(liftString(loadInt(base + 4), loadInt(base + 8))) + 1 -> NumBound.Excluded(liftString(loadInt(base + 4), loadInt(base + 8))) + else -> NumBound.Unbounded +} +private fun storeNumBound(base: Int, b: NumBound) { + when (b) { + is NumBound.Included -> { + storeByte(base, 0) + val (p, l) = lowerStringToPtrLen(b.value) + storeInt(base + 4, p) + storeInt(base + 8, l) + } + is NumBound.Excluded -> { + storeByte(base, 1) + val (p, l) = lowerStringToPtrLen(b.value) + storeInt(base + 4, p) + storeInt(base + 8, l) + } + NumBound.Unbounded -> storeByte(base, 2) + } +} + +private fun liftTsBound(base: Int): TsBound = when (loadByte(base).toInt() and 0xFF) { + 0 -> TsBound.Included(liftDbTimestamp(base + 4)) + 1 -> TsBound.Excluded(liftDbTimestamp(base + 4)) + else -> TsBound.Unbounded +} +private fun storeTsBound(base: Int, b: TsBound) { + when (b) { + is TsBound.Included -> { + storeByte(base, 0) + storeDbTimestamp(base + 4, b.value) + } + is TsBound.Excluded -> { + storeByte(base, 1) + storeDbTimestamp(base + 4, b.value) + } + TsBound.Unbounded -> storeByte(base, 2) + } +} + +private fun liftTsTzBound(base: Int): TsTzBound = when (loadByte(base).toInt() and 0xFF) { + 0 -> TsTzBound.Included(liftDbTimestampTz(base + 4)) + 1 -> TsTzBound.Excluded(liftDbTimestampTz(base + 4)) + else -> TsTzBound.Unbounded +} +private fun storeTsTzBound(base: Int, b: TsTzBound) { + when (b) { + is TsTzBound.Included -> { + storeByte(base, 0) + storeDbTimestampTz(base + 4, b.value) + } + is TsTzBound.Excluded -> { + storeByte(base, 1) + storeDbTimestampTz(base + 4, b.value) + } + TsTzBound.Unbounded -> storeByte(base, 2) + } +} + +private fun liftDateBound(base: Int): DateBound = when (loadByte(base).toInt() and 0xFF) { + 0 -> DateBound.Included(liftDbDate(base + 4)) + 1 -> DateBound.Excluded(liftDbDate(base + 4)) + else -> DateBound.Unbounded +} +private fun storeDateBound(base: Int, b: DateBound) { + when (b) { + is DateBound.Included -> { + storeByte(base, 0) + storeDbDate(base + 4, b.value) + } + is DateBound.Excluded -> { + storeByte(base, 1) + storeDbDate(base + 4, b.value) + } + DateBound.Unbounded -> storeByte(base, 2) + } +} + +// Ranges: {start: Bound @0, end: Bound @}. +private fun liftInt4Range(base: Int): Int4Range = Int4Range(liftInt4Bound(base), liftInt4Bound(base + 8)) +private fun storeInt4Range(base: Int, r: Int4Range) { + storeInt4Bound(base, r.start) + storeInt4Bound(base + 8, r.end) +} +private fun liftInt8Range(base: Int): Int8Range = Int8Range(liftInt8Bound(base), liftInt8Bound(base + 16)) +private fun storeInt8Range(base: Int, r: Int8Range) { + storeInt8Bound(base, r.start) + storeInt8Bound(base + 16, r.end) +} +private fun liftNumRange(base: Int): NumRange = NumRange(liftNumBound(base), liftNumBound(base + 12)) +private fun storeNumRange(base: Int, r: NumRange) { + storeNumBound(base, r.start) + storeNumBound(base + 12, r.end) +} +private fun liftTsRange(base: Int): TsRange = TsRange(liftTsBound(base), liftTsBound(base + 20)) +private fun storeTsRange(base: Int, r: TsRange) { + storeTsBound(base, r.start) + storeTsBound(base + 20, r.end) +} +private fun liftTsTzRange(base: Int): TsTzRange = TsTzRange(liftTsTzBound(base), liftTsTzBound(base + 24)) +private fun storeTsTzRange(base: Int, r: TsTzRange) { + storeTsTzBound(base, r.start) + storeTsTzBound(base + 24, r.end) +} +private fun liftDateRange(base: Int): DateRange = DateRange(liftDateBound(base), liftDateBound(base + 12)) +private fun storeDateRange(base: Int, r: DateRange) { + storeDateBound(base, r.start) + storeDateBound(base + 12, r.end) +} + +private fun liftListOfFloat(base: Int): List { + val ptr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> loadFloat(ptr + i * 4) } +} +private fun lowerListOfFloat(base: Int, values: List) { + val arr = alloc(values.size * 4, 4) + values.forEachIndexed { i, v -> storeFloat(arr + i * 4, v) } + storeInt(base, arr) + storeInt(base + 4, values.size) +} +private fun liftListOfInt(base: Int): List { + val ptr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> loadInt(ptr + i * 4) } +} +private fun lowerListOfInt(base: Int, values: List) { + val arr = alloc(values.size * 4, 4) + values.forEachIndexed { i, v -> storeInt(arr + i * 4, v) } + storeInt(base, arr) + storeInt(base + 4, values.size) +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `sparse-vec` record (20 bytes, align 4). */ +data class SparseVec(val dim: Int, val indices: List, val values: List) + +// sparse-vec: dim@0(4,4), indices@4(list,8,4), values@12(list,8,4). +private fun liftSparseVec(base: Int): SparseVec = SparseVec(loadInt(base), liftListOfInt(base + 4), liftListOfFloat(base + 12)) +private fun storeSparseVec(base: Int, v: SparseVec) { + storeInt(base, v.dim) + lowerListOfInt(base + 4, v.indices) + lowerListOfFloat(base + 12, v.values) +} + +// Resolves an owned lazy-db-value handle to its PostgresDbValue and drops the handle in the +// same step. Every consumer of a nested value (composite.values, domain.value, array elements, +// value-bound's included/excluded) wants the fully-materialized Kotlin value, not a live +// handle -- matching this SDK's established style of fully-materialized value trees (see +// SchemaValue.kt) rather than exposing resource handles to callers for recursive data. This is +// a deliberate ergonomic choice: real Postgres composite/array/domain/range nesting is finite, +// so eager recursive resolution is the right default (a lazy, handle-exposing API would only +// matter for pathologically deep or wide values, not realistic schemas). +private fun liftAndConsumeLazyDbValue(handle: Int): PostgresDbValue { + val retPtr = alloc(DBV_SIZE, DBV_ALIGN) + hostLazyDbValueGet(handle, retPtr) + val v = liftDbValue(retPtr) + hostLazyDbValueDrop(handle) + return v +} + +private fun lowerToLazyDbValueHandle(value: PostgresDbValue): Int { + val base = alloc(DBV_SIZE, DBV_ALIGN) + lowerDbValue(base, value) + return hostLazyDbValueConstructor(base) +} + +private fun liftListOfLazyDbValue(base: Int): List { + val ptr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> liftAndConsumeLazyDbValue(loadInt(ptr + i * 4)) } +} +private fun lowerListOfLazyDbValue(base: Int, values: List) { + val arr = alloc(values.size * 4, 4) + values.forEachIndexed { i, v -> storeInt(arr + i * 4, lowerToLazyDbValueHandle(v)) } + storeInt(base, arr) + storeInt(base + 4, values.size) +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `enumeration` record (16 bytes, align 4) -- flat, no `lazy-db-value` involved. */ +data class PostgresEnumeration(val name: String, val value: String) + +/** Matches `golem:rdbms/postgres@1.5.0`'s `composite` record (16 bytes, align 4). `values` is fully materialized (see [liftAndConsumeLazyDbValue]). */ +data class PostgresComposite(val name: String, val values: List) + +/** Matches `golem:rdbms/postgres@1.5.0`'s `domain` record (12 bytes, align 4). `value` is fully materialized. */ +data class PostgresDomain(val name: String, val value: PostgresDbValue) + +/** Matches `golem:rdbms/postgres@1.5.0`'s `value-bound` variant (8 bytes, align 4). Payloads are fully materialized. */ +sealed class ValueBound { + data class Included(val value: PostgresDbValue) : ValueBound() + data class Excluded(val value: PostgresDbValue) : ValueBound() + object Unbounded : ValueBound() +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `values-range` record (16 bytes, align 4). */ +data class ValuesRange(val start: ValueBound, val end: ValueBound) + +/** Matches `golem:rdbms/postgres@1.5.0`'s `range` record (24 bytes, align 4). */ +data class PostgresRange(val name: String, val value: ValuesRange) + +private fun liftEnumeration(base: Int): PostgresEnumeration = PostgresEnumeration(liftString(loadInt(base), loadInt(base + 4)), liftString(loadInt(base + 8), loadInt(base + 12))) +private fun storeEnumeration(base: Int, e: PostgresEnumeration) { + val (np, nl) = lowerStringToPtrLen(e.name) + storeInt(base, np) + storeInt(base + 4, nl) + val (vp, vl) = lowerStringToPtrLen(e.value) + storeInt(base + 8, vp) + storeInt(base + 12, vl) +} + +// composite: name@0(8,4), values@8(list,8,4). +private fun liftComposite(base: Int): PostgresComposite { + val name = liftString(loadInt(base), loadInt(base + 4)) + return PostgresComposite(name, liftListOfLazyDbValue(base + 8)) +} +private fun storeComposite(base: Int, c: PostgresComposite) { + val (np, nl) = lowerStringToPtrLen(c.name) + storeInt(base, np) + storeInt(base + 4, nl) + lowerListOfLazyDbValue(base + 8, c.values) +} + +// domain: name@0(8,4), value@8(own: i32 handle, 4,4). +private fun liftDomain(base: Int): PostgresDomain { + val name = liftString(loadInt(base), loadInt(base + 4)) + return PostgresDomain(name, liftAndConsumeLazyDbValue(loadInt(base + 8))) +} +private fun storeDomain(base: Int, d: PostgresDomain) { + val (np, nl) = lowerStringToPtrLen(d.name) + storeInt(base, np) + storeInt(base + 4, nl) + storeInt(base + 8, lowerToLazyDbValueHandle(d.value)) +} + +// value-bound: tag@0(1,1), payload@4 (own: i32 handle, only for included/excluded). +private fun liftValueBound(base: Int): ValueBound = when (loadByte(base).toInt() and 0xFF) { + 0 -> ValueBound.Included(liftAndConsumeLazyDbValue(loadInt(base + 4))) + 1 -> ValueBound.Excluded(liftAndConsumeLazyDbValue(loadInt(base + 4))) + else -> ValueBound.Unbounded +} +private fun storeValueBound(base: Int, b: ValueBound) { + when (b) { + is ValueBound.Included -> { + storeByte(base, 0) + storeInt(base + 4, lowerToLazyDbValueHandle(b.value)) + } + is ValueBound.Excluded -> { + storeByte(base, 1) + storeInt(base + 4, lowerToLazyDbValueHandle(b.value)) + } + ValueBound.Unbounded -> storeByte(base, 2) + } +} + +private fun liftValuesRange(base: Int): ValuesRange = ValuesRange(liftValueBound(base), liftValueBound(base + 8)) +private fun storeValuesRange(base: Int, r: ValuesRange) { + storeValueBound(base, r.start) + storeValueBound(base + 8, r.end) +} + +// range: name@0(8,4), value@8(values-range,16,4). +private fun liftRange(base: Int): PostgresRange { + val name = liftString(loadInt(base), loadInt(base + 4)) + return PostgresRange(name, liftValuesRange(base + 8)) +} +private fun storeRange(base: Int, r: PostgresRange) { + val (np, nl) = lowerStringToPtrLen(r.name) + storeInt(base, np) + storeInt(base + 4, nl) + storeValuesRange(base + 8, r.value) +} + +/** + * Matches `golem:rdbms/postgres@1.5.0`'s `db-value` variant IN FULL (45 cases, tags 0-44). + * `enumeration`/`composite`/`domain`/`array`/`range` (tags 36-40) go through the + * `lazy-db-value` resource (`constructor(value: db-value); get() -> db-value;`) for their + * nested values, but are fully materialized into plain Kotlin data here -- see + * [liftAndConsumeLazyDbValue]'s doc comment for why. `query-stream`/`db-result-stream` (lazy, + * paginated results) remain a separate deferred piece -- a different resource, for a different + * purpose (streaming a `db-result` incrementally, not a single recursive value). + */ +sealed class PostgresDbValue { + data class Character(val value: Byte) : PostgresDbValue() + data class Int2(val value: Short) : PostgresDbValue() + data class Int4(val value: Int) : PostgresDbValue() + data class Int8(val value: Long) : PostgresDbValue() + data class Float4(val value: Float) : PostgresDbValue() + data class Float8(val value: Double) : PostgresDbValue() + + /** WIT represents `numeric` as an arbitrary-precision decimal encoded as a string. */ + data class Numeric(val value: String) : PostgresDbValue() + data class BooleanVal(val value: Boolean) : PostgresDbValue() + data class Text(val value: String) : PostgresDbValue() + data class Varchar(val value: String) : PostgresDbValue() + data class Bpchar(val value: String) : PostgresDbValue() + data class TimestampVal(val value: DbTimestamp) : PostgresDbValue() + data class TimestampTzVal(val value: DbTimestampTz) : PostgresDbValue() + data class DateVal(val value: DbDate) : PostgresDbValue() + data class TimeVal(val value: DbTime) : PostgresDbValue() + data class TimeTzVal(val value: DbTimeTz) : PostgresDbValue() + data class IntervalVal(val value: DbInterval) : PostgresDbValue() + data class Bytea(val value: List) : PostgresDbValue() + data class Json(val value: String) : PostgresDbValue() + data class Jsonb(val value: String) : PostgresDbValue() + data class JsonPath(val value: String) : PostgresDbValue() + data class Xml(val value: String) : PostgresDbValue() + data class Uuid(val highBits: Long, val lowBits: Long) : PostgresDbValue() + data class InetVal(val value: IpAddress) : PostgresDbValue() + data class CidrVal(val value: IpAddress) : PostgresDbValue() + data class MacaddrVal(val value: MacAddress) : PostgresDbValue() + data class Bit(val value: List) : PostgresDbValue() + data class Varbit(val value: List) : PostgresDbValue() + data class Int4RangeVal(val value: Int4Range) : PostgresDbValue() + data class Int8RangeVal(val value: Int8Range) : PostgresDbValue() + data class NumRangeVal(val value: NumRange) : PostgresDbValue() + data class TsRangeVal(val value: TsRange) : PostgresDbValue() + data class TsTzRangeVal(val value: TsTzRange) : PostgresDbValue() + data class DateRangeVal(val value: DateRange) : PostgresDbValue() + data class Money(val value: Long) : PostgresDbValue() + data class Oid(val value: UInt) : PostgresDbValue() + data class EnumerationVal(val value: PostgresEnumeration) : PostgresDbValue() + data class CompositeVal(val value: PostgresComposite) : PostgresDbValue() + data class DomainVal(val value: PostgresDomain) : PostgresDbValue() + data class ArrayVal(val value: List) : PostgresDbValue() + data class RangeVal(val value: PostgresRange) : PostgresDbValue() + object Null : PostgresDbValue() + data class VectorVal(val value: List) : PostgresDbValue() + data class HalfvecVal(val value: List) : PostgresDbValue() + data class SparsevecVal(val value: SparseVec) : PostgresDbValue() +} + +// db-value: size=56 align=8, tag_size=1, payload_offset=8. Tag numbers are this variant's +// absolute case indices (0-44), verified via abi-dump against the full 45-case variant. +private const val DBV_SIZE = 56 +private const val DBV_ALIGN = 8 +private const val DBV_PAYLOAD_OFFSET = 8 + +private fun liftDbValue(base: Int): PostgresDbValue { + val tag = loadByte(base).toInt() and 0xFF + val p = base + DBV_PAYLOAD_OFFSET + return when (tag) { + 0 -> PostgresDbValue.Character(loadByte(p)) + 1 -> PostgresDbValue.Int2(loadShort(p)) + 2 -> PostgresDbValue.Int4(loadInt(p)) + 3 -> PostgresDbValue.Int8(loadLong(p)) + 4 -> PostgresDbValue.Float4(loadFloat(p)) + 5 -> PostgresDbValue.Float8(loadDouble(p)) + 6 -> PostgresDbValue.Numeric(liftString(loadInt(p), loadInt(p + 4))) + 7 -> PostgresDbValue.BooleanVal(loadByte(p).toInt() != 0) + 8 -> PostgresDbValue.Text(liftString(loadInt(p), loadInt(p + 4))) + 9 -> PostgresDbValue.Varchar(liftString(loadInt(p), loadInt(p + 4))) + 10 -> PostgresDbValue.Bpchar(liftString(loadInt(p), loadInt(p + 4))) + 11 -> PostgresDbValue.TimestampVal(liftDbTimestamp(p)) + 12 -> PostgresDbValue.TimestampTzVal(liftDbTimestampTz(p)) + 13 -> PostgresDbValue.DateVal(liftDbDate(p)) + 14 -> PostgresDbValue.TimeVal(liftDbTime(p)) + 15 -> PostgresDbValue.TimeTzVal(liftDbTimeTz(p)) + 16 -> PostgresDbValue.IntervalVal(liftDbInterval(p)) + 17 -> { + val ptr = loadInt(p) + val len = loadInt(p + 4) + PostgresDbValue.Bytea((0 until len).map { i -> loadByte(ptr + i).toUByte() }) + } + 18 -> PostgresDbValue.Json(liftString(loadInt(p), loadInt(p + 4))) + 19 -> PostgresDbValue.Jsonb(liftString(loadInt(p), loadInt(p + 4))) + 20 -> PostgresDbValue.JsonPath(liftString(loadInt(p), loadInt(p + 4))) + 21 -> PostgresDbValue.Xml(liftString(loadInt(p), loadInt(p + 4))) + 22 -> PostgresDbValue.Uuid(loadLong(p), loadLong(p + 8)) + 23 -> PostgresDbValue.InetVal(liftIpAddress(p)) + 24 -> PostgresDbValue.CidrVal(liftIpAddress(p)) + 25 -> PostgresDbValue.MacaddrVal(liftMacAddress(p)) + 26 -> PostgresDbValue.Bit(liftListOfBool(p)) + 27 -> PostgresDbValue.Varbit(liftListOfBool(p)) + 28 -> PostgresDbValue.Int4RangeVal(liftInt4Range(p)) + 29 -> PostgresDbValue.Int8RangeVal(liftInt8Range(p)) + 30 -> PostgresDbValue.NumRangeVal(liftNumRange(p)) + 31 -> PostgresDbValue.TsRangeVal(liftTsRange(p)) + 32 -> PostgresDbValue.TsTzRangeVal(liftTsTzRange(p)) + 33 -> PostgresDbValue.DateRangeVal(liftDateRange(p)) + 34 -> PostgresDbValue.Money(loadLong(p)) + 35 -> PostgresDbValue.Oid(loadInt(p).toUInt()) + 36 -> PostgresDbValue.EnumerationVal(liftEnumeration(p)) + 37 -> PostgresDbValue.CompositeVal(liftComposite(p)) + 38 -> PostgresDbValue.DomainVal(liftDomain(p)) + 39 -> PostgresDbValue.ArrayVal(liftListOfLazyDbValue(p)) + 40 -> PostgresDbValue.RangeVal(liftRange(p)) + 41 -> PostgresDbValue.Null + 42 -> PostgresDbValue.VectorVal(liftListOfFloat(p)) + 43 -> PostgresDbValue.HalfvecVal(liftListOfFloat(p)) + 44 -> PostgresDbValue.SparsevecVal(liftSparseVec(p)) + else -> error("native Rdbms: unknown db-value tag $tag") + } +} + +private fun lowerDbValue(base: Int, value: PostgresDbValue) { + val p = base + DBV_PAYLOAD_OFFSET + when (value) { + is PostgresDbValue.Character -> { + storeByte(base, 0) + storeByte(p, value.value) + } + is PostgresDbValue.Int2 -> { + storeByte(base, 1) + storeShort(p, value.value) + } + is PostgresDbValue.Int4 -> { + storeByte(base, 2) + storeInt(p, value.value) + } + is PostgresDbValue.Int8 -> { + storeByte(base, 3) + storeLong(p, value.value) + } + is PostgresDbValue.Float4 -> { + storeByte(base, 4) + storeFloat(p, value.value) + } + is PostgresDbValue.Float8 -> { + storeByte(base, 5) + storeDouble(p, value.value) + } + is PostgresDbValue.Numeric -> { + storeByte(base, 6) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is PostgresDbValue.BooleanVal -> { + storeByte(base, 7) + storeByte(p, if (value.value) 1 else 0) + } + is PostgresDbValue.Text -> { + storeByte(base, 8) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is PostgresDbValue.Varchar -> { + storeByte(base, 9) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is PostgresDbValue.Bpchar -> { + storeByte(base, 10) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is PostgresDbValue.TimestampVal -> { + storeByte(base, 11) + storeDbTimestamp(p, value.value) + } + is PostgresDbValue.TimestampTzVal -> { + storeByte(base, 12) + storeDbTimestampTz(p, value.value) + } + is PostgresDbValue.DateVal -> { + storeByte(base, 13) + storeDbDate(p, value.value) + } + is PostgresDbValue.TimeVal -> { + storeByte(base, 14) + storeDbTime(p, value.value) + } + is PostgresDbValue.TimeTzVal -> { + storeByte(base, 15) + storeDbTimeTz(p, value.value) + } + is PostgresDbValue.IntervalVal -> { + storeByte(base, 16) + storeDbInterval(p, value.value) + } + is PostgresDbValue.Bytea -> { + storeByte(base, 17) + val arr = alloc(value.value.size, 1) + value.value.forEachIndexed { i, b -> storeByte(arr + i, b.toByte()) } + storeInt(p, arr) + storeInt(p + 4, value.value.size) + } + is PostgresDbValue.Json -> { + storeByte(base, 18) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is PostgresDbValue.Jsonb -> { + storeByte(base, 19) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is PostgresDbValue.JsonPath -> { + storeByte(base, 20) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is PostgresDbValue.Xml -> { + storeByte(base, 21) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is PostgresDbValue.Uuid -> { + storeByte(base, 22) + storeLong(p, value.highBits) + storeLong(p + 8, value.lowBits) + } + is PostgresDbValue.InetVal -> { + storeByte(base, 23) + storeIpAddress(p, value.value) + } + is PostgresDbValue.CidrVal -> { + storeByte(base, 24) + storeIpAddress(p, value.value) + } + is PostgresDbValue.MacaddrVal -> { + storeByte(base, 25) + storeMacAddress(p, value.value) + } + is PostgresDbValue.Bit -> { + storeByte(base, 26) + lowerListOfBool(p, value.value) + } + is PostgresDbValue.Varbit -> { + storeByte(base, 27) + lowerListOfBool(p, value.value) + } + is PostgresDbValue.Int4RangeVal -> { + storeByte(base, 28) + storeInt4Range(p, value.value) + } + is PostgresDbValue.Int8RangeVal -> { + storeByte(base, 29) + storeInt8Range(p, value.value) + } + is PostgresDbValue.NumRangeVal -> { + storeByte(base, 30) + storeNumRange(p, value.value) + } + is PostgresDbValue.TsRangeVal -> { + storeByte(base, 31) + storeTsRange(p, value.value) + } + is PostgresDbValue.TsTzRangeVal -> { + storeByte(base, 32) + storeTsTzRange(p, value.value) + } + is PostgresDbValue.DateRangeVal -> { + storeByte(base, 33) + storeDateRange(p, value.value) + } + is PostgresDbValue.Money -> { + storeByte(base, 34) + storeLong(p, value.value) + } + is PostgresDbValue.Oid -> { + storeByte(base, 35) + storeInt(p, value.value.toInt()) + } + is PostgresDbValue.EnumerationVal -> { + storeByte(base, 36) + storeEnumeration(p, value.value) + } + is PostgresDbValue.CompositeVal -> { + storeByte(base, 37) + storeComposite(p, value.value) + } + is PostgresDbValue.DomainVal -> { + storeByte(base, 38) + storeDomain(p, value.value) + } + is PostgresDbValue.ArrayVal -> { + storeByte(base, 39) + lowerListOfLazyDbValue(p, value.value) + } + is PostgresDbValue.RangeVal -> { + storeByte(base, 40) + storeRange(p, value.value) + } + PostgresDbValue.Null -> storeByte(base, 41) + is PostgresDbValue.VectorVal -> { + storeByte(base, 42) + lowerListOfFloat(p, value.value) + } + is PostgresDbValue.HalfvecVal -> { + storeByte(base, 43) + lowerListOfFloat(p, value.value) + } + is PostgresDbValue.SparsevecVal -> { + storeByte(base, 44) + storeSparseVec(p, value.value) + } + } +} + +private fun lowerDbValueList(values: List): Pair { + val arr = alloc(values.size * DBV_SIZE, DBV_ALIGN) + values.forEachIndexed { i, v -> lowerDbValue(arr + i * DBV_SIZE, v) } + return arr to values.size +} + +/** Mirrors Scala's own `DbColumn`: deliberately omits `db-type` (the 40-case `db-column-type` + * variant describing the column's SQL type structurally) -- Scala's reference `DbColumn` case + * class only carries `dbTypeName`, not the structural type, so this SDK matches that scope + * rather than decoding a type variant nothing consumes. */ +data class DbColumn(val ordinal: Long, val name: String, val dbTypeName: String) + +/** + * A textual representation of [this] value for [PostgresDbRow]'s convenience accessors. + * `Text`/`Varchar`/`Bpchar`/`Numeric`/`Json`/`Jsonb`/`JsonPath`/`Xml` return their raw string + * content directly; simple numeric/boolean cases return their number/boolean's own + * `toString()`; everything else (temporal, network, bit, range, composite/domain/array/range, + * vector types) falls back to Kotlin's data-class `toString()`, which is at least an accurate + * structural dump. + * + * NOTE: Scala's reference `PostgresDbRow.getString`/`getInt`/`getLong` fall back to `v.toString` + * with no `PostgresDbValue` `toString` override, so for anything other than the numeric fast + * paths it returns Scala's auto-generated case-class dump (e.g. `Text("hello").toString == + * "Text(hello)"`, not `"hello"`) -- almost certainly an unintentional bug in the reference, not + * a deliberate scope choice (a `getString` that can't extract a text column's actual text isn't + * a usable accessor). This port fixes that by extracting the real value for the common cases + * instead of reproducing the bug; the accessors' overall shape (Null -> null, Int4/Int2 fast + * path for `getInt`, Int8/Int4 fast path for `getLong`) still matches Scala's intent exactly. + */ +private fun PostgresDbValue.asDisplayString(): String = when (this) { + is PostgresDbValue.Character -> value.toString() + is PostgresDbValue.Int2 -> value.toString() + is PostgresDbValue.Int4 -> value.toString() + is PostgresDbValue.Int8 -> value.toString() + is PostgresDbValue.Float4 -> value.toString() + is PostgresDbValue.Float8 -> value.toString() + is PostgresDbValue.Numeric -> value + is PostgresDbValue.BooleanVal -> value.toString() + is PostgresDbValue.Text -> value + is PostgresDbValue.Varchar -> value + is PostgresDbValue.Bpchar -> value + is PostgresDbValue.Json -> value + is PostgresDbValue.Jsonb -> value + is PostgresDbValue.JsonPath -> value + is PostgresDbValue.Xml -> value + is PostgresDbValue.Money -> value.toString() + is PostgresDbValue.Oid -> value.toString() + else -> toString() +} + +data class PostgresDbRow(val values: List) { + fun getString(index: Int): String? = when (val v = values[index]) { + PostgresDbValue.Null -> null + else -> v.asDisplayString() + } + + fun getInt(index: Int): Int? = when (val v = values[index]) { + PostgresDbValue.Null -> null + is PostgresDbValue.Int4 -> v.value + is PostgresDbValue.Int2 -> v.value.toInt() + else -> v.asDisplayString().toInt() + } + + fun getLong(index: Int): Long? = when (val v = values[index]) { + PostgresDbValue.Null -> null + is PostgresDbValue.Int8 -> v.value + is PostgresDbValue.Int4 -> v.value.toLong() + else -> v.asDisplayString().toLong() + } +} + +data class PostgresDbResult(val columns: List, val rows: List) + +// db-column: size=48 align=8 { ordinal@0(8,8), name@8(8,4), db-type@16(20,4, skipped), db-type-name@36(8,4) }. +private fun liftDbColumn(base: Int): DbColumn = DbColumn( + ordinal = loadLong(base), + name = liftString(loadInt(base + 8), loadInt(base + 12)), + dbTypeName = liftString(loadInt(base + 36), loadInt(base + 40)), +) + +// db-row: size=8 align=4 { values: list @0 }. +private fun liftDbRow(base: Int): PostgresDbRow { + val ptr = loadInt(base) + val len = loadInt(base + 4) + return PostgresDbRow((0 until len).map { i -> liftDbValue(ptr + i * DBV_SIZE) }) +} + +// db-result: size=16 align=4 { columns: list @0, rows: list @8 }. +private fun liftDbResult(base: Int): PostgresDbResult { + val colPtr = loadInt(base) + val colLen = loadInt(base + 4) + val rowPtr = loadInt(base + 8) + val rowLen = loadInt(base + 12) + return PostgresDbResult( + columns = (0 until colLen).map { i -> liftDbColumn(colPtr + i * 48) }, + rows = (0 until rowLen).map { i -> liftDbRow(rowPtr + i * 8) }, + ) +} + +/** Matches `golem:rdbms/postgres@1.5.0`'s `error` variant (5 cases, all string payloads) -- same shape as Scala's `DbError`. */ +sealed class DbError { + data class ConnectionFailure(val message: String) : DbError() + data class QueryParameterFailure(val message: String) : DbError() + data class QueryExecutionFailure(val message: String) : DbError() + data class QueryResponseFailure(val message: String) : DbError() + data class Other(val message: String) : DbError() +} + +// error: size=12 align=4, tag_size=1, payload_offset=4 (all 5 cases: string, 8 bytes). +private fun liftDbError(base: Int): DbError { + val tag = loadByte(base).toInt() and 0xFF + val p = base + 4 + val msg = liftString(loadInt(p), loadInt(p + 4)) + return when (tag) { + 0 -> DbError.ConnectionFailure(msg) + 1 -> DbError.QueryParameterFailure(msg) + 2 -> DbError.QueryExecutionFailure(msg) + 3 -> DbError.QueryResponseFailure(msg) + 4 -> DbError.Other(msg) + else -> error("unknown golem:rdbms error tag $tag") + } +} + +/** A live connection to a Postgres database. MUST be [close]d when done. */ +class PostgresConnection internal constructor(private val handle: Int) { + private var closed = false + + fun query(statement: String, params: List = emptyList()): Either { + check(!closed) { "PostgresConnection already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerDbValueList(params) + val retPtr = alloc(20, 4) // result: tag@0(1,1), payload@4(max(16,12)=16) + hostConnectionQuery(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(liftDbResult(retPtr + 4)) else Either.Left(liftDbError(retPtr + 4)) + } + + fun execute(statement: String, params: List = emptyList()): Either { + check(!closed) { "PostgresConnection already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerDbValueList(params) + val retPtr = alloc(24, 8) // result: tag@0(1,1), payload@8 (rounded to align8) -- max(8,12)=12 -> 8+12=20 -> round to 24 + hostConnectionExecute(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(loadLong(retPtr + 8)) else Either.Left(liftDbError(retPtr + 8)) + } + + fun beginTransaction(): Either { + check(!closed) { "PostgresConnection already closed" } + val retPtr = alloc(16, 4) // result: tag@0(1,1), payload@4(max(4,12)=12) + hostConnectionBeginTransaction(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(PostgresTransaction(loadInt(retPtr + 4))) else Either.Left(liftDbError(retPtr + 4)) + } + + fun close() { + if (!closed) { + hostConnectionDrop(handle) + closed = true + } + } + + companion object { + fun open(address: String): Either { + val (ptr, len) = lowerStringToPtrLen(address) + val retPtr = alloc(16, 4) // result: tag@0(1,1), payload@4(max(4,12)=12) + hostConnectionOpen(ptr, len, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(PostgresConnection(loadInt(retPtr + 4))) else Either.Left(liftDbError(retPtr + 4)) + } + } +} + +/** An open transaction on a [PostgresConnection]. MUST be [close]d when done, whether or not [commit]/[rollback] was called. */ +class PostgresTransaction internal constructor(private val handle: Int) { + private var closed = false + + fun query(statement: String, params: List = emptyList()): Either { + check(!closed) { "PostgresTransaction already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerDbValueList(params) + val retPtr = alloc(20, 4) + hostTransactionQuery(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(liftDbResult(retPtr + 4)) else Either.Left(liftDbError(retPtr + 4)) + } + + fun execute(statement: String, params: List = emptyList()): Either { + check(!closed) { "PostgresTransaction already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerDbValueList(params) + val retPtr = alloc(24, 8) + hostTransactionExecute(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(loadLong(retPtr + 8)) else Either.Left(liftDbError(retPtr + 8)) + } + + fun commit(): Either { + check(!closed) { "PostgresTransaction already closed" } + val retPtr = alloc(16, 4) // result<_, error>: tag@0(1,1), payload@4(12) + hostTransactionCommit(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(Unit) else Either.Left(liftDbError(retPtr + 4)) + } + + fun rollback(): Either { + check(!closed) { "PostgresTransaction already closed" } + val retPtr = alloc(16, 4) + hostTransactionRollback(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(Unit) else Either.Left(liftDbError(retPtr + 4)) + } + + fun close() { + if (!closed) { + hostTransactionDrop(handle) + closed = true + } + } +} + +// =========================================================================== +// MySQL (golem:rdbms/mysql@1.5.0) +// =========================================================================== +// +// Entirely flat -- unlike Postgres, MySQL's db-value has no lazy-db-value-style recursive +// cases at all (no composite/domain/array/range equivalents in this WIT interface), so this +// port is complete in one increment, mirroring Postgres's Connection/Transaction/query/ +// execute/beginTransaction/commit/rollback shape exactly. Signatures verified via abi-dump. + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[static]db-connection.open") +private external fun hostMysqlConnectionOpen(addrPtr: Int, addrLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[method]db-connection.query") +private external fun hostMysqlConnectionQuery(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[method]db-connection.execute") +private external fun hostMysqlConnectionExecute(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[method]db-connection.begin-transaction") +private external fun hostMysqlConnectionBeginTransaction(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[resource-drop]db-connection") +private external fun hostMysqlConnectionDrop(handle: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[method]db-transaction.query") +private external fun hostMysqlTransactionQuery(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[method]db-transaction.execute") +private external fun hostMysqlTransactionExecute(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[method]db-transaction.commit") +private external fun hostMysqlTransactionCommit(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[method]db-transaction.rollback") +private external fun hostMysqlTransactionRollback(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/mysql@1.5.0", "[resource-drop]db-transaction") +private external fun hostMysqlTransactionDrop(handle: Int) + +/** Matches `golem:rdbms/mysql@1.5.0`'s `db-value` variant in full (36 cases, tags 0-35). */ +sealed class MysqlDbValue { + data class BooleanVal(val value: Boolean) : MysqlDbValue() + data class TinyInt(val value: Byte) : MysqlDbValue() + data class SmallInt(val value: Short) : MysqlDbValue() + data class MediumInt(val value: Int) : MysqlDbValue() + data class IntVal(val value: Int) : MysqlDbValue() + data class BigInt(val value: Long) : MysqlDbValue() + data class TinyIntUnsigned(val value: UByte) : MysqlDbValue() + data class SmallIntUnsigned(val value: UShort) : MysqlDbValue() + data class MediumIntUnsigned(val value: UInt) : MysqlDbValue() + data class IntUnsigned(val value: UInt) : MysqlDbValue() + data class BigIntUnsigned(val value: ULong) : MysqlDbValue() + data class FloatVal(val value: Float) : MysqlDbValue() + data class DoubleVal(val value: Double) : MysqlDbValue() + + /** WIT represents `decimal` as an arbitrary-precision decimal encoded as a string. */ + data class Decimal(val value: String) : MysqlDbValue() + data class DateVal(val value: DbDate) : MysqlDbValue() + data class DateTimeVal(val value: DbTimestamp) : MysqlDbValue() + data class TimestampVal(val value: DbTimestamp) : MysqlDbValue() + data class TimeVal(val value: DbTime) : MysqlDbValue() + data class Year(val value: UShort) : MysqlDbValue() + data class FixChar(val value: String) : MysqlDbValue() + data class VarChar(val value: String) : MysqlDbValue() + data class TinyText(val value: String) : MysqlDbValue() + data class Text(val value: String) : MysqlDbValue() + data class MediumText(val value: String) : MysqlDbValue() + data class LongText(val value: String) : MysqlDbValue() + data class Binary(val value: List) : MysqlDbValue() + data class VarBinary(val value: List) : MysqlDbValue() + data class TinyBlob(val value: List) : MysqlDbValue() + data class Blob(val value: List) : MysqlDbValue() + data class MediumBlob(val value: List) : MysqlDbValue() + data class LongBlob(val value: List) : MysqlDbValue() + data class Enumeration(val value: String) : MysqlDbValue() + data class SetVal(val value: String) : MysqlDbValue() + data class Bit(val value: List) : MysqlDbValue() + data class Json(val value: String) : MysqlDbValue() + object Null : MysqlDbValue() +} + +// db-value: size=24 align=8, tag_size=1, payload_offset=8. +private const val MYSQL_DBV_SIZE = 24 +private const val MYSQL_DBV_ALIGN = 8 +private const val MYSQL_DBV_PAYLOAD_OFFSET = 8 + +private fun liftListOfUByte(base: Int): List { + val ptr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> loadByte(ptr + i).toUByte() } +} +private fun lowerListOfUByte(base: Int, bytes: List) { + val arr = alloc(bytes.size, 1) + bytes.forEachIndexed { i, b -> storeByte(arr + i, b.toByte()) } + storeInt(base, arr) + storeInt(base + 4, bytes.size) +} + +private fun liftMysqlDbValue(base: Int): MysqlDbValue { + val tag = loadByte(base).toInt() and 0xFF + val p = base + MYSQL_DBV_PAYLOAD_OFFSET + return when (tag) { + 0 -> MysqlDbValue.BooleanVal(loadByte(p).toInt() != 0) + 1 -> MysqlDbValue.TinyInt(loadByte(p)) + 2 -> MysqlDbValue.SmallInt(loadShort(p)) + 3 -> MysqlDbValue.MediumInt(loadInt(p)) + 4 -> MysqlDbValue.IntVal(loadInt(p)) + 5 -> MysqlDbValue.BigInt(loadLong(p)) + 6 -> MysqlDbValue.TinyIntUnsigned(loadByte(p).toUByte()) + 7 -> MysqlDbValue.SmallIntUnsigned(loadShort(p).toUShort()) + 8 -> MysqlDbValue.MediumIntUnsigned(loadInt(p).toUInt()) + 9 -> MysqlDbValue.IntUnsigned(loadInt(p).toUInt()) + 10 -> MysqlDbValue.BigIntUnsigned(loadLong(p).toULong()) + 11 -> MysqlDbValue.FloatVal(loadFloat(p)) + 12 -> MysqlDbValue.DoubleVal(loadDouble(p)) + 13 -> MysqlDbValue.Decimal(liftString(loadInt(p), loadInt(p + 4))) + 14 -> MysqlDbValue.DateVal(liftDbDate(p)) + 15 -> MysqlDbValue.DateTimeVal(liftDbTimestamp(p)) + 16 -> MysqlDbValue.TimestampVal(liftDbTimestamp(p)) + 17 -> MysqlDbValue.TimeVal(liftDbTime(p)) + 18 -> MysqlDbValue.Year(loadShort(p).toUShort()) + 19 -> MysqlDbValue.FixChar(liftString(loadInt(p), loadInt(p + 4))) + 20 -> MysqlDbValue.VarChar(liftString(loadInt(p), loadInt(p + 4))) + 21 -> MysqlDbValue.TinyText(liftString(loadInt(p), loadInt(p + 4))) + 22 -> MysqlDbValue.Text(liftString(loadInt(p), loadInt(p + 4))) + 23 -> MysqlDbValue.MediumText(liftString(loadInt(p), loadInt(p + 4))) + 24 -> MysqlDbValue.LongText(liftString(loadInt(p), loadInt(p + 4))) + 25 -> MysqlDbValue.Binary(liftListOfUByte(p)) + 26 -> MysqlDbValue.VarBinary(liftListOfUByte(p)) + 27 -> MysqlDbValue.TinyBlob(liftListOfUByte(p)) + 28 -> MysqlDbValue.Blob(liftListOfUByte(p)) + 29 -> MysqlDbValue.MediumBlob(liftListOfUByte(p)) + 30 -> MysqlDbValue.LongBlob(liftListOfUByte(p)) + 31 -> MysqlDbValue.Enumeration(liftString(loadInt(p), loadInt(p + 4))) + 32 -> MysqlDbValue.SetVal(liftString(loadInt(p), loadInt(p + 4))) + 33 -> MysqlDbValue.Bit(liftListOfBool(p)) + 34 -> MysqlDbValue.Json(liftString(loadInt(p), loadInt(p + 4))) + 35 -> MysqlDbValue.Null + else -> error("native Rdbms: unknown mysql db-value tag $tag") + } +} + +private fun lowerMysqlDbValue(base: Int, value: MysqlDbValue) { + val p = base + MYSQL_DBV_PAYLOAD_OFFSET + when (value) { + is MysqlDbValue.BooleanVal -> { + storeByte(base, 0) + storeByte(p, if (value.value) 1 else 0) + } + is MysqlDbValue.TinyInt -> { + storeByte(base, 1) + storeByte(p, value.value) + } + is MysqlDbValue.SmallInt -> { + storeByte(base, 2) + storeShort(p, value.value) + } + is MysqlDbValue.MediumInt -> { + storeByte(base, 3) + storeInt(p, value.value) + } + is MysqlDbValue.IntVal -> { + storeByte(base, 4) + storeInt(p, value.value) + } + is MysqlDbValue.BigInt -> { + storeByte(base, 5) + storeLong(p, value.value) + } + is MysqlDbValue.TinyIntUnsigned -> { + storeByte(base, 6) + storeByte(p, value.value.toByte()) + } + is MysqlDbValue.SmallIntUnsigned -> { + storeByte(base, 7) + storeShort(p, value.value.toShort()) + } + is MysqlDbValue.MediumIntUnsigned -> { + storeByte(base, 8) + storeInt(p, value.value.toInt()) + } + is MysqlDbValue.IntUnsigned -> { + storeByte(base, 9) + storeInt(p, value.value.toInt()) + } + is MysqlDbValue.BigIntUnsigned -> { + storeByte(base, 10) + storeLong(p, value.value.toLong()) + } + is MysqlDbValue.FloatVal -> { + storeByte(base, 11) + storeFloat(p, value.value) + } + is MysqlDbValue.DoubleVal -> { + storeByte(base, 12) + storeDouble(p, value.value) + } + is MysqlDbValue.Decimal -> { + storeByte(base, 13) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.DateVal -> { + storeByte(base, 14) + storeDbDate(p, value.value) + } + is MysqlDbValue.DateTimeVal -> { + storeByte(base, 15) + storeDbTimestamp(p, value.value) + } + is MysqlDbValue.TimestampVal -> { + storeByte(base, 16) + storeDbTimestamp(p, value.value) + } + is MysqlDbValue.TimeVal -> { + storeByte(base, 17) + storeDbTime(p, value.value) + } + is MysqlDbValue.Year -> { + storeByte(base, 18) + storeShort(p, value.value.toShort()) + } + is MysqlDbValue.FixChar -> { + storeByte(base, 19) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.VarChar -> { + storeByte(base, 20) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.TinyText -> { + storeByte(base, 21) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.Text -> { + storeByte(base, 22) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.MediumText -> { + storeByte(base, 23) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.LongText -> { + storeByte(base, 24) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.Binary -> { + storeByte(base, 25) + lowerListOfUByte(p, value.value) + } + is MysqlDbValue.VarBinary -> { + storeByte(base, 26) + lowerListOfUByte(p, value.value) + } + is MysqlDbValue.TinyBlob -> { + storeByte(base, 27) + lowerListOfUByte(p, value.value) + } + is MysqlDbValue.Blob -> { + storeByte(base, 28) + lowerListOfUByte(p, value.value) + } + is MysqlDbValue.MediumBlob -> { + storeByte(base, 29) + lowerListOfUByte(p, value.value) + } + is MysqlDbValue.LongBlob -> { + storeByte(base, 30) + lowerListOfUByte(p, value.value) + } + is MysqlDbValue.Enumeration -> { + storeByte(base, 31) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.SetVal -> { + storeByte(base, 32) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is MysqlDbValue.Bit -> { + storeByte(base, 33) + lowerListOfBool(p, value.value) + } + is MysqlDbValue.Json -> { + storeByte(base, 34) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + MysqlDbValue.Null -> storeByte(base, 35) + } +} + +private fun lowerMysqlDbValueList(values: List): Pair { + val arr = alloc(values.size * MYSQL_DBV_SIZE, MYSQL_DBV_ALIGN) + values.forEachIndexed { i, v -> lowerMysqlDbValue(arr + i * MYSQL_DBV_SIZE, v) } + return arr to values.size +} + +/** Mirrors Scala's own `DbColumn` for MySQL: only `dbTypeName`, no structural `db-type` (mysql's `db-column-type` is a plain 1-byte tag-only enum, cheaper to skip than to decode for no consumer). */ +data class MysqlDbColumn(val ordinal: Long, val name: String, val dbTypeName: String) + +/** + * See [PostgresDbValue.asDisplayString]'s doc comment: same reasoning applies here -- Scala's + * `MysqlDbRow.getString`/`getInt` fall back to `v.toString` with no `MysqlDbValue` override, + * so this fixes the value extraction instead of reproducing that bug. + */ +private fun MysqlDbValue.asDisplayString(): String = when (this) { + is MysqlDbValue.TinyInt -> value.toString() + is MysqlDbValue.SmallInt -> value.toString() + is MysqlDbValue.MediumInt -> value.toString() + is MysqlDbValue.IntVal -> value.toString() + is MysqlDbValue.BigInt -> value.toString() + is MysqlDbValue.TinyIntUnsigned -> value.toString() + is MysqlDbValue.SmallIntUnsigned -> value.toString() + is MysqlDbValue.MediumIntUnsigned -> value.toString() + is MysqlDbValue.IntUnsigned -> value.toString() + is MysqlDbValue.BigIntUnsigned -> value.toString() + is MysqlDbValue.FloatVal -> value.toString() + is MysqlDbValue.DoubleVal -> value.toString() + is MysqlDbValue.Decimal -> value + is MysqlDbValue.BooleanVal -> value.toString() + is MysqlDbValue.FixChar -> value + is MysqlDbValue.VarChar -> value + is MysqlDbValue.TinyText -> value + is MysqlDbValue.Text -> value + is MysqlDbValue.MediumText -> value + is MysqlDbValue.LongText -> value + is MysqlDbValue.Enumeration -> value + is MysqlDbValue.SetVal -> value + is MysqlDbValue.Json -> value + is MysqlDbValue.Year -> value.toString() + else -> toString() +} + +/** Mirrors Scala's `MysqlDbRow` -- only `getString`/`getInt` (Scala's own doesn't expose `getLong`). */ +data class MysqlDbRow(val values: List) { + fun getString(index: Int): String? = when (val v = values[index]) { + MysqlDbValue.Null -> null + else -> v.asDisplayString() + } + + fun getInt(index: Int): Int? = when (val v = values[index]) { + MysqlDbValue.Null -> null + is MysqlDbValue.IntVal -> v.value + is MysqlDbValue.TinyInt -> v.value.toInt() + is MysqlDbValue.SmallInt -> v.value.toInt() + is MysqlDbValue.MediumInt -> v.value + else -> v.asDisplayString().toInt() + } +} + +data class MysqlDbResult(val columns: List, val rows: List) + +// mysql db-column: size=32 align=8 { ordinal@0(8,8), name@8(8,4), db-type@16(1,1, skipped), db-type-name@20(8,4) }. +private fun liftMysqlDbColumn(base: Int): MysqlDbColumn = MysqlDbColumn( + ordinal = loadLong(base), + name = liftString(loadInt(base + 8), loadInt(base + 12)), + dbTypeName = liftString(loadInt(base + 20), loadInt(base + 24)), +) + +// mysql db-row: size=8 align=4 { values: list @0 }. +private fun liftMysqlDbRow(base: Int): MysqlDbRow { + val ptr = loadInt(base) + val len = loadInt(base + 4) + return MysqlDbRow((0 until len).map { i -> liftMysqlDbValue(ptr + i * MYSQL_DBV_SIZE) }) +} + +// mysql db-result: size=16 align=4 { columns: list @0, rows: list @8 }. +private fun liftMysqlDbResult(base: Int): MysqlDbResult { + val colPtr = loadInt(base) + val colLen = loadInt(base + 4) + val rowPtr = loadInt(base + 8) + val rowLen = loadInt(base + 12) + return MysqlDbResult( + columns = (0 until colLen).map { i -> liftMysqlDbColumn(colPtr + i * 32) }, + rows = (0 until rowLen).map { i -> liftMysqlDbRow(rowPtr + i * 8) }, + ) +} + +/** Matches `golem:rdbms/mysql@1.5.0`'s `error` variant (5 cases, all string payloads). */ +sealed class MysqlDbError { + data class ConnectionFailure(val message: String) : MysqlDbError() + data class QueryParameterFailure(val message: String) : MysqlDbError() + data class QueryExecutionFailure(val message: String) : MysqlDbError() + data class QueryResponseFailure(val message: String) : MysqlDbError() + data class Other(val message: String) : MysqlDbError() +} + +private fun liftMysqlDbError(base: Int): MysqlDbError { + val tag = loadByte(base).toInt() and 0xFF + val p = base + 4 + val msg = liftString(loadInt(p), loadInt(p + 4)) + return when (tag) { + 0 -> MysqlDbError.ConnectionFailure(msg) + 1 -> MysqlDbError.QueryParameterFailure(msg) + 2 -> MysqlDbError.QueryExecutionFailure(msg) + 3 -> MysqlDbError.QueryResponseFailure(msg) + 4 -> MysqlDbError.Other(msg) + else -> error("unknown golem:rdbms mysql error tag $tag") + } +} + +/** A live connection to a MySQL database. MUST be [close]d when done. */ +class MysqlConnection internal constructor(private val handle: Int) { + private var closed = false + + fun query(statement: String, params: List = emptyList()): Either { + check(!closed) { "MysqlConnection already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerMysqlDbValueList(params) + val retPtr = alloc(20, 4) // result: tag@0(1,1), payload@4(max(16,12)=16) + hostMysqlConnectionQuery(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(liftMysqlDbResult(retPtr + 4)) else Either.Left(liftMysqlDbError(retPtr + 4)) + } + + fun execute(statement: String, params: List = emptyList()): Either { + check(!closed) { "MysqlConnection already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerMysqlDbValueList(params) + val retPtr = alloc(24, 8) // result: tag@0(1,1), payload@8 (rounded to align8) + hostMysqlConnectionExecute(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(loadLong(retPtr + 8)) else Either.Left(liftMysqlDbError(retPtr + 8)) + } + + fun beginTransaction(): Either { + check(!closed) { "MysqlConnection already closed" } + val retPtr = alloc(16, 4) // result: tag@0(1,1), payload@4(max(4,12)=12) + hostMysqlConnectionBeginTransaction(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(MysqlTransaction(loadInt(retPtr + 4))) else Either.Left(liftMysqlDbError(retPtr + 4)) + } + + fun close() { + if (!closed) { + hostMysqlConnectionDrop(handle) + closed = true + } + } + + companion object { + fun open(address: String): Either { + val (ptr, len) = lowerStringToPtrLen(address) + val retPtr = alloc(16, 4) + hostMysqlConnectionOpen(ptr, len, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(MysqlConnection(loadInt(retPtr + 4))) else Either.Left(liftMysqlDbError(retPtr + 4)) + } + } +} + +/** An open transaction on a [MysqlConnection]. MUST be [close]d when done, whether or not [commit]/[rollback] was called. */ +class MysqlTransaction internal constructor(private val handle: Int) { + private var closed = false + + fun query(statement: String, params: List = emptyList()): Either { + check(!closed) { "MysqlTransaction already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerMysqlDbValueList(params) + val retPtr = alloc(20, 4) + hostMysqlTransactionQuery(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(liftMysqlDbResult(retPtr + 4)) else Either.Left(liftMysqlDbError(retPtr + 4)) + } + + fun execute(statement: String, params: List = emptyList()): Either { + check(!closed) { "MysqlTransaction already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerMysqlDbValueList(params) + val retPtr = alloc(24, 8) + hostMysqlTransactionExecute(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(loadLong(retPtr + 8)) else Either.Left(liftMysqlDbError(retPtr + 8)) + } + + fun commit(): Either { + check(!closed) { "MysqlTransaction already closed" } + val retPtr = alloc(16, 4) + hostMysqlTransactionCommit(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(Unit) else Either.Left(liftMysqlDbError(retPtr + 4)) + } + + fun rollback(): Either { + check(!closed) { "MysqlTransaction already closed" } + val retPtr = alloc(16, 4) + hostMysqlTransactionRollback(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(Unit) else Either.Left(liftMysqlDbError(retPtr + 4)) + } + + fun close() { + if (!closed) { + hostMysqlTransactionDrop(handle) + closed = true + } + } +} + +// =========================================================================== +// Ignite (golem:rdbms/ignite2@1.5.0) +// =========================================================================== +// +// Entirely flat, like MySQL -- no lazy-db-value-style recursion. Smallest of the three +// backends (16 db-value cases including null, vs Postgres's 45 and MySQL's 36). db-column is +// simpler too: just {ordinal, name}, no db-type-name at all (matching Scala's own +// IgniteDbColumn, which likewise has no type-name field). execute returns s64 here, not u64 +// like Postgres/MySQL. Signatures verified via abi-dump. + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[static]db-connection.open") +private external fun hostIgniteConnectionOpen(addrPtr: Int, addrLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[method]db-connection.query") +private external fun hostIgniteConnectionQuery(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[method]db-connection.execute") +private external fun hostIgniteConnectionExecute(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[method]db-connection.begin-transaction") +private external fun hostIgniteConnectionBeginTransaction(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[resource-drop]db-connection") +private external fun hostIgniteConnectionDrop(handle: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[method]db-transaction.query") +private external fun hostIgniteTransactionQuery(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[method]db-transaction.execute") +private external fun hostIgniteTransactionExecute(handle: Int, stmtPtr: Int, stmtLen: Int, paramsPtr: Int, paramsLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[method]db-transaction.commit") +private external fun hostIgniteTransactionCommit(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[method]db-transaction.rollback") +private external fun hostIgniteTransactionRollback(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:rdbms/ignite2@1.5.0", "[resource-drop]db-transaction") +private external fun hostIgniteTransactionDrop(handle: Int) + +/** Matches `golem:rdbms/ignite2@1.5.0`'s `db-value` variant in full (16 cases, tags 0-15). */ +sealed class IgniteDbValue { + object DbNull : IgniteDbValue() + data class DbBoolean(val value: Boolean) : IgniteDbValue() + data class DbByte(val value: Byte) : IgniteDbValue() + data class DbShort(val value: Short) : IgniteDbValue() + data class DbInt(val value: Int) : IgniteDbValue() + data class DbLong(val value: Long) : IgniteDbValue() + data class DbFloat(val value: Float) : IgniteDbValue() + data class DbDouble(val value: Double) : IgniteDbValue() + + /** A 16-bit Unicode code unit (Java `char`). */ + data class DbChar(val value: Char) : IgniteDbValue() + data class DbString(val value: String) : IgniteDbValue() + data class DbUuid(val highBits: Long, val lowBits: Long) : IgniteDbValue() + + /** Milliseconds since Unix epoch (UTC). */ + data class DbDate(val millis: Long) : IgniteDbValue() + + /** Milliseconds since epoch, plus sub-millisecond nanoseconds (0..999_999). */ + data class DbTimestamp(val millis: Long, val nanos: Int) : IgniteDbValue() + + /** Nanoseconds since midnight. */ + data class DbTime(val nanos: Long) : IgniteDbValue() + data class DbDecimal(val value: String) : IgniteDbValue() + data class DbByteArray(val value: List) : IgniteDbValue() +} + +// db-value: size=24 align=8, tag_size=1, payload_offset=8. +private const val IGNITE_DBV_SIZE = 24 +private const val IGNITE_DBV_ALIGN = 8 +private const val IGNITE_DBV_PAYLOAD_OFFSET = 8 + +private fun liftIgniteDbValue(base: Int): IgniteDbValue { + val tag = loadByte(base).toInt() and 0xFF + val p = base + IGNITE_DBV_PAYLOAD_OFFSET + return when (tag) { + 0 -> IgniteDbValue.DbNull + 1 -> IgniteDbValue.DbBoolean(loadByte(p).toInt() != 0) + 2 -> IgniteDbValue.DbByte(loadByte(p)) + 3 -> IgniteDbValue.DbShort(loadShort(p)) + 4 -> IgniteDbValue.DbInt(loadInt(p)) + 5 -> IgniteDbValue.DbLong(loadLong(p)) + 6 -> IgniteDbValue.DbFloat(loadFloat(p)) + 7 -> IgniteDbValue.DbDouble(loadDouble(p)) + 8 -> IgniteDbValue.DbChar((loadShort(p).toInt() and 0xFFFF).toChar()) + 9 -> IgniteDbValue.DbString(liftString(loadInt(p), loadInt(p + 4))) + 10 -> IgniteDbValue.DbUuid(loadLong(p), loadLong(p + 8)) + 11 -> IgniteDbValue.DbDate(loadLong(p)) + 12 -> IgniteDbValue.DbTimestamp(loadLong(p), loadInt(p + 8)) + 13 -> IgniteDbValue.DbTime(loadLong(p)) + 14 -> IgniteDbValue.DbDecimal(liftString(loadInt(p), loadInt(p + 4))) + 15 -> IgniteDbValue.DbByteArray(liftListOfUByte(p)) + else -> error("native Rdbms: unknown ignite db-value tag $tag") + } +} + +private fun lowerIgniteDbValue(base: Int, value: IgniteDbValue) { + val p = base + IGNITE_DBV_PAYLOAD_OFFSET + when (value) { + IgniteDbValue.DbNull -> storeByte(base, 0) + is IgniteDbValue.DbBoolean -> { + storeByte(base, 1) + storeByte(p, if (value.value) 1 else 0) + } + is IgniteDbValue.DbByte -> { + storeByte(base, 2) + storeByte(p, value.value) + } + is IgniteDbValue.DbShort -> { + storeByte(base, 3) + storeShort(p, value.value) + } + is IgniteDbValue.DbInt -> { + storeByte(base, 4) + storeInt(p, value.value) + } + is IgniteDbValue.DbLong -> { + storeByte(base, 5) + storeLong(p, value.value) + } + is IgniteDbValue.DbFloat -> { + storeByte(base, 6) + storeFloat(p, value.value) + } + is IgniteDbValue.DbDouble -> { + storeByte(base, 7) + storeDouble(p, value.value) + } + is IgniteDbValue.DbChar -> { + storeByte(base, 8) + storeShort(p, value.value.code.toShort()) + } + is IgniteDbValue.DbString -> { + storeByte(base, 9) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is IgniteDbValue.DbUuid -> { + storeByte(base, 10) + storeLong(p, value.highBits) + storeLong(p + 8, value.lowBits) + } + is IgniteDbValue.DbDate -> { + storeByte(base, 11) + storeLong(p, value.millis) + } + is IgniteDbValue.DbTimestamp -> { + storeByte(base, 12) + storeLong(p, value.millis) + storeInt(p + 8, value.nanos) + } + is IgniteDbValue.DbTime -> { + storeByte(base, 13) + storeLong(p, value.nanos) + } + is IgniteDbValue.DbDecimal -> { + storeByte(base, 14) + val (ptr, len) = lowerStringToPtrLen(value.value) + storeInt(p, ptr) + storeInt(p + 4, len) + } + is IgniteDbValue.DbByteArray -> { + storeByte(base, 15) + lowerListOfUByte(p, value.value) + } + } +} + +private fun lowerIgniteDbValueList(values: List): Pair { + val arr = alloc(values.size * IGNITE_DBV_SIZE, IGNITE_DBV_ALIGN) + values.forEachIndexed { i, v -> lowerIgniteDbValue(arr + i * IGNITE_DBV_SIZE, v) } + return arr to values.size +} + +/** Matches `golem:rdbms/ignite2@1.5.0`'s `db-column` record -- no type-name field at all (matching Scala's own `IgniteDbColumn`, which likewise omits it). */ +data class IgniteDbColumn(val ordinal: Long, val name: String) + +/** See [PostgresDbValue.asDisplayString]'s doc comment: same `v.toString`-bug fix applies here. */ +private fun IgniteDbValue.asDisplayString(): String = when (this) { + is IgniteDbValue.DbByte -> value.toString() + is IgniteDbValue.DbShort -> value.toString() + is IgniteDbValue.DbInt -> value.toString() + is IgniteDbValue.DbLong -> value.toString() + is IgniteDbValue.DbFloat -> value.toString() + is IgniteDbValue.DbDouble -> value.toString() + is IgniteDbValue.DbBoolean -> value.toString() + is IgniteDbValue.DbString -> value + is IgniteDbValue.DbDecimal -> value + is IgniteDbValue.DbChar -> value.toString() + else -> toString() +} + +/** Mirrors Scala's `IgniteDbRow` in full (`getString`/`getInt`/`getLong`, same as `PostgresDbRow`). */ +data class IgniteDbRow(val values: List) { + fun getString(index: Int): String? = when (val v = values[index]) { + IgniteDbValue.DbNull -> null + else -> v.asDisplayString() + } + + fun getInt(index: Int): Int? = when (val v = values[index]) { + IgniteDbValue.DbNull -> null + is IgniteDbValue.DbInt -> v.value + is IgniteDbValue.DbByte -> v.value.toInt() + is IgniteDbValue.DbShort -> v.value.toInt() + else -> v.asDisplayString().toInt() + } + + fun getLong(index: Int): Long? = when (val v = values[index]) { + IgniteDbValue.DbNull -> null + is IgniteDbValue.DbLong -> v.value + is IgniteDbValue.DbInt -> v.value.toLong() + else -> v.asDisplayString().toLong() + } +} + +data class IgniteDbResult(val columns: List, val rows: List) + +// ignite db-column: size=16 align=8 { ordinal@0(8,8), name@8(8,4) }. +private fun liftIgniteDbColumn(base: Int): IgniteDbColumn = IgniteDbColumn(loadLong(base), liftString(loadInt(base + 8), loadInt(base + 12))) + +// ignite db-row: size=8 align=4 { values: list @0 }. +private fun liftIgniteDbRow(base: Int): IgniteDbRow { + val ptr = loadInt(base) + val len = loadInt(base + 4) + return IgniteDbRow((0 until len).map { i -> liftIgniteDbValue(ptr + i * IGNITE_DBV_SIZE) }) +} + +// ignite db-result: size=16 align=4 { columns: list @0, rows: list @8 }. +private fun liftIgniteDbResult(base: Int): IgniteDbResult { + val colPtr = loadInt(base) + val colLen = loadInt(base + 4) + val rowPtr = loadInt(base + 8) + val rowLen = loadInt(base + 12) + return IgniteDbResult( + columns = (0 until colLen).map { i -> liftIgniteDbColumn(colPtr + i * 16) }, + rows = (0 until rowLen).map { i -> liftIgniteDbRow(rowPtr + i * 8) }, + ) +} + +/** Matches `golem:rdbms/ignite2@1.5.0`'s `error` variant (5 cases, all string payloads). */ +sealed class IgniteDbError { + data class ConnectionFailure(val message: String) : IgniteDbError() + data class QueryParameterFailure(val message: String) : IgniteDbError() + data class QueryExecutionFailure(val message: String) : IgniteDbError() + data class QueryResponseFailure(val message: String) : IgniteDbError() + data class Other(val message: String) : IgniteDbError() +} + +private fun liftIgniteDbError(base: Int): IgniteDbError { + val tag = loadByte(base).toInt() and 0xFF + val p = base + 4 + val msg = liftString(loadInt(p), loadInt(p + 4)) + return when (tag) { + 0 -> IgniteDbError.ConnectionFailure(msg) + 1 -> IgniteDbError.QueryParameterFailure(msg) + 2 -> IgniteDbError.QueryExecutionFailure(msg) + 3 -> IgniteDbError.QueryResponseFailure(msg) + 4 -> IgniteDbError.Other(msg) + else -> error("unknown golem:rdbms ignite error tag $tag") + } +} + +/** A live connection to an Apache Ignite 2.x node. MUST be [close]d when done. */ +class IgniteConnection internal constructor(private val handle: Int) { + private var closed = false + + fun query(statement: String, params: List = emptyList()): Either { + check(!closed) { "IgniteConnection already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerIgniteDbValueList(params) + val retPtr = alloc(20, 4) // result: tag@0(1,1), payload@4(max(16,12)=16) + hostIgniteConnectionQuery(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(liftIgniteDbResult(retPtr + 4)) else Either.Left(liftIgniteDbError(retPtr + 4)) + } + + fun execute(statement: String, params: List = emptyList()): Either { + check(!closed) { "IgniteConnection already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerIgniteDbValueList(params) + val retPtr = alloc(24, 8) // result: tag@0(1,1), payload@8 (rounded to align8) + hostIgniteConnectionExecute(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(loadLong(retPtr + 8)) else Either.Left(liftIgniteDbError(retPtr + 8)) + } + + fun beginTransaction(): Either { + check(!closed) { "IgniteConnection already closed" } + val retPtr = alloc(16, 4) // result: tag@0(1,1), payload@4(max(4,12)=12) + hostIgniteConnectionBeginTransaction(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(IgniteTransaction(loadInt(retPtr + 4))) else Either.Left(liftIgniteDbError(retPtr + 4)) + } + + fun close() { + if (!closed) { + hostIgniteConnectionDrop(handle) + closed = true + } + } + + companion object { + fun open(address: String): Either { + val (ptr, len) = lowerStringToPtrLen(address) + val retPtr = alloc(16, 4) + hostIgniteConnectionOpen(ptr, len, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(IgniteConnection(loadInt(retPtr + 4))) else Either.Left(liftIgniteDbError(retPtr + 4)) + } + } +} + +/** An open transaction on an [IgniteConnection]. MUST be [close]d when done, whether or not [commit]/[rollback] was called. */ +class IgniteTransaction internal constructor(private val handle: Int) { + private var closed = false + + fun query(statement: String, params: List = emptyList()): Either { + check(!closed) { "IgniteTransaction already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerIgniteDbValueList(params) + val retPtr = alloc(20, 4) + hostIgniteTransactionQuery(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(liftIgniteDbResult(retPtr + 4)) else Either.Left(liftIgniteDbError(retPtr + 4)) + } + + fun execute(statement: String, params: List = emptyList()): Either { + check(!closed) { "IgniteTransaction already closed" } + val (stmtPtr, stmtLen) = lowerStringToPtrLen(statement) + val (paramsPtr, paramsLen) = lowerIgniteDbValueList(params) + val retPtr = alloc(24, 8) + hostIgniteTransactionExecute(handle, stmtPtr, stmtLen, paramsPtr, paramsLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(loadLong(retPtr + 8)) else Either.Left(liftIgniteDbError(retPtr + 8)) + } + + fun commit(): Either { + check(!closed) { "IgniteTransaction already closed" } + val retPtr = alloc(16, 4) + hostIgniteTransactionCommit(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(Unit) else Either.Left(liftIgniteDbError(retPtr + 4)) + } + + fun rollback(): Either { + check(!closed) { "IgniteTransaction already closed" } + val retPtr = alloc(16, 4) + hostIgniteTransactionRollback(handle, retPtr) + return if (loadByte(retPtr).toInt() == 0) Either.Right(Unit) else Either.Left(liftIgniteDbError(retPtr + 4)) + } + + fun close() { + if (!closed) { + hostIgniteTransactionDrop(handle) + closed = true + } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/RetryApi.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/RetryApi.kt new file mode 100644 index 0000000000..88c3f82fbd --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/RetryApi.kt @@ -0,0 +1,495 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.host + +import cloud.golem.runtime.lowerStringToPtrLen +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadDouble +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeDouble +import cloud.golem.wasm.storeInt +import cloud.golem.wasm.storeLong +import cloud.golem.wasm.writeStringField + +// Native SDK access to golem:api/retry@1.5.0 -- the semantic retry-policy API. Unlike the Scala +// SDK (which keeps the policy/predicate trees OPAQUE, passing JS objects straight through), the +// native path has no opaque-JS escape hatch: the flattened `retry-policy`/`retry-predicate` +// node-list trees (structurally like schema-value-tree: a list of nodes with s32 index +// cross-references, root = nodes[0]) are marshalled field-for-field here. +// +// Full surface: DECODE (get-retry-policies / get-retry-policy-by-name / remove) and ENCODE +// (set-retry-policy / resolve-retry-policy). Every layout below (variant tags/payload offsets, +// config record field offsets, named-retry-policy field offsets, the resolve properties tuple) +// was verified via abi-dump against wit-native/deps/golem-1.x/golem-retry.wit, not hand-derived. + +@kotlin.wasm.WasmImport("golem:api/retry@1.5.0", "get-retry-policies") +private external fun hostGetRetryPolicies(retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/retry@1.5.0", "get-retry-policy-by-name") +private external fun hostGetRetryPolicyByName(namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("golem:api/retry@1.5.0", "remove-retry-policy") +private external fun hostRemoveRetryPolicy(namePtr: Int, nameLen: Int) + +// set-retry-policy(policy: named-retry-policy) is FULLY FLATTENED (indirect_params=false): the +// record's fields become args -- name(ptr,len), priority(i32), predicate.nodes(ptr,len), +// policy.nodes(ptr,len). Verified via abi-dump `sig`. +@kotlin.wasm.WasmImport("golem:api/retry@1.5.0", "set-retry-policy") +private external fun hostSetRetryPolicy( + namePtr: Int, + nameLen: Int, + priority: Int, + predPtr: Int, + predLen: Int, + polPtr: Int, + polLen: Int, +) + +// resolve-retry-policy(verb, noun-uri, properties: list>) +// -> option. retptr=true; the properties list element is a 24-byte tuple +// {string@0, predicate-value@8} (align 8). +@kotlin.wasm.WasmImport("golem:api/retry@1.5.0", "resolve-retry-policy") +private external fun hostResolveRetryPolicy( + verbPtr: Int, + verbLen: Int, + nounPtr: Int, + nounLen: Int, + propsPtr: Int, + propsLen: Int, + retPtr: Int, +) + +// ── Kotlin model (mirrors the golem:api/retry WIT) ─────────────────────────────────────────── + +/** `predicate-value` variant: a dynamic value for property comparisons. */ +sealed class PredicateValue { + data class Text(val value: String) : PredicateValue() + data class Integer(val value: Long) : PredicateValue() + data class Bool(val value: Boolean) : PredicateValue() +} + +data class PropertyComparison(val propertyName: String, val value: PredicateValue) +data class PropertySetCheck(val propertyName: String, val values: List) +data class PropertyPattern(val propertyName: String, val pattern: String) + +/** `predicate-node` variant. Tuple/index cases carry `predicate-node-index` (s32) into the node list. */ +sealed class PredicateNode { + data class PropEq(val cmp: PropertyComparison) : PredicateNode() + data class PropNeq(val cmp: PropertyComparison) : PredicateNode() + data class PropGt(val cmp: PropertyComparison) : PredicateNode() + data class PropGte(val cmp: PropertyComparison) : PredicateNode() + data class PropLt(val cmp: PropertyComparison) : PredicateNode() + data class PropLte(val cmp: PropertyComparison) : PredicateNode() + data class PropExists(val propertyName: String) : PredicateNode() + data class PropIn(val check: PropertySetCheck) : PredicateNode() + data class PropMatches(val pattern: PropertyPattern) : PredicateNode() + data class PropStartsWith(val pattern: PropertyPattern) : PredicateNode() + data class PropContains(val pattern: PropertyPattern) : PredicateNode() + data class PredAnd(val left: Int, val right: Int) : PredicateNode() + data class PredOr(val left: Int, val right: Int) : PredicateNode() + data class PredNot(val inner: Int) : PredicateNode() + object PredTrue : PredicateNode() + object PredFalse : PredicateNode() +} + +/** `retry-predicate`: a flattened predicate tree. Root is `nodes[0]`; children by index. */ +data class RetryPredicate(val nodes: List) + +data class ExponentialConfig(val baseDelayNanos: Long, val factor: Double) +data class FibonacciConfig(val firstNanos: Long, val secondNanos: Long) +data class CountBoxConfig(val maxRetries: UInt, val inner: Int) +data class TimeBoxConfig(val limitNanos: Long, val inner: Int) +data class ClampConfig(val minDelayNanos: Long, val maxDelayNanos: Long, val inner: Int) +data class AddDelayConfig(val delayNanos: Long, val inner: Int) +data class JitterConfig(val factor: Double, val inner: Int) +data class FilteredConfig(val predicate: RetryPredicate, val inner: Int) + +/** `policy-node` variant. Durations are total nanoseconds (WIT `duration`). */ +sealed class PolicyNode { + data class Periodic(val nanos: Long) : PolicyNode() + data class Exponential(val config: ExponentialConfig) : PolicyNode() + data class Fibonacci(val config: FibonacciConfig) : PolicyNode() + object Immediate : PolicyNode() + object Never : PolicyNode() + data class CountBox(val config: CountBoxConfig) : PolicyNode() + data class TimeBox(val config: TimeBoxConfig) : PolicyNode() + data class ClampDelay(val config: ClampConfig) : PolicyNode() + data class AddDelay(val config: AddDelayConfig) : PolicyNode() + data class Jitter(val config: JitterConfig) : PolicyNode() + data class FilteredOn(val config: FilteredConfig) : PolicyNode() + data class AndThen(val left: Int, val right: Int) : PolicyNode() + data class PolicyUnion(val left: Int, val right: Int) : PolicyNode() + data class PolicyIntersect(val left: Int, val right: Int) : PolicyNode() +} + +/** `retry-policy`: a flattened policy tree. Root is `nodes[0]`; children by index. */ +data class RetryPolicy(val nodes: List) + +/** `named-retry-policy`: a named rule (predicate selects when it applies, policy is the strategy). */ +data class NamedRetryPolicy( + val name: String, + val priority: UInt, + val predicate: RetryPredicate, + val policy: RetryPolicy, +) + +// ── DECODE (verified layouts) ──────────────────────────────────────────────────────────────── + +private const val NODE_STRIDE = 32 // policy-node / predicate-node: size=32 align=8 +private const val NODE_PAYLOAD = 8 // both variants: tag@0, payload_offset=8 +private const val PREDICATE_VALUE_SIZE = 16 // predicate-value: size=16, payload_offset=8 +private const val NAMED_POLICY_STRIDE = 28 // named-retry-policy: size=28 align=4 + +private fun liftStringAt(base: Int): String = liftString(loadInt(base), loadInt(base + 4)) + +/** predicate-value: tag@base, payload@base+8. */ +private fun liftPredicateValue(base: Int): PredicateValue { + val pay = base + 8 + return when (loadByte(base).toInt() and 0xFF) { + 0 -> PredicateValue.Text(liftStringAt(pay)) + 1 -> PredicateValue.Integer(loadLong(pay)) + else -> PredicateValue.Bool(loadByte(pay).toInt() != 0) + } +} + +private fun liftPropertyComparison(base: Int): PropertyComparison = PropertyComparison(liftStringAt(base), liftPredicateValue(base + 8)) // name@0, value@8 + +private fun liftPropertySetCheck(base: Int): PropertySetCheck { + val name = liftStringAt(base) // property-name@0 + val valuesPtr = loadInt(base + 8) // values list@8 (ptr@8, len@12) + val len = loadInt(base + 12) + val values = (0 until len).map { liftPredicateValue(valuesPtr + it * PREDICATE_VALUE_SIZE) } + return PropertySetCheck(name, values) +} + +private fun liftPropertyPattern(base: Int): PropertyPattern = PropertyPattern(liftStringAt(base), liftStringAt(base + 8)) // property-name@0, pattern@8 + +private fun liftPredicateNode(nodePtr: Int): PredicateNode { + val pay = nodePtr + NODE_PAYLOAD + return when (loadByte(nodePtr).toInt() and 0xFF) { + 0 -> PredicateNode.PropEq(liftPropertyComparison(pay)) + 1 -> PredicateNode.PropNeq(liftPropertyComparison(pay)) + 2 -> PredicateNode.PropGt(liftPropertyComparison(pay)) + 3 -> PredicateNode.PropGte(liftPropertyComparison(pay)) + 4 -> PredicateNode.PropLt(liftPropertyComparison(pay)) + 5 -> PredicateNode.PropLte(liftPropertyComparison(pay)) + 6 -> PredicateNode.PropExists(liftStringAt(pay)) + 7 -> PredicateNode.PropIn(liftPropertySetCheck(pay)) + 8 -> PredicateNode.PropMatches(liftPropertyPattern(pay)) + 9 -> PredicateNode.PropStartsWith(liftPropertyPattern(pay)) + 10 -> PredicateNode.PropContains(liftPropertyPattern(pay)) + 11 -> PredicateNode.PredAnd(loadInt(pay), loadInt(pay + 4)) // tuple + 12 -> PredicateNode.PredOr(loadInt(pay), loadInt(pay + 4)) + 13 -> PredicateNode.PredNot(loadInt(pay)) + 14 -> PredicateNode.PredTrue + else -> PredicateNode.PredFalse + } +} + +/** retry-predicate = {nodes: list}. [listBase] points at the list's (ptr,len). */ +private fun liftRetryPredicate(listBase: Int): RetryPredicate { + val nodesPtr = loadInt(listBase) + val len = loadInt(listBase + 4) + return RetryPredicate((0 until len).map { liftPredicateNode(nodesPtr + it * NODE_STRIDE) }) +} + +private fun liftPolicyNode(nodePtr: Int): PolicyNode { + val pay = nodePtr + NODE_PAYLOAD + return when (loadByte(nodePtr).toInt() and 0xFF) { + 0 -> PolicyNode.Periodic(loadLong(pay)) // duration + 1 -> PolicyNode.Exponential(ExponentialConfig(loadLong(pay), loadDouble(pay + 8))) + 2 -> PolicyNode.Fibonacci(FibonacciConfig(loadLong(pay), loadLong(pay + 8))) + 3 -> PolicyNode.Immediate + 4 -> PolicyNode.Never + 5 -> PolicyNode.CountBox(CountBoxConfig(loadInt(pay).toUInt(), loadInt(pay + 4))) + 6 -> PolicyNode.TimeBox(TimeBoxConfig(loadLong(pay), loadInt(pay + 8))) + 7 -> PolicyNode.ClampDelay(ClampConfig(loadLong(pay), loadLong(pay + 8), loadInt(pay + 16))) + 8 -> PolicyNode.AddDelay(AddDelayConfig(loadLong(pay), loadInt(pay + 8))) + 9 -> PolicyNode.Jitter(JitterConfig(loadDouble(pay), loadInt(pay + 8))) + 10 -> PolicyNode.FilteredOn(FilteredConfig(liftRetryPredicate(pay), loadInt(pay + 8))) // predicate list@0, inner@8 + 11 -> PolicyNode.AndThen(loadInt(pay), loadInt(pay + 4)) + 12 -> PolicyNode.PolicyUnion(loadInt(pay), loadInt(pay + 4)) + else -> PolicyNode.PolicyIntersect(loadInt(pay), loadInt(pay + 4)) + } +} + +/** retry-policy = {nodes: list}. [listBase] points at the list's (ptr,len). */ +internal fun liftRetryPolicy(listBase: Int): RetryPolicy { + val nodesPtr = loadInt(listBase) + val len = loadInt(listBase + 4) + return RetryPolicy((0 until len).map { liftPolicyNode(nodesPtr + it * NODE_STRIDE) }) +} + +/** named-retry-policy: name@0, priority@8, predicate list@12, policy list@20. */ +internal fun liftNamedRetryPolicy(base: Int): NamedRetryPolicy = NamedRetryPolicy( + name = liftStringAt(base), + priority = loadInt(base + 8).toUInt(), + predicate = liftRetryPredicate(base + 12), + policy = liftRetryPolicy(base + 20), +) + +// ── ENCODE (mirror of the decode; same verified layouts) ───────────────────────────────────── +// Node arrays are alloc'd fresh; each variant writes only its tag + active-case payload -- unused +// payload/tail bytes are never read by the host (canonical ABI), so no zeroing is needed. + +/** Lowers a predicate-value (16B) at [base]: tag@0, payload@8. */ +private fun lowerPredicateValueInto(base: Int, v: PredicateValue) { + when (v) { + is PredicateValue.Text -> { + storeByte(base, 0) + writeStringField(base, 8, v.value) + } + is PredicateValue.Integer -> { + storeByte(base, 1) + storeLong(base + 8, v.value) + } + is PredicateValue.Bool -> { + storeByte(base, 2) + storeByte(base + 8, if (v.value) 1 else 0) + } + } +} + +private fun lowerPropertyComparisonInto(base: Int, c: PropertyComparison) { + writeStringField(base, 0, c.propertyName) // property-name@0 + lowerPredicateValueInto(base + 8, c.value) // value@8 +} + +private fun lowerPropertySetCheckInto(base: Int, c: PropertySetCheck) { + writeStringField(base, 0, c.propertyName) + val (ptr, len) = lowerPredicateValueList(c.values) + storeInt(base + 8, ptr) + storeInt(base + 12, len) // values list@8 +} + +private fun lowerPropertyPatternInto(base: Int, p: PropertyPattern) { + writeStringField(base, 0, p.propertyName) // property-name@0 + writeStringField(base, 8, p.pattern) // pattern@8 +} + +/** Lowers a list (element 16B, align 8); returns (dataPtr, len). */ +private fun lowerPredicateValueList(values: List): Pair { + if (values.isEmpty()) return 0 to 0 + val arr = alloc(values.size * PREDICATE_VALUE_SIZE, 8) + values.forEachIndexed { i, v -> lowerPredicateValueInto(arr + i * PREDICATE_VALUE_SIZE, v) } + return arr to values.size +} + +/** Lowers a predicate-node (32B) at [nodePtr]: tag@0, payload@8. */ +private fun lowerPredicateNodeInto(nodePtr: Int, n: PredicateNode) { + val pay = nodePtr + NODE_PAYLOAD + when (n) { + is PredicateNode.PropEq -> { + storeByte(nodePtr, 0) + lowerPropertyComparisonInto(pay, n.cmp) + } + is PredicateNode.PropNeq -> { + storeByte(nodePtr, 1) + lowerPropertyComparisonInto(pay, n.cmp) + } + is PredicateNode.PropGt -> { + storeByte(nodePtr, 2) + lowerPropertyComparisonInto(pay, n.cmp) + } + is PredicateNode.PropGte -> { + storeByte(nodePtr, 3) + lowerPropertyComparisonInto(pay, n.cmp) + } + is PredicateNode.PropLt -> { + storeByte(nodePtr, 4) + lowerPropertyComparisonInto(pay, n.cmp) + } + is PredicateNode.PropLte -> { + storeByte(nodePtr, 5) + lowerPropertyComparisonInto(pay, n.cmp) + } + is PredicateNode.PropExists -> { + storeByte(nodePtr, 6) + writeStringField(pay, 0, n.propertyName) + } + is PredicateNode.PropIn -> { + storeByte(nodePtr, 7) + lowerPropertySetCheckInto(pay, n.check) + } + is PredicateNode.PropMatches -> { + storeByte(nodePtr, 8) + lowerPropertyPatternInto(pay, n.pattern) + } + is PredicateNode.PropStartsWith -> { + storeByte(nodePtr, 9) + lowerPropertyPatternInto(pay, n.pattern) + } + is PredicateNode.PropContains -> { + storeByte(nodePtr, 10) + lowerPropertyPatternInto(pay, n.pattern) + } + is PredicateNode.PredAnd -> { + storeByte(nodePtr, 11) + storeInt(pay, n.left) + storeInt(pay + 4, n.right) + } + is PredicateNode.PredOr -> { + storeByte(nodePtr, 12) + storeInt(pay, n.left) + storeInt(pay + 4, n.right) + } + is PredicateNode.PredNot -> { + storeByte(nodePtr, 13) + storeInt(pay, n.inner) + } + PredicateNode.PredTrue -> storeByte(nodePtr, 14) + PredicateNode.PredFalse -> storeByte(nodePtr, 15) + } +} + +/** Lowers a retry-predicate's node list (element 32B, align 8); returns (dataPtr, len). */ +private fun lowerRetryPredicateNodes(pred: RetryPredicate): Pair { + if (pred.nodes.isEmpty()) return 0 to 0 + val arr = alloc(pred.nodes.size * NODE_STRIDE, 8) + pred.nodes.forEachIndexed { i, n -> lowerPredicateNodeInto(arr + i * NODE_STRIDE, n) } + return arr to pred.nodes.size +} + +/** Lowers a policy-node (32B) at [nodePtr]: tag@0, payload@8. */ +private fun lowerPolicyNodeInto(nodePtr: Int, n: PolicyNode) { + val pay = nodePtr + NODE_PAYLOAD + when (n) { + is PolicyNode.Periodic -> { + storeByte(nodePtr, 0) + storeLong(pay, n.nanos) + } + is PolicyNode.Exponential -> { + storeByte(nodePtr, 1) + storeLong(pay, n.config.baseDelayNanos) + storeDouble(pay + 8, n.config.factor) + } + is PolicyNode.Fibonacci -> { + storeByte(nodePtr, 2) + storeLong(pay, n.config.firstNanos) + storeLong(pay + 8, n.config.secondNanos) + } + PolicyNode.Immediate -> storeByte(nodePtr, 3) + PolicyNode.Never -> storeByte(nodePtr, 4) + is PolicyNode.CountBox -> { + storeByte(nodePtr, 5) + storeInt(pay, n.config.maxRetries.toInt()) + storeInt(pay + 4, n.config.inner) + } + is PolicyNode.TimeBox -> { + storeByte(nodePtr, 6) + storeLong(pay, n.config.limitNanos) + storeInt(pay + 8, n.config.inner) + } + is PolicyNode.ClampDelay -> { + storeByte(nodePtr, 7) + storeLong(pay, n.config.minDelayNanos) + storeLong(pay + 8, n.config.maxDelayNanos) + storeInt(pay + 16, n.config.inner) + } + is PolicyNode.AddDelay -> { + storeByte(nodePtr, 8) + storeLong(pay, n.config.delayNanos) + storeInt(pay + 8, n.config.inner) + } + is PolicyNode.Jitter -> { + storeByte(nodePtr, 9) + storeDouble(pay, n.config.factor) + storeInt(pay + 8, n.config.inner) + } + is PolicyNode.FilteredOn -> { + storeByte(nodePtr, 10) + val (ptr, len) = lowerRetryPredicateNodes(n.config.predicate) // filtered-config.predicate@0 + storeInt(pay, ptr) + storeInt(pay + 4, len) + storeInt(pay + 8, n.config.inner) // inner@8 + } + is PolicyNode.AndThen -> { + storeByte(nodePtr, 11) + storeInt(pay, n.left) + storeInt(pay + 4, n.right) + } + is PolicyNode.PolicyUnion -> { + storeByte(nodePtr, 12) + storeInt(pay, n.left) + storeInt(pay + 4, n.right) + } + is PolicyNode.PolicyIntersect -> { + storeByte(nodePtr, 13) + storeInt(pay, n.left) + storeInt(pay + 4, n.right) + } + } +} + +/** Lowers a retry-policy's node list (element 32B, align 8); returns (dataPtr, len). */ +private fun lowerRetryPolicyNodes(policy: RetryPolicy): Pair { + if (policy.nodes.isEmpty()) return 0 to 0 + val arr = alloc(policy.nodes.size * NODE_STRIDE, 8) + policy.nodes.forEachIndexed { i, n -> lowerPolicyNodeInto(arr + i * NODE_STRIDE, n) } + return arr to policy.nodes.size +} + +object RetryApi { + /** All retry policies active for this agent, in host-defined order. */ + fun getRetryPolicies(): List { + val ret = alloc(8, 4) // list: {ptr, len} + hostGetRetryPolicies(ret) + val dataPtr = loadInt(ret) + val len = loadInt(ret + 4) + return (0 until len).map { liftNamedRetryPolicy(dataPtr + it * NAMED_POLICY_STRIDE) } + } + + /** The named retry policy with [name], or null if none is registered. */ + fun getRetryPolicyByName(name: String): NamedRetryPolicy? { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val ret = alloc(32, 4) // option: tag@0, payload@4 (28B) + hostGetRetryPolicyByName(namePtr, nameLen, ret) + return if (loadByte(ret).toInt() == 0) null else liftNamedRetryPolicy(ret + 4) + } + + /** Removes a named retry policy (persisted to the oplog). No-op if it doesn't exist. */ + fun removeRetryPolicy(name: String) { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + hostRemoveRetryPolicy(namePtr, nameLen) + } + + /** Adds or overwrites a named retry policy (persisted to the oplog). */ + fun setRetryPolicy(policy: NamedRetryPolicy) { + val (namePtr, nameLen) = lowerStringToPtrLen(policy.name) + val (predPtr, predLen) = lowerRetryPredicateNodes(policy.predicate) + val (polPtr, polLen) = lowerRetryPolicyNodes(policy.policy) + hostSetRetryPolicy(namePtr, nameLen, policy.priority.toInt(), predPtr, predLen, polPtr, polLen) + } + + /** + * Resolves the matching retry policy for an operation context (verb + noun URI + dynamic + * properties), or null if no named rule's predicate matches. [properties] pairs a property + * name with a [PredicateValue]. + */ + fun resolveRetryPolicy( + verb: String, + nounUri: String, + properties: List>, + ): RetryPolicy? { + val (verbPtr, verbLen) = lowerStringToPtrLen(verb) + val (nounPtr, nounLen) = lowerStringToPtrLen(nounUri) + val (propsPtr, propsLen) = if (properties.isEmpty()) { + 0 to 0 + } else { + val arr = alloc(properties.size * 24, 8) // tuple: string@0, pv@8, 24B align8 + properties.forEachIndexed { i, (key, pv) -> + val e = arr + i * 24 + writeStringField(e, 0, key) + lowerPredicateValueInto(e + 8, pv) + } + arr to properties.size + } + val ret = alloc(12, 4) // option: tag@0, payload@4 (retry-policy {nodes list} 8B) + hostResolveRetryPolicy(verbPtr, verbLen, nounPtr, nounLen, propsPtr, propsLen, ret) + return if (loadByte(ret).toInt() == 0) null else liftRetryPolicy(ret + 4) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/RetryDsl.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/RetryDsl.kt new file mode 100644 index 0000000000..a2b0e8135e --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/RetryDsl.kt @@ -0,0 +1,397 @@ +package cloud.golem.runtime.host + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.nanoseconds + +/** + * An idiomatic Kotlin DSL for building `golem:api/retry` policies and predicates, layered on top + * of [RetryApi]'s flat node-list model. Ported from the Scala SDK's `Retry` object, but recast in + * Kotlin idiom: + * + * - **`kotlin.time.Duration`** everywhere a delay/limit is taken (`100.milliseconds`, `2.seconds`). + * - **infix / operator combinators**: `p1 and p2`, `p1 or p2`, `!p`; `policy andThen other`. + * - **value-class [Prop]** with infix comparisons: `Props.statusCode eq 503`, `Props.errorType eq "timeout"`. + * - **fail-fast `require`** validation (finite/positive factors, `min <= max`, uint32 ranges, + * non-negative durations) instead of Scala's `Either[ValidationError, _]` plumbing. + * - **round-trips**: [Policy.toRetryPolicy]/[Predicate.toRetryPredicate] flatten the recursive + * tree into the index-referenced node lists; [RetryPolicy.toPolicy]/[RetryPredicate.toPredicate] + * rebuild the tree (with cycle detection), so [RetryApi] results read back ergonomically. + * + * Example: + * ``` + * val np = NamedPolicy( + * name = "flaky-http", + * policy = Policy.exponential(100.milliseconds, factor = 2.0) + * .withJitter(0.2) + * .maxRetries(5) + * .onlyWhen(Props.statusCode eq 503 or (Props.errorType eq "timeout")), + * priority = 10, + * ) + * RetryApi.setRetryPolicy(np) // DSL overload -> flattens + calls the host + * val active = RetryApi.namedPolicies() // List, rebuilt from the host + * ``` + */ + +// ── Predicate tree ─────────────────────────────────────────────────────────────────────────── + +/** A composable retry predicate. Build leaves via [Props]/[Prop]; combine with `and`/`or`/`!`. */ +sealed class Predicate { + infix fun and(that: Predicate): Predicate = And(this, that) + infix fun or(that: Predicate): Predicate = Or(this, that) + + /** Enables `!predicate`. */ + operator fun not(): Predicate = Not(this) + + data class Eq(val property: String, val value: PredicateValue) : Predicate() + data class Neq(val property: String, val value: PredicateValue) : Predicate() + data class Gt(val property: String, val value: PredicateValue) : Predicate() + data class Gte(val property: String, val value: PredicateValue) : Predicate() + data class Lt(val property: String, val value: PredicateValue) : Predicate() + data class Lte(val property: String, val value: PredicateValue) : Predicate() + data class Exists(val property: String) : Predicate() + data class OneOf(val property: String, val values: List) : Predicate() + data class MatchesGlob(val property: String, val pattern: String) : Predicate() + data class StartsWith(val property: String, val prefix: String) : Predicate() + data class Contains(val property: String, val substring: String) : Predicate() + data class And(val left: Predicate, val right: Predicate) : Predicate() + data class Or(val left: Predicate, val right: Predicate) : Predicate() + data class Not(val inner: Predicate) : Predicate() + + /** Always matches (`pred-true`). */ + object Always : Predicate() + + /** Never matches (`pred-false`). */ + object Never : Predicate() + + companion object { + val always: Predicate get() = Always + val never: Predicate get() = Never + } +} + +/** A retry context property (e.g. `status-code`). Build predicate leaves with its infix operators. */ +class Prop(val name: String) { + infix fun eq(value: PredicateValue): Predicate = Predicate.Eq(name, value) + infix fun eq(value: String): Predicate = eq(PredicateValue.Text(value)) + infix fun eq(value: Long): Predicate = eq(PredicateValue.Integer(value)) + infix fun eq(value: Int): Predicate = eq(PredicateValue.Integer(value.toLong())) + infix fun eq(value: Boolean): Predicate = eq(PredicateValue.Bool(value)) + + infix fun neq(value: PredicateValue): Predicate = Predicate.Neq(name, value) + infix fun neq(value: String): Predicate = neq(PredicateValue.Text(value)) + infix fun neq(value: Long): Predicate = neq(PredicateValue.Integer(value)) + infix fun neq(value: Int): Predicate = neq(PredicateValue.Integer(value.toLong())) + infix fun neq(value: Boolean): Predicate = neq(PredicateValue.Bool(value)) + + infix fun gt(value: Long): Predicate = Predicate.Gt(name, PredicateValue.Integer(value)) + infix fun gt(value: Int): Predicate = gt(value.toLong()) + infix fun gte(value: Long): Predicate = Predicate.Gte(name, PredicateValue.Integer(value)) + infix fun gte(value: Int): Predicate = gte(value.toLong()) + infix fun lt(value: Long): Predicate = Predicate.Lt(name, PredicateValue.Integer(value)) + infix fun lt(value: Int): Predicate = lt(value.toLong()) + infix fun lte(value: Long): Predicate = Predicate.Lte(name, PredicateValue.Integer(value)) + infix fun lte(value: Int): Predicate = lte(value.toLong()) + + infix fun matchesGlob(pattern: String): Predicate = Predicate.MatchesGlob(name, pattern) + infix fun startsWith(prefix: String): Predicate = Predicate.StartsWith(name, prefix) + infix fun contains(substring: String): Predicate = Predicate.Contains(name, substring) + + fun oneOf(vararg values: String): Predicate = Predicate.OneOf(name, values.map { PredicateValue.Text(it) }) + fun oneOf(vararg values: Long): Predicate = Predicate.OneOf(name, values.map { PredicateValue.Integer(it) }) + fun oneOf(vararg values: Int): Predicate = Predicate.OneOf(name, values.map { PredicateValue.Integer(it.toLong()) }) + + /** `prop exists` -> the property is present in the context. */ + val exists: Predicate get() = Predicate.Exists(name) +} + +/** The retry context properties the host exposes, plus [custom] for anything else. */ +object Props { + val verb = Prop("verb") + val nounUri = Prop("noun-uri") + val uriScheme = Prop("uri-scheme") + val uriHost = Prop("uri-host") + val uriPort = Prop("uri-port") + val uriPath = Prop("uri-path") + val statusCode = Prop("status-code") + val errorType = Prop("error-type") + val function = Prop("function") + val targetComponentId = Prop("target-component-id") + val targetAgentType = Prop("target-agent-type") + val dbType = Prop("db-type") + val trapType = Prop("trap-type") + + fun custom(name: String): Prop = Prop(name) + operator fun invoke(name: String): Prop = Prop(name) +} + +// ── Policy tree ────────────────────────────────────────────────────────────────────────────── + +/** + * A composable retry policy. Start from a base ([Policy.exponential], [Policy.periodic], …) and + * layer modifiers fluently ([maxRetries], [within], [clamp], [addDelay], [withJitter], + * [onlyWhen]) or combine whole policies ([andThen], [union], [intersect]). + */ +sealed class Policy { + /** Cap the total number of retries (`count-box`). [maxRetries] must fit an unsigned 32-bit int. */ + fun maxRetries(maxRetries: Long): Policy = CountBox(requireUint32(maxRetries, "maxRetries"), this) + + /** Give up once [limit] has elapsed (`time-box`). */ + fun within(limit: Duration): Policy = TimeBox(requireNonNegative(limit, "within.limit"), this) + + /** Clamp each computed delay to `[minDelay, maxDelay]`. */ + fun clamp(minDelay: Duration, maxDelay: Duration): Policy { + requireNonNegative(minDelay, "clamp.minDelay") + requireNonNegative(maxDelay, "clamp.maxDelay") + require(minDelay <= maxDelay) { "clamp requires minDelay <= maxDelay, got $minDelay > $maxDelay" } + return Clamp(minDelay, maxDelay, this) + } + + /** Add a fixed [delay] on top of each computed delay. */ + fun addDelay(delay: Duration): Policy = AddDelay(requireNonNegative(delay, "addDelay.delay"), this) + + /** Randomize each delay by up to [factor] (0.0..; e.g. 0.2 = ±20%). */ + fun withJitter(factor: Double): Policy = Jitter(requireFactor(factor, "withJitter.factor", allowZero = true), this) + + /** Only apply this policy when [predicate] matches the retry context (`filtered-on`). */ + fun onlyWhen(predicate: Predicate): Policy = FilteredOn(predicate, this) + + /** Fall back to [that] once this policy is exhausted (`and-then`). */ + infix fun andThen(that: Policy): Policy = AndThen(this, that) + + /** Retry if *either* policy would (`policy-union`). */ + infix fun union(that: Policy): Policy = Union(this, that) + + /** Retry only if *both* policies would (`policy-intersect`). */ + infix fun intersect(that: Policy): Policy = Intersect(this, that) + + data class Periodic(val delay: Duration) : Policy() + data class Exponential(val baseDelay: Duration, val factor: Double) : Policy() + data class Fibonacci(val first: Duration, val second: Duration) : Policy() + object Immediate : Policy() + object Never : Policy() + data class CountBox(val maxRetries: UInt, val inner: Policy) : Policy() + data class TimeBox(val limit: Duration, val inner: Policy) : Policy() + data class Clamp(val minDelay: Duration, val maxDelay: Duration, val inner: Policy) : Policy() + data class AddDelay(val delay: Duration, val inner: Policy) : Policy() + data class Jitter(val factor: Double, val inner: Policy) : Policy() + data class FilteredOn(val predicate: Predicate, val inner: Policy) : Policy() + data class AndThen(val left: Policy, val right: Policy) : Policy() + data class Union(val left: Policy, val right: Policy) : Policy() + data class Intersect(val left: Policy, val right: Policy) : Policy() + + companion object { + /** Retry immediately with no delay. */ + val immediate: Policy get() = Immediate + + /** Never retry. */ + val never: Policy get() = Never + + /** A fixed [delay] between attempts. */ + fun periodic(delay: Duration): Policy = Periodic(requireNonNegative(delay, "periodic.delay")) + + /** Exponential backoff: `baseDelay * factor^attempt`. [factor] must be finite and > 0. */ + fun exponential(baseDelay: Duration, factor: Double): Policy = Exponential(requireNonNegative(baseDelay, "exponential.baseDelay"), requireFactor(factor, "exponential.factor", allowZero = false)) + + /** Fibonacci backoff seeded by [first] and [second]. */ + fun fibonacci(first: Duration, second: Duration): Policy = Fibonacci(requireNonNegative(first, "fibonacci.first"), requireNonNegative(second, "fibonacci.second")) + } +} + +/** + * A named retry rule: [predicate] selects when it applies, [policy] is the strategy, [priority] + * controls evaluation order (higher = checked first). Defaults mirror the Scala SDK + * (`priority = 0`, `predicate = always`). + */ +data class NamedPolicy( + val name: String, + val policy: Policy, + val priority: Long = 0, + val predicate: Predicate = Predicate.always, +) { + fun withPriority(value: Long): NamedPolicy = copy(priority = value) + fun appliesWhen(value: Predicate): NamedPolicy = copy(predicate = value) +} + +// ── Flatten (DSL tree -> RetryApi node lists) ───────────────────────────────────────────────── + +/** Flattens this predicate tree into a [RetryPredicate] (root = `nodes[0]`, children by index). */ +fun Predicate.toRetryPredicate(): RetryPredicate { + val nodes = ArrayList() + fun append(p: Predicate): Int { + val index = nodes.size + nodes.add(PredicateNode.PredFalse) // reserve; overwritten below after children are appended + nodes[index] = when (p) { + is Predicate.Eq -> PredicateNode.PropEq(PropertyComparison(p.property, p.value)) + is Predicate.Neq -> PredicateNode.PropNeq(PropertyComparison(p.property, p.value)) + is Predicate.Gt -> PredicateNode.PropGt(PropertyComparison(p.property, p.value)) + is Predicate.Gte -> PredicateNode.PropGte(PropertyComparison(p.property, p.value)) + is Predicate.Lt -> PredicateNode.PropLt(PropertyComparison(p.property, p.value)) + is Predicate.Lte -> PredicateNode.PropLte(PropertyComparison(p.property, p.value)) + is Predicate.Exists -> PredicateNode.PropExists(p.property) + is Predicate.OneOf -> PredicateNode.PropIn(PropertySetCheck(p.property, p.values)) + is Predicate.MatchesGlob -> PredicateNode.PropMatches(PropertyPattern(p.property, p.pattern)) + is Predicate.StartsWith -> PredicateNode.PropStartsWith(PropertyPattern(p.property, p.prefix)) + is Predicate.Contains -> PredicateNode.PropContains(PropertyPattern(p.property, p.substring)) + is Predicate.And -> PredicateNode.PredAnd(append(p.left), append(p.right)) + is Predicate.Or -> PredicateNode.PredOr(append(p.left), append(p.right)) + is Predicate.Not -> PredicateNode.PredNot(append(p.inner)) + Predicate.Always -> PredicateNode.PredTrue + Predicate.Never -> PredicateNode.PredFalse + } + return index + } + append(this) + return RetryPredicate(nodes) +} + +/** Flattens this policy tree into a [RetryPolicy] (root = `nodes[0]`, children by index). */ +fun Policy.toRetryPolicy(): RetryPolicy { + val nodes = ArrayList() + fun append(p: Policy): Int { + val index = nodes.size + nodes.add(PolicyNode.Immediate) // reserve; overwritten below after children are appended + nodes[index] = when (p) { + is Policy.Periodic -> PolicyNode.Periodic(p.delay.inWholeNanoseconds) + is Policy.Exponential -> PolicyNode.Exponential(ExponentialConfig(p.baseDelay.inWholeNanoseconds, p.factor)) + is Policy.Fibonacci -> PolicyNode.Fibonacci(FibonacciConfig(p.first.inWholeNanoseconds, p.second.inWholeNanoseconds)) + Policy.Immediate -> PolicyNode.Immediate + Policy.Never -> PolicyNode.Never + is Policy.CountBox -> PolicyNode.CountBox(CountBoxConfig(p.maxRetries, append(p.inner))) + is Policy.TimeBox -> PolicyNode.TimeBox(TimeBoxConfig(p.limit.inWholeNanoseconds, append(p.inner))) + is Policy.Clamp -> PolicyNode.ClampDelay(ClampConfig(p.minDelay.inWholeNanoseconds, p.maxDelay.inWholeNanoseconds, append(p.inner))) + is Policy.AddDelay -> PolicyNode.AddDelay(AddDelayConfig(p.delay.inWholeNanoseconds, append(p.inner))) + is Policy.Jitter -> PolicyNode.Jitter(JitterConfig(p.factor, append(p.inner))) + is Policy.FilteredOn -> PolicyNode.FilteredOn(FilteredConfig(p.predicate.toRetryPredicate(), append(p.inner))) + is Policy.AndThen -> PolicyNode.AndThen(append(p.left), append(p.right)) + is Policy.Union -> PolicyNode.PolicyUnion(append(p.left), append(p.right)) + is Policy.Intersect -> PolicyNode.PolicyIntersect(append(p.left), append(p.right)) + } + return index + } + append(this) + return RetryPolicy(nodes) +} + +/** Flattens this named policy into the [NamedRetryPolicy] [RetryApi] expects. */ +fun NamedPolicy.toNamedRetryPolicy(): NamedRetryPolicy = NamedRetryPolicy( + name = name, + priority = requireUint32(priority, "priority"), + predicate = predicate.toRetryPredicate(), + policy = policy.toRetryPolicy(), +) + +// ── Unflatten (RetryApi node lists -> DSL tree) ─────────────────────────────────────────────── + +/** Rebuilds the predicate tree from a flat [RetryPredicate], detecting cycles/out-of-range refs. */ +fun RetryPredicate.toPredicate(): Predicate { + require(nodes.isNotEmpty()) { "retry predicate must contain at least one node" } + val cache = HashMap() + val inProgress = HashSet() + fun build(i: Int): Predicate { + require(i in nodes.indices) { "predicate node index $i out of range (${nodes.size} nodes)" } + cache[i]?.let { return it } + require(inProgress.add(i)) { "cycle detected at predicate node $i" } + val p: Predicate = when (val n = nodes[i]) { + is PredicateNode.PropEq -> Predicate.Eq(n.cmp.propertyName, n.cmp.value) + is PredicateNode.PropNeq -> Predicate.Neq(n.cmp.propertyName, n.cmp.value) + is PredicateNode.PropGt -> Predicate.Gt(n.cmp.propertyName, n.cmp.value) + is PredicateNode.PropGte -> Predicate.Gte(n.cmp.propertyName, n.cmp.value) + is PredicateNode.PropLt -> Predicate.Lt(n.cmp.propertyName, n.cmp.value) + is PredicateNode.PropLte -> Predicate.Lte(n.cmp.propertyName, n.cmp.value) + is PredicateNode.PropExists -> Predicate.Exists(n.propertyName) + is PredicateNode.PropIn -> Predicate.OneOf(n.check.propertyName, n.check.values) + is PredicateNode.PropMatches -> Predicate.MatchesGlob(n.pattern.propertyName, n.pattern.pattern) + is PredicateNode.PropStartsWith -> Predicate.StartsWith(n.pattern.propertyName, n.pattern.pattern) + is PredicateNode.PropContains -> Predicate.Contains(n.pattern.propertyName, n.pattern.pattern) + is PredicateNode.PredAnd -> Predicate.And(build(n.left), build(n.right)) + is PredicateNode.PredOr -> Predicate.Or(build(n.left), build(n.right)) + is PredicateNode.PredNot -> Predicate.Not(build(n.inner)) + PredicateNode.PredTrue -> Predicate.Always + PredicateNode.PredFalse -> Predicate.Never + } + inProgress.remove(i) + cache[i] = p + return p + } + return build(0) +} + +/** Rebuilds the policy tree from a flat [RetryPolicy], detecting cycles/out-of-range refs. */ +fun RetryPolicy.toPolicy(): Policy { + require(nodes.isNotEmpty()) { "retry policy must contain at least one node" } + val cache = HashMap() + val inProgress = HashSet() + fun build(i: Int): Policy { + require(i in nodes.indices) { "policy node index $i out of range (${nodes.size} nodes)" } + cache[i]?.let { return it } + require(inProgress.add(i)) { "cycle detected at policy node $i" } + val p: Policy = when (val n = nodes[i]) { + is PolicyNode.Periodic -> Policy.Periodic(n.nanos.nanoseconds) + is PolicyNode.Exponential -> Policy.Exponential(n.config.baseDelayNanos.nanoseconds, n.config.factor) + is PolicyNode.Fibonacci -> Policy.Fibonacci(n.config.firstNanos.nanoseconds, n.config.secondNanos.nanoseconds) + PolicyNode.Immediate -> Policy.Immediate + PolicyNode.Never -> Policy.Never + is PolicyNode.CountBox -> Policy.CountBox(n.config.maxRetries, build(n.config.inner)) + is PolicyNode.TimeBox -> Policy.TimeBox(n.config.limitNanos.nanoseconds, build(n.config.inner)) + is PolicyNode.ClampDelay -> Policy.Clamp(n.config.minDelayNanos.nanoseconds, n.config.maxDelayNanos.nanoseconds, build(n.config.inner)) + is PolicyNode.AddDelay -> Policy.AddDelay(n.config.delayNanos.nanoseconds, build(n.config.inner)) + is PolicyNode.Jitter -> Policy.Jitter(n.config.factor, build(n.config.inner)) + is PolicyNode.FilteredOn -> Policy.FilteredOn(n.config.predicate.toPredicate(), build(n.config.inner)) + is PolicyNode.AndThen -> Policy.AndThen(build(n.left), build(n.right)) + is PolicyNode.PolicyUnion -> Policy.Union(build(n.left), build(n.right)) + is PolicyNode.PolicyIntersect -> Policy.Intersect(build(n.left), build(n.right)) + } + inProgress.remove(i) + cache[i] = p + return p + } + return build(0) +} + +/** Rebuilds a [NamedPolicy] from a flat [NamedRetryPolicy]. */ +fun NamedRetryPolicy.toNamedPolicy(): NamedPolicy = NamedPolicy( + name = name, + policy = policy.toPolicy(), + priority = priority.toLong(), + predicate = predicate.toPredicate(), +) + +// ── RetryApi ergonomic overloads (DSL-typed) ────────────────────────────────────────────────── + +/** Adds or overwrites a named retry policy, flattening the DSL [NamedPolicy] for the host. */ +fun RetryApi.setRetryPolicy(policy: NamedPolicy): Unit = setRetryPolicy(policy.toNamedRetryPolicy()) + +/** All active retry policies as DSL [NamedPolicy] trees. */ +fun RetryApi.namedPolicies(): List = getRetryPolicies().map { it.toNamedPolicy() } + +/** The named retry policy [name] as a DSL [NamedPolicy] tree, or null. */ +fun RetryApi.namedPolicy(name: String): NamedPolicy? = getRetryPolicyByName(name)?.toNamedPolicy() + +/** Resolves the matching policy for a context as a DSL [Policy] tree, or null. */ +fun RetryApi.resolvePolicy( + verb: String, + nounUri: String, + properties: List> = emptyList(), +): Policy? = resolveRetryPolicy(verb, nounUri, properties)?.toPolicy() + +// ── Validation helpers (fail-fast, Kotlin-idiomatic) ───────────────────────────────────────── + +private fun requireNonNegative(duration: Duration, label: String): Duration { + require(duration >= Duration.ZERO) { "$label must be a non-negative duration, got $duration" } + return duration +} + +private fun requireFactor(value: Double, label: String, allowZero: Boolean): Double { + require(value.isFinite()) { "$label must be finite, got $value" } + if (allowZero) { + require(value >= 0.0) { "$label must be >= 0, got $value" } + } else { + require(value > 0.0) { "$label must be > 0, got $value" } + } + return value +} + +private fun requireUint32(value: Long, label: String): UInt { + require(value in 0..0xFFFF_FFFFL) { "$label must fit an unsigned 32-bit integer (0..4294967295), got $value" } + return value.toUInt() +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/SecretApi.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/SecretApi.kt new file mode 100644 index 0000000000..34dae69a18 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/host/SecretApi.kt @@ -0,0 +1,113 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.host + +import cloud.golem.runtime.SchemaValue +import cloud.golem.runtime.collectTypeNodes +import cloud.golem.runtime.lowerSchemaGraphInto +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftSingleValue +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.storeInt + +// golem:secrets/reveal@0.1.0 -- the capability-gated escape hatch that unpacks a `secret` resource +// to its inner typed value. reveal(s: borrow, expected: schema-graph) -> +// result. The schema-graph is flattened into params (verified via +// abi-dump `sig`): [secret, type-nodes.ptr, type-nodes.len, defs.ptr, defs.len, root, retptr]. +@kotlin.wasm.WasmImport("golem:secrets/reveal@0.1.0", "reveal") +private external fun hostSecretReveal( + secret: Int, + typeNodesPtr: Int, + typeNodesLen: Int, + defsPtr: Int, + defsLen: Int, + root: Int, + retPtr: Int, +) + +// schema-graph: 20B { type-nodes list @0 (ptr@0,len@4), defs list @8 (ptr@8,len@12), root @16 }. +private const val SG_SIZE = 20 +private const val SG_ROOT = 16 + +/** The error arm of `reveal`'s `result` (golem:secrets@0.1.0). */ +sealed class SecretError { + /** The secret was bound but its current resolution failed (store entry deleted / partitioned). */ + data class Unavailable(val message: String) : SecretError() + + /** The pinned version no longer exists; carries `secret-version.bytes`. */ + data class VersionNotFound(val versionBytes: List) : SecretError() + + /** Internal runtime error (opaque message, never plaintext). */ + data class Internal(val message: String) : SecretError() +} + +/** Thrown by [SecretApi.reveal] when the host returns a [SecretError]. */ +class SecretRevealException(val error: SecretError) : RuntimeException("secret reveal failed: $error") + +// Decodes a `secret-error` at [base] (tag@0, payload@4). Cases (verified against types.wit): +// 0 unavailable(string), 1 version-not-found(secret-version{bytes: list}), 2 internal(string). +// string and list share the (ptr@base+4, len@base+8) shape. +internal fun liftSecretError(base: Int): SecretError { + val ptr = loadInt(base + 4) + val len = loadInt(base + 8) + return when (val tag = loadByte(base).toInt() and 0xFF) { + 0 -> SecretError.Unavailable(liftString(ptr, len)) + 1 -> SecretError.VersionNotFound((0 until len).map { loadByte(ptr + it).toUByte() }) + 2 -> SecretError.Internal(liftString(ptr, len)) + else -> SecretError.Internal("unknown secret-error tag=$tag") + } +} + +/** + * Native access to `golem:secrets/reveal@0.1.0`. The single capability the interface grants: + * [reveal] unpacks a `secret` resource handle back to its inner plaintext value, as a + * [SchemaValue] of the caller-declared type. The import itself IS the capability -- a component + * that does not import this interface cannot reveal secrets -- and every successful reveal is + * recorded in the calling agent's oplog. Prefer host-mediated substitution (host capabilities + * taking `borrow` directly) where available; reveal is the loud-by-design fallback for + * genuinely custom protocols the host doesn't natively support. + * + * Mirrors the Scala SDK's `golem.host.SecretApi.reveal`. + */ +object SecretApi { + + /** Reveals [secret] as its inner value, expected to have type [witType]. See [reveal]. */ + fun reveal(secret: SchemaValue.SecretVal, witType: String): SchemaValue = reveal(secret.handle, witType) + + /** + * Reveals the `secret` resource [secretHandle] as its inner value, expected to have type + * [witType] -- the same rich witType-string grammar the agent surface uses (a primitive such + * as `"string"`, or a composite such as `"record"`). The host + * validates [witType] against the secret's pinned inner type and returns the stored value. + * + * `secret` is *borrowed*: the caller keeps ownership of the handle (drop it via `dropSecret` + * when done). Throws [SecretRevealException] if the host returns a [SecretError]. + */ + fun reveal(secretHandle: Int, witType: String): SchemaValue { + // Build `expected`: a schema-graph whose semantic root is `witType`. collectTypeNodes + // registers the root type last, so its index is typeIndex[witType]; lowerSchemaGraphInto + // leaves root=0 (a placeholder valid only for the agent-type), so set it explicitly. + val typeIndex = collectTypeNodes(listOf(witType)) + val graph = alloc(SG_SIZE, 4) + lowerSchemaGraphInto(graph, 0, typeIndex) + storeInt(graph + SG_ROOT, typeIndex.getValue(witType)) + + val ret = alloc(16, 4) // result: tag@0, payload@4 + hostSecretReveal( + secretHandle, + loadInt(graph), + loadInt(graph + 4), // type-nodes ptr/len + loadInt(graph + 8), + loadInt(graph + 12), // defs ptr/len + loadInt(graph + SG_ROOT), // root + ret, + ) + if (loadByte(ret).toInt() and 0xFF == 0) { + // ok: schema-value-tree inline @ ret+4 (value-nodes.ptr@4, len@8, root@12). + return liftSingleValue(ret + 4, witType) + } + throw SecretRevealException(liftSecretError(ret + 4)) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Blobstore.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Blobstore.kt new file mode 100644 index 0000000000..3f29162631 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Blobstore.kt @@ -0,0 +1,405 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.wasi + +import cloud.golem.runtime.Either +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.loadLong +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt + +// Raw canonical-ABI bindings to wasi:blobstore's types/container/blobstore interfaces (the +// subset Scala's Blobstore.scala wraps). Package is UNVERSIONED (`package wasi:blobstore;`, +// confirmed in wit-native/deps/blobstore/*.wit) -- same situation as wasi:logging (see +// runtime/wasi/Logging.kt); every raw module string below has no @version suffix. Signatures +// verified via abi-dump's `sig`/`resulttype` modes. +// +// Unlike wasi:keyvalue's resource-typed error, wasi:blobstore's `error` is a plain +// `type error = string;` -- one less resource to manage, Either throughout. +// +// `Container.writeData` is the deepest resource chain built so far: blobstore's own +// `outgoing-value` resource -> `outgoing-value-write-body()` returns a wasi:io `output-stream` +// resource (a DIFFERENT package) -> `blocking-write-and-flush` can fail with a `stream-error` +// variant whose one payload case wraps YET ANOTHER resource, wasi:io/error's `error` +// (`to-debug-string(): string`). wasi:io/streams@0.2.3 and wasi:io/error@0.2.3 are already +// part of this world's import surface (present in every build since before this SDK added any +// custom host API -- likely pulled in by the Kotlin/Wasm runtime's own stdout/stderr support), +// so no wit-native/main.wit edit was needed for those two; only the wasi:blobstore interfaces +// themselves needed adding. + +@kotlin.wasm.WasmImport("wasi:blobstore/types", "[static]outgoing-value.new-outgoing-value") +private external fun hostNewOutgoingValue(): Int + +@kotlin.wasm.WasmImport("wasi:blobstore/types", "[method]outgoing-value.outgoing-value-write-body") +private external fun hostOutgoingValueWriteBody(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/types", "[resource-drop]outgoing-value") +private external fun hostOutgoingValueDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/types", "[method]incoming-value.incoming-value-consume-sync") +private external fun hostIncomingValueConsumeSync(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/types", "[resource-drop]incoming-value") +private external fun hostIncomingValueDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:io/streams@0.2.3", "[method]output-stream.blocking-write-and-flush") +private external fun hostOutputStreamBlockingWriteAndFlush(handle: Int, dataPtr: Int, dataLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:io/streams@0.2.3", "[resource-drop]output-stream") +private external fun hostOutputStreamDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:io/error@0.2.3", "[method]error.to-debug-string") +private external fun hostIoErrorToDebugString(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:io/error@0.2.3", "[resource-drop]error") +private external fun hostIoErrorDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.name") +private external fun hostContainerName(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.info") +private external fun hostContainerInfo(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.get-data") +private external fun hostContainerGetData(handle: Int, namePtr: Int, nameLen: Int, start: Long, end: Long, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.write-data") +private external fun hostContainerWriteData(handle: Int, namePtr: Int, nameLen: Int, outgoingValueHandle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.list-objects") +private external fun hostContainerListObjects(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.delete-object") +private external fun hostContainerDeleteObject(handle: Int, namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.delete-objects") +private external fun hostContainerDeleteObjects(handle: Int, namesPtr: Int, namesLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.has-object") +private external fun hostContainerHasObject(handle: Int, namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.object-info") +private external fun hostContainerObjectInfo(handle: Int, namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]container.clear") +private external fun hostContainerClear(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[resource-drop]container") +private external fun hostContainerDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[method]stream-object-names.read-stream-object-names") +private external fun hostStreamObjectNamesRead(handle: Int, len: Long, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/container", "[resource-drop]stream-object-names") +private external fun hostStreamObjectNamesDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/blobstore", "create-container") +private external fun hostCreateContainer(namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/blobstore", "get-container") +private external fun hostGetContainer(namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/blobstore", "delete-container") +private external fun hostDeleteContainer(namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/blobstore", "container-exists") +private external fun hostContainerExists(namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:blobstore/blobstore", "copy-object") +private external fun hostCopyObject( + srcContainerPtr: Int, + srcContainerLen: Int, + srcObjectPtr: Int, + srcObjectLen: Int, + destContainerPtr: Int, + destContainerLen: Int, + destObjectPtr: Int, + destObjectLen: Int, + retPtr: Int, +) + +@kotlin.wasm.WasmImport("wasi:blobstore/blobstore", "move-object") +private external fun hostMoveObject( + srcContainerPtr: Int, + srcContainerLen: Int, + srcObjectPtr: Int, + srcObjectLen: Int, + destContainerPtr: Int, + destContainerLen: Int, + destObjectPtr: Int, + destObjectLen: Int, + retPtr: Int, +) + +data class ContainerMetadata(val name: String, val createdAt: Long) +data class ObjectMetadata(val name: String, val container: String, val createdAt: Long, val size: Long) +data class ObjectId(val container: String, val name: String) + +private fun lowerStringToPtrLen(s: String): Pair { + val bytes = s.encodeToByteArray() + val ptr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + return ptr to bytes.size +} + +private fun lowerListOfStringToPtrLen(items: List): Pair { + val arr = alloc(items.size * 8, 4) + items.forEachIndexed { i, s -> + val (ptr, len) = lowerStringToPtrLen(s) + storeInt(arr + i * 8, ptr) + storeInt(arr + i * 8 + 4, len) + } + return arr to items.size +} + +private fun liftListOfString(base: Int): List { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> + val elemPtr = dataPtr + i * 8 + liftString(loadInt(elemPtr), loadInt(elemPtr + 4)) + } +} + +/** `result` where `error` is `wasi:blobstore`'s plain `type error = string;`. [payloadOffset] varies per function (4 or 8, depending on whether the ok payload needs 8-byte alignment). */ +private fun liftBlobResult(base: Int, payloadOffset: Int, liftOk: (Int) -> T): Either { + val payload = base + payloadOffset + return if (loadByte(base).toInt() == 0) { + Either.Right(liftOk(payload)) + } else { + Either.Left(liftString(loadInt(payload), loadInt(payload + 4))) + } +} + +// Resolves a wasi:io stream-error into a message: last-operation-failed wraps a wasi:io/error +// resource (resolved via to-debug-string, then dropped); closed has no payload. +private fun liftStreamErrorMessage(base: Int): String { + val tag = loadByte(base).toInt() and 0xFF + return if (tag == 0) { + val errHandle = loadInt(base + 4) + val retPtr = alloc(8, 4) + hostIoErrorToDebugString(errHandle, retPtr) + val msg = liftString(loadInt(retPtr), loadInt(retPtr + 4)) + hostIoErrorDrop(errHandle) + msg + } else { + "stream closed" + } +} + +// Writes `data` to a fresh outgoing-value's output-stream and returns its handle for the +// caller to pass (borrowed) to container.write-data, or an error message. The outgoing-value +// itself is NOT dropped here -- write-data still needs to borrow it; the caller drops it after. +private fun writeOutgoingValue(data: ByteArray): Either { + val ovHandle = hostNewOutgoingValue() + val streamRetPtr = alloc(8, 4) // result: tag@0(1,1), payload@4(i32 handle or nothing) + hostOutgoingValueWriteBody(ovHandle, streamRetPtr) + if (loadByte(streamRetPtr).toInt() != 0) { + hostOutgoingValueDrop(ovHandle) + return Either.Left("failed to open output-stream for outgoing-value") + } + val streamHandle = loadInt(streamRetPtr + 4) + val dataPtr = alloc(data.size, 1) + data.forEachIndexed { i, b -> storeByte(dataPtr + i, b) } + val writeRetPtr = alloc(12, 4) // result<_, stream-error>: tag@0(1,1), payload@4(8,4) + hostOutputStreamBlockingWriteAndFlush(streamHandle, dataPtr, data.size, writeRetPtr) + hostOutputStreamDrop(streamHandle) + return if (loadByte(writeRetPtr).toInt() == 0) { + Either.Right(ovHandle) + } else { + val msg = liftStreamErrorMessage(writeRetPtr + 4) + hostOutgoingValueDrop(ovHandle) + Either.Left(msg) + } +} + +/** A collection of objects (`wasi:blobstore/container`'s `container` resource). MUST be [close]d when done. */ +class Container internal constructor(private val handle: Int) { + private var closed = false + + fun name(): Either { + check(!closed) { "Container already closed" } + val retPtr = alloc(12, 4) + hostContainerName(handle, retPtr) + return liftBlobResult(retPtr, 4) { liftString(loadInt(it), loadInt(it + 4)) } + } + + fun info(): Either { + check(!closed) { "Container already closed" } + val retPtr = alloc(24, 8) // container-metadata: name@0(8,4), created-at@8(8,8) -> size16,align8 + hostContainerInfo(handle, retPtr) + return liftBlobResult(retPtr, 8) { b -> ContainerMetadata(liftString(loadInt(b), loadInt(b + 4)), loadLong(b + 8)) } + } + + fun getData(objectName: String, start: Long, end: Long): Either { + check(!closed) { "Container already closed" } + val (namePtr, nameLen) = lowerStringToPtrLen(objectName) + val retPtr = alloc(12, 4) // result + hostContainerGetData(handle, namePtr, nameLen, start, end, retPtr) + return liftBlobResult(retPtr, 4) { consumeIncomingValue(loadInt(it)) } + } + + fun writeData(objectName: String, data: ByteArray): Either { + check(!closed) { "Container already closed" } + val (namePtr, nameLen) = lowerStringToPtrLen(objectName) + val ov = writeOutgoingValue(data) + if (ov is Either.Left) return ov + val ovHandle = (ov as Either.Right).value + val retPtr = alloc(12, 4) // result<_, error> + hostContainerWriteData(handle, namePtr, nameLen, ovHandle, retPtr) + hostOutgoingValueDrop(ovHandle) + return liftBlobResult(retPtr, 4) { } + } + + /** + * Names of objects in the container. Mirrors the Scala reference's own limitation: reads a + * single batch of up to 1000 names and does not paginate further (Scala's `listObjects` + * does the same -- calls `readStreamObjectNames(1000)` once and returns just that batch, + * discarding the "end of stream" flag). Not a full listing for containers with >1000 objects. + */ + fun listObjects(): Either> { + check(!closed) { "Container already closed" } + val retPtr = alloc(12, 4) // result + hostContainerListObjects(handle, retPtr) + val sonResult = liftBlobResult(retPtr, 4) { loadInt(it) } + if (sonResult is Either.Left) return sonResult + val sonHandle = (sonResult as Either.Right).value + val readRetPtr = alloc(16, 4) // result, bool>, error> + hostStreamObjectNamesRead(sonHandle, 1000L, readRetPtr) + hostStreamObjectNamesDrop(sonHandle) + return liftBlobResult(readRetPtr, 4) { liftListOfString(it) } + } + + fun deleteObject(name: String): Either { + check(!closed) { "Container already closed" } + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val retPtr = alloc(12, 4) + hostContainerDeleteObject(handle, namePtr, nameLen, retPtr) + return liftBlobResult(retPtr, 4) { } + } + + fun deleteObjects(names: List): Either { + check(!closed) { "Container already closed" } + val (namesPtr, namesLen) = lowerListOfStringToPtrLen(names) + val retPtr = alloc(12, 4) + hostContainerDeleteObjects(handle, namesPtr, namesLen, retPtr) + return liftBlobResult(retPtr, 4) { } + } + + fun hasObject(name: String): Either { + check(!closed) { "Container already closed" } + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val retPtr = alloc(12, 4) + hostContainerHasObject(handle, namePtr, nameLen, retPtr) + return liftBlobResult(retPtr, 4) { loadByte(it).toInt() != 0 } + } + + fun objectInfo(name: String): Either { + check(!closed) { "Container already closed" } + val (namePtr, nameLen) = lowerStringToPtrLen(name) + // object-metadata: name@0(8,4), container@8(8,4), created-at@16(8,8), size@24(8,8) -> size32,align8. + val retPtr = alloc(40, 8) + hostContainerObjectInfo(handle, namePtr, nameLen, retPtr) + return liftBlobResult(retPtr, 8) { b -> + ObjectMetadata( + name = liftString(loadInt(b), loadInt(b + 4)), + container = liftString(loadInt(b + 8), loadInt(b + 12)), + createdAt = loadLong(b + 16), + size = loadLong(b + 24), + ) + } + } + + fun clear(): Either { + check(!closed) { "Container already closed" } + val retPtr = alloc(12, 4) + hostContainerClear(handle, retPtr) + return liftBlobResult(retPtr, 4) { } + } + + fun close() { + if (!closed) { + hostContainerDrop(handle) + closed = true + } + } +} + +// Consumes and drops an incoming-value handle in one step, same pattern as KeyValue.kt's +// consumeIncomingValue -- incoming-value isn't exposed as public API. +private fun consumeIncomingValue(handle: Int): ByteArray { + val retPtr = alloc(12, 4) // result, error> + hostIncomingValueConsumeSync(handle, retPtr) + val result = liftBlobResult(retPtr, 4) { b -> + val dataPtr = loadInt(b) + val len = loadInt(b + 4) + ByteArray(len) { i -> loadByte(dataPtr + i) } + } + hostIncomingValueDrop(handle) + return when (result) { + is Either.Right -> result.value + is Either.Left -> error("incoming-value-consume-sync failed: ${result.value}") + } +} + +/** Native SDK access to `wasi:blobstore`. Mirrors the Scala SDK's `Blobstore` object. */ +object Blobstore { + fun createContainer(name: String): Either { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val retPtr = alloc(12, 4) + hostCreateContainer(namePtr, nameLen, retPtr) + return liftBlobResult(retPtr, 4) { Container(loadInt(it)) } + } + + fun getContainer(name: String): Either { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val retPtr = alloc(12, 4) + hostGetContainer(namePtr, nameLen, retPtr) + return liftBlobResult(retPtr, 4) { Container(loadInt(it)) } + } + + fun deleteContainer(name: String): Either { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val retPtr = alloc(12, 4) + hostDeleteContainer(namePtr, nameLen, retPtr) + return liftBlobResult(retPtr, 4) { } + } + + fun containerExists(name: String): Either { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val retPtr = alloc(12, 4) + hostContainerExists(namePtr, nameLen, retPtr) + return liftBlobResult(retPtr, 4) { loadByte(it).toInt() != 0 } + } + + fun copyObject(src: ObjectId, dest: ObjectId): Either { + val (srcContainerPtr, srcContainerLen) = lowerStringToPtrLen(src.container) + val (srcObjectPtr, srcObjectLen) = lowerStringToPtrLen(src.name) + val (destContainerPtr, destContainerLen) = lowerStringToPtrLen(dest.container) + val (destObjectPtr, destObjectLen) = lowerStringToPtrLen(dest.name) + val retPtr = alloc(12, 4) + hostCopyObject( + srcContainerPtr, srcContainerLen, srcObjectPtr, srcObjectLen, + destContainerPtr, destContainerLen, destObjectPtr, destObjectLen, + retPtr, + ) + return liftBlobResult(retPtr, 4) { } + } + + fun moveObject(src: ObjectId, dest: ObjectId): Either { + val (srcContainerPtr, srcContainerLen) = lowerStringToPtrLen(src.container) + val (srcObjectPtr, srcObjectLen) = lowerStringToPtrLen(src.name) + val (destContainerPtr, destContainerLen) = lowerStringToPtrLen(dest.container) + val (destObjectPtr, destObjectLen) = lowerStringToPtrLen(dest.name) + val retPtr = alloc(12, 4) + hostMoveObject( + srcContainerPtr, srcContainerLen, srcObjectPtr, srcObjectLen, + destContainerPtr, destContainerLen, destObjectPtr, destObjectLen, + retPtr, + ) + return liftBlobResult(retPtr, 4) { } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Config.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Config.kt new file mode 100644 index 0000000000..7895c4e329 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Config.kt @@ -0,0 +1,84 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.wasi + +import cloud.golem.runtime.Either +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.storeByte + +// Raw canonical-ABI import bindings to wasi:config/store@0.2.0-draft. Package version carries a +// "-draft" pre-release suffix (confirmed in wit-native/deps/config/world.wit's +// `package wasi:config@0.2.0-draft;`) -- included verbatim in the raw module string, matching +// the Scala facade's own `@JSImport("wasi:config/store@0.2.0-draft", ...)` exactly. Signatures +// verified via abi-dump's `sig` mode. +@kotlin.wasm.WasmImport("wasi:config/store@0.2.0-draft", "get") +private external fun hostGet(keyPtr: Int, keyLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:config/store@0.2.0-draft", "get-all") +private external fun hostGetAll(retPtr: Int) + +/** Matches `wasi:config/store@0.2.0-draft`'s `error` variant. */ +sealed class ConfigError { + data class Upstream(val message: String) : ConfigError() + data class Io(val message: String) : ConfigError() +} + +// error: size=12 align=4, tag_size=1, payload_offset=4 (both cases: string, 8 bytes). +private fun liftConfigError(base: Int): ConfigError { + val tag = loadByte(base).toInt() and 0xFF + val payload = base + 4 + return when (tag) { + 0 -> ConfigError.Upstream(liftString(loadInt(payload), loadInt(payload + 4))) + 1 -> ConfigError.Io(liftString(loadInt(payload), loadInt(payload + 4))) + else -> error("unknown wasi:config error tag: $tag") + } +} + +private fun liftListOfStringPair(base: Int): List> { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> + val elemPtr = dataPtr + i * 16 + liftString(loadInt(elemPtr), loadInt(elemPtr + 4)) to liftString(loadInt(elemPtr + 8), loadInt(elemPtr + 12)) + } +} + +private fun lowerStringToPtrLen(s: String): Pair { + val bytes = s.encodeToByteArray() + val ptr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + return ptr to bytes.size +} + +/** Native SDK access to the WASI config store (`wasi:config/store@0.2.0-draft`). Mirrors the Scala SDK's `Config` object. */ +object Config { + /** A configuration value of type `string` associated with [key]. `Right(null)` if the key is not found. */ + fun get(key: String): Either { + val (keyPtr, keyLen) = lowerStringToPtrLen(key) + // result, error>: tag@0(1,1), payload@4 (max(option 12,4, error 12,4) = 12) -> 16 total. + val retPtr = alloc(16, 4) + hostGet(keyPtr, keyLen, retPtr) + return if (loadByte(retPtr).toInt() == 0) { + val optBase = retPtr + 4 + val value = if (loadByte(optBase).toInt() == 0) null else liftString(loadInt(optBase + 4), loadInt(optBase + 8)) + Either.Right(value) + } else { + Either.Left(liftConfigError(retPtr + 4)) + } + } + + /** Every configuration key-value pair of type `string`. */ + fun getAll(): Either> { + // result>, error>: tag@0(1,1), payload@4 (max(list 8,4, error 12,4) = 12) -> 16 total. + val retPtr = alloc(16, 4) + hostGetAll(retPtr) + return if (loadByte(retPtr).toInt() == 0) { + Either.Right(liftListOfStringPair(retPtr + 4).toMap()) + } else { + Either.Left(liftConfigError(retPtr + 4)) + } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Environment.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Environment.kt new file mode 100644 index 0000000000..0f7eeefb8e --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Environment.kt @@ -0,0 +1,61 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.wasi + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt + +// Raw canonical-ABI import bindings to wasi:cli/environment@0.2.3. Signatures verified via +// abi-dump's `sig` mode against wit-native/deps/cli/environment.wit. +@kotlin.wasm.WasmImport("wasi:cli/environment@0.2.3", "get-environment") +private external fun hostGetEnvironment(retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:cli/environment@0.2.3", "get-arguments") +private external fun hostGetArguments(retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:cli/environment@0.2.3", "initial-cwd") +private external fun hostInitialCwd(retPtr: Int) + +private fun liftListOfString(base: Int): List { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> + val elemPtr = dataPtr + i * 8 + liftString(loadInt(elemPtr), loadInt(elemPtr + 4)) + } +} + +private fun liftListOfStringPair(base: Int): List> { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> + val elemPtr = dataPtr + i * 16 + liftString(loadInt(elemPtr), loadInt(elemPtr + 4)) to liftString(loadInt(elemPtr + 8), loadInt(elemPtr + 12)) + } +} + +/** Native SDK access to `wasi:cli/environment@0.2.3`. Mirrors the Scala SDK's `Environment` object. */ +object Environment { + /** The POSIX-style environment variables. */ + fun getEnvironment(): Map { + val retPtr = alloc(8, 4) // list>: {ptr: i32, len: i32} + hostGetEnvironment(retPtr) + return liftListOfStringPair(retPtr).toMap() + } + + /** The POSIX-style arguments to the program. */ + fun getArguments(): List { + val retPtr = alloc(8, 4) // list: {ptr: i32, len: i32} + hostGetArguments(retPtr) + return liftListOfString(retPtr) + } + + /** A path programs should use as their initial current working directory, if any. */ + fun initialCwd(): String? { + val retPtr = alloc(12, 4) // option: tag@0(1,1), payload@4(8,4) + hostInitialCwd(retPtr) + return if (loadByte(retPtr).toInt() == 0) null else liftString(loadInt(retPtr + 4), loadInt(retPtr + 8)) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/KeyValue.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/KeyValue.kt new file mode 100644 index 0000000000..9b689590af --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/KeyValue.kt @@ -0,0 +1,243 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.wasi + +import cloud.golem.runtime.Either +import cloud.golem.wasm.alloc +import cloud.golem.wasm.liftString +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadInt +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt + +// Raw canonical-ABI bindings to wasi:keyvalue@0.1.0's types/eventual/eventual-batch interfaces +// (the subset the Scala SDK's KeyValue.scala wraps; wasi:keyvalue also has atomic/cache/ +// handle-watch interfaces this SDK doesn't cover, matching Scala's own scope). Signatures +// verified via abi-dump's `sig`/`resulttype` modes against wit-native/deps/keyvalue/*.wit. +// +// New wrinkles vs everything built so far: +// - `error` (wasi:keyvalue/wasi-keyvalue-error) is itself a RESOURCE (a `trace(): string` +// method), not a plain variant like wasi:config's `error` -- every `result` err +// case carries an owned handle that must be resolved via [method]error.trace and then +// dropped, not decoded inline. +// - `bucket`/`outgoing-value` are obtained via STATIC resource functions +// (`open-bucket: static func(...)`, `new-outgoing-value: static func()`), a new intrinsic +// name shape: `[static].` (confirmed via abi-dump's `sig` mode -- these +// appear as ordinary `iface.functions` entries with this name, same discoverability as +// `[constructor]`/`[method]`). +// - `eventual`'s functions take `borrow`/`borrow` params, not owned +// handles -- flattens identically to `own` (a plain i32), the difference being purely +// ownership bookkeeping the canonical ABI enforces at the host side, not the wire shape. + +@kotlin.wasm.WasmImport("wasi:keyvalue/wasi-keyvalue-error@0.1.0", "[method]error.trace") +private external fun hostErrorTrace(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/wasi-keyvalue-error@0.1.0", "[resource-drop]error") +private external fun hostErrorDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/types@0.1.0", "[static]bucket.open-bucket") +private external fun hostOpenBucket(namePtr: Int, nameLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/types@0.1.0", "[resource-drop]bucket") +private external fun hostBucketDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/types@0.1.0", "[static]outgoing-value.new-outgoing-value") +private external fun hostNewOutgoingValue(): Int + +@kotlin.wasm.WasmImport("wasi:keyvalue/types@0.1.0", "[method]outgoing-value.outgoing-value-write-body-sync") +private external fun hostOutgoingValueWriteBodySync(handle: Int, dataPtr: Int, dataLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/types@0.1.0", "[resource-drop]outgoing-value") +private external fun hostOutgoingValueDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/types@0.1.0", "[method]incoming-value.incoming-value-consume-sync") +private external fun hostIncomingValueConsumeSync(handle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/types@0.1.0", "[resource-drop]incoming-value") +private external fun hostIncomingValueDrop(handle: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/eventual@0.1.0", "get") +private external fun hostEventualGet(bucketHandle: Int, keyPtr: Int, keyLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/eventual@0.1.0", "set") +private external fun hostEventualSet(bucketHandle: Int, keyPtr: Int, keyLen: Int, outgoingValueHandle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/eventual@0.1.0", "delete") +private external fun hostEventualDelete(bucketHandle: Int, keyPtr: Int, keyLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/eventual@0.1.0", "exists") +private external fun hostEventualExists(bucketHandle: Int, keyPtr: Int, keyLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/eventual-batch@0.1.0", "get-many") +private external fun hostEventualBatchGetMany(bucketHandle: Int, keysPtr: Int, keysLen: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/eventual-batch@0.1.0", "keys") +private external fun hostEventualBatchKeys(bucketHandle: Int, retPtr: Int) + +@kotlin.wasm.WasmImport("wasi:keyvalue/eventual-batch@0.1.0", "delete-many") +private external fun hostEventualBatchDeleteMany(bucketHandle: Int, keysPtr: Int, keysLen: Int, retPtr: Int) + +private fun lowerStringToPtrLen(s: String): Pair { + val bytes = s.encodeToByteArray() + val ptr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + return ptr to bytes.size +} + +private fun lowerListOfStringToPtrLen(items: List): Pair { + val arr = alloc(items.size * 8, 4) + items.forEachIndexed { i, s -> + val (ptr, len) = lowerStringToPtrLen(s) + storeInt(arr + i * 8, ptr) + storeInt(arr + i * 8 + 4, len) + } + return arr to items.size +} + +private fun liftListOfString(base: Int): List { + val dataPtr = loadInt(base) + val len = loadInt(base + 4) + return (0 until len).map { i -> + val elemPtr = dataPtr + i * 8 + liftString(loadInt(elemPtr), loadInt(elemPtr + 4)) + } +} + +/** + * A `wasi:keyvalue` error, resolved eagerly to its trace message (and dropped) at construction + * time -- unlike every other resource in this SDK, callers never hold onto a `KvError` handle, + * so there's no `close()` to forget: the underlying resource is released the moment this + * wrapper is built. + */ +class KvError internal constructor(handle: Int) { + val message: String = run { + val retPtr = alloc(8, 4) + hostErrorTrace(handle, retPtr) + val msg = liftString(loadInt(retPtr), loadInt(retPtr + 4)) + hostErrorDrop(handle) + msg + } +} + +private fun liftKvResult(base: Int, liftOk: (Int) -> T): Either = if (loadByte(base).toInt() == 0) Either.Right(liftOk(base + 4)) else Either.Left(KvError(loadInt(base + 4))) + +private fun liftKvUnitResult(base: Int): Either = if (loadByte(base).toInt() == 0) Either.Right(Unit) else Either.Left(KvError(loadInt(base + 4))) + +/** A collection of key-value pairs (`wasi:keyvalue/types@0.1.0`'s `bucket` resource). MUST be [close]d when done. */ +class Bucket internal constructor(private val handle: Int) { + private var closed = false + + fun get(key: String): Either { + check(!closed) { "Bucket already closed" } + val (keyPtr, keyLen) = lowerStringToPtrLen(key) + val retPtr = alloc(12, 4) // result, error>: tag@0(1,1), payload@4(8,4) + hostEventualGet(handle, keyPtr, keyLen, retPtr) + return liftKvResult(retPtr) { optBase -> + if (loadByte(optBase).toInt() == 0) null else consumeIncomingValue(loadInt(optBase + 4)) + } + } + + fun set(key: String, value: ByteArray): Either { + check(!closed) { "Bucket already closed" } + val (keyPtr, keyLen) = lowerStringToPtrLen(key) + val ovHandle = hostNewOutgoingValue() + val dataPtr = alloc(value.size, 1) + value.forEachIndexed { i, b -> storeByte(dataPtr + i, b) } + val writeRetPtr = alloc(8, 4) // result<_, error> + hostOutgoingValueWriteBodySync(ovHandle, dataPtr, value.size, writeRetPtr) + val writeResult = liftKvUnitResult(writeRetPtr) + if (writeResult is Either.Left) { + hostOutgoingValueDrop(ovHandle) + return writeResult + } + val retPtr = alloc(8, 4) // result<_, error> + hostEventualSet(handle, keyPtr, keyLen, ovHandle, retPtr) + hostOutgoingValueDrop(ovHandle) + return liftKvUnitResult(retPtr) + } + + fun delete(key: String): Either { + check(!closed) { "Bucket already closed" } + val (keyPtr, keyLen) = lowerStringToPtrLen(key) + val retPtr = alloc(8, 4) + hostEventualDelete(handle, keyPtr, keyLen, retPtr) + return liftKvUnitResult(retPtr) + } + + fun exists(key: String): Either { + check(!closed) { "Bucket already closed" } + val (keyPtr, keyLen) = lowerStringToPtrLen(key) + val retPtr = alloc(8, 4) // result: tag@0(1,1), payload@4(max(bool 1, error 4)=4) + hostEventualExists(handle, keyPtr, keyLen, retPtr) + return liftKvResult(retPtr) { loadByte(it).toInt() != 0 } + } + + fun keys(): Either> { + check(!closed) { "Bucket already closed" } + val retPtr = alloc(12, 4) // result, error>: tag@0(1,1), payload@4(8,4) + hostEventualBatchKeys(handle, retPtr) + return liftKvResult(retPtr) { liftListOfString(it) } + } + + fun getMany(keys: List): Either> { + check(!closed) { "Bucket already closed" } + val (keysPtr, keysLen) = lowerListOfStringToPtrLen(keys) + val retPtr = alloc(12, 4) // result>, error>: tag@0(1,1), payload@4(8,4) + hostEventualBatchGetMany(handle, keysPtr, keysLen, retPtr) + return liftKvResult(retPtr) { listBase -> + val dataPtr = loadInt(listBase) + val len = loadInt(listBase + 4) + (0 until len).map { i -> + val elemPtr = dataPtr + i * 8 // option: tag@0(1,1), payload@4(i32 handle,4,4) + if (loadByte(elemPtr).toInt() == 0) null else consumeIncomingValue(loadInt(elemPtr + 4)) + } + } + } + + fun deleteMany(keys: List): Either { + check(!closed) { "Bucket already closed" } + val (keysPtr, keysLen) = lowerListOfStringToPtrLen(keys) + val retPtr = alloc(8, 4) + hostEventualBatchDeleteMany(handle, keysPtr, keysLen, retPtr) + return liftKvUnitResult(retPtr) + } + + fun close() { + if (!closed) { + hostBucketDrop(handle) + closed = true + } + } + + companion object { + /** Opens a bucket with the given name. */ + fun open(name: String): Either { + val (namePtr, nameLen) = lowerStringToPtrLen(name) + val retPtr = alloc(8, 4) // result: tag@0(1,1), payload@4(4,4) + hostOpenBucket(namePtr, nameLen, retPtr) + return liftKvResult(retPtr) { Bucket(loadInt(it)) } + } + } +} + +// Consumes and drops an incoming-value handle in one step -- this SDK doesn't expose +// incoming-value as public API (Scala's own Bucket.get/getMany do the same: construct an +// IncomingValue, consume it immediately, discard the wrapper). +private fun consumeIncomingValue(handle: Int): ByteArray { + val retPtr = alloc(8, 4) // result, error>: tag@0(1,1), payload@4(8,4) -- errors here trap, see below + hostIncomingValueConsumeSync(handle, retPtr) + val result = liftKvResult(retPtr) { listBase -> + val dataPtr = loadInt(listBase) + val len = loadInt(listBase + 4) + ByteArray(len) { i -> loadByte(dataPtr + i) } + } + hostIncomingValueDrop(handle) + return when (result) { + is Either.Right -> result.value + // get/get-many's own result is already Either; consume-sync failing after + // a successful get is an unexpected host-side inconsistency, not a normal error path -- + // mirrors this SDK's existing convention of surfacing unexpected failures as a message + // (see HostApi.trap's callers) rather than inventing a third error channel here. + is Either.Left -> error("incoming-value-consume-sync failed: ${result.value.message}") + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Logging.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Logging.kt new file mode 100644 index 0000000000..52a14ad921 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/runtime/wasi/Logging.kt @@ -0,0 +1,39 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class, kotlin.wasm.ExperimentalWasmInterop::class) + +package cloud.golem.runtime.wasi + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.storeByte + +// Raw canonical-ABI import binding to wasi:logging/logging (package `wasi:logging`, unversioned +// -- confirmed via wit-native/deps/logging/logging.wit's `package wasi:logging;` declaration, +// matching the Scala facade's own `@JSImport("wasi:logging/logging", ...)` exactly, no @version +// suffix). Signature verified via abi-dump's `sig` mode. +@kotlin.wasm.WasmImport("wasi:logging/logging", "log") +private external fun hostLog(level: Int, contextPtr: Int, contextLen: Int, messagePtr: Int, messageLen: Int) + +/** Matches `wasi:logging/logging`'s `level` enum case order exactly. */ +enum class LogLevel { TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL } + +private fun lowerStringToPtrLen(s: String): Pair { + val bytes = s.encodeToByteArray() + val ptr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + return ptr to bytes.size +} + +/** Native SDK access to WASI logging (`wasi:logging/logging`). Mirrors the Scala SDK's `Logging` object. */ +object Logging { + fun log(level: LogLevel, context: String, message: String) { + val (contextPtr, contextLen) = lowerStringToPtrLen(context) + val (messagePtr, messageLen) = lowerStringToPtrLen(message) + hostLog(level.ordinal, contextPtr, contextLen, messagePtr, messageLen) + } + + fun trace(message: String, context: String = "") = log(LogLevel.TRACE, context, message) + fun debug(message: String, context: String = "") = log(LogLevel.DEBUG, context, message) + fun info(message: String, context: String = "") = log(LogLevel.INFO, context, message) + fun warn(message: String, context: String = "") = log(LogLevel.WARN, context, message) + fun error(message: String, context: String = "") = log(LogLevel.ERROR, context, message) + fun critical(message: String, context: String = "") = log(LogLevel.CRITICAL, context, message) +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Lift.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Lift.kt new file mode 100644 index 0000000000..a6dde18dba --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Lift.kt @@ -0,0 +1,328 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.wasm + +import cloud.golem.runtime.SchemaValue + +// Canonical-ABI lift: linear memory -> Kotlin, for the golem:core/types@2.0.0 `schema-value-tree` +// wire format (the actual `input` parameter of golem:agent/guest@2.0.0's initialize/invoke). +// Offsets verified via wit-parser::SizeAlign against the real WIT (see +// cloud.golem.runtime.AgentTypeModel's Layout object / docs/spikes for the verification tool): +// +// schema-value-tree: {value-nodes: list @0 (8B: ptr,len), root: s32 @8} +// size=12 align=4 +// schema-value-node: variant, size=32 align=8, tag @0 (1 byte), payload @8 +// s32-value(s32) tag=3, payload = s32 @+8 +// string-value(string) tag=12, payload = (ptr,len) @+8 +// record-value(list) tag=13, payload = (ptr,len) @+8 (child value-node indices) +// +// The `input` tree's root is ALWAYS a record-value whose ordered children are the call's +// parameters (one per declared parameter, in declaration order) -- confirmed by the WIT doc +// comment on `initialize`/`invoke`. + +private const val SVN_SIZE = 32 +private const val SVN_PAYLOAD_OFFSET = 8 +private const val SVN_TAG_BOOL = 0 +private const val SVN_TAG_S8 = 1 +private const val SVN_TAG_S16 = 2 +private const val SVN_TAG_S32 = 3 +private const val SVN_TAG_S64 = 4 +private const val SVN_TAG_U8 = 5 +private const val SVN_TAG_U16 = 6 +private const val SVN_TAG_U32 = 7 +private const val SVN_TAG_U64 = 8 +private const val SVN_TAG_F32 = 9 +private const val SVN_TAG_F64 = 10 +private const val SVN_TAG_CHAR = 11 +private const val SVN_TAG_STRING = 12 +private const val SVN_TAG_RECORD = 13 +private const val SVN_TAG_VARIANT = 14 +private const val SVN_TAG_ENUM = 15 +private const val SVN_TAG_FLAGS = 16 +private const val SVN_TAG_TUPLE = 17 +private const val SVN_TAG_LIST = 18 +private const val SVN_TAG_FIXED_LIST = 19 +private const val SVN_TAG_MAP = 20 +private const val SVN_TAG_OPTION = 21 +private const val SVN_TAG_RESULT = 22 +private const val SVN_TAG_TEXT = 23 +private const val SVN_TAG_BINARY = 24 +private const val SVN_TAG_PATH = 25 +private const val SVN_TAG_URL = 26 +private const val SVN_TAG_DATETIME = 27 +private const val SVN_TAG_DURATION = 28 +private const val SVN_TAG_QUANTITY = 29 +private const val SVN_TAG_UNION = 30 +private const val SVN_TAG_SECRET = 31 +private const val SVN_TAG_QUOTA_TOKEN = 32 + +/** + * A minimal WIT-type-string grammar for the structural types this SDK supports as VALUES (not + * as agent-method parameter/return declarations -- [cloud.golem.ksp.TypeMapper] is unrelated): + * `list`, `option`, `tuple`, `result`, `map`, + * `variant` (per-case payload type, `_` for a case with no payload), `enum`, `flags` + * -- where `T`/`E`/`K`/`V`/`Ti` may be `_` or any nested type from this same grammar. Case/flag + * NAMES are intentionally not part of this grammar (they live in the schema-graph, not the + * value -- see SchemaValue.kt's header comment); this only needs enough type info to interpret + * a case's PAYLOAD, addressed purely by index. + */ +// splitTopLevelCommas / innerOf now live in WitTypeGrammar.kt (shared with the schema-graph builder). + +/** Lift a UTF-8 string given its (ptr,len) canonical-ABI representation. */ +fun liftString(ptr: Int, len: Int): String { + val bytes = ByteArray(len) { loadByte(ptr + it) } + return bytes.decodeToString() +} + +/** Lift an `option` payload: tag@base(1B), string(ptr,len)@base+4(8B, only if some). */ +private fun liftOptionString(base: Int): String? = if (loadByte(base).toInt() == 0) null else liftString(loadInt(base + 4), loadInt(base + 8)) + +/** Lift the schema-value-node at `valueNodesPtr + index*32` into a [SchemaValue], per [witType]. */ +private fun liftNode(valueNodesPtr: Int, index: Int, witType: String): SchemaValue { + val nodePtr = valueNodesPtr + index * SVN_SIZE + val tag = loadByte(nodePtr).toInt() and 0xFF + val payload = nodePtr + SVN_PAYLOAD_OFFSET + fun expect(expected: Int, name: String) = require(tag == expected) { "expected $name (tag=$expected), got tag=$tag" } + return when (witType) { + "bool" -> { + expect(SVN_TAG_BOOL, "bool-value") + SchemaValue.Bool(loadByte(payload).toInt() != 0) + } + "s8" -> { + expect(SVN_TAG_S8, "s8-value") + SchemaValue.S8(loadByte(payload)) + } + "s16" -> { + expect(SVN_TAG_S16, "s16-value") + SchemaValue.S16(loadShort(payload)) + } + "s32" -> { + expect(SVN_TAG_S32, "s32-value") + SchemaValue.S32(loadInt(payload)) + } + "s64" -> { + expect(SVN_TAG_S64, "s64-value") + SchemaValue.S64(loadLong(payload)) + } + "u8" -> { + expect(SVN_TAG_U8, "u8-value") + SchemaValue.U8(loadByte(payload).toUByte()) + } + "u16" -> { + expect(SVN_TAG_U16, "u16-value") + SchemaValue.U16(loadShort(payload).toUShort()) + } + "u32" -> { + expect(SVN_TAG_U32, "u32-value") + SchemaValue.U32(loadInt(payload).toUInt()) + } + "u64" -> { + expect(SVN_TAG_U64, "u64-value") + SchemaValue.U64(loadLong(payload).toULong()) + } + "f32" -> { + expect(SVN_TAG_F32, "f32-value") + SchemaValue.F32(loadFloat(payload)) + } + "f64" -> { + expect(SVN_TAG_F64, "f64-value") + SchemaValue.F64(loadDouble(payload)) + } + "char" -> { + expect(SVN_TAG_CHAR, "char-value") + SchemaValue.Chr(loadInt(payload).toChar()) + } + "string" -> { + expect(SVN_TAG_STRING, "string-value") + SchemaValue.Str(liftString(loadInt(payload), loadInt(payload + 4))) + } + else -> when { + witType.startsWith("record<") && witType.endsWith(">") -> { + // record: a record-value node whose children are the field values + // in field order. Field NAMES live in the schema-graph, not the value -- lift is + // positional, so we drop them (substringAfter(':') keeps the type, which may itself + // contain ':' only inside a nested record<...>). + expect(SVN_TAG_RECORD, "record-value") + val fieldTypes = splitTopLevelCommas(innerOf(witType, "record<")).map { it.substringAfter(':') } + val ptr = loadInt(payload) + SchemaValue.Record( + fieldTypes.mapIndexed { i, t -> + liftNode(valueNodesPtr, loadInt(ptr + i * 4), t) + }, + ) + } + witType.startsWith("list<") && witType.endsWith(">") -> { + expect(SVN_TAG_LIST, "list-value") + val elemType = innerOf(witType, "list<") + val ptr = loadInt(payload) + val len = loadInt(payload + 4) + SchemaValue.ListVal( + (0 until len).map { i -> + liftNode(valueNodesPtr, loadInt(ptr + i * 4), elemType) + }, + ) + } + witType.startsWith("fixed-list<") && witType.endsWith(">") -> { + // fixed-list-value is structurally identical to list-value (a list of child indices); + // the schema's declared length is not carried in the value, so it lifts to a ListVal. + expect(SVN_TAG_FIXED_LIST, "fixed-list-value") + val elemType = innerOf(witType, "fixed-list<") + val ptr = loadInt(payload) + val len = loadInt(payload + 4) + SchemaValue.ListVal( + (0 until len).map { i -> + liftNode(valueNodesPtr, loadInt(ptr + i * 4), elemType) + }, + ) + } + witType.startsWith("tuple<") && witType.endsWith(">") -> { + expect(SVN_TAG_TUPLE, "tuple-value") + val elemTypes = splitTopLevelCommas(innerOf(witType, "tuple<")) + val ptr = loadInt(payload) + SchemaValue.TupleVal( + elemTypes.mapIndexed { i, t -> + liftNode(valueNodesPtr, loadInt(ptr + i * 4), t) + }, + ) + } + witType.startsWith("option<") && witType.endsWith(">") -> { + expect(SVN_TAG_OPTION, "option-value") + val innerType = innerOf(witType, "option<") + val some = loadByte(payload).toInt() != 0 + SchemaValue.OptionVal(if (some) liftNode(valueNodesPtr, loadInt(payload + 4), innerType) else null) + } + witType.startsWith("result<") && witType.endsWith(">") -> { + expect(SVN_TAG_RESULT, "result-value") + val (okType, errType) = splitTopLevelCommas(innerOf(witType, "result<")).let { it[0] to it[1] } + val isOk = loadByte(payload).toInt() == 0 // ok-value=0 / err-value=1 + val optPayload = payload + 4 // result-value-payload: tag@0, option@4 + val some = loadByte(optPayload).toInt() != 0 + val innerType = if (isOk) okType else errType + val inner = if (!some) null else liftNode(valueNodesPtr, loadInt(optPayload + 4), innerType) + SchemaValue.ResultVal(isOk, inner) + } + witType.startsWith("variant<") && witType.endsWith(">") -> { + expect(SVN_TAG_VARIANT, "variant-value") + // variant or legacy positional variant; drop case names + // (substringAfter(':') returns the whole string when there's no colon). + val caseTypes = splitTopLevelCommas(innerOf(witType, "variant<")).map { it.substringAfter(':') } + val caseIndex = loadInt(payload) // variant-value-payload: case@0(u32) + val optPayload = payload + 4 // payload@4: option + val some = loadByte(optPayload).toInt() != 0 + val inner = if (!some) null else liftNode(valueNodesPtr, loadInt(optPayload + 4), caseTypes[caseIndex]) + SchemaValue.VariantVal(caseIndex, inner) + } + witType == "enum" || (witType.startsWith("enum<") && witType.endsWith(">")) -> { + expect(SVN_TAG_ENUM, "enum-value") // enum value carries only a case index + SchemaValue.EnumVal(loadInt(payload)) + } + witType == "flags" -> { + expect(SVN_TAG_FLAGS, "flags-value") + val ptr = loadInt(payload) + val len = loadInt(payload + 4) + SchemaValue.FlagsVal((0 until len).map { i -> loadByte(ptr + i).toInt() != 0 }) + } + witType.startsWith("map<") && witType.endsWith(">") -> { + expect(SVN_TAG_MAP, "map-value") + val (keyType, valType) = splitTopLevelCommas(innerOf(witType, "map<")).let { it[0] to it[1] } + val ptr = loadInt(payload) + val len = loadInt(payload + 4) + SchemaValue.MapVal( + (0 until len).map { i -> + val entryBase = ptr + i * 8 // map-entry: key@0(index), value@4(index) + val k = liftNode(valueNodesPtr, loadInt(entryBase), keyType) + val v = liftNode(valueNodesPtr, loadInt(entryBase + 4), valType) + k to v + }, + ) + } + witType == "path" -> { + expect(SVN_TAG_PATH, "path-value") + SchemaValue.PathVal(liftString(loadInt(payload), loadInt(payload + 4))) + } + witType == "url" -> { + expect(SVN_TAG_URL, "url-value") + SchemaValue.UrlVal(liftString(loadInt(payload), loadInt(payload + 4))) + } + witType == "datetime" -> { + expect(SVN_TAG_DATETIME, "datetime-value") + SchemaValue.DatetimeVal(loadLong(payload), loadInt(payload + 8)) + } + witType == "duration" -> { + expect(SVN_TAG_DURATION, "duration-value") + SchemaValue.DurationVal(loadLong(payload)) + } + witType == "quantity" -> { + expect(SVN_TAG_QUANTITY, "quantity-value-node") + SchemaValue.QuantityVal(loadLong(payload), loadInt(payload + 8), liftString(loadInt(payload + 12), loadInt(payload + 16))) + } + witType == "text" -> { + expect(SVN_TAG_TEXT, "text-value") + SchemaValue.TextVal(liftString(loadInt(payload), loadInt(payload + 4)), liftOptionString(payload + 8)) + } + witType == "binary" -> { + expect(SVN_TAG_BINARY, "binary-value") + val ptr = loadInt(payload) + val len = loadInt(payload + 4) + val bytes = (0 until len).map { i -> loadByte(ptr + i).toUByte() } + SchemaValue.BinaryVal(bytes, liftOptionString(payload + 8)) + } + witType.startsWith("union<") && witType.endsWith(">") -> { + // union: branches are heterogeneous. The value carries the + // matched branch's string tag (union-value-payload: tag@0, body-index@8); select + // that branch's body type to lift the body. A legacy single-body `union` (no + // colon) or a one-branch union still works via the singleOrNull fallback. + expect(SVN_TAG_UNION, "union-value") + val tag = liftString(loadInt(payload), loadInt(payload + 4)) + val branches = splitTopLevelCommas(innerOf(witType, "union<")).map { + val c = it.indexOf(':') + if (c < 0) "" to it else it.substring(0, c) to it.substring(c + 1) + } + val bodyType = branches.firstOrNull { it.first == tag }?.second + ?: branches.singleOrNull()?.second + ?: error("native lift: union tag '$tag' not among branches ${branches.map { it.first }}") + val body = liftNode(valueNodesPtr, loadInt(payload + 8), bodyType) + SchemaValue.UnionVal(tag, body) + } + witType == "secret" -> { + // own: no nested structure to recurse into -- the payload IS the handle. + expect(SVN_TAG_SECRET, "secret-value") + SchemaValue.SecretVal(loadInt(payload)) + } + witType == "quota-token" -> { + expect(SVN_TAG_QUOTA_TOKEN, "quota-token-handle") + SchemaValue.QuotaTokenVal(loadInt(payload)) + } + else -> error("native lift: unsupported type $witType") + } + } +} + +/** + * Reads a `schema-value-tree` at [treePtr] whose root node is a single value of [witType] (NOT a + * record wrapper -- unlike [liftParamRecord]) and lifts it. Used to decode the value half of a + * `typed-schema-value`, whose value tree's root is the value itself. + */ +fun liftSingleValue(treePtr: Int, witType: String): SchemaValue { + val valueNodesPtr = loadInt(treePtr) + val root = loadInt(treePtr + 8) + return liftNode(valueNodesPtr, root, witType) +} + +/** + * Reads a `schema-value-tree` at [treePtr] (the invoke/initialize `input` parameter) whose root + * is a `record-value` -- one child per declared parameter, in declaration order -- and lifts + * each child by its WIT type. An empty parameter list is a `record-value` with zero children. + */ +fun liftParamRecord(treePtr: Int, witTypes: List): List { + val valueNodesPtr = loadInt(treePtr) + val root = loadInt(treePtr + 8) + val rootNodePtr = valueNodesPtr + root * SVN_SIZE + val rootTag = loadByte(rootNodePtr).toInt() and 0xFF + require(rootTag == SVN_TAG_RECORD) { "expected record-value root (tag=$SVN_TAG_RECORD), got tag=$rootTag" } + val childrenPtr = loadInt(rootNodePtr + SVN_PAYLOAD_OFFSET) + return witTypes.mapIndexed { i, witType -> + val childIndex = loadInt(childrenPtr + i * 4) + liftNode(valueNodesPtr, childIndex, witType) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Lower.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Lower.kt new file mode 100644 index 0000000000..400d9fc0bb --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Lower.kt @@ -0,0 +1,288 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.wasm + +import cloud.golem.runtime.SchemaValue + +// Canonical-ABI lower: Kotlin `SchemaValue` -> linear memory. Two paths live here: the +// direct-return fast path (`lowerSingle`) for return values that fit a single core value +// (currently s32 and string), and the full `schema-value-tree` encoder +// (`buildSchemaValueTree` / `flattenNode`) covering every SchemaValue variant in the model. +// +// NOTE (empirically verified via jco, see SelfTestN2 roundtrip): a bare `s32` return fits +// entirely in one core `i32` result value, so the canonical ABI needs no linear-memory +// indirection for it — the exported wasm function's own i32 return *is* the value. A `string` +// return does not fit in one core value; its canonical-ABI representation is the pair +// (ptr: i32, len: i32) written into an 8-byte result area (allocated via cabi_realloc/alloc), +// with the function returning a *pointer* to that area. +// +// `lowerSingle` therefore does not have one uniform "return an Int pointer" contract across +// types: for S32 the caller should use the value directly (see `lowerS32` / `asDirectReturn`); +// for Str (and anything else that doesn't fit a single core value) it returns the pointer to +// the result area. Guest.kt dispatches on the SchemaValue's own type to pick +// the right calling convention per WIT return type. + +// S32 lowers to itself: no linear-memory indirection needed for a bare `s32` return. +fun lowerS32(value: SchemaValue.S32): Int = value.v + +// String lowers to an 8-byte (ptr, len) result area, returned as a pointer. +fun lowerString(value: SchemaValue.Str): Int { + val bytes = value.v.encodeToByteArray() + val strPtr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(strPtr + i, bytes[i]) + val area = alloc(8, 4) + storeInt(area, strPtr) + storeInt(area + 4, bytes.size) + return area +} + +// Generic single-value lowering entry point, for callers that don't want to dispatch on the +// SchemaValue subtype themselves. Returns the canonical-ABI representation appropriate to the +// value's WIT type: for S32 this IS the value (no pointer); for Str this is a pointer to the +// (ptr,len) result area. Callers must know which convention applies to their WIT signature. +fun lowerSingle(value: SchemaValue): Int = when (value) { + is SchemaValue.S32 -> lowerS32(value) + is SchemaValue.Str -> lowerString(value) + else -> error( + "native lower: bare-return lowering for ${value::class.simpleName} not supported -- " + + "only s32/string have a direct wasm-return convention; other types must be " + + "returned via a schema-value-tree (see buildSchemaValueTree)", + ) +} + +// ---- Generic record/list/option/variant field writers (used by AgentTypeModel.kt to lower +// golem:agent/common@2.0.0's agent-type and golem:core/types@2.0.0's schema-graph). These write +// INLINE record fields at `recordBase + offset` -- for nested record-typed fields (not +// string/list, which always carry their own (ptr,len) indirection per the canonical ABI), the +// record's own fields must be written directly into that inline region, not a separate +// allocation. Byte offsets used by callers are taken from wit-parser::SizeAlign against the +// real WIT (the same algorithm wasmtime/wit-bindgen use) -- not hand-derived; see +// docs/spikes/compile-to-wasm-poc or cloud.golem.runtime.AgentTypeModel's Layout object. + +/** Write a canonical-ABI `string` field (an (i32 ptr, i32 len) pair) at recordBase+offset. */ +fun writeStringField(recordBase: Int, offset: Int, value: String) { + val bytes = value.encodeToByteArray() + val strPtr = alloc(bytes.size, 1) + for (i in bytes.indices) storeByte(strPtr + i, bytes[i]) + storeInt(recordBase + offset, strPtr) + storeInt(recordBase + offset + 4, bytes.size) +} + +/** + * Lower a homogeneous `list` field (an (i32 ptr, i32 len) pair) at recordBase+offset. + * Allocates count*elementSize bytes (elementAlign-aligned, one contiguous buffer of `count` + * elements) and calls writeElement(i, elementPtr) for each element; the caller writes that + * element's own fields directly into [elementPtr, elementPtr+elementSize). + */ +fun writeListField( + recordBase: Int, + offset: Int, + count: Int, + elementSize: Int, + elementAlign: Int, + writeElement: (index: Int, elementPtr: Int) -> Unit, +) { + val base = alloc(count * elementSize, elementAlign) + for (i in 0 until count) writeElement(i, base + i * elementSize) + storeInt(recordBase + offset, base) + storeInt(recordBase + offset + 4, count) +} + +/** Write an empty `list` field (ptr=0, len=0 -- never dereferenced by a conforming decoder). */ +fun writeEmptyListField(recordBase: Int, offset: Int) { + storeInt(recordBase + offset, 0) + storeInt(recordBase + offset + 4, 0) +} + +/** + * Write `option = none` at recordBase+offset: just the 0 discriminant byte. The payload + * region reserved for `some` (whatever bytes follow, sized per T's type-level size/align) is + * left unwritten -- a conforming decoder only reads it when the discriminant says `some`. + */ +fun writeOptionNone(recordBase: Int, offset: Int) { + storeByte(recordBase + offset, 0) +} + +// ---- schema-value-tree construction (single-value trees) ---- +// +// Builds a real golem:core/types@2.0.0 `schema-value-tree` (NOT the bare lowerS32/lowerString +// above, which are for WIT functions that return a bare s32/string directly). This is the +// return-value wire format `invoke` actually needs: a one-node tree whose root IS the value. +// Layout verified via wit-parser::SizeAlign (see cloud.golem.runtime's Guest.kt / AgentTypeModel): +// schema-value-tree: {value-nodes: list @0, root: s32 @8} size=12 align=4 +// schema-value-node: variant, size=32 align=8, tag @0 (1 byte), payload @8 +// s32-value(s32) tag=3, payload=s32 @+8 ; string-value(string) tag=12, payload=(ptr,len) @+8 + +/** + * Build a `schema-value-tree` for a (possibly nested) output [value], returning the tree's base + * pointer (a 12-byte {value-nodes, root} record: see the layout note above). Structural values + * (record/list/tuple/option/result) recurse: each child is flattened into the SAME + * `value-nodes` array first (post-order), and the composite node's payload references its + * children by index -- matching the wire format's flat-array-of-indices design (see the WIT + * doc comment on `schema-value-tree`: "Indices refer to entries in `value-nodes` within this + * same tree"). Tag numbers and payload shapes are from `schema-value-node`'s case list and its + * payload records (`variant-value-payload`/`map-entry`/`result-value-payload`), verified via + * wit-parser against wit-native/deps/golem-core-v2/golem-core-v2.wit. + */ +fun buildSchemaValueTree(value: SchemaValue): Int { + val nodes = ArrayList Unit>>() // (tag, writer) in flattened post-order + val rootIndex = flattenNode(value, nodes) + + val nodesBase = alloc(nodes.size * 32, 8) // schema-value-node: size=32 align=8 + nodes.forEachIndexed { i, (tag, write) -> + val nodePtr = nodesBase + i * 32 + storeByte(nodePtr, tag.toByte()) + write(nodePtr + 8) + } + val treePtr = alloc(12, 4) // schema-value-tree: size=12 align=4 + storeInt(treePtr, nodesBase) // value-nodes.ptr + storeInt(treePtr + 4, nodes.size) // value-nodes.len + storeInt(treePtr + 8, rootIndex) // root + return treePtr +} + +/** Recursively appends [value]'s node(s) to [nodes] (children before parent) and returns the + * index the value's own node ends up at. */ +private fun flattenNode(value: SchemaValue, nodes: MutableList Unit>>): Int { + fun add(tag: Int, write: (Int) -> Unit): Int { + nodes.add(tag to write) + return nodes.size - 1 + } + fun addChildren(items: List): List = items.map { flattenNode(it, nodes) } + + /** Writes a `list` payload (record-value/tuple-value/list-value shape). */ + fun writeIndexList(base: Int, indices: List) { + val arr = alloc(indices.size * 4, 4) + indices.forEachIndexed { i, idx -> storeInt(arr + i * 4, idx) } + storeInt(base, arr) + storeInt(base + 4, indices.size) + } + + /** Writes an `option` payload: tag@0(1B), index@4(4B, only if some). */ + fun writeOptionIndex(base: Int, index: Int?) { + if (index == null) { + storeByte(base, 0) + } else { + storeByte(base, 1) + storeInt(base + 4, index) + } + } + + /** Writes an `option` payload: tag@0(1B), string(ptr,len)@4(8B, only if some). */ + fun writeOptionString(base: Int, s: String?) { + if (s == null) { + storeByte(base, 0) + } else { + storeByte(base, 1) + writeStringField(base, 4, s) + } + } + + return when (value) { + is SchemaValue.Bool -> add(0) { storeByte(it, if (value.v) 1 else 0) } + is SchemaValue.S8 -> add(1) { storeByte(it, value.v) } + is SchemaValue.S16 -> add(2) { storeShort(it, value.v) } + is SchemaValue.S32 -> add(3) { storeInt(it, value.v) } + is SchemaValue.S64 -> add(4) { storeLong(it, value.v) } + is SchemaValue.U8 -> add(5) { storeByte(it, value.v.toByte()) } + is SchemaValue.U16 -> add(6) { storeShort(it, value.v.toShort()) } + is SchemaValue.U32 -> add(7) { storeInt(it, value.v.toInt()) } + is SchemaValue.U64 -> add(8) { storeLong(it, value.v.toLong()) } + is SchemaValue.F32 -> add(9) { storeFloat(it, value.v) } + is SchemaValue.F64 -> add(10) { storeDouble(it, value.v) } + is SchemaValue.Chr -> add(11) { storeInt(it, value.v.code) } // canonical ABI: u32 scalar value + is SchemaValue.Str -> add(12) { writeStringField(it, 0, value.v) } + is SchemaValue.Record -> { + val childIndices = addChildren(value.fields) + add(13) { writeIndexList(it, childIndices) } + } + is SchemaValue.TupleVal -> { + val childIndices = addChildren(value.items) + add(17) { writeIndexList(it, childIndices) } + } + is SchemaValue.ListVal -> { + val childIndices = addChildren(value.items) + add(18) { writeIndexList(it, childIndices) } + } + is SchemaValue.OptionVal -> { + val childIndex = value.inner?.let { flattenNode(it, nodes) } + add(21) { writeOptionIndex(it, childIndex) } + } + is SchemaValue.ResultVal -> { + val childIndex = value.inner?.let { flattenNode(it, nodes) } + add(22) { base -> + storeByte(base, if (value.ok) 0 else 1) // ok-value=0 / err-value=1 + writeOptionIndex(base + 4, childIndex) + } + } + is SchemaValue.VariantVal -> { + val childIndex = value.payload?.let { flattenNode(it, nodes) } + add(14) { base -> + // variant-value-payload: case@0(u32), payload@4(option) + storeInt(base, value.caseIndex) + writeOptionIndex(base + 4, childIndex) + } + } + is SchemaValue.EnumVal -> add(15) { storeInt(it, value.caseIndex) } // enum-value(u32) + is SchemaValue.FlagsVal -> add(16) { base -> + // flags-value(list) + val arr = alloc(value.flags.size, 1) + value.flags.forEachIndexed { i, b -> storeByte(arr + i, if (b) 1 else 0) } + storeInt(base, arr) + storeInt(base + 4, value.flags.size) + } + is SchemaValue.MapVal -> { + val entryIndices = value.entries.map { (k, v) -> flattenNode(k, nodes) to flattenNode(v, nodes) } + add(20) { base -> + // map-value(list): map-entry {key: index@0, value: index@4}, size=8 align=4 + val arr = alloc(entryIndices.size * 8, 4) + entryIndices.forEachIndexed { i, (k, v) -> + storeInt(arr + i * 8, k) + storeInt(arr + i * 8 + 4, v) + } + storeInt(base, arr) + storeInt(base + 4, entryIndices.size) + } + } + is SchemaValue.TextVal -> add(23) { base -> + // text-value-payload: text@0(8B), language@8(12B, option) + writeStringField(base, 0, value.text) + writeOptionString(base + 8, value.language) + } + is SchemaValue.BinaryVal -> add(24) { base -> + // binary-value-payload: bytes@0(8B), mime-type@8(12B, option) + val arr = alloc(value.bytes.size, 1) + value.bytes.forEachIndexed { i, b -> storeByte(arr + i, b.toByte()) } + storeInt(base, arr) + storeInt(base + 4, value.bytes.size) + writeOptionString(base + 8, value.mimeType) + } + is SchemaValue.PathVal -> add(25) { writeStringField(it, 0, value.v) } // path-value(string) + is SchemaValue.UrlVal -> add(26) { writeStringField(it, 0, value.v) } // url-value(string) + is SchemaValue.DatetimeVal -> add(27) { base -> + // datetime: seconds@0(8B), nanoseconds@8(4B) + storeLong(base, value.seconds) + storeInt(base + 8, value.nanoseconds) + } + is SchemaValue.DurationVal -> add(28) { storeLong(it, value.nanoseconds) } // duration-value-payload: nanoseconds@0 + is SchemaValue.QuantityVal -> add(29) { base -> + // quantity-value: mantissa@0(8B), scale@8(4B), unit@12(8B string) + storeLong(base, value.mantissa) + storeInt(base + 8, value.scale) + writeStringField(base, 12, value.unit) + } + is SchemaValue.UnionVal -> { + val bodyIndex = flattenNode(value.body, nodes) + add(30) { base -> + // union-value-payload: tag@0(8B string), body@8(4B index, NOT optional) + writeStringField(base, 0, value.tag) + storeInt(base + 8, bodyIndex) + } + } + // own: writing the raw handle here TRANSFERS ownership to whoever reads this + // value next (standard own move semantics) -- no drop call needed on our side. + is SchemaValue.SecretVal -> add(31) { storeInt(it, value.handle) } // secret-value(own) + is SchemaValue.QuotaTokenVal -> add(32) { storeInt(it, value.handle) } // quota-token-handle(own) + is SchemaValue.Unit_ -> error("native lower: unit has no schema-value-tree -- return option=none instead") + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Memory.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Memory.kt new file mode 100644 index 0000000000..f56a93d6fd --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/Memory.kt @@ -0,0 +1,60 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.wasm + +import kotlin.wasm.unsafe.Pointer +import kotlin.wasm.unsafe.withScopedMemoryAllocator + +// Persistent bump allocator over a high linear-memory region. Force one growth so the pages exist; +// the stdlib's scoped allocator works in low addresses (resetting per scope), so a hand-managed +// bump pointer above BASE persists across the host's realloc -> invoke -> post-return calls. +private const val BASE = 16 * 1024 * 1024 +private const val SIZE = 16 * 1024 * 1024 +private var bump = 0 + +private fun ensureInit() { + if (bump == 0) { + withScopedMemoryAllocator { a -> a.allocate(BASE + SIZE) } // grow; pages persist after free + bump = BASE + } +} + +fun alloc(size: Int, align: Int): Int { + ensureInit() + val mask = align - 1 + bump = (bump + mask) and mask.inv() + val p = bump + bump += size + return p +} + +fun resetHeap() { + if (bump != 0) bump = BASE +} + +// NOT @WasmExport here: a Kotlin/Wasm library dependency's @WasmExport declarations do not +// survive into a consuming executable's final linked module (verified empirically -- the +// same class of cross-module DCE limitation documented on Guest.kt). The actual +// `cabi_realloc` export is generated by KSP (NativeRegistrationEmitter) in the agent's own +// module, delegating to this function. +fun cabiRealloc(oldPtr: Int, oldSize: Int, align: Int, newSize: Int): Int = alloc(newSize, if (align == 0) 1 else align) + +fun storeByte(ptr: Int, v: Byte) = Pointer(ptr.toUInt()).storeByte(v) +fun loadByte(ptr: Int): Byte = Pointer(ptr.toUInt()).loadByte() +fun storeInt(ptr: Int, v: Int) = Pointer(ptr.toUInt()).storeInt(v) +fun loadInt(ptr: Int): Int = Pointer(ptr.toUInt()).loadInt() +fun storeLong(ptr: Int, v: Long) = Pointer(ptr.toUInt()).storeLong(v) +fun loadLong(ptr: Int): Long = Pointer(ptr.toUInt()).loadLong() +fun storeShort(ptr: Int, v: Short) = Pointer(ptr.toUInt()).storeShort(v) +fun loadShort(ptr: Int): Short = Pointer(ptr.toUInt()).loadShort() + +// kotlin.wasm.unsafe.Pointer has no store/loadFloat/Double -- reinterpret bits via the +// existing store/loadInt (Float, 32 bits) and store/loadLong (Double, 64 bits). +fun storeFloat(ptr: Int, v: Float) = Pointer(ptr.toUInt()).storeInt(v.toRawBits()) +fun loadFloat(ptr: Int): Float = Float.fromBits(Pointer(ptr.toUInt()).loadInt()) +fun storeDouble(ptr: Int, v: Double) = Pointer(ptr.toUInt()).storeLong(v.toRawBits()) +fun loadDouble(ptr: Int): Double = Double.fromBits(Pointer(ptr.toUInt()).loadLong()) + +// The wasmWasi target is declared as an executable (binaries.executable()); it requires a `main`. +// The real reactor entrypoint (agent registration) arrives with the native runtime. +fun main() {} diff --git a/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/WitTypeGrammar.kt b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/WitTypeGrammar.kt new file mode 100644 index 0000000000..173a4638a1 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiMain/kotlin/cloud/golem/wasm/WitTypeGrammar.kt @@ -0,0 +1,74 @@ +package cloud.golem.wasm + +// The witType-string grammar shared by the value lift (Lift.kt) and the schema-graph builder +// (cloud.golem.runtime.AgentTypeModel / ToolModel). A witType is one of: +// primitives: bool, s8, s16, s32, s64, u8, u16, u32, u64, f32, f64, char, string +// record (field names; body is positional at the value level) +// variant (case names; `_` = no payload) +// enum or enum (case names; the value carries only a case index) +// list option tuple map result (`_` = unit ok/err) +// Field/case NAMES matter only for the schema-graph; the value tree is positional, so lift drops +// them. Kotlin identifiers can't contain ',' '<' '>' or ':', so the split helpers are unambiguous. + +/** + * Splits [s] on top-level commas, ignoring commas nested inside `<...>`. An empty string yields an + * empty list (NOT `[""]`) so an empty composite -- `record<>` / `tuple<>` / `variant<>`, e.g. a + * no-argument method's `function-input` -- has zero fields/cases rather than one phantom `""` field. + * The phantom field previously made the value lift read past a zero-length child list, producing a + * garbage value-node index and an out-of-bounds dereference on live oplog/tsv data. + */ +internal fun splitTopLevelCommas(s: String): List { + if (s.isEmpty()) return emptyList() + val parts = mutableListOf() + var depth = 0 + var start = 0 + for (i in s.indices) { + when (s[i]) { + '<' -> depth++ + '>' -> depth-- + ',' -> if (depth == 0) { + parts.add(s.substring(start, i)) + start = i + 1 + } + } + } + parts.add(s.substring(start)) + return parts +} + +/** For a `prefix` witType, returns `INNER`. */ +internal fun innerOf(witType: String, prefix: String): String = witType.substring(prefix.length, witType.length - 1) + +/** + * The immediate child type strings of a composite witType (for recursive type-node registration). + * Primitives and enums have no children. `_` (unit ok/err/payload) is not a real child type. + */ +internal fun childWitTypes(wit: String): List = when { + wit.startsWith("record<") && wit.endsWith(">") -> + splitTopLevelCommas(innerOf(wit, "record<")).map { it.substringAfter(':') } + wit.startsWith("variant<") && wit.endsWith(">") -> + splitTopLevelCommas(innerOf(wit, "variant<")).map { it.substringAfter(':') }.filter { it != "_" } + wit.startsWith("list<") && wit.endsWith(">") -> listOf(innerOf(wit, "list<")) + wit.startsWith("option<") && wit.endsWith(">") -> listOf(innerOf(wit, "option<")) + wit.startsWith("tuple<") && wit.endsWith(">") -> splitTopLevelCommas(innerOf(wit, "tuple<")) + wit.startsWith("map<") && wit.endsWith(">") -> splitTopLevelCommas(innerOf(wit, "map<")) + wit.startsWith("result<") && wit.endsWith(">") -> splitTopLevelCommas(innerOf(wit, "result<")).filter { it != "_" } + else -> emptyList() +} + +/** `record` -> [(name, type)]. Each field is `name:type` (name required). */ +internal fun recordFields(wit: String): List> = splitTopLevelCommas(innerOf(wit, "record<")).map { + val c = it.indexOf(':') + it.substring(0, c) to it.substring(c + 1) +} + +/** `variant` -> [(name, type-or-null)]. `_` payload -> null. */ +internal fun variantCases(wit: String): List> = splitTopLevelCommas(innerOf(wit, "variant<")).map { + val c = it.indexOf(':') + val name = it.substring(0, c) + val t = it.substring(c + 1) + name to (if (t == "_") null else t) +} + +/** `enum` or `enum` -> [c0,c1,...]. */ +internal fun enumCases(wit: String): List = if (wit == "enum") emptyList() else splitTopLevelCommas(innerOf(wit, "enum<")) diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/PrincipalBytesRoundTripTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/PrincipalBytesRoundTripTest.kt new file mode 100644 index 0000000000..dbf34be55a --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/PrincipalBytesRoundTripTest.kt @@ -0,0 +1,24 @@ +package cloud.golem.runtime + +import cloud.golem.Principal +import cloud.golem.Uuid +import kotlin.test.Test +import kotlin.test.assertEquals + +class PrincipalBytesRoundTripTest { + private fun rt(p: Principal) = assertEquals(p, PrincipalBytes.decode(PrincipalBytes.encode(p)), "principal round-trip: $p") + + @Test fun anonymous() = rt(Principal.Anonymous) + + @Test fun agent() = rt(Principal.Agent("agent-123")) + + @Test fun golemUser() = rt(Principal.GolemUser(Uuid(0x0123456789ABCDEFuL, 0xFEDCBA9876543210uL))) + + @Test fun oidcFull() = rt( + Principal.Oidc("s", "iss", "e@x", "n", true, "g", "f", "p", "u", "{}"), + ) + + @Test fun oidcNulls() = rt( + Principal.Oidc("s", "iss", null, null, null, null, null, null, null, ""), + ) +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/PrincipalDecodeTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/PrincipalDecodeTest.kt new file mode 100644 index 0000000000..02068dca23 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/PrincipalDecodeTest.kt @@ -0,0 +1,79 @@ +package cloud.golem.runtime + +import cloud.golem.Principal +import cloud.golem.Uuid +import cloud.golem.wasm.alloc +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeLong +import cloud.golem.wasm.writeStringField +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +/** + * Decodes hand-built, host-shaped `principal` variant buffers (the identity the host passes to + * initialize/invoke) via [liftPrincipal], at the abi-dump-verified offsets: variant tag@0, + * payload@8; oidc-principal fields at their own offsets; agent-id string @16 within agent-id; + * account-id uuid at payload. Pure linear-memory ops (no host WIT import), so they run under the + * plain wasmWasi nodejs runner. + */ +class PrincipalDecodeTest { + + private fun buf() = alloc(112, 8) + + @Test + fun anonymous() { + val b = buf() + storeByte(b, 3) // tag = anonymous + assertEquals(Principal.Anonymous, liftPrincipal(b)) + } + + @Test + fun agent_carries_agent_id_string() { + val b = buf() + storeByte(b, 1) // tag = agent + // agent-principal { agent-id } @8; agent-id { component-id @0 (16B), agent-id: string @16 }. + // string field lives at b + 8 (payload) + 16 = b+24. + writeStringField(b, 24, "example:counter/CounterAgent(\"c1\")") + val p = assertIs(liftPrincipal(b)) + assertEquals("example:counter/CounterAgent(\"c1\")", p.agentId) + } + + @Test + fun golem_user_carries_account_uuid() { + val b = buf() + storeByte(b, 2) // tag = golem-user + storeLong(b + 8, 0x0123456789ABCDEFL) // account-id.uuid.high-bits @ payload+0 + storeLong(b + 16, 0x76543210FEDCBA98L) // low-bits @ payload+8 + val p = assertIs(liftPrincipal(b)) + assertEquals(Uuid(0x0123456789ABCDEFuL, 0x76543210FEDCBA98uL), p.accountId) + } + + @Test + fun oidc_decodes_strings_options_and_email_verified() { + val b = buf() + storeByte(b, 0) // tag = oidc; oidc-principal payload @ b+8 + val o = b + 8 + writeStringField(o, 0, "user-123") // sub @0 + writeStringField(o, 8, "https://issuer") // issuer @8 + storeByte(o + 16, 1) + writeStringField(o, 20, "u@example.com") // email @16 = some + storeByte(o + 28, 0) // name @28 = none + storeByte(o + 40, 1) + storeByte(o + 41, 1) // email-verified @40 = some(true) + storeByte(o + 44, 0) // given-name none + storeByte(o + 56, 0) // family-name none + storeByte(o + 68, 0) // picture none + storeByte(o + 80, 0) // preferred-username none + writeStringField(o, 92, "{\"role\":\"admin\"}") // claims @92 + + val p = assertIs(liftPrincipal(b)) + assertEquals("user-123", p.sub) + assertEquals("https://issuer", p.issuer) + assertEquals("u@example.com", p.email) + assertEquals(null, p.name) + assertEquals(true, p.emailVerified) + assertEquals(null, p.givenName) + assertEquals("{\"role\":\"admin\"}", p.claims) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/ReadOnlyLoweringTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/ReadOnlyLoweringTest.kt new file mode 100644 index 0000000000..0e66e4fde7 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/ReadOnlyLoweringTest.kt @@ -0,0 +1,63 @@ +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.loadByte +import cloud.golem.wasm.loadLong +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Verifies [lowerReadOnlyInto] writes `option` at the abi-dump-verified offsets: + * option tag@0; read-only-config @8 { cache-policy @0 (tag@8, ttl-duration u64 @16), + * uses-principal: bool @24 }. Pure linear-memory ops (no host WIT import), so they run under the + * plain wasmWasi nodejs runner. [lowerOptionStringInto] (the @Prompt hint wiring) is covered here too. + */ +class ReadOnlyLoweringTest { + + private fun buf() = alloc(32, 8) // option(8) + read-only-config(24) + + @Test + fun not_read_only_is_none() { + val b = buf() + lowerReadOnlyInto(b, 0, null) + assertEquals(0, loadByte(b).toInt() and 0xFF, "option tag should be none") + } + + @Test + fun until_write_is_the_default_policy() { + val b = buf() + lowerReadOnlyInto(b, 0, "until-write") + assertEquals(1, loadByte(b).toInt() and 0xFF, "option = some") + assertEquals(1, loadByte(b + 8).toInt() and 0xFF, "cache-policy = until-write") + assertEquals(0, loadByte(b + 8 + 16).toInt() and 0xFF, "uses-principal = false") + } + + @Test + fun no_cache_policy() { + val b = buf() + lowerReadOnlyInto(b, 0, "no-cache") + assertEquals(1, loadByte(b).toInt() and 0xFF) + assertEquals(0, loadByte(b + 8).toInt() and 0xFF, "cache-policy = no-cache") + } + + @Test + fun ttl_policy_carries_nanos() { + val b = buf() + lowerReadOnlyInto(b, 0, "ttl(5000000000)") + assertEquals(1, loadByte(b).toInt() and 0xFF) + assertEquals(2, loadByte(b + 8).toInt() and 0xFF, "cache-policy = ttl") + assertEquals(5_000_000_000L, loadLong(b + 8 + 8), "ttl duration nanos") + assertEquals(0, loadByte(b + 8 + 16).toInt() and 0xFF, "uses-principal = false") + } + + @Test + fun option_string_none_and_some() { + val none = buf() + lowerOptionStringInto(none, 0, "") + assertEquals(0, loadByte(none).toInt() and 0xFF, "empty hint = none") + + val some = buf() + lowerOptionStringInto(some, 0, "do the thing") + assertEquals(1, loadByte(some).toInt() and 0xFF, "non-empty hint = some") + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaGraphBuilderTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaGraphBuilderTest.kt new file mode 100644 index 0000000000..439a0c3ae0 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaGraphBuilderTest.kt @@ -0,0 +1,74 @@ +package cloud.golem.runtime + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Tests the recursive schema-graph type-node collector + the composite `agent-type` lowering. + * collectTypeNodes is pure logic (index assignment / dedup / child-before-parent ordering); + * lowerAgentType exercises every composite type-body writer against real linear memory (runs + * under the wasmWasi nodejs runner -- no host WIT import). + */ +class SchemaGraphBuilderTest { + + @Test + fun children_are_registered_before_their_parent() { + val idx = collectTypeNodes(listOf("record")) + assertEquals(0, idx["s32"]) + assertEquals(1, idx["string"]) + assertEquals(2, idx["record"]) + } + + @Test + fun duplicate_child_types_are_deduped() { + val idx = collectTypeNodes(listOf("record")) + assertEquals(setOf("s32", "record"), idx.keys) + assertEquals(0, idx["s32"]) + assertEquals(1, idx["record"]) + } + + @Test + fun nested_composites_register_all_transitive_types() { + val idx = collectTypeNodes(listOf("list>>")) + // s32 -> option -> record<...> -> list<...>, each once, children first. + assertEquals(listOf("s32", "option", "record>", "list>>"), idx.keys.toList()) + } + + @Test + fun empty_roots_fall_back_to_a_single_s32_node() { + assertEquals(mapOf("s32" to 0), collectTypeNodes(emptyList())) + } + + private fun descriptorWith(paramWit: String, outputWit: String): NativeAgentDescriptor = NativeAgentDescriptor( + typeName = "T", + description = "d", + mountPath = "", + constructorParams = listOf(NativeParamSchema("p", paramWit)), + methods = listOf( + NativeMethodDescriptor("m", outputWit, listOf(NativeParamSchema("q", paramWit)), emptyList()) { _, _ -> SchemaValue.Unit_ }, + ), + factory = { _ -> Any() }, + ) + + @Test + fun lowerAgentType_runs_for_every_composite_kind() { + // Each returns a non-zero agent-type pointer iff its type-body writer ran without an + // out-of-bounds / missing-index error -- exercising record/variant/enum/list/option/ + // tuple/map/result bodies + the recursive child references. + val kinds = listOf( + "record", + "variant", + "enum", + "list", + "option", + "tuple", + "map", + "result", + "list>>", // deeply nested + ) + for (wit in kinds) { + assertTrue(lowerAgentType(descriptorWith(wit, wit)) != 0, "lowerAgentType failed for $wit") + } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaTypeDecodeTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaTypeDecodeTest.kt new file mode 100644 index 0000000000..e622d9ae8c --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaTypeDecodeTest.kt @@ -0,0 +1,126 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Directly exercises [schemaNodeToWitType] -- the schema-graph -> witType-string reconstruction used + * when decoding a live host `typed-schema-value` (oplog / durable persist / tool-rpc). Builds minimal + * schema-graphs in linear memory (the abi-dump-crafted style) and asserts the reconstructed type + * string for the schema-type-body cases the SDK's own encoder never emits but a real host graph can: + * ref-type (named-definition indirection), the rich semantic scalars, fixed-list, union, and the + * WASI-P3 future/stream stubs. Regression for the "unsupported schema-type-body tag" trap. + */ +class SchemaTypeDecodeTest { + + // schema-type-node: 144B, body@0 (tag@0, payload@8). schema-type-def: 24B (id@0, name@8, body@20). + private companion object { + const val NODE = 144 + const val NODE_PAYLOAD = 8 + const val DEF = 24 + const val DEF_BODY = 20 + } + + /** Allocates [n] zeroed schema-type-nodes and returns the base pointer. */ + private fun nodes(n: Int): Int { + val base = alloc(n * NODE, 8) + for (i in 0 until n * NODE) storeByte(base + i, 0) + return base + } + + /** Writes schema-type-body [tag] into node [idx]. */ + private fun tag(base: Int, idx: Int, tag: Int) = storeByte(base + idx * NODE, tag.toByte()) + + /** node payload address for node [idx]. */ + private fun payload(base: Int, idx: Int) = base + idx * NODE + NODE_PAYLOAD + + /** Writes a canonical-ABI string (ptr,len) field at [addr]. */ + private fun writeStr(addr: Int, s: String) { + val bytes = s.encodeToByteArray() + val ptr = alloc(bytes.size.coerceAtLeast(1), 1) + for (i in bytes.indices) storeByte(ptr + i, bytes[i]) + storeInt(addr, ptr) + storeInt(addr + 4, bytes.size) + } + + private fun witTypeOf(typeNodesPtr: Int, defsPtr: Int, root: Int) = schemaNodeToWitType(typeNodesPtr, defsPtr, root) + + @Test + fun rich_semantic_scalars_reconstruct_by_fixed_name() { + // tag -> expected string, for the leaf semantic types whose value ignores the restrictions. + val cases = mapOf( + 17 to "flags", 24 to "text", 25 to "binary", 26 to "path", + 27 to "url", 29 to "duration", 30 to "quantity", 32 to "secret", 33 to "quota-token", + ) + for ((t, expected) in cases) { + val n = nodes(1) + tag(n, 0, t) + assertEquals(expected, witTypeOf(n, 0, 0), "schema-type-body tag=$t") + } + } + + @Test + fun ref_type_resolves_through_defs() { + // node0 = ref-type(def 0); defs[0].body = node1 = string-type. + val n = nodes(2) + tag(n, 0, 0) // ref-type + storeInt(payload(n, 0), 0) // def-index = 0 + tag(n, 1, 13) // string-type + val defs = alloc(DEF, 4) + for (i in 0 until DEF) storeByte(defs + i, 0) + storeInt(defs + DEF_BODY, 1) // def.body -> node1 + assertEquals("string", witTypeOf(n, defs, 0)) + } + + @Test + fun fixed_list_reconstructs_element() { + // node0 = fixed-list-type(element = node1 = string). + val n = nodes(2) + tag(n, 0, 20) + storeInt(payload(n, 0), 1) // fixed-list-spec.element @0 + tag(n, 1, 13) + assertEquals("fixed-list", witTypeOf(n, 0, 0)) + } + + @Test + fun union_reconstructs_tagged_branches() { + // node0 = union-type with branches [a -> node1(string), b -> node2(s32)]. + val n = nodes(3) + tag(n, 0, 31) + tag(n, 1, 13) // string + tag(n, 2, 4) // s32 + val branches = alloc(2 * 92, 4) + for (i in 0 until 2 * 92) storeByte(branches + i, 0) + writeStr(branches + 0, "a") + storeInt(branches + 8, 1) // branch0: tag "a", body node1 + writeStr(branches + 92, "b") + storeInt(branches + 92 + 8, 2) // branch1: tag "b", body node2 + storeInt(payload(n, 0), branches) // union-spec.branches ptr + storeInt(payload(n, 0) + 4, 2) // union-spec.branches len + assertEquals("union", witTypeOf(n, 0, 0)) + } + + @Test + fun future_and_stream_stubs() { + // some(inner) -> future/stream; none -> bare future/stream. + run { + val n = nodes(2) + tag(n, 0, 34) // future + storeByte(payload(n, 0), 1) // option = some + storeInt(payload(n, 0) + 4, 1) // inner -> node1 + tag(n, 1, 13) + assertEquals("future", witTypeOf(n, 0, 0)) + } + run { + val n = nodes(1) + tag(n, 0, 35) // stream + storeByte(payload(n, 0), 0) // option = none + assertEquals("stream", witTypeOf(n, 0, 0)) + } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaValueBytesRoundTripTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaValueBytesRoundTripTest.kt new file mode 100644 index 0000000000..a515dfdb75 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SchemaValueBytesRoundTripTest.kt @@ -0,0 +1,42 @@ +package cloud.golem.runtime + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SchemaValueBytesRoundTripTest { + private fun rt(v: SchemaValue) = assertEquals(v, SchemaValueBytes.decode(SchemaValueBytes.encode(v)), "round-trip: $v") + + @Test fun primitives() { + rt(SchemaValue.Bool(true)) + rt(SchemaValue.S8(-8)) + rt(SchemaValue.S16(-16)) + rt(SchemaValue.S64(-64L)) + rt(SchemaValue.U8(8u)) + rt(SchemaValue.U16(16u)) + rt(SchemaValue.U64(64uL)) + rt(SchemaValue.F32(1.5f)) + rt(SchemaValue.F64(2.5)) + rt(SchemaValue.Chr('x')) + rt(SchemaValue.Str("hello")) + rt(SchemaValue.Unit_) + } + + @Test fun nestedComposite() { + rt( + SchemaValue.Record( + listOf( + SchemaValue.ListVal(listOf(SchemaValue.S32(1), SchemaValue.S32(2))), + SchemaValue.OptionVal(SchemaValue.Str("x")), + SchemaValue.OptionVal(null), + SchemaValue.EnumVal(2), + SchemaValue.VariantVal(1, SchemaValue.S32(9)), + SchemaValue.VariantVal(0, null), + SchemaValue.TupleVal(listOf(SchemaValue.S32(3), SchemaValue.Str("t"))), + SchemaValue.MapVal(listOf(SchemaValue.Str("k") to SchemaValue.S32(1))), + SchemaValue.ResultVal(true, SchemaValue.S32(7)), + SchemaValue.DatetimeVal(1000L, 500), + ), + ), + ) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SnapshotEnvelopeRoundTripTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SnapshotEnvelopeRoundTripTest.kt new file mode 100644 index 0000000000..766a98787f --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/SnapshotEnvelopeRoundTripTest.kt @@ -0,0 +1,23 @@ +package cloud.golem.runtime + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class SnapshotEnvelopeRoundTripTest { + @Test fun roundTrip() { + val d = SnapshotEnvelope.decode(SnapshotEnvelope.encode(byteArrayOf(1, 2, 3), byteArrayOf(9, 8, 7, 6))) + assertEquals(listOf(1, 2, 3), d.principal.toList()) + assertEquals(listOf(9, 8, 7, 6), d.state.toList()) + } + + @Test fun emptyState() { + val d = SnapshotEnvelope.decode(SnapshotEnvelope.encode(byteArrayOf(3), ByteArray(0))) + assertEquals(listOf(3), d.principal.toList()) + assertEquals(0, d.state.size) + } + + @Test fun rejectsUnknownVersion() { + assertFailsWith { SnapshotEnvelope.decode(byteArrayOf(99, 0, 0, 0, 0)) } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/ToolInvokeResultDecodeTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/ToolInvokeResultDecodeTest.kt new file mode 100644 index 0000000000..a17d2156fe --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/ToolInvokeResultDecodeTest.kt @@ -0,0 +1,75 @@ +package cloud.golem.runtime + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt +import cloud.golem.wasm.writeStringField +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull + +/** + * Decodes hand-built, host-shaped `result` buffers (the wire shape + * `tool-rpc.invoke-and-await` / `future-invoke-result.get` return) via [liftToolInvokeResult], at + * the abi-dump-verified literal offsets. Pure linear-memory ops (no host WIT import), so they run + * under the plain wasmWasi nodejs runner. The host-calling invoke paths themselves are + * compile-verified; this exercises the decode logic they depend on. + * + * Layout: `result` = 48B, tag@0, payload@4. + * ok = invocation-result @4: result option (tag@4, tsv@8..39), stdout option (tag@40, handle@44). + * err = rpc-error @4: tag@4, string ptr@8 / len@12. + */ +class ToolInvokeResultDecodeTest { + + @Test + fun ok_with_composite_result_and_no_stdout() { + val tsv = TypedSchemaValue( + "record", + SchemaValue.Record(listOf(SchemaValue.S32(7), SchemaValue.Str("hi"))), + ) + val buf = alloc(48, 4) + storeByte(buf, 0) // result tag = ok + storeByte(buf + 4, 1) // invocation-result.result option = some + lowerTypedSchemaValueInto(buf + 8, tsv) + storeByte(buf + 40, 0) // stdout option = none + + val ok = assertIs(liftToolInvokeResult(buf)) + assertEquals(tsv, ok.value.result) + assertNull(ok.value.stdoutHandle) + } + + @Test + fun ok_with_stdout_handle_and_no_result() { + val buf = alloc(48, 4) + storeByte(buf, 0) // ok + storeByte(buf + 4, 0) // result option = none + storeByte(buf + 40, 1) // stdout option = some + storeInt(buf + 44, 4242) // stdout output-stream handle + + val ok = assertIs(liftToolInvokeResult(buf)) + assertNull(ok.value.result) + assertEquals(4242, ok.value.stdoutHandle) + } + + @Test + fun err_not_found_carries_message() { + val buf = alloc(48, 4) + storeByte(buf, 1) // result tag = err + storeByte(buf + 4, 2) // rpc-error tag = not-found + writeStringField(buf + 4, 4, "no such command") // string @ rpc-error+4 (ptr@8 / len@12) + + val err = assertIs(liftToolInvokeResult(buf)) + assertEquals(ToolRpcError.NotFound("no such command"), err.error) + } + + @Test + fun err_remote_tool_error_has_no_message() { + val buf = alloc(48, 4) + storeByte(buf, 1) // err + storeByte(buf + 4, 4) // rpc-error tag = remote-tool-error (no string payload) + + val err = assertIs(liftToolInvokeResult(buf)) + assertEquals(ToolRpcError.RemoteToolError, err.error) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/TypedSchemaValueRoundTripTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/TypedSchemaValueRoundTripTest.kt new file mode 100644 index 0000000000..f597ab93f8 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/TypedSchemaValueRoundTripTest.kt @@ -0,0 +1,112 @@ +package cloud.golem.runtime + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Round-trips `typed-schema-value`s through [lowerTypedSchemaValue] (encode: builds the + * self-describing schema-graph + value tree) -> [liftTypedSchemaValue] (decode: walks the + * schema-graph back to a witType-string, then lifts the value). Asserts both the reconstructed + * WIT type and the value match. Pure linear-memory ops (no host WIT import), so they run under the + * plain wasmWasi nodejs runner. Exercises the composite schema-graph decode (records, variants, + * enums, lists, options, tuples, maps, results — arbitrarily nested). + */ +class TypedSchemaValueRoundTripTest { + + private fun roundTrip(witType: String, value: SchemaValue) { + val tsv = TypedSchemaValue(witType, value) + val ptr = lowerTypedSchemaValue(tsv) + assertEquals(tsv, liftTypedSchemaValue(ptr), "typed-schema-value round-trip mismatch for $witType") + } + + @Test + fun primitives() { + roundTrip("s32", SchemaValue.S32(42)) + roundTrip("string", SchemaValue.Str("hello")) + roundTrip("bool", SchemaValue.Bool(true)) + roundTrip("u64", SchemaValue.U64(9uL)) + roundTrip("f64", SchemaValue.F64(-1.25)) + } + + @Test + fun record_of_primitives() { + roundTrip( + "record", + SchemaValue.Record(listOf(SchemaValue.S32(7), SchemaValue.Str("hi"), SchemaValue.Bool(false))), + ) + } + + @Test + fun list_and_option() { + roundTrip("list", SchemaValue.ListVal(listOf(SchemaValue.Str("a"), SchemaValue.Str("b")))) + roundTrip("option", SchemaValue.OptionVal(SchemaValue.S32(5))) + roundTrip("option", SchemaValue.OptionVal(null)) + } + + @Test + fun tuple_and_map() { + roundTrip( + "tuple", + SchemaValue.TupleVal(listOf(SchemaValue.S32(1), SchemaValue.Str("x"))), + ) + roundTrip( + "map", + SchemaValue.MapVal(listOf(SchemaValue.Str("k1") to SchemaValue.S32(1), SchemaValue.Str("k2") to SchemaValue.S32(2))), + ) + } + + @Test + fun variant_and_enum() { + // variant: case a carries an s32 payload, case b is payloadless. + roundTrip("variant", SchemaValue.VariantVal(0, SchemaValue.S32(99))) + roundTrip("variant", SchemaValue.VariantVal(1, null)) + roundTrip("enum", SchemaValue.EnumVal(2)) + } + + @Test + fun result() { + roundTrip("result", SchemaValue.ResultVal(true, SchemaValue.S32(3))) + roundTrip("result", SchemaValue.ResultVal(false, SchemaValue.Str("boom"))) + } + + @Test + fun datetime() { + roundTrip("datetime", SchemaValue.DatetimeVal(1_700_000_000L, 500)) + // as a record field, to prove datetime participates in the composite schema-graph + roundTrip( + "record", + SchemaValue.Record(listOf(SchemaValue.DatetimeVal(42L, 7), SchemaValue.Str("x"))), + ) + } + + @Test + fun empty_record_and_tuple() { + // A no-argument agent method's `function-input` decodes to an EMPTY record/tuple. The graph + // yields the type string "record<>" / "tuple<>"; the value tree's node has a zero-length + // child list. Regression for the live-oplog OOB trap: splitTopLevelCommas("") used to return + // a single phantom "" field, so the lift read a child index past the empty list -> a garbage + // value-node index -> out-of-bounds dereference. Must round-trip to an empty collection. + roundTrip("record<>", SchemaValue.Record(emptyList())) + roundTrip("tuple<>", SchemaValue.TupleVal(emptyList())) + // The same empty record nested as a field, to prove the graph/value walk stays in bounds + // when the empty collection is not the root. + roundTrip( + "record,label:string>", + SchemaValue.Record(listOf(SchemaValue.Record(emptyList()), SchemaValue.Str("x"))), + ) + } + + @Test + fun deeply_nested() { + // record,meta:option>> + val witType = "record,meta:option>>" + val value = SchemaValue.Record( + listOf( + SchemaValue.S32(1), + SchemaValue.ListVal(listOf(SchemaValue.Str("hot"), SchemaValue.Str("new"))), + SchemaValue.OptionVal(SchemaValue.Record(listOf(SchemaValue.Str("region"), SchemaValue.S32(7)))), + ), + ) + roundTrip(witType, value) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/host/RetryDslTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/host/RetryDslTest.kt new file mode 100644 index 0000000000..0d02460f97 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/host/RetryDslTest.kt @@ -0,0 +1,76 @@ +package cloud.golem.runtime.host + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +/** + * Pure-logic tests for the RetryDsl flatten/unflatten round-trips + validation. These don't touch + * any host WIT import, so they run under the plain wasmWasi nodejs runner. + */ +class RetryDslTest { + + @Test + fun predicate_roundtrips_through_the_flat_node_list() { + val predicate = (Props.statusCode eq 503) or + (Props.errorType eq "timeout") and + !(Props.function startsWith "internal.") + // Flatten then rebuild; the tree must survive intact. + assertEquals(predicate, predicate.toRetryPredicate().toPredicate()) + } + + @Test + fun policy_roundtrips_with_nested_modifiers_and_a_filter() { + val policy = Policy.exponential(100.milliseconds, factor = 2.0) + .withJitter(0.2) + .clamp(50.milliseconds, 5.seconds) + .maxRetries(5) + .onlyWhen(Props.statusCode gte 500 and (Props.dbType neq "sqlite")) + .andThen(Policy.periodic(1.seconds).maxRetries(3)) + assertEquals(policy, policy.toRetryPolicy().toPolicy()) + } + + @Test + fun named_policy_roundtrips() { + val named = NamedPolicy( + name = "flaky-http", + policy = Policy.fibonacci(100.milliseconds, 200.milliseconds).maxRetries(10), + priority = 42, + predicate = Props.uriScheme eq "https", + ).withPriority(7).appliesWhen(Props.statusCode.oneOf(502, 503, 504)) + assertEquals(named, named.toNamedRetryPolicy().toNamedPolicy()) + } + + @Test + fun flattened_root_is_node_zero() { + val flat = Policy.periodic(1.seconds).maxRetries(3).toRetryPolicy() + // The outermost combinator (count-box) is the root and must sit at index 0. + assertTrue(flat.nodes[0] is PolicyNode.CountBox) + } + + @Test + fun predicate_values_are_typed_by_the_infix_overloads() { + assertEquals(Predicate.Eq("status-code", PredicateValue.Integer(500)), Props.statusCode eq 500) + assertEquals(Predicate.Eq("error-type", PredicateValue.Text("x")), Props.errorType eq "x") + assertEquals(Predicate.Eq("k", PredicateValue.Bool(true)), Props("k") eq true) + } + + @Test + fun validation_rejects_bad_inputs() { + assertFailsWith { Policy.exponential(1.seconds, factor = 0.0) } // factor must be > 0 + assertFailsWith { Policy.exponential(1.seconds, factor = Double.NaN) } // must be finite + assertFailsWith { Policy.immediate.clamp(5.seconds, 1.seconds) } // min > max + assertFailsWith { Policy.immediate.maxRetries(-1) } // uint32 range + assertFailsWith { Policy.immediate.maxRetries(0x1_0000_0000L) } // uint32 range + } + + @Test + fun unflatten_detects_a_cycle() { + // A hand-built policy whose only node references itself. + val cyclic = RetryPolicy(listOf(PolicyNode.CountBox(CountBoxConfig(1u, inner = 0)))) + assertFailsWith { cyclic.toPolicy() } + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/host/SecretErrorDecodeTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/host/SecretErrorDecodeTest.kt new file mode 100644 index 0000000000..0e47201696 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/runtime/host/SecretErrorDecodeTest.kt @@ -0,0 +1,48 @@ +package cloud.golem.runtime.host + +import cloud.golem.wasm.alloc +import cloud.golem.wasm.storeByte +import cloud.golem.wasm.storeInt +import cloud.golem.wasm.writeStringField +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Decodes hand-built, host-shaped `secret-error` buffers (the err arm of `reveal`'s + * `result`) via [liftSecretError], at the abi-dump-verified + * offsets (tag@0, string/list payload ptr@4/len@8). Pure linear-memory ops (no host WIT import), + * so they run under the plain wasmWasi nodejs runner. The reveal call path itself is + * compile-verified; the request-side schema-graph build is covered by the schema-graph/typed- + * schema-value round-trip tests. + */ +class SecretErrorDecodeTest { + + @Test + fun unavailable_carries_message() { + val b = alloc(12, 4) + storeByte(b, 0) // tag = unavailable + writeStringField(b, 4, "store gone") // string @ b+4 (ptr) / b+8 (len) + assertEquals(SecretError.Unavailable("store gone"), liftSecretError(b)) + } + + @Test + fun internal_carries_message() { + val b = alloc(12, 4) + storeByte(b, 2) // tag = internal + writeStringField(b, 4, "boom") + assertEquals(SecretError.Internal("boom"), liftSecretError(b)) + } + + @Test + fun version_not_found_carries_bytes() { + val bytes = alloc(3, 1) + storeByte(bytes, 1) + storeByte(bytes + 1, 2) + storeByte(bytes + 2, 3) + val b = alloc(12, 4) + storeByte(b, 1) // tag = version-not-found + storeInt(b + 4, bytes) // secret-version.bytes list: ptr@b+4 + storeInt(b + 8, 3) // len@b+8 + assertEquals(SecretError.VersionNotFound(listOf(1u, 2u, 3u)), liftSecretError(b)) + } +} diff --git a/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/wasm/ValueRoundTripTest.kt b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/wasm/ValueRoundTripTest.kt new file mode 100644 index 0000000000..bf395734f0 --- /dev/null +++ b/sdks/kotlin/sdk/src/wasmWasiTest/kotlin/cloud/golem/wasm/ValueRoundTripTest.kt @@ -0,0 +1,112 @@ +@file:OptIn(kotlin.wasm.unsafe.UnsafeWasmMemoryApi::class) + +package cloud.golem.wasm + +import cloud.golem.runtime.SchemaValue +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Round-trips composite `schema-value-tree` VALUES through buildSchemaValueTree (lower) -> + * liftSingleValue (lift), asserting structural equality. Pure linear-memory ops (no host WIT + * import), so they run under the plain wasmWasi nodejs runner. Verifies the new `record<...>` + * lift case + the previously-untested list/option/tuple/map/variant/enum composite cases. + */ +class ValueRoundTripTest { + + private fun roundTrip(value: SchemaValue, witType: String) { + val tree = buildSchemaValueTree(value) + assertEquals(value, liftSingleValue(tree, witType), "round-trip mismatch for $witType") + } + + @Test + fun primitives() { + roundTrip(SchemaValue.S32(42), "s32") + roundTrip(SchemaValue.Str("hello"), "string") + roundTrip(SchemaValue.Bool(true), "bool") + roundTrip(SchemaValue.S64(-9L), "s64") + roundTrip(SchemaValue.F64(3.5), "f64") + roundTrip(SchemaValue.U32(7u), "u32") + } + + @Test + fun record_of_primitives() { + roundTrip( + SchemaValue.Record(listOf(SchemaValue.S32(7), SchemaValue.Str("hi"), SchemaValue.Bool(false))), + "record", + ) + } + + @Test + fun nested_record() { + roundTrip( + SchemaValue.Record( + listOf( + SchemaValue.Str("a"), + SchemaValue.Record(listOf(SchemaValue.S32(1), SchemaValue.S32(2))), + ), + ), + "record>", + ) + } + + @Test + fun list_of_records() { + roundTrip( + SchemaValue.ListVal( + listOf( + SchemaValue.Record(listOf(SchemaValue.S32(1))), + SchemaValue.Record(listOf(SchemaValue.S32(2))), + ), + ), + "list>", + ) + } + + @Test + fun list_option_tuple_map_variant_enum() { + roundTrip(SchemaValue.ListVal(listOf(SchemaValue.S32(1), SchemaValue.S32(2))), "list") + roundTrip(SchemaValue.OptionVal(SchemaValue.Str("x")), "option") + roundTrip(SchemaValue.OptionVal(null), "option") + roundTrip(SchemaValue.TupleVal(listOf(SchemaValue.S32(1), SchemaValue.Str("a"))), "tuple") + roundTrip(SchemaValue.MapVal(listOf(SchemaValue.Str("k") to SchemaValue.S32(9))), "map") + roundTrip(SchemaValue.VariantVal(1, SchemaValue.S32(5)), "variant") + roundTrip(SchemaValue.EnumVal(2), "enum") + } + + @Test + fun union_lifts_body_by_matched_branch_tag() { + // A union value carries the matched branch's string tag; the lift must select that branch's + // body type (branches are heterogeneous) rather than assume a single shared body type. + roundTrip(SchemaValue.UnionVal("b", SchemaValue.S32(5)), "union") + roundTrip(SchemaValue.UnionVal("a", SchemaValue.Str("hi")), "union") + } + + @Test + fun fixed_list_value_lifts_like_a_list() { + // No encoder emits fixed-list-value (schema-value-node tag 19), so craft one by patching a + // list-value tree's root tag 18 -> 19, then lift against the "fixed-list" the schema + // decoder now produces. Value shape is identical to a list, so it lifts to a ListVal. + val tree = buildSchemaValueTree(SchemaValue.ListVal(listOf(SchemaValue.S32(1), SchemaValue.S32(2)))) + val nodesPtr = loadInt(tree) // schema-value-tree.value-nodes ptr @0 + val root = loadInt(tree + 8) // schema-value-tree.root @8 + storeByte(nodesPtr + root * 32, 19) // list-value(18) -> fixed-list-value(19); node size 32, tag @0 + assertEquals( + SchemaValue.ListVal(listOf(SchemaValue.S32(1), SchemaValue.S32(2))), + liftSingleValue(tree, "fixed-list"), + ) + } + + @Test + fun record_containing_a_list_and_an_option() { + roundTrip( + SchemaValue.Record( + listOf( + SchemaValue.ListVal(listOf(SchemaValue.S32(1), SchemaValue.S32(2), SchemaValue.S32(3))), + SchemaValue.OptionVal(SchemaValue.Str("present")), + ), + ), + "record,note:option>", + ) + } +} From 3f9967848b645010515952b208e4bfc260057fc2 Mon Sep 17 00:00:00 2001 From: JohnSColeman Date: Mon, 13 Jul 2026 00:58:49 +0700 Subject: [PATCH 04/11] feat(kotlin): KSP annotation processor (agent registration + codegen) --- sdks/kotlin/ksp/.gitignore | 3 + sdks/kotlin/ksp/build.gradle.kts | 40 ++ .../ksp/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + sdks/kotlin/ksp/gradlew | 248 +++++++++ sdks/kotlin/ksp/gradlew.bat | 93 ++++ sdks/kotlin/ksp/settings.gradle.kts | 1 + .../main/kotlin/cloud/golem/ksp/AgentModel.kt | 69 +++ .../cloud/golem/ksp/ConverterCodegen.kt | 100 ++++ .../cloud/golem/ksp/GolemAgentProcessor.kt | 199 +++++++ .../golem/ksp/GolemAgentProcessorProvider.kt | 12 + .../kotlin/cloud/golem/ksp/HttpValidation.kt | 96 ++++ .../golem/ksp/NativeRegistrationEmitter.kt | 276 +++++++++ .../cloud/golem/ksp/RemoteAgentEmitter.kt | 93 ++++ .../main/kotlin/cloud/golem/ksp/TypeDesc.kt | 78 +++ .../main/kotlin/cloud/golem/ksp/TypeMapper.kt | 154 +++++ .../main/kotlin/cloud/golem/ksp/WitEmitter.kt | 80 +++ ...ols.ksp.processing.SymbolProcessorProvider | 1 + .../cloud/golem/ksp/ConverterCodegenTest.kt | 59 ++ .../golem/ksp/GolemAgentProcessorTest.kt | 525 ++++++++++++++++++ .../cloud/golem/ksp/HttpValidationTest.kt | 124 +++++ .../kotlin/cloud/golem/ksp/TypeMapperTest.kt | 47 ++ 22 files changed, 2305 insertions(+) create mode 100644 sdks/kotlin/ksp/.gitignore create mode 100644 sdks/kotlin/ksp/build.gradle.kts create mode 100644 sdks/kotlin/ksp/gradle/wrapper/gradle-wrapper.jar create mode 100644 sdks/kotlin/ksp/gradle/wrapper/gradle-wrapper.properties create mode 100755 sdks/kotlin/ksp/gradlew create mode 100644 sdks/kotlin/ksp/gradlew.bat create mode 100644 sdks/kotlin/ksp/settings.gradle.kts create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/AgentModel.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/ConverterCodegen.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/GolemAgentProcessor.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/GolemAgentProcessorProvider.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/HttpValidation.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/NativeRegistrationEmitter.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/RemoteAgentEmitter.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/TypeDesc.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/TypeMapper.kt create mode 100644 sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/WitEmitter.kt create mode 100644 sdks/kotlin/ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider create mode 100644 sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/ConverterCodegenTest.kt create mode 100644 sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/GolemAgentProcessorTest.kt create mode 100644 sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/HttpValidationTest.kt create mode 100644 sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/TypeMapperTest.kt diff --git a/sdks/kotlin/ksp/.gitignore b/sdks/kotlin/ksp/.gitignore new file mode 100644 index 0000000000..3d0fb34319 --- /dev/null +++ b/sdks/kotlin/ksp/.gitignore @@ -0,0 +1,3 @@ +/build +/.gradle +/.kotlin diff --git a/sdks/kotlin/ksp/build.gradle.kts b/sdks/kotlin/ksp/build.gradle.kts new file mode 100644 index 0000000000..1b8b84969a --- /dev/null +++ b/sdks/kotlin/ksp/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + kotlin("jvm") version "2.4.0" + `maven-publish` + id("org.jlleitschuh.gradle.ktlint") version "14.2.0" +} + +group = "cloud.golem" +version = "0.0.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + // KSP2 API (standalone versioning, decoupled from the Kotlin patch; works with 2.4.0). + // The processor runs on the JVM, not Wasm. + implementation("com.google.devtools.ksp:symbol-processing-api:2.3.9") + + testImplementation(kotlin("test")) + // Maintained kotlin-compile-testing fork (supports KSP2 + recent Kotlin). + testImplementation("dev.zacsweers.kctfork:ksp:0.13.0") +} + +tasks.test { + useJUnitPlatform() +} + +// kctfork's compile-testing API is annotated @ExperimentalCompilerApi. +tasks.withType().configureEach { + compilerOptions { + optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") + } +} + +publishing { + repositories { mavenLocal() } + publications { + create("maven") { from(components["java"]) } + } +} diff --git a/sdks/kotlin/ksp/gradle/wrapper/gradle-wrapper.jar b/sdks/kotlin/ksp/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu z%-lD<@61@6?qUPjUg|mF7!P7`hx+st`i!^L7HVHtzwnM z)LuOANIzT#9tU4)C^WIXhZWqrO;jr_O5aErkklzt)R-JmAh8xHMJ>x>OvTiuRi}FY z-o@0kFwwl7p|ro=*2q*cFRX5GCq-v!LPD)Sq+Uz~UkOwx-?X&!Q^4H)$|;=n9{idC z0mJl`tCTs3+e_EFVzQ}s`f_4fijsucWy5y zarHoT>Q06Z4yI1RPNpW`@4hSzZT|J`MU3i(GqNhm*9O@MndJ{31uA^i zXo&^c`EZ}5W)(|YMl##@MuSK#wyZ3dwJEz*n@C(Ry$|d`^D=thayXFqxt*WW&sWdI zdm1wv#VCKa<7d2Qc#qzvUvivhK5wq*djL7Wqjvf}-c~}d#G)eG`(u<`NGei`BFe4Q ztTSs?Gc8Ff%_5T4ce&J0v*FT`y_9r!Po=sPtHs5~BlV6VEUNzxU+)+sX}ffdPTRI^ z+qP}ns9yQgjY^t0ddMx1Yd`|OB{sHnUC-B;qum1|`tR#P_@llx>d z=qpNN&?nZib(t90A9F*U%1GbB+O;dq!cNgmmdCrK=(zS1zg*9(7VMfv)QMkt_F=wz zHX2p4X-R*=tJI4A)3SrL`H^peBNHh&XC#sVR3D zt17qeF>BaCZNlQO7n@@BuWs&l(FtRjaVn~wW^x-GsjpFH!ETyl7Od{Wf;4=bzL5nj zW9c^ZodMnN{3Jkz2j2;qhCm1ede*6891vR9?(Dy)N|iENw}HKLIOrjB0x)pEs-aS{ zZR$tEyZxbP(;(l43^KjRtSuirNmw~Bg&6p;)vqM*>S#L>0+Pw5CU%4@&)8OX2ykYQ z^f^hk-5%!QzuzYniL*1Gs#S5Kp_*ld1EAmkInP+^w?#(?rbC2Bm&0c5Ko@6`_ zi!Nvd391nu^@AmpZ$_0fPR2~kQGJS7lSGwA7U>s@+!d_`(P5y;MT#U~_ONSo9d+bf zVj6MgWN=|%#Qn;vl*TNLE$Mw|*89{yJ=WN>j{?T*vqa$U$2_dg46R)8wl&CNS&iK{ z>HDBC9e3b3roJd}gK!T>takKP);KLj_9T;%knG_fN^S$4hb`E|)qy__^=mm&Z{~CF zhc*PxdrJ@xRkQ-8lbh3Ys@2ZaR)Q3z**-VSgeMHE>c5AH1bpSUor&dgTiMd5Wn|(# z8Rwb{#uWZG(Jo0co98|mg5zF}M*d>gAg|Zdex@}Ps&`51({MmNyHF;GD4EBT`oP|X zd=Tq9JYz*IP%@2oujruVrK#jAT97|%ww60Ov2He^5zA4)VihJ$-bxoaqE7zU$rmK) z#O!xp&k$!TOEiC8+p6`Q)uNg4u8*chnx*aw=#oP~05DS&8gnL>^zpBkqqiSQA{Ita z%-)qosk1^`p&aB@rZ#)&3_|u{QqZO z{f{A3)XMprL}2{=pM$*`z*fY;{=4e=u7&=s+zI)ANd+V!L%#^2hpy@#N-WbB%U2Zl zgD_E0AVVWdMiFi_u2qqxeAsRzD%>l|g-|#$ayD3wHoT{EUS2Qe zEq=ryLi%iMZ`b}tSYzHInTJ{mY{OXy0)T&Rly3ippqpTk%A{T+e?K}j zURM^%!ZIWxW$32?Z&q9)Rao;#KQuLv+^ft>o|6c@QD=_}ql%5Th=cR{P)_51Qxjh# zRJW<|qmpRn3(K1lMwU-ayxjsgKS`Q7J5m0kw|LQb=CbyahnoQTWY z?g8-#_J+=*r`Jc|A0(MOvTc0kT-tBLIIFCd6Y5iCr>cqubJu0`Ox+FkDWs^L{;0mc zxk-nf?rxh(N<1B;<;9PSrR4D<*5!DvA()O7{vl9sps3x_-Y_w>qC3OI!_Wyza8K|E zAvJvWYyu)(z*TK7e+Q#dFWd_7%;fn4Ex*lEY2$X%SP9K9d6yWC2M!3>3>tu}g4R*V zRMC!~oYyF#Izu$lGjfQ?q}KD$rpDMRjF?f>6kuBlE`z4Yxy(Y(Y+Dr#PKA}UsSWD? zm|ER_O==Y22{m%cO1jhu`8bQ05@MlII86NP>-_`<|Q4g1f7Jh*4%=yY_ zafIlUJ2zA?dT8&WTGLE&gvPl|<0zKa=DLzzPOU7i#nate!Z3u|9R6E(6FZ|(EZ%+b zsB!MEkGz1K*oXGdp^tGOWyF0SI{tq>^nbgX|L>uTert_v9gIv#Ma|5OTy0(c_qQUz z!2+;T+eysD^IV+aC=aX$FPzbq+lZ7Gsa%r9l;b5{L-%qurFp89kpztdmZa8Uo!Btl zu7_NZMXQ=6T6+OFOCou6Xc_6tf!t+bSBNk)mLTlQ5ftr247OV6Mc0v+;x&BNW0wvJ zjRR9TWG^(<$&{@;eSs-b796_N#nMB4$rfzYM1jb>Gu$tEpL8-n>zGXVye2xB-qpV z&IZjhW#ka?h8F{QJqaK&xT~T;$AcKQD$V>$$-$x~1&qfWks(mJ8#7v7m4zpWw(NS( z5j0d&Bs4g)>{7yzl-7Fw`07Sj6{vw5nwVyVt8`;Rg5bzISP26=y}0htlPKRa8CaG# z=gw7__ltw`BWvICf>5(LFDFzC7u-Ij7*OKwd7685%wb6a=QD1CjpQs$^2~cx`@xS` zNMz6?Q4OgIR8LYa&m`q*QJ%!CbD#=ha?38!M&7yLA1Wn}M{$nV3-G0@@bD#WjCYI) zKFZ`bf$tFF#}GYZ7MK2U4AKI-GY*y(&DCt~4F1!3!{>cK+7XAfKw<)Jv$b1vHkpC;gl=VNy?f-RI(r=&j z@Dy@&vHYi$GBI*-`1j-=qpI@{qwt%et&>`VuG+PYzF>DUM1!h|8sz~*0>sA7|IH_y zskL`MJ4Yw|Ru~}gzgCOOEDSyuM+ivsjt@13h-SLD|INP2zRO|RKEDz$_zlt)ZWYQg zKHk`_;gygz9b$7*)WKC(<}zQUY8M94a#Tu_OEyX$Lej=Cs`b}zjTYvv-Jt6E^_bV) zCt>gvm2{y2tK8Uy*;ruhTa_?lSIlV;r8b zX?jME!z32pO8`g9ga%`RQ*v=F0O`bnPZebx@b#ZfQWvqZPAb@zl>ORo<_o7Dp&F?6 zP(tBH@~c-Zfx?Ulkb{F`C1S8y3F;;)^MwWBiBPQ1D=;yC{M-i~ILSfh3K!Ai{5c?J zdLm0OmDsWuV>%}MT*Qf<$UT+M=7pMVdJGRi-rdW>7iM&2UO%v@>_!inA`JD)lrKC& z75Y)Lg~PVq0Ge}-g$8cy0w@sHjUuwMm1|~u6X!*fGG>%bAbv5cEU3nR6&6o03J2ff z)*M)kj|gyvZ6Md8Y!m#IuWuP0<9daW2gPDp*=aQA2qm)VLJ($UUQ>-4&3LX|)=-g5 zDTzngTm?JwMM46$Z22o7jlr3Vp3K15k^@=c7JJx9WQg*XbLRkdC zYapmoZr8J8X5n5}a2xjY35bC^@Ez{}9JA&aex@>JiMr#&GtJGn$)Tt=HVKx@B+w50tPaNkh{N0!^9>r<#h(fr3kP@a(N1!O)$rdf&Dd!hhJNtXD zIbx!f3YSHV50oNza38Kzd9Vze|NZlyBd{fKzZOSB7NqO*qDh)*>XW~VnmJ^ zji(MF3D>tHCk-^y37b-c7t1Zrt)VBlefNnY+NH0u=9IPbDZ1z8XbK{5_W?~aGs@o& zTbi2gdn~PB;M%^{Q*d9xWhw;xy?E}nCbBs0rn@{51pJ@6e=LQg2dvlq_FM0;Iel9= zz?V~4Y+a&wJIgvt5@%1FDtB9(A<-f!NpP^nl51v_hp$v8$w{ z=Rh2*Y?stNGlx7wbOLqrFbxg3lqpaaN{@9c)nNxe#D=Xouh@g7Wd}stZ!B8jrc4HPmOW%Xt^a!LcN8M4^efD8wWziBkha6&KggDq^9beRoiLH_z9 zGUiqkIvsoqX!3F)6qr+_HfB$D%@)T=XV3YUews|Tg-Hwn^wh3)q=N>FC*4nHJ+L$K zpR;I6Gt%?U%!6mxrP$mlEEiT&BVf$x(VJRuEIXdqtS+qfX^-@UKefF=?Q z(jc2Y2oyEyr3_bP|F%)C?~RzdfbNXgw%b_zaAs2QbA_QL+IyP^@l+{#{17?2dn80k zljl~W{3$~wO4E?SSij&`vnbpKCUzN%8GY^!-wNR8=XKiz>yng^Xj99@bTW|TDw5XGfDje2@E z*~-mJF8z}cI1eTpHlg*7?K(U5q3H%{y84gCiDbksT+HB=ca!YVTu zgPDuJzB@76rs{is=F^_95WD#mg}F*~wRr~vgN4^*Gy=hUUD_~f0QPh!&J7XP9zv&H zY}Zm4O#rej< zQmBNK_0>1jXd)Y3cJi(*1U|!mL(;nU#j_WV33)oK-!s$XS(mQqWqQ7&ZZ54iT5+r| zi|MH>VJs`1ZQr<{eTMqC#Y~41>Ga4BuQynUV!QuZeaFa6aP(B)SxC~V-r0K5 z5BJ<3nuAkX12%0k5qI=#D*PNg{NNjn>VUnvH!{DfD}FX=e%E5lw-IZgDqD$1an(zv z95TXS9wGg?Bl{w91nOC8HvvD1&ENr~L>4u{^bNaBD>ZHXIw1Ko!;wjz1%zZMbWE8# z7f5xlDTQWK%rH+)0KY&O>*EHs@Ha5t9ltEE{qv`K0tO?W=jgzciZhHZ4As;i<7{@M(!#&K$4UGQ?~d6rbu|rCYd`D!Bgha2*v# z?6){N62Wq7br9`S=y(rk$xKExQsyv0H~Z<~f!Z7~Wt6SlJBO4_KeNahC?2rxh%Z14 z{6vx|=@Pd?8vwjCEbf?V*zgc>36eg4u4w8WMluPe+qB=i60{qnN+XKmud{LfKvd^Rf{8@jDa#RaXtvGeC92KvnMDV3m2 z4Xt7QB96VazV=Z?RrMXb$#mb85@y7X+OE;c6PL94T|ssUhD|n8IM`GhqU%%}=6E(! z@O+LF*%Uy084M_#De*pBSU<)G3|%go1vt<|<(ZKk{3&*44f?ftxS-a(+@u_92o7ot zYq%I+Ztyt1x5RPt_1it>&+05XbK1B{-T~aA+FN6BiF@>|QCJ`#y*u z@e*p+J|+Jzl4qtDnLJPde6Gl8Qfu5eP#Lr_}cyBzGaR912ca0h5s# zbgocm38uvIstvyAPMEgVj^>{XqR&db7$(XJRTRiR@!lH>>CTe{+zRJEgcn{?M627> zsw6}Y)J+s3)u#g*Mo19)oWp785&T@;fee1**^o5#bgS4epuPWP>~Y2v-~{)-me7SK zd!AQUXsd{A=;C;8>vRTE5Dol&>XJ&AYMijyXV3|_46Fr#lz`uF9dT^PhX2e>lDN?r z>wx*9-Pr~siloVs7@`dn*kGmY0xP)2odnz6S437Hi&}MSb1iiwEiwfy=f;yg# zDZojIe7{n|lnmh@$rU>6-%oUGrG#^0y%z_Niq4LG38Yq&Dq<~B-3qLMHLbL;&A)i3w zq0}L%{J2P1a z2OC$%f4j5C`~!#oBU=IP{19v?%zqxLR77sUDKZWk1TEdClEz1yHB10F7>l{;9l0L|=ADc&?i zK#F90YE|)m(u4LGC%M^0?53NrH3M`xl2{P!5+fC(H)Yt|t=X~m+os4b6}Wj|nDvL8 z8n=Bhi`Mq$&2sm(8n4F2)~_ylMf-R2rn!V)Bfzhv7v2SF{79o}>ITpgUpe=zcRpds zp^3fse>q!&ohi{7gYJM|qD$1?s^vyP1XP=26O)1AFu)?|OCYHCJm*LP4*zJ8Raq1u z)9(U+oYRkni_C&!f4&%ORK?w$g6<;rT((@LunPCC_#2P zxJ&Q13mCI_U+H?IvV89Y)i_#NnNt!>xavHwF$|O zXuHG5oCo;G6F&W`KV4I0A-(zyjQ;ws!05mAr~eli{U77e_#bTiA4Hr~$mBnaBxQ^3 zlOJG&4aI|YIUi&Z#TBHjLS(GmY^z5R28NolKW$l^Ym#0I3|0lI-ggSR?CgqX8f;MBaPl&YzSG} z4(9gprQ%M^N3g+r;f^a0BNw0BQ9}e{Op$ssU!0cTdbP z1%BNUh*RkAe#+jya`#(*p*uQ|spESDMarSs8h3e`E#gtvYi=8d#ADvy9g>R@*^D~F z2t#h@kzA0JK)w;AMPg^lWi2XAU}jpiDF!akXK|rSi6}wmaK)KT*81I6M}f%l3XCMR z-&LC;?s53?Q?B;UuDeB{5^S+oOfSGE^CnkvgEc9^13~<4(iGap$VY8}3$6;-sL}t1 z4d0l&nxB@pZuYHH` z{ONm|SH}iy2^)Zg%Ou?*Q?I+u&ZmckE<;nVG0STB`M9GzLE5UAMeRQQJzJxXBBwA&_T6LHe4yGpP7i~lax~#Ub5BlJE zg>YF0Yn0Wcsv`EJIW^d7i>M?PO5_+)OxDS;9?zPfCH;#_rpR4-*9!|aogttErPHlR zUf2d~4Xa7AEaZSe)Mn9=Nd;=@JUDKUaJU-Rx~HXERZPZJTiBwHdXup>tP-Z$yw6H? z{D8e~w09((x@w&~)75oSpJ7o&u#DUKXAP}9afG;3qf=+XWeC!=Ip8PJvw~{@B3H)k zZr>U-w?x^Y3%$zAfoF_*V2Mlr?I=_C57F2k-rurm=_3`CHmW^yY`ye5aJG#E#oU&y z^R4vJ!2z7aF;V5BD1dbHn6(R25;-0cu1Cet+$J~Uw}=H_%79gf!-W2#1g=S`%zSN- zwVT1}5o>Hi-DpkU76(;YW&Y92O;@cEU^coXt>XfiRWI$}_*t&RQ_K?A8!$gpQKZe> z6VsBW458Q0>X1E#m*K&U%))^SmEntSPBAZb7VW{C@EA7Plo3r-`7EMb;;WeQn0bRTSxW7MTSYNoW=(qCsKsMVCbY?$#Z{|k#%NHM zA*6=sc(VKVE`UVqumIooHMGYRSh$SD{ErAy8%i_*n<=4ODdFErVql6WIx-X4fyaoz&jU+aYlbi=W`&5GJ~zS*@5IRv9cn<|il?|!d8>N94!OI0)aLF!Q0nlhtv zV$SFv61Ek9=p#mMT*~J{BfjK)?1ss~7B8LE@RPM6>=Q&sCt<9ZWOlek61x3T53zDy z_Ki;P_XP~dr)aCdrp;^Xx&4zy791bkXYcFE&ul#uoMVnctVZzl-Azp*+fw1N@S40^ zWBY6U4w+j|T8!q!)5)=7rk~;72u(J{qztk$Rb^WOCbU62Z^s|pn=)TqT4{gYcX?y1 z?|~>Cvir?R7Ga#&UI_thW{axhKZmGsOKK2*Z5|H*2nrEoD6q0cA?LAuQGqE#iVxT) zkKFW#vDut&E=}&^_xyn@nKhBk4S$!WNK~%$ z0c&2{SDdyuxlzV0ph!Peph$e2NH|n4;u};Z5-fDRQCkV`hd9~Qhw#l z5yeB&7zlX?y>QU?3e8P%Gzk1X934Q9LPIvcZi~Q>$tU#A^%^O!FsqRvO1M){#{wo# zBk9bs(!8G_zMYJ-^KkkOmXlld6&M}R+at4#TYfha^(?3_OqFsw=T6Gudap+sqFPF0 z*6D8MYBS6E;rkj8{7GbNPpnUPv9*l#u0T^M#yAbod>pw)srdC}u6;9n!}f|*m@!$~ z1aL-1&ei+i_Mkf0!?>5p@ss}z+(4GaIZ0Tu^mr{+M1{}bS8k3r~HKz!?C`p>TW)1H#Yg*vr z7Y{a{9Z}e1N<7QR%urOa_cLshyVKNaKNU@l7j~j>PeI7MIZZ|r0*YSjU6P_&ia|jH zDoChFYF-JCkoNDw*&*{QG3x+J%2L5_4`n1Tg9hatvloFoYL01#hFFj~!}MRSdgSSl z=m-yq{#uwWUIpuCs@%BEy5ob11|s~&TVX8~-XV)oMfeNdXD?Z9E10-tP#Krhiv$@dBpKj5J%t@Y2xI!*8s~Z z29}0zR`_9s&89Brq4Tru3F{G&uQu{ujBFqN`NY$Hb>qnXc(a!g%hbv!R@n6sNonM) zg649UVVIiIE)_J6eMZ?R^6HGdRMn-UD36*c8_Z2r&xc^Cs2p^v6x-_j{J)k91n!wt9I-~_PA$GNiLi=u7ixtk`YUQ4uIF+`SI~U z1J;MiD+DHLSA)nBsc8CJW1Z4F5uFXI0GzFHhs4egAoxF&>1&8*Nl_OA^!wW4GJCRO zwS%7>sOyj*5EN! zUpux=mBP|Q*_J!@%f6V&EZf{?`H}D&1^^@HO#Gta8P{W+FkdO5OW;fnD1|4&tlh3} z@YGnJ3d(Y0t#ep+bksNs#e?8*u-V=@#Dvz21#EB=jam5x3MtG&IuRHU$pr(K+Y-AX zn7FqKEk!?hw{HWBS~^ioY8Dbe(VtwFva+1h5$-}M9!~UYHGIL>zwFFN1`lcLe zwaMY%;tKHw`EL=C_^}jKY3YhWzg-&!anlG&@4E|`Vl}0q!EvCtT1I@}=Ug2;8OzB) zmllrTJ}RHtO2N@|-7)oaf*v0`{>2c|j?-t&WbDWOUDsBIUR24HnS0{I;>(%9+r)y* zg2K$nGPerx{E6HXH@h?eRQC~Y44A2^$`xKRwnOj_7pT5_!?K%>JT+F+ z6(@ZUF%FqvCBG2v8WL04A5>D=m|;&N?Hzcdj=|%{4JK2j_;hMKOfU}I+5PVH87xo# zc>v2%1gFE>V^6x3$7#ymLM62}*)(ex+`ImB7=eUwa2O&zcN_th9iPz)#fXNbq_VnK zg>+Fagfb53(>-Y^v23^|gST@kT%3pG*YUyrd-zn|F0Cr_;Qh)MO;mTE$%x&%B^Oc= zO-<|3$Nplt0sdxXQO`|RVIbVxm_^24G_6XuTxk&{Yyl+?OeXa-!t}8&fuTGLZpS|{?$S9qu^8TDrgtdOu`4*Sqx20lCJ(;z6u7&0EbrB@495}e zvjfw8yG7#Eo7QX+`k$3*tbTCwGm9LGOvTam&Kk&4&(T!!b0d-h(+s160p@Pn+_M|) zwasiA7r)El>t5DJfiBLb@2=gQDN0N*FfYuh&F<6BNcc)=oqju*S(+ucbzy4pyN1%s zgS@}T`xoCKJdeoM>hW-Zt9xSNRYI8RfX^{UPSJ}y8$_k~4-2G8KZDJQl``0lf>>)j z^q^y@`VIX~W%W-QAF*8U#?c|>tGQ{a09;)CL{-NfEv_2<$o(R8`V7xFRTl$)d~KX! zxG^v#xd(Z9R*`P* z8NwYSrl;qaYDzF0iB%{|A(v0($}TDr##;!y6paThkw{fnuKExakKusCdM>46hESJo z6Z4inrJpt`IzSB{l1R?`XS)o3@M9OZsiP&{y4g5QBH!U*Fvdd|9inn^a}Nz>2&)`? zh!|tcpGBMA4e|H2Y3)~7iyNUBsc|aN0$HM9Uc2MDIL(61;J!I)NmIwv>&&25`&+6M zq1}!I%Azc>=L(6nYlCWwU59Ea*szPa>sE|5)2pJsAnOmce3ZqxF(4^b@uZ6D1K#-5 zD6|eu@+l+j4}V7yxluQ@oX?sla^=5dw}yP&j6E+69hswg1L1c=)OyvZ7^wHQJl;ml z_2lX#$i;=Fs}vkh=ukc4y2Vj2Lu7vAHQ*E%@5?3`^a{BzDVU zF)O4|`;uuAO@)kfdwp~fqS#rR$4Oj@c*zBS`-fL6qu8<7qzl8rl--^kjiCV!(vbxC2vIdMo2I^X@+ID zcT&$52_`~JOBXh&mXX+ceO*m*0_=9ArqG>xjMR;+M=q{e-N#QEj-BCAzAVeGSrXNh zCV`uX4qS?7l$u+*J~5P?9xlU2%6rgo30lJ)cd|FHtEmloD@8tO@5y7N5t*NZN|hrm z*0FP5k0_1u5$>dp#I>8az>my1NoIAqBZ!Lx(!ohP^U@&Vmqd8 zH=75V+`}JpR;Wj8!j6BT1WSjMs>H+3_*52JYs(04P<@$3WEVZ7V%N-CLN$onNB~*- za-hT{!s~K{EUyaw7zDbp7n5T~SRV3$*>Zhpg-*51L=Zj|oeHx)1Mr4juj_5;_<5%8 ziMWWR&MhgdLq0$}U0q=ol1xb)TQBdcV!(3$iF4x~ue+F-gFAGMn^|`*YBjuP=jx!~ z06>UuQAq?Ix&zn0^To|<4!CSXZW7o6VrM}5dYxV+Q~8-h^Y9DzNs{5%+kyFy5cysy za}2EkZyRxQ^Rgq)T6r=({uw7y@%D4S?wd{Ck@D0(;mjg4NbY$Z$xd6rCGrNITO04Y zO%6aZ!9hMp%kU=V6dLc($d`AHMbf`&G9BXY%xr$$hovCbBj@|K2-4_HjW4Xn{knIL zaKV)PQkC?JIKYK?u)1`rzd)G(eO222!%q#U6QaT;SUl*MO9AvJ_$WC-@uTOjb58L_ zQo63V8+G)0D~=S&a%3>qqG`7N+Wfi$Logc=SXGBq3&TV|=!!;Nzi4VeqP9=hV>H5k ziX8p2v_i>9nc1rQm(7T8t#sTSGnI9T#Ms(_k_%sm3mT6gc=YrdUm@Ip6xRqL0H93*Yx0O!3Qw+_Y!81*n-ovS%iBlXx62TFNbk8K-j=LOV=1s zwc7i_TsS%sk!R7r81r4v*Ec`Rrl_m zr2$@wBrDGJ1`%wG6Ar259e%+MkZzK88-X>M^WgfA@HcWJmPUeFdO?d0>gvCTn0-ZWgb;$}~gdQiffS0?*jk$T`izb=V-&N#O_U4yp?Y!Mdlk09!o82t}+5dEvSj%vN5 zCBperFlf(sXr6C$n?zYvm=YYyz=~W1tkhvu1wODh>tKoBEiRB9*Py%96luTxm11-k?Q=g$c>y=q9%J< zVbw|kc=&DAiz8G*&G@8XlevEthbWV6a7nM1@VjKNkP|sl%x3(c9h#|9HIdVuC_??C z!MaVTrRI4=oMEugDa}D)#f1zPsr&vLR0Zy!7;QA4?x1w?=X%tH7o_(2z@8LjA`t^# zft3pe@**E=P;MFXEB+)Zh$?+;5%i6ECfT?A^~N`o&QHR5@V8a13HuA~omH+0(xm&s zJn#ru(@aCcl%uY66t2-NPi-*^o`hAyJ}I5kdqib+qh*CNP|jg>f!Wj#HJ<4r?4uCX zvkf`dDbhurH>#bk@3|Ap%0+kV-0PkcrZb0Q6)EJKBfaiae*!zLC7wkQ?cY#avSAHH z-b1`V^N9SgFL7-JrVQZS2rsHMA5v)j^@ga==T4XfE9yy6w7~pXILh8O)Le{Zg)9`|o`-$nca zc~hvlgOB$pGXop$oW3PzOuUbE^uRf@bo%^%%GEHQ}3uc0E<9SxbN+Fk6DEin>4 zHcD4f(K{ENOe$J0HJ#urqwE!{iYCcrgQT6kUmRQ&pZsx(U*x5m938GK3cceA-25P7 z?4_>Rtm;@LOJc>-Es0d2lZed7(#_R8eGm|eZ(xhjbvF{TQvs1jaS#K%R>_hqN0n}TZ* zkc089?X9=$pO*FdJ8a~1LwKU&Tl*+PUpFFBdK=aX&m5jxjDg5G1pXXNL&FXtQoDIi z%I2VE+_J15PN$4XB^X2Yje8=^qT3Q6Up)7auJ|SXIn8t2lJM#_5ql$SZ|nXfb&U<5 z+WD;cxsrkAy@tew0gl8PHWX0(qf>97u#=sJz7BD=`gp*W%GmlPa|+rCER@9rjcWg_ zl26OYrAyJyc>(x*jhp9DekXff;UF2NN;Ui}MJ?5ICzv@f9ALbJ?E#ZUr9Ic3 zzA*o$&I=Ta@JfZOEAMmeNUz9k93p!8X=>FBD$#aW*rJBSOJG_{E4u;M3A)vn3ZA*FCGn+Fg(4w7}cEUuvHYjNe3srT? zjGbTt%LY~=@?&|zrxYJ%v<6_xj4<+!VwleU+BF+z4)}b&?KFik zy?KZ%qJSTxm)WSC(-)vC z_LTIFihr!^y%i5PBEEPCOyW1(0O<=Ad}++TAQlUVUet+p^E3c}!Hm6Ker0kttjBIWHFAYVE28@r68QPb>)Vg<;d0ndg zIOg|&%Z^&B5koUj%;;F55>#Cd>y`X1^41GHDSIjVmR%4uBt$XKaBh6+p3un1m6DKK zM5nC$KuQFHa!O+A!tnBN$&WmSvCPz#nQaEXC!g(?sW+Y@AB1kdg2dM^(Gjmzs6*J zi>IYc&r4tXJ{{+;xx*UGux7GmUyf}GKo{&yc+i^CQk+fM5xwnR=XN< z!u~>Gl{|8NtTsKC_us}+!JbSFv?wd*)?I^VPt2vT`c;a6orPS2Qhe`>N1KB~dB}yP zspLQzZ>`?Hbq-7qJC#l@Vh{gOd0-=i*!QkM8LpL1X8-}g1mS#mh6v^#lwH+V0EAht zLRoZn@;eAS)m=80s0Jn#+sLq@zuIq|XFXByZxLIoN4=#LqQuVVkJJJoqdv}YdIi8` za&=Ppx)n$aP&MKW_^PY6l=m-iPXIGakyd*1%=})EsxHySwRk^AE?qcrR8hTjF`nFh z)+UT>wL0VXkVCY=24X|7B}!a=Gf)c2+1jXZ;lwogP%J5l_LHb4lWDj;(dv}Vr1IJ% zBzmFhafX~i#<1bqv&puIYKuHOPY|K%X&v{<{=yTL{$8uDcy(HHi}VDVjHC}Z7W0`b zEvA9p60jBWkkB5Rk#%5BJPS(P7jy(H&ZM=!PzvrzF1=cb@j0B{!WqXMl>4hvAUG#n zJd@sf-hvm66(tgSb~I9O>_*OH9ggr<9(jkPzpUP5U;9oi{-`RXFkT6&7UzshGl7YK z=w!GA{fajfE6<@$!92K|Md|hQp!i-X2J~nt=D;7#M2;}9l3LG<6`3C2w+L(}Swn*C-B*?`-k7j87(HI0e zOg>|2NSSo0G$Db|yJ=}l3XfUHc3P)1NIM4OhMgn9utTLY8mQE#BnS7N{&WXwxbPTC zj>^Vmu=6JO$5zNwB5NNSl0w;}jb@J-VA6wNi{X~PSBBYYx)&mpWiwGyMd~%>340*O<^m+;13xv+nsl@@4vWer8?fJpf?QLDsIAYG$AW; zLaEVbXdlU68j5l)of@<#27i#8e9acN)RqV5SD02bMKnOYW!RB{72(fvCCTBSVi?ru zbgDA#*GRW68N(c0E>5u>u(SP<+gV#x)7`Bp@SBKiVu<5JAQnY_TkLETuOirHXdSvS zvj3FIepQF6dAlF4aI!UHW_6)6yAM7CrBvn^#Qb^(|KMPUas1SycQijlWVnLIlvayxabGnXVuaQ^dHa@y9)=$QZH>SPegN=OO*~ zE)SFDbmX`%K>u)QKvO4)0Q6_1yp?lfgooarhtt<$z~YTO+(JVl(~ASc`owLsRkis`U_?MIJW!nR@Mo{TY+o9Pv7gjq0Br6 z69CC^k3Y>byZiTYSu$_l7lJPB2#srl$j1$McL;9;1JwOOnTj&h4}mWH-Vn?pBA#s3 zjm-omv~5W85u0g%GVKXOn)WQaVM*sXOrslhX;tKH6?3k};k`m#5;f?oYG{A|jfzVI zEawoElA5$S+%=j>B{ljl6OB6dMOtiz$z|zws<7A7tg64qMADNf&^>0E_v(v4Xo_qH zV^U-nQmvG1&4lmI`ITySApjtTHJlbWG-M3T*jAxeFp8eXd~QuT_;Rtxq6gbbb-=tw zoQ(PY91W&wSS2@?%S!N+c&XI*-Qe>8h;>EoRGL|8iL5JVmPFo`8mCcY@G7$%vVy7X z7@ReiXO;L?;tk6Mm3?VrP%a+9@9N45(_m|XD$^pZCLI=|=N&b3Eye{UTf~qseLt&P z!#sl$Vu>mfVC$4UM*S1iA&A8WT0&j2yWtx^d_y<4cNyNemon|ChjXI5IDRb_6+)L6 zHL>y7N+Zt&p4YiL#W9q4j^;U#_Uo|iALm532s#R|g|RtF1ga%u9(|3q*VEV07-Y_# z={jfTg|b)%84CRox5B4Px#rve>wV`e>F+Ihvw2o<_Q-Nv6Oskz6Xf0(P5Qe*HQ7l- zcH%D^p0}1DkU?Oh5Luxsh!wO zKUM!6-)%F>W(*eN%I<=x(m0rDftloG$@?ufi_0FJPvZ3#aSQ)qBP??BlZ)n3kR!u( ztnUxe)+T0*JsBGnx*NQaQ*rbN@u7$&a*QhLA>#~Ru<77+YbIJviqYiex1fq>1{FT# zFdi=DsQwOIHD+foydCEv&;U6m{f)}zJS3hga=b91my!N=YxAFN>}t3rbzl6j(22F3 zN=wsJ^$u!O$eS~g%{1`E%Z4(MfN(74t3fvCmpBFL^Zwb}W|;;%1`>f&|3*$y)Z>cJ zb4L4u3{QiD>q8`;X78t!poKbPNQ3F!N5@gjzIaM@VHUUjjLWq@kvi9sqbqS?nXGE8 z#+GiOoSb3agPl)kT>OYk63q+oSkS>R1&~Kn8mWrR@Ghg2kK(O=B0gr7cqQS&ZU#=n z!fuWk@yB<^!ZQXKgv|$6V&t7P%_Pw;Z6eX>n7u0VO2tT?Md1A_{XTzc4f!^fy@J`@ zL_xHu4pQ2%+0gi2MYpK?iQ^gAY+ZY~Gl4zpRA+4JCqhte=){_!sS#6~-(u2O33{G&qyu-3N|Q&_I& zrYu8ewgXs?(VGq;pSXyDqUfrqm8MV7=*kn-gajV?A&2rCKCU2b%V#8DjIS?*Vby zKbhSHwl(aey@M#B8n8X&2S?C9fc+T=k|2m>1p1jE^8a*p7GPC1+y5t}yFEv0biZjerCkVf)}=vc*AQeLaes5@b#F77Z6qAz%l-99zN7!krPb@WE@*haV*6;&%ac`t z$p+!J!?T5Q(0fA5a}OU8+PZ!Ndhf30kT((m^9FiJ79WS^vcFZ6gGuSj{S`e2Q%u8$ z*$=`FNUwnT3MQXg2wm@iypIy_wtTRvyLm345nt~Hjh{W&yk9bNXi)x$TYOmqRkBjR z62UrkX=#b5CsQ=dI{nd9hLOmmydWim_?39xb1J`JjsCP(>wNM~^8+bwt(VJK^`0=s z%97EYPT=bjs((ZFX-|N_y>DS zvWRyIuDcghz}MpyZE#*nQw|a4uW0zgqtA>*CLBdpjUhRD`mJFRa&;l=cRkT3S(l<+ zO8=_HSCLh~y|ftK(ajUECd|EE=Wy?Hb%c%#nHYPZLw9akcR7u!w5#-PioD>8RhE)< zt{&UjCzWN|o#^vd8j;6KXf=4}kMkCW| zVSxvE=u0vh*r$0-S(9P7Q5CW%^7bKVu=| zk>ZOJ}2*@xw z%?i%k;pi|RUQ44_+hrd+)y{B|7lfBZp}F!E)I)8)h6ld30f2zQD zTA+dMr02cDX+vCzfK9iwIK=x(6Jyzg^uR7;c;;@nWi3y`O@AqwhJ>;X- zN7gfZGgG5gwbGh~E(12E`qln~DWZnEFRDh%yxmP)2=<8>_4(`U0+5>T-4EU{^0T?< z`+eP>KTJFH+2mikxF_l^Z@%c<4BZl2RS?NPZ1r~7eLM)%xk}0y=Acd)Cm(z~Xvwb0 zQk7zx^wnc%U@M7vM_a$zg(1pPLqISuKU(`;+GHB;XjQ`ED5yW)tP!0z#M2FKs+Ds` z@d($Yzm}Bw#6VTT%Ge5*n?cNZ-1wB^I44Q442Ll-=xb?uqN`n``RUrAJG2xmJW}#I zW1SCEJv%R%*ur!4a{!F-lTBUWI$4=GO;;xgrKZ*Jp3sa<>ilJ{rnNT~(~B#*XEmiU z1~Ed`QBgYpk>YsHbLx#%E)o9--i+ZC9f^_7T3q*re!~_iq1d4WhP8%?V(#=QM(g^7 z>2+F74STNRx~BuypUTi!+)M{gS@jyMH($ZDu zKjsY7wy_tY=^3B$W08}!&<@2c!l~K6&#D)VB-K$kGlCyqCHZOrNP@szFIP8$SAP6l zAIjazY5FRXfEyma)Kg?SYc6gqIrvj&$otnW`!RzBpQi4fq)s=P5CdQP@)yndY7bUH zan{vp_Qu7}wY$KTn$j1%Y@h6=n?MZNqDJhm%WboRANR6CQby3{gRzTJfUkwKimRra z>v20v{=}dJ`%D)e01bVn*OnnAnvxkDMidvnnJEF&DTbM&P+`Ujq+6c9syhcdm!joG z*1W2nVX)Y4=7jc_kF3u24hP6*6e_ugdd-Zx2G;^;ugxy^C3B;tZE{9i)S#}n+Tm^Wl z^%KpO#g^>$))G%Ak1-6LUD#ZTRTn(7!9<4(>I$Q9zeW_j9T{_T6J6i{a*yI=rhgd@ z)gG{9+1{|l$zFGeY|`t&%G=$#LakN(kclKjR)UF-Ix%+c&+>+~j$d4Qmb}LruYMO@ z`qpSxlDi`75!wy{eqU`gG<%ZOL3iz#AK@!h!=>|j1B+Oe$GKu9eUZ!k_(1T+S7_kA zbJn;fO_sAts`Puo#$t6E;ze2?q_a>$w#+0nuk}*bYY8_IQmYk^aF^PtEnm9%vS?g- zl=f(*i$v;};DFLu)Ie}{;wBfYcRZ;#gqu}?q$J)G2lLswTD<(sxB!k1pp9in$Y8=k z^3JyAcETT9MmAB~bYMX>W~mpKeS-AdzQ{3eH)NL0Fva9G(r77Eq^5@T^jqfFHlZW6 zX`)orA@BS6J(?KBp+#ABTs)dY-6)A)m=B$=fl;)gp0w5h=kVgFEy%>zT==t#)Oswq zTr?{tmWGWFbDOksn&?;8ZO@~z1|4maoHqnx;)hZai1Oa97qKZ2`=>=Tqbi7E&k^Na zZ{=(CC~B6eo5t-^lBcfd9J7-)zKvBA>K}~;QMU(%+w1B)Tm0HTIfLh#lU;3Yn~+}d zUP0S|jo8kZ7+vu!d=$BZlVeRdZn#XTYejHx3KQ;O9%HU#dW(r^FcXBZC(y~Sm~%N} z2AJNk$S5a5XzSgPM7Rj`gO_&{#IQ+BaJI7%Cg(lRcrdBsB{DM zT8d*WSa9l7$|3s+xddzetVv2FvHpTmi>HO0ST5olCxQvl(GCf3Q9y&j7i|TuS52RC z$Mq$-RNqf4At8+FuTKP}#H=tDX#`r?5dsa5dEA@$R5+ZaAl)jTIpWtmtDot`nN#*n zhU~NvwXJ2@?Ng4=Ga)ngqKekQp9>riEd9DzgA}4BUwqIm0%Wss9jHUl$nKYqO;2N7 zknpSn9IQrcJR>i>8i4TbCiE{yOjELbLUDeF)~y3Xq^W(@CXkZSMd`R;HHADm=DLkJ zS;1I$?g$Acj(p>KT3D?`z_4LUo}Uvij?k=_H9S~+>bx^)AG{@fB`}K$xi6WJ!FPJGW zB~LoXg!SC`+S#|tF_WQeoMF^8u?W?f)9v=3VwpXM#@dD`br&6k3%WzaC(pjfR0`fM zChRRAn~rhB-s|T5e1XI1$7!j+-kyB4Yw?uPR@@9KfpTk%nATjRS13yeX_R>U?NRR* zYr(<$9=%ADVmjc*1V?@FRwNrtIjAjb6~xw zC-sWFLtc2tkj`HGvT-)9R$lY{zLj=HPa%BG;Eej@!{!SgZ7uQSkiTpuyam5P z5rGi-YQWO|GMX=FapkU`5NRBgpyZCbC47f9)TZ5%PIz1ivCfeoh~;Vbi@p|Pw7gM> zwb+um?aH84>hd{#m`B&9Hw?kAeS3;L=R7r;t*zfqC&7JCTJ}UUynqaE9fG)Oeo+9~ z<)#K&_ox+Nw&lB+9i|2E!p?w#If|`6#-*70{+ZT9cyNps75*mHJhbjb(M$RiL#Im7 zkt@=c&>5xhMt!=^u@mJ>AD$D_6u+1VyRkNNNm4B-5;&h9$MT0M8s71AN$h*tvfb!k&(H`x-=+RpQI>om@b>eBy%{M}3KN2#u_7ZsoV&Xy#uDxoRl2 zhZ9oKR?*q};PbY(m7gWgt{z{7YV^%w zc`Y^X^W2*`zFzR@pZ`FAYXD7ajJxrE>}I9XGO?tURZlH3Izhh)mjN#;L|i9=q<*Nz zeJ$l3es%o;Vkm2YSg0p_sEJfD;4905eJ~)3KL*>sr?_0fwyGKtmV*Mx?gOY(=^nPy z75*rmkv2($3TAtHYhv>G)jB4hBOwj?+DEI7B7nKguhhz2Yd1 z5R{LN%C|hj+rB0#%?eMKUp2KkGARiM^w%6HC3B_ajcD)SC*>BKm^LzSenJ0Ao&OwF zP*SjP9n;qLfKIW#zSsN6#KjQ=N9BF<<&EVWEqo{0Wy95oba_&mA2}DQZ?GFIAE4+$ zTSWyjBPuJ{I>+2{`XjGQUK|-8z?*tIei@>sC0eceal?yJ)H4CGLcpm&tzj$W8yN`# zWW`Z58t<@KB$*M=mUB3S1Ewuu;KvZt)Q44I^sc9(<6KD zz8jzDcL^6W2q>?&+~@GAhGm!bSVyKo4FcZIG@w+Qpt=z*Ug35;iTEV_r3KuuIY@AP z86i%AyiC(GJ?msLDzV2q&uEWf<036blx`(bK34rhL@TD$CD~KAPmc@j?tv4i(U$`9 zcWk#E6!Y?LEsmMJ0&nlU1XdZxd)a(3uMfNLXuUp;?^_>tzV(jaTa$0?-?6+ps6I8M z^B+WMTXsb|tcon?N_dCOn5B9n=!X7x%?0 zTWoPArre~5nAqwvGIZK;G@h1ctA0q9aR>+@?}8?$AnXuMICs=!+GRwXA9E?Tb*cs~c2&|aJbq|eJ7f#q| zoxW$gW$NCNCCs5dI)Z^%IkU1tA%66_qyJRWe0$h5=C+eor|YD9VtX=mo9i~)qd6;iM;BM3`Er9%Vbh*xkQP$9s^g?<6<&loxpnjh84ZhlM9LxMJBc zLXJ0K3!L}(&LVO@gM{JDV-#1QVN~`dv!T2 z2Qn;Li&$}sd(ekuw=gm4*!C?zfH%!{5U? zO_#Y7qV!K-j*(lr3xK97+d&CUgC{~Jh<6M)O$r&FwN{1 z20nbi=4jRBh^n!*wjSy8azByNjBI_hrIYM>2DjX@lKe#Cjb~HNQHwH_8rD&4I!0l; z_yD1aD4HlIRpaTe{;-Dp(o62$P92GK;Vp2_eF?x?niw86wX|gzR^&6S9>(;XlZu!P zg%R|xezBab&$a_p^tvy_W@JtUC?XN}cgE^{$r@Jj0O-eGw1y~*_g%tgOnARkghNuL z-{~{vK;QbpL8{T(kM6bO^)h}ux~es@-LTd;R=9)sxy<}5O;v>vrHj%91Z$l;<`Y(w zbdlOcHl_DeY2!3@#q;ILT9*;B7%PjE-TI@nj;lVk>o~L@x38XcbQ>sb4Q_ergjle2 z=1TP)RfEaI9>j4(%Pj#eMlOU;E^SAsx1HlY$8Ha+YL5x9-9of5SP~`Q!TTkHjuEe( z^@Be9fgW2rMRKH_{6?-ncAL`peXi#-uUai?&<79D<|qcq#{*VhfR0^Bu#$m}waU-a zf?oVYeZ&@3KR+@Wsj@7H(vYJuPF8)?g;g1qgAbPp;Ih|4hUftITYkRimR-QPGaWd7JcGhKSRpMGT&ZPF3KZi+UYK+VsaLymr zv>(Eeqzvw$N+M$wu# z>3e49=_k#bazg|41_rGVT0nT<(dcOP7(s1Ur0>eqr0e92dZHT8*{A<=?8f_)wMpo0 z{|aanXhtrN0z4$6y^uuRVHQ*`pV$MvaOW$EvoxJGG@+{pg z{B(^TDMUY~v>>L4)O#sr#wBegOIOE&*2iEbQW`BhEFF0u>@prRi!1xGtL|1g#KAS$ z2z`cSn6L;ja0_%*HV*2mK3AE;kjTw^YqTooD;21_$*D_&YbZt7kr0YIgDiIM+h3av zgXsG{{f0}-p6NrnC_K3|jZ}V2#|Q~}&q&yQGGhGuzGQpOxN92O13je4X(I|k==cr~ z){SHv(u91WcbB0wZRt+%i7bMlv;!;=?yyQRrb<4vGj{OKNm9nxng!4NsvZZwIjObb z@KC~nsdPY69@6BqZ5_xo2)t2U7f?&S-~;ZL?M-P+2NvUqJyv1rd0k&{^ggm|X#DvU zA1-EY8=0$XfC4GdfipYcF7$esav-K`gw%(SpA#*Orbj6niv@8kHC8^~J1)}`9(X#r zWe+dN@#5LahIxdUkkOvtdVCuX)hsK*ev-=yc~?~I&5QnUdA&FOi2aQH#JHqpMANea zI;p)iNmoZdlH(Y%N7`Q z$tJQ{7&y_+s7g)E&Jh({721M{ps2~O(9SBcraCmcZ0}dc5$rEJ!v9Pbl&6ubxH@S& ztYob|2_`2;c^Oa>H*AXv!H4p7jIMDi7;0~m>)a$fmh^tqSUKkGutJV0J%@winXVE} z1%Efz)uZZ}4@jH2eb^k(9K)`8{RrURx2bPm4BcAoetOQG1Yd9lGtN|#HSUjX16N>h zgp&z_RHqL2#CB%Ab+D{k$HbPfS>)o3Tge}(!1u2$?BrpEgXExq>_cGo??dcNzwR(V z`2az=)m9(}T9VsMQ)TcvTmoO*co=y?Ehmv68vM8`XAYc}We zjk&~={oCs$W&`ksP}g8;6e0#Qzfi1(I;sI<8?wAN#=S{q>b48Z8FtBqMe3Lo?t!EY z^itX@b~44Vwu5KIb~f1^NSYKTZoKLnZZe6uiSTR9JbuYG=>r+hd$|$O8?Z9?6eW!k zTvcHux%(;faiU}^r84lESQ4bMI=%MtQE>xOs(mCe>RrTGIvDfQnE0D5LQjK%wz@pq z{80dAMVzvl{BgUGwK)lIPb$1`LijJNSCwa+)WkhJcWqqlj9V`-C$fYU5EheRA zYafq_r_hB0^C}Z2UoB0XSs!8%AUq)yVUO) zwX6RI_&)zfJ?O}QN})B zszeLFN+26+QHH@RthaWS#8B>Gj$1KjY3qnj(efg95O48)}Hn;x28!H&jZ`_1+LeOo1{$L zw1a-o%V@mzgD3f2q79xeeEC1aKOyC7B61gS*S?_Zh`&^p>&?}@RO{q0!(DW^ec6;M zYT#36iu`t^u4YK394UnkPHrG6(vS#2#W7^a)DseTl(SK{_mRx$SSO(;R_bGn<;tZ{ z)`77$`ig8YMyqtHF!Oe^VW=Tk_L10)5Fg6Lmp5r4<(4)Vuimrx8er5B(n2pC(7r5? z#p<4o`2yc+!ZWADaFv&@35Yi_ve!%T@*JOz%$|SD0Vg&dWx_ie8OD<1#3l8(_F|Jo zCmXF1Uv%5xfF-Fk3?4k)4sbvl&!T!idJn0sbY#s!A+COh21I8hGu6fXK(MHhwc<^7 zjk#}tUy&wBpV8PzVY|f#+K#Y!YbCTm*g~AP zgs!E>RURoH8CYZ1E6;(H%K|7or+2N9^-bbqr-9b9nv)Xdd--LXSApu89O>+r&{j(e zsoCK3=YM5>U@;s1%m%t8n8Ez6Tl$-szkla^0A(mQvov>gGWtbU4d3`(1<+GX_por* zJEnKK!ZAfXWakj?oanK>w98Y9u$CH^O}GD3ny%d#s%lo*wAAtBn7P_V4@?f6B`EFdP27|nUbv{J6fxz z&di#|ozz#*%c7NKR-|Rr$zJ`G^W7UZb$KrG$#u0iQ!4Pom1;dBDrR`K5>p%fuIim| z)uO7-JkL@}EF$p2sMc%(@TkgyPCk7K`eakofj`y_h6>Tv{FFOv?|n8K1nWY~c$J7O zo$OnJ8VwVPt8`m#*V2+6*PL2&p-b36MazIZ^`hSGmUdct9ltF~lGm8yY_CPrcVPqF zbm=0sw{Pc%=v4NPkOWx#dk#Lxd4?Z0s9pr?U_k))RlmZg8}zO3szcme$P5m32;ToK?74f|_(j%4_CBhdvdOZ zAAS*wBz1AnzmDxfU@^OsTn#5a;%Jrku_al3e{

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/sdks/kotlin/ksp/gradle/wrapper/gradle-wrapper.properties b/sdks/kotlin/ksp/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..94113f200e --- /dev/null +++ b/sdks/kotlin/ksp/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/sdks/kotlin/ksp/gradlew b/sdks/kotlin/ksp/gradlew new file mode 100755 index 0000000000..739907dfd1 --- /dev/null +++ b/sdks/kotlin/ksp/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/sdks/kotlin/ksp/gradlew.bat b/sdks/kotlin/ksp/gradlew.bat new file mode 100644 index 0000000000..c4bdd3ab8e --- /dev/null +++ b/sdks/kotlin/ksp/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sdks/kotlin/ksp/settings.gradle.kts b/sdks/kotlin/ksp/settings.gradle.kts new file mode 100644 index 0000000000..cbd82d0c6d --- /dev/null +++ b/sdks/kotlin/ksp/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "golem-kotlin-ksp" diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/AgentModel.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/AgentModel.kt new file mode 100644 index 0000000000..b306700c98 --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/AgentModel.kt @@ -0,0 +1,69 @@ +package cloud.golem.ksp + +/** + * Full description of a single @Agent-annotated class, built during KSP processing. + * All WIT-type strings are already resolved (e.g. "s32", "string"). + */ +data class AgentModel( + /** Simple class name, e.g. "CounterAgent" */ + val className: String, + /** Fully qualified class name, e.g. "counter.CounterAgent" */ + val qualifiedName: String, + /** Package name, e.g. "counter" */ + val packageName: String, + /** Value of @Agent(mount = ...) */ + val mountPath: String, + /** Value of @Agent(description = ...) / @Description(...) on the class */ + val classDescription: String, + /** Value of @Agent(auth = ...) */ + val mountAuth: Boolean, + /** Value of @Agent(cors = ...) */ + val mountCors: List, + /** Value of @Agent(mode = ...) -- "durable" or "ephemeral" */ + val mode: String, + /** Value of @Agent(snapshotting = ...) -- the Scala-DSL string, parsed at runtime */ + val snapshotting: String, + /** Primary constructor parameters */ + val constructorParams: List, + /** Methods annotated with @Endpoint (and optionally @Prompt / @Description) */ + val methods: List, + /** + * The resolved state type `S` when the agent mixes in `Snapshotted`; `null` otherwise. + * Drives the generated `SnapshotCodec` in `Registration.kt`. + */ + val snapshotStateType: TypeDesc? = null, +) + +data class ParamModel( + val name: String, + /** The resolved agent-surface type. [witType] is its rich WIT string; drives the converters too. */ + val typeDesc: TypeDesc, +) { + /** Rich WIT type string, e.g. "string", "s32", "record". */ + val witType: String get() = typeDesc.toWit() +} + +data class MethodModel( + val name: String, + val description: String, + val promptHint: String, + val inputParams: List, + /** The resolved return type (`TypeDesc.UnitT` for a method with no return). */ + val outputTypeDesc: TypeDesc, + val httpEndpoints: List, + /** `@ReadOnly(cache = ...)` DSL string, or `null` if the method is not annotated read-only. */ + val readOnlyCache: String? = null, +) { + /** Rich WIT return type string, e.g. "s32", "()", "record<...>". */ + val outputWitType: String get() = outputTypeDesc.toWit() +} + +data class HttpEndpointModel( + /** HTTP verb in upper-case, e.g. "GET", "POST" */ + val verb: String, + val path: String, + /** Value of @Endpoint(auth = ...) */ + val auth: Boolean = false, + /** Value of @Endpoint(cors = ...) */ + val cors: List = emptyList(), +) diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/ConverterCodegen.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/ConverterCodegen.kt new file mode 100644 index 0000000000..f467c277a0 --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/ConverterCodegen.kt @@ -0,0 +1,100 @@ +package cloud.golem.ksp + +/** + * Recursive `SchemaValue` <-> Kotlin converter codegen, shared by the agent-registration emitter + * ([NativeRegistrationEmitter]) and the RPC typed-client emitter ([RemoteAgentEmitter]). Given a + * [TypeDesc] and a Kotlin expression string, produces a Kotlin expression string for the other + * direction. All generated references to `SchemaValue` are short (the emitting file must import + * `cloud.golem.runtime.SchemaValue`). + */ +object ConverterCodegen { + + /** Recursively decodes a lifted [SchemaValue] expression [sv] into a Kotlin value of type [td]. */ + fun decode(td: TypeDesc, sv: String): String = when (td) { + is TypeDesc.Prim -> "($sv as SchemaValue.${svVariant(td.wit)}).v" + is TypeDesc.Record -> + "${td.kotlinFqn}(" + + td.fields.mapIndexed { i, f -> decode(f.type, "($sv as SchemaValue.Record).fields[$i]") }.joinToString(", ") + + ")" + is TypeDesc.ListT -> "($sv as SchemaValue.ListVal).items.map { ${decode(td.elem, "it")} }" + is TypeDesc.OptionT -> "($sv as SchemaValue.OptionVal).inner?.let { ${decode(td.inner, "it")} }" + is TypeDesc.EnumT -> "${td.kotlinFqn}.entries[($sv as SchemaValue.EnumVal).caseIndex]" + is TypeDesc.VariantT -> { + // `it` = the VariantVal; branch bodies read it.payload!! (a Record for payload cases). + val branches = td.cases.mapIndexed { i, c -> + val body = if (c.payload == null) c.kotlinFqn else decode(c.payload, "it.payload!!") + "$i -> $body" + }.joinToString("; ") + "($sv as SchemaValue.VariantVal).let { when (it.caseIndex) { $branches; else -> error(\"unknown variant case\") } }" + } + is TypeDesc.MapT -> + "($sv as SchemaValue.MapVal).entries.associate { (kSv, vSv) -> ${decode(td.key, "kSv")} to ${decode(td.value, "vSv")} }" + is TypeDesc.TupleT -> + "${td.kotlinFqn}(" + + td.elems.mapIndexed { i, e -> decode(e, "($sv as SchemaValue.TupleVal).items[$i]") }.joinToString(", ") + + ")" + // ResultVal -> Either (ok -> Right, err -> Left); `it` = the ResultVal. + is TypeDesc.ResultT -> + "($sv as SchemaValue.ResultVal).let { if (it.ok) cloud.golem.runtime.Either.Right(${armDecode(td.ok)}) " + + "else cloud.golem.runtime.Either.Left(${armDecode(td.err)}) }" + TypeDesc.DatetimeT -> "($sv as SchemaValue.DatetimeVal).let { cloud.golem.Datetime(it.seconds, it.nanoseconds) }" + TypeDesc.UnitT -> error("converter codegen: cannot decode a Unit value") + } + + /** Decodes one arm of a `result` — `Unit` for a unit arm, else the arm's value at `it.inner!!`. */ + private fun armDecode(t: TypeDesc): String = if (t is TypeDesc.UnitT) "Unit" else decode(t, "it.inner!!") + + /** Recursively encodes a Kotlin value expression [k] of type [td] into a [SchemaValue]. */ + fun encode(td: TypeDesc, k: String): String = when (td) { + is TypeDesc.Prim -> "SchemaValue.${svVariant(td.wit)}($k)" + is TypeDesc.Record -> + "SchemaValue.Record(listOf(" + + td.fields.joinToString(", ") { f -> encode(f.type, "$k.${f.name}") } + + "))" + is TypeDesc.ListT -> "SchemaValue.ListVal(($k).map { ${encode(td.elem, "it")} })" + is TypeDesc.OptionT -> "SchemaValue.OptionVal(($k)?.let { ${encode(td.inner, "it")} })" + is TypeDesc.EnumT -> "SchemaValue.EnumVal(($k).ordinal)" + is TypeDesc.VariantT -> { + // `it` = the sealed value (smart-cast per branch); exhaustive over all subclasses. + val branches = td.cases.mapIndexed { i, c -> + val payloadExpr = c.payload?.let { pl -> encode(pl, "it") } ?: "null" + "is ${c.kotlinFqn} -> SchemaValue.VariantVal($i, $payloadExpr)" + }.joinToString("; ") + "($k).let { when (it) { $branches } }" + } + is TypeDesc.MapT -> + "SchemaValue.MapVal(($k).entries.map { (key, value) -> ${encode(td.key, "key")} to ${encode(td.value, "value")} })" + is TypeDesc.TupleT -> + "SchemaValue.TupleVal(listOf(" + + td.elems.mapIndexed { i, e -> encode(e, "($k).component${i + 1}()") }.joinToString(", ") + + "))" + // Either -> ResultVal; `it` = the Either (smart-cast per branch, so `it.value` is the arm). + is TypeDesc.ResultT -> + "($k).let { when (it) { " + + "is cloud.golem.runtime.Either.Right -> SchemaValue.ResultVal(true, ${armEncode(td.ok)}); " + + "is cloud.golem.runtime.Either.Left -> SchemaValue.ResultVal(false, ${armEncode(td.err)}) } }" + TypeDesc.DatetimeT -> "SchemaValue.DatetimeVal(($k).seconds, ($k).nanoseconds)" + TypeDesc.UnitT -> "SchemaValue.Unit_" + } + + /** Encodes one arm of a `result` — `null` for a unit arm, else the arm's value at `it.value`. */ + private fun armEncode(t: TypeDesc): String = if (t is TypeDesc.UnitT) "null" else encode(t, "it.value") + + /** The `SchemaValue` subclass name for a WIT primitive (e.g. s32 -> S32, string -> Str). */ + private fun svVariant(wit: String): String = when (wit) { + "bool" -> "Bool" + "s8" -> "S8" + "s16" -> "S16" + "s32" -> "S32" + "s64" -> "S64" + "u8" -> "U8" + "u16" -> "U16" + "u32" -> "U32" + "u64" -> "U64" + "f32" -> "F32" + "f64" -> "F64" + "char" -> "Chr" + "string" -> "Str" + else -> error("converter codegen: no SchemaValue variant for WIT primitive '$wit'") + } +} diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/GolemAgentProcessor.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/GolemAgentProcessor.kt new file mode 100644 index 0000000000..95fd33ab5e --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/GolemAgentProcessor.kt @@ -0,0 +1,199 @@ +package cloud.golem.ksp + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSValueParameter +import com.google.devtools.ksp.validate + +class GolemAgentProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, +) : SymbolProcessor { + + private val agentFqn = "cloud.golem.annotations.Agent" + private val remoteAgentFqn = "cloud.golem.annotations.RemoteAgent" + + override fun process(resolver: Resolver): List { + val (valid, deferred) = resolver + .getSymbolsWithAnnotation(agentFqn) + .partition { it.validate() } + + // @RemoteAgent interfaces -> typed RPC clients (independent of @Agent registration). + val (rpcValid, rpcDeferred) = resolver + .getSymbolsWithAnnotation(remoteAgentFqn) + .partition { it.validate() } + rpcValid.filterIsInstance().forEach { iface -> + val typeName = iface.annotation("RemoteAgent")?.arg("typeName") as? String + ?: error("@RemoteAgent on ${iface.simpleName.asString()} is missing typeName") + logger.info("GolemKSP: generating RPC client for ${iface.qualifiedName?.asString()}") + RemoteAgentEmitter(codeGenerator).emit(iface, typeName) + } + + val models = valid + .filterIsInstance() + .map { classDecl -> + val model = buildAgentModel(classDecl) + logger.info("GolemKSP: processing ${model.qualifiedName}") + HttpValidation.validate(model)?.let { logger.error(it, classDecl) } + NativeRegistrationEmitter(codeGenerator).emit(model) + WitEmitter(codeGenerator).emit(model) + model + } + .toList() + + if (models.isNotEmpty()) { + // Mirrors the JS-path processor: all @Agent classes must currently share one + // package so the generated entry point can call every register() via a + // single fully-qualified reference set without per-package import plumbing. + val distinctPackages = models.map { it.packageName }.distinct() + if (distinctPackages.size > 1) { + logger.error( + "GolemKSP: All @Agent classes must currently share one package so the " + + "generated native registration entry point can see them; " + + "multi-package support is not yet available. " + + "Found packages: ${distinctPackages.joinToString()}", + ) + } else { + // A single entry point that registers every @Agent and declares the real + // @WasmExport golem:agent/guest@2.0.0 functions (see NativeRegistrationEmitter's + // doc comment for why registration is triggered from here, not from main()). + NativeRegistrationEmitter(codeGenerator).emitEntryPoint(models) + } + } + + return deferred + rpcDeferred + } + + // ------------------------------------------------------------------------- + // Model building + // ------------------------------------------------------------------------- + + private fun buildAgentModel(classDecl: KSClassDeclaration): AgentModel { + val agent = classDecl.annotation("Agent") + ?: error("Class ${classDecl.simpleName.asString()} is missing @Agent") + val mount = agent.arg("mount") as? String ?: "" + // @Agent(description=...) is the primary source; a class-level @Description(text=...) wins if present. + val agentDesc = agent.arg("description") as? String ?: "" + val classDesc = classDecl.annotationByFqn("cloud.golem.annotations.Description")?.arg("text") as? String + ?: agentDesc + val mountAuth = agent.arg("auth") as? Boolean ?: false + + @Suppress("UNCHECKED_CAST") + val mountCors = (agent.arg("cors") as? List) ?: emptyList() + val mode = agent.arg("mode") as? String ?: "durable" + val snapshotting = agent.arg("snapshotting") as? String ?: "disabled" + + val constructorParams = classDecl.primaryConstructor + ?.parameters + ?.map { buildParamModel(it) } + ?: emptyList() + + // Only DECLARED members (no inherited ones), filtered to KSFunctionDeclaration -- + // avoids inherited/overridden @Endpoint methods appearing twice via getAllFunctions(). + // Dedupe by name keeping first occurrence as an extra guard against duplicate declarations. + val methods = classDecl.declarations + .filterIsInstance() + .filter { fn -> fn.annotationByFqn("cloud.golem.annotations.Endpoint") != null } + .map { fn -> buildMethodModel(fn) } + .toList() + .distinctBy { it.name } + + // Detect `cloud.golem.Snapshotted` in the supertypes and resolve `S` to a TypeDesc. + // A non-WIT-mappable `S` is a compile error (logger.error), never a silent skip. + val snapshotStateType: TypeDesc? = classDecl.superTypes + .map { it.resolve() } + .firstOrNull { it.declaration.qualifiedName?.asString() == "cloud.golem.Snapshotted" } + ?.arguments?.firstOrNull()?.type?.resolve() + ?.let { s -> + try { + TypeMapper.resolve(s) + } catch (e: Exception) { + logger.error( + "@Agent ${classDecl.simpleName.asString()} implements Snapshotted " + + "but S is not a WIT-mappable type: ${e.message}", + classDecl, + ) + null + } + } + + return AgentModel( + className = classDecl.simpleName.asString(), + qualifiedName = classDecl.qualifiedName?.asString() + ?: error("Anonymous class cannot be @Agent"), + packageName = classDecl.packageName.asString(), + mountPath = mount, + classDescription = classDesc, + mountAuth = mountAuth, + mountCors = mountCors, + mode = mode, + snapshotting = snapshotting, + constructorParams = constructorParams, + methods = methods, + snapshotStateType = snapshotStateType, + ) + } + + private fun buildParamModel(param: KSValueParameter): ParamModel = ParamModel( + name = param.name?.asString() ?: error("Unnamed constructor param"), + typeDesc = TypeMapper.resolve(param.type.resolve()), + ) + + private fun buildMethodModel(fn: KSFunctionDeclaration): MethodModel { + val endpoint = fn.annotationByFqn("cloud.golem.annotations.Endpoint")!! + val promptHint = fn.annotationByFqn("cloud.golem.annotations.Prompt")?.arg("hint") as? String ?: "" + val methodDesc = fn.annotationByFqn("cloud.golem.annotations.Description")?.arg("text") as? String ?: "" + + val endpointAuth = endpoint.arg("auth") as? Boolean ?: false + + @Suppress("UNCHECKED_CAST") + val endpointCors = (endpoint.arg("cors") as? List) ?: emptyList() + + val httpEndpoints = buildList { + val get = endpoint.arg("get") as? String ?: "" + val post = endpoint.arg("post") as? String ?: "" + val put = endpoint.arg("put") as? String ?: "" + val delete = endpoint.arg("delete") as? String ?: "" + if (get.isNotEmpty()) add(HttpEndpointModel("GET", get, endpointAuth, endpointCors)) + if (post.isNotEmpty()) add(HttpEndpointModel("POST", post, endpointAuth, endpointCors)) + if (put.isNotEmpty()) add(HttpEndpointModel("PUT", put, endpointAuth, endpointCors)) + if (delete.isNotEmpty()) add(HttpEndpointModel("DELETE", delete, endpointAuth, endpointCors)) + } + + val inputParams = fn.parameters.map { buildParamModel(it) } + val returnType = fn.returnType?.resolve() + ?: error("Method ${fn.simpleName.asString()} has no return type") + + // @ReadOnly(cache=...) -> the read-only-config's cache policy; absent => not read-only. + val readOnly = fn.annotationByFqn("cloud.golem.annotations.ReadOnly") + val readOnlyCache = if (readOnly != null) (readOnly.arg("cache") as? String ?: "until-write") else null + + return MethodModel( + name = fn.simpleName.asString(), + description = methodDesc, + promptHint = promptHint, + inputParams = inputParams, + outputTypeDesc = TypeMapper.resolve(returnType), + httpEndpoints = httpEndpoints, + readOnlyCache = readOnlyCache, + ) + } + + // ------------------------------------------------------------------------- + // Annotation helpers + // ------------------------------------------------------------------------- + + private fun KSAnnotated.annotationByFqn(fqn: String): KSAnnotation? = annotations.firstOrNull { + it.annotationType.resolve().declaration.qualifiedName?.asString() == fqn + } + + private fun KSAnnotated.annotation(shortName: String): KSAnnotation? = annotations.firstOrNull { it.shortName.asString() == shortName } + + private fun KSAnnotation.arg(name: String): Any? = arguments.firstOrNull { it.name?.asString() == name }?.value +} diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/GolemAgentProcessorProvider.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/GolemAgentProcessorProvider.kt new file mode 100644 index 0000000000..4735d43245 --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/GolemAgentProcessorProvider.kt @@ -0,0 +1,12 @@ +package cloud.golem.ksp + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class GolemAgentProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = GolemAgentProcessor( + codeGenerator = environment.codeGenerator, + logger = environment.logger, + ) +} diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/HttpValidation.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/HttpValidation.kt new file mode 100644 index 0000000000..25339f1bff --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/HttpValidation.kt @@ -0,0 +1,96 @@ +package cloud.golem.ksp + +/** + * Compile-time validation of `@Agent(mount = ...)` / `@Endpoint` HTTP route templates against the + * agent's constructor and method parameters. Ported from the Scala SDK's + * `golem.runtime.http.HttpValidation`, restricted to the subset the Kotlin native surface exposes: + * only *path* variables (there are no header/query-variable or `Principal` annotations yet), and + * using the **native path-segment convention** — `"{name}"` is a path variable and `"{+name}"` is a + * catch-all (remaining) variable — matching `lowerPathSegmentsInto` in the runtime's + * `AgentTypeModel.kt`. Header/query/Principal checks from the Scala version are intentionally not + * ported because the annotations they validate do not exist on the Kotlin surface. + * + * Runs at KSP time (see [GolemAgentProcessor]); a violation is reported via `logger.error`, failing + * the build with a message pointing at the offending agent. + */ +internal object HttpValidation { + + /** Returns a human-readable message for the first rule [model] violates, or `null` if valid. */ + fun validate(model: AgentModel): String? { + val hasMount = model.mountPath.isNotEmpty() + val ctorParamNames = model.constructorParams.map { it.name }.toSet() + val agent = model.className + + if (hasMount) { + val mountSegments = parse(model.mountPath) + + // 1. Mount paths must not contain a catch-all (remaining) variable. + mountSegments.filterIsInstance().firstOrNull()?.let { seg -> + return "HTTP mount '${model.mountPath}' for agent '$agent' cannot contain a " + + "catch-all path variable '{+${seg.name}}'." + } + + val mountVars = mountSegments.filterIsInstance().map { it.name } + + // 2. Every mount path variable must name a constructor parameter. + mountVars.firstOrNull { it !in ctorParamNames }?.let { name -> + return "HTTP mount path variable '{$name}' of agent '$agent' is not a constructor " + + "parameter (constructor parameters: ${ctorParamNames.sorted().joinToString(", ").ifEmpty { "none" }})." + } + + // 3. Every constructor parameter must be provided by the mount path — a mounted agent's + // identity has to be fully addressable from its URL. + val provided = mountVars.toSet() + model.constructorParams.firstOrNull { it.name !in provided }?.let { param -> + return "Agent '$agent' constructor parameter '${param.name}' is not provided by the " + + "HTTP mount path '${model.mountPath}'." + } + } + + // Endpoint-level checks. + for (method in model.methods) { + if (method.httpEndpoints.isEmpty()) continue + + // 4. A method cannot expose HTTP endpoints on an unmounted agent. + if (!hasMount) { + return "Method '${method.name}' of agent '$agent' defines HTTP endpoint(s) but the " + + "agent has no HTTP mount. Add mount = \"...\" to @Agent." + } + + val methodParamNames = method.inputParams.map { it.name }.toSet() + for (endpoint in method.httpEndpoints) { + // 5. Every endpoint path variable (plain or catch-all) must name a method parameter. + // Catch-all IS allowed in an endpoint suffix (only the mount forbids it). + val endpointVars = parse(endpoint.path).mapNotNull { seg -> + when (seg) { + is Segment.Variable -> seg.name + is Segment.Remaining -> seg.name + is Segment.Literal -> null + } + } + endpointVars.firstOrNull { it !in methodParamNames }?.let { name -> + return "HTTP endpoint path variable '{$name}' in method '${method.name}' of agent " + + "'$agent' is not a parameter of that method." + } + } + } + + return null + } + + private sealed interface Segment { + data class Literal(val text: String) : Segment + data class Variable(val name: String) : Segment + data class Remaining(val name: String) : Segment + } + + /** Splits a route template into segments using the runtime's `lowerPathSegmentsInto` rules. */ + private fun parse(path: String): List = path.split("/").filter { it.isNotEmpty() }.map { seg -> + if (seg.startsWith("{") && seg.endsWith("}")) { + val inner = seg.substring(1, seg.length - 1) + if (inner.startsWith("+")) Segment.Remaining(inner.substring(1)) else Segment.Variable(inner) + } else { + Segment.Literal(seg) + } + } +} diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/NativeRegistrationEmitter.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/NativeRegistrationEmitter.kt new file mode 100644 index 0000000000..7cde6778d3 --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/NativeRegistrationEmitter.kt @@ -0,0 +1,276 @@ +package cloud.golem.ksp + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies + +/** + * Generates the native (Kotlin/Wasm) registration + `@WasmExport` entry points. + * + * The agent is compiled against `golem-kotlin-sdk` as a normal Kotlin/Wasm dependency, so the + * generated code uses the SDK's real `NativeAgentDescriptor`/`SchemaValue` types directly. + * + * Two outputs: + * - `Registration.kt` : `fun register()` per `@Agent` class, calling + * `NativeAgentRuntime.registerAgent(NativeAgentDescriptor(...))` with handler/factory lambdas + * that read `List` (via `.asString()`/`.asInt()`) and return a `SchemaValue`. + * - `GolemGeneratedGuest.kt` : the actual `@WasmExport("golem:agent/guest@2.0.0#...")` functions. + * + * WHY the wrappers live here, not in the SDK's `Guest.kt` (see that file's doc comment for the + * full rationale): a Kotlin/Wasm reactor component does not run top-level `val` initializers + * automatically at instantiation, and unreferenced top-level code is dead-code-eliminated -- so + * agent registration (which the SDK cannot see ahead of time) needs a guaranteed-reachable + * trigger. Generating the real `@WasmExport` functions here guarantees a call to + * `registerAllAgents()` happens before any SDK logic runs, because the host is contractually + * required to call these exports to talk to the agent at all. + */ +class NativeRegistrationEmitter(private val codeGenerator: CodeGenerator) { + + /** Per-agent registration function. */ + fun emit(model: AgentModel) { + val code = buildString { + appendLine("// AUTO-GENERATED by golem-kotlin-ksp — do not edit") + appendLine("package ${model.packageName}") + appendLine() + appendLine("import cloud.golem.runtime.NativeAgentRuntime") + appendLine("import cloud.golem.runtime.NativeAgentDescriptor") + appendLine("import cloud.golem.runtime.NativeMethodDescriptor") + appendLine("import cloud.golem.runtime.NativeParamSchema") + appendLine("import cloud.golem.runtime.NativeHttpEndpoint") + appendLine("import cloud.golem.runtime.SchemaValue") + appendLine() + appendLine("fun register${model.className}() {") + appendLine(" NativeAgentRuntime.registerAgent(") + appendLine(" NativeAgentDescriptor(") + appendLine(" ${lit(model.className)},") + appendLine(" ${lit(model.classDescription)},") + appendLine(" ${lit(model.mountPath)},") + appendLine(" ${paramSchemaList(model.constructorParams)},") + appendLine(" listOf(") + model.methods.forEachIndexed { i, m -> + val comma = if (i < model.methods.size - 1) "," else "" + appendLine(" ${methodDescriptor(model.className, m)}$comma") + } + appendLine(" ),") + appendLine(" ${factoryLambda(model)},") + appendLine(" ${model.mountAuth},") + appendLine(" ${corsList(model.mountCors)},") + appendLine(" ${lit(model.mode)},") + append(" ${lit(model.snapshotting)}") + appendLine(snapshotArg(model)) + appendLine(" )") + appendLine(" )") + appendLine("}") + } + write(model.packageName, "${model.className}Registration", code) + } + + /** Single entry point: idempotent registration + the real @WasmExport guest functions. */ + fun emitEntryPoint(models: List) { + val pkg = models.first().packageName + val code = buildString { + appendLine("// AUTO-GENERATED by golem-kotlin-ksp — do not edit") + appendLine("package $pkg") + appendLine() + appendLine("import kotlin.wasm.WasmExport") + appendLine() + // cabi_realloc must be declared HERE (the agent's own module), not in the SDK: a + // Kotlin/Wasm library dependency's @WasmExport declarations do not survive into the + // consuming executable's final linked module (verified empirically -- the same + // cross-module DCE limitation as the golem:agent/guest exports below). + appendLine("@WasmExport(\"cabi_realloc\")") + appendLine("fun golemGuestCabiRealloc(oldPtr: Int, oldSize: Int, align: Int, newSize: Int): Int =") + appendLine(" cloud.golem.wasm.cabiRealloc(oldPtr, oldSize, align, newSize)") + appendLine() + appendLine("private var golemAgentsRegistered = false") + appendLine() + appendLine("fun registerAllAgents() {") + appendLine(" if (golemAgentsRegistered) return") + appendLine(" golemAgentsRegistered = true") + models.forEach { m -> + appendLine(" ${m.packageName}.register${m.className}()") + } + appendLine("}") + appendLine() + appendLine("@WasmExport(\"golem:agent/guest@2.0.0#initialize\")") + appendLine("fun golemGuestInitialize(argsPtr: Int): Int {") + appendLine(" registerAllAgents()") + appendLine(" return cloud.golem.runtime.initialize(argsPtr)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:agent/guest@2.0.0#initialize\")") + appendLine("fun golemGuestCabiPostInitialize(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostInitialize(resultPtr)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"golem:agent/guest@2.0.0#invoke\")") + appendLine("fun golemGuestInvoke(argsPtr: Int): Int {") + appendLine(" registerAllAgents()") + appendLine(" return cloud.golem.runtime.invoke(argsPtr)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:agent/guest@2.0.0#invoke\")") + appendLine("fun golemGuestCabiPostInvoke(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostInvoke(resultPtr)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"golem:agent/guest@2.0.0#get-definition\")") + appendLine("fun golemGuestGetDefinition(): Int {") + appendLine(" registerAllAgents()") + appendLine(" return cloud.golem.runtime.getDefinition()") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:agent/guest@2.0.0#get-definition\")") + appendLine("fun golemGuestCabiPostGetDefinition(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostGetDefinition(resultPtr)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"golem:agent/guest@2.0.0#discover-agent-types\")") + appendLine("fun golemGuestDiscoverAgentTypes(): Int {") + appendLine(" registerAllAgents()") + appendLine(" return cloud.golem.runtime.discoverAgentTypes()") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:agent/guest@2.0.0#discover-agent-types\")") + appendLine("fun golemGuestCabiPostDiscoverAgentTypes(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostDiscoverAgentTypes(resultPtr)") + appendLine("}") + appendLine() + // wit-native's shared world always includes golem:tool/tool-guest@0.1.0 (component + // embed requires every world export to exist), so these are emitted unconditionally + // -- even a project with zero @Tool-annotated methods needs a (functionally empty) + // tool interface. cloud.golem.runtime's tool functions already handle an empty + // NativeToolRuntime registry gracefully (discover-tools -> ok([]), get-tool/invoke -> + // "unknown tool" errors). No @Tool KSP parsing exists yet (a follow-up), so no + // registerAllTools() call is emitted here. + appendLine("@WasmExport(\"golem:tool/guest@0.1.0#discover-tools\")") + appendLine("fun golemToolDiscoverTools(): Int = cloud.golem.runtime.discoverTools()") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:tool/guest@0.1.0#discover-tools\")") + appendLine("fun golemToolCabiPostDiscoverTools(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostDiscoverTools(resultPtr)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"golem:tool/guest@0.1.0#get-tool\")") + appendLine("fun golemToolGetTool(namePtr: Int, nameLen: Int): Int =") + appendLine(" cloud.golem.runtime.getTool(namePtr, nameLen)") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:tool/guest@0.1.0#get-tool\")") + appendLine("fun golemToolCabiPostGetTool(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostGetTool(resultPtr)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"golem:tool/guest@0.1.0#invoke\")") + appendLine("fun golemToolInvoke(argsPtr: Int): Int = cloud.golem.runtime.invokeTool(argsPtr)") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:tool/guest@0.1.0#invoke\")") + appendLine("fun golemToolCabiPostInvoke(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostInvokeTool(resultPtr)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"golem:api/save-snapshot@1.5.0#save\")") + appendLine("fun golemSaveSnapshot(): Int {") + appendLine(" registerAllAgents()") + appendLine(" return cloud.golem.runtime.saveSnapshot()") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:api/save-snapshot@1.5.0#save\")") + appendLine("fun golemCabiPostSaveSnapshot(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostSaveSnapshot(resultPtr)") + appendLine("}") + appendLine() + // Canonical ABI flattens record { payload: list, mime-type: string } into + // four I32 params; wasm-tools component embed expects [I32,I32,I32,I32] -> [I32]. + appendLine("@WasmExport(\"golem:api/load-snapshot@1.5.0#load\")") + appendLine("fun golemLoadSnapshot(payloadPtr: Int, payloadLen: Int, mimeTypePtr: Int, mimeTypeLen: Int): Int {") + appendLine(" registerAllAgents()") + appendLine(" return cloud.golem.runtime.loadSnapshotFlat(payloadPtr, payloadLen, mimeTypePtr, mimeTypeLen)") + appendLine("}") + appendLine() + appendLine("@WasmExport(\"cabi_post_golem:api/load-snapshot@1.5.0#load\")") + appendLine("fun golemCabiPostLoadSnapshot(resultPtr: Int) {") + appendLine(" cloud.golem.runtime.cabiPostLoadSnapshot(resultPtr)") + appendLine("}") + } + write(pkg, "GolemGeneratedGuest", code) + } + + // -- code generation ----------------------------------------------------- + + private fun methodDescriptor(className: String, m: MethodModel): String { + val inputs = paramSchemaList(m.inputParams) + val endpoints = if (m.httpEndpoints.isEmpty()) { + "emptyList()" + } else { + "listOf(${m.httpEndpoints.joinToString(", ") { + "NativeHttpEndpoint(${lit(it.verb)}, ${lit(it.path)}, ${it.auth}, ${corsList(it.cors)})" + }})" + } + val readOnly = m.readOnlyCache?.let { lit(it) } ?: "null" + return "NativeMethodDescriptor(${lit(m.name)}, ${lit(m.outputWitType)}, $inputs, $endpoints, ${lit(m.promptHint)}, $readOnly) ${handlerLambda(className, m)}" + } + + private fun handlerLambda(className: String, m: MethodModel): String { + val args = m.inputParams.mapIndexed { i, p -> decode(p.typeDesc, "input[$i]") }.joinToString(", ") + val secondParam = if (m.inputParams.isEmpty()) "_" else "input" + val call = "(instance as $className).${m.name}($args)" + return when (m.outputTypeDesc) { + is TypeDesc.UnitT -> "{ instance, $secondParam -> $call; SchemaValue.Unit_ }" + is TypeDesc.Prim -> "{ instance, $secondParam -> ${encode(m.outputTypeDesc, call)} }" // single-use, inline + else -> // bind the result so composite encoders can reference its fields without re-invoking + "{ instance, $secondParam -> val golemResult = $call; ${encode(m.outputTypeDesc, "golemResult")} }" + } + } + + /** + * The `snapshotCodec = SnapshotCodec(...)` argument for the `NativeAgentDescriptor(...)` call, + * or "" when the agent doesn't mix in `Snapshotted`. Composes ConverterCodegen (Kotlin <-> + * SchemaValue) with SchemaValueBytes (SchemaValue <-> ByteArray). Fully-qualifies the runtime + * references so no extra imports are needed in the generated file. + */ + private fun snapshotArg(model: AgentModel): String { + val td = model.snapshotStateType ?: return "" + val enc = encode(td, "(inst as ${model.className}).state") + val dec = decode(td, "cloud.golem.runtime.SchemaValueBytes.decode(bytes)") + return ",\n" + + " snapshotCodec = cloud.golem.runtime.SnapshotCodec(\n" + + " save = { inst -> cloud.golem.runtime.SchemaValueBytes.encode($enc) },\n" + + " load = { inst, bytes -> (inst as ${model.className}).state = $dec },\n" + + " )" + } + + private fun factoryLambda(model: AgentModel): String { + if (model.constructorParams.isEmpty()) return "{ _ -> ${model.className}() }" + val args = model.constructorParams.mapIndexed { i, p -> decode(p.typeDesc, "input[$i]") }.joinToString(", ") + return "{ input -> ${model.className}($args) }" + } + + private fun decode(td: TypeDesc, sv: String): String = ConverterCodegen.decode(td, sv) + private fun encode(td: TypeDesc, k: String): String = ConverterCodegen.encode(td, k) + + private fun paramSchemaList(params: List): String = if (params.isEmpty()) { + "emptyList()" + } else { + "listOf(${params.joinToString(", ") { "NativeParamSchema(${lit(it.name)}, ${lit(it.witType)})" }})" + } + + private fun corsList(patterns: List): String = if (patterns.isEmpty()) { + "emptyList()" + } else { + "listOf(${patterns.joinToString(", ") { lit(it) }})" + } + + /** Kotlin string literal with escaping. */ + private fun lit(s: String): String { + val escaped = s.replace("\\", "\\\\").replace("\"", "\\\"") + return "\"$escaped\"" + } + + private fun write(packageName: String, fileName: String, code: String) { + codeGenerator.createNewFile( + dependencies = Dependencies.ALL_FILES, + packageName = packageName, + fileName = fileName, + extensionName = "kt", + ).bufferedWriter().use { it.write(code) } + } +} diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/RemoteAgentEmitter.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/RemoteAgentEmitter.kt new file mode 100644 index 0000000000..261de0a93c --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/RemoteAgentEmitter.kt @@ -0,0 +1,93 @@ +package cloud.golem.ksp + +import com.google.devtools.ksp.getDeclaredFunctions +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSType + +/** + * Generates a typed RPC client (`Rpc`) for a `@RemoteAgent`-annotated interface. Each of + * the interface's abstract methods becomes an override that encodes its arguments to a + * `schema-value-tree` (via [ConverterCodegen]), invokes the remote agent through [WasmRpc], and + * decodes the result back to the method's Kotlin return type. The client's constructor takes the + * remote agent's constructor arguments as a `SchemaValue` (typically a `Record`); method calls are + * fully typed. + */ +class RemoteAgentEmitter(private val codeGenerator: CodeGenerator) { + + fun emit(iface: KSClassDeclaration, typeName: String) { + val pkg = iface.packageName.asString() + val ifaceFqn = iface.qualifiedName?.asString() ?: error("@RemoteAgent interface has no qualified name") + val clientName = "${iface.simpleName.asString()}Rpc" + val methods = iface.getDeclaredFunctions().filter { it.isAbstract }.toList() + + val code = buildString { + appendLine("// AUTO-GENERATED by golem-kotlin-ksp — do not edit") + appendLine("package $pkg") + appendLine() + appendLine("import cloud.golem.runtime.SchemaValue") + appendLine("import cloud.golem.runtime.WasmRpc") + appendLine("import cloud.golem.runtime.RpcResult") + appendLine("import cloud.golem.runtime.RpcException") + appendLine() + appendLine("/** Typed RPC client for the remote agent \"$typeName\" (implements $ifaceFqn). */") + appendLine("class $clientName(constructorArgs: SchemaValue) : $ifaceFqn {") + appendLine(" private val rpc = WasmRpc(${lit(typeName)}, constructorArgs)") + appendLine() + methods.forEach { appendLine(renderMethod(it)) } + appendLine(" /** Releases the underlying wasm-rpc handle. */") + appendLine(" fun close() = rpc.close()") + appendLine("}") + } + write(pkg, clientName, code) + } + + private fun renderMethod(fn: KSFunctionDeclaration): String { + val name = fn.simpleName.asString() + val params = fn.parameters.map { (it.name?.asString() ?: error("unnamed param")) to it.type.resolve() } + val returnType = fn.returnType?.resolve() ?: error("method $name has no return type") + val retTd = TypeMapper.resolve(returnType) + + val sig = params.joinToString(", ") { (n, t) -> "$n: ${renderType(t)}" } + val argsList = params.joinToString(", ") { (n, t) -> ConverterCodegen.encode(TypeMapper.resolve(t), n) } + + return buildString { + appendLine(" override fun $name($sig): ${renderType(returnType)} {") + appendLine(" val golemArgs = SchemaValue.Record(listOf($argsList))") + appendLine(" val golemR = rpc.invokeAndAwait(${lit(name)}, golemArgs, ${lit(retTd.toWit())})") + if (retTd is TypeDesc.UnitT) { + appendLine(" when (golemR) { is RpcResult.Ok -> Unit; is RpcResult.Err -> throw RpcException(golemR.error) }") + } else { + appendLine(" return when (golemR) {") + appendLine(" is RpcResult.Ok -> ${ConverterCodegen.decode(retTd, "golemR.value!!")}") + appendLine(" is RpcResult.Err -> throw RpcException(golemR.error)") + appendLine(" }") + } + append(" }") + } + } + + /** Renders a [KSType] back to fully-qualified Kotlin source (handles generics + nullability). */ + private fun renderType(type: KSType): String { + val fqn = type.declaration.qualifiedName?.asString() ?: type.declaration.simpleName.asString() + val args = if (type.arguments.isEmpty()) { + "" + } else { + "<" + type.arguments.joinToString(", ") { arg -> arg.type?.resolve()?.let { renderType(it) } ?: "*" } + ">" + } + return fqn + args + (if (type.isMarkedNullable) "?" else "") + } + + private fun lit(s: String): String = "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"" + + private fun write(packageName: String, fileName: String, code: String) { + codeGenerator.createNewFile( + dependencies = Dependencies.ALL_FILES, + packageName = packageName, + fileName = fileName, + extensionName = "kt", + ).bufferedWriter().use { it.write(code) } + } +} diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/TypeDesc.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/TypeDesc.kt new file mode 100644 index 0000000000..7fe5d5358e --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/TypeDesc.kt @@ -0,0 +1,78 @@ +package cloud.golem.ksp + +/** + * A resolved agent-surface type, produced from a Kotlin `KSType` and used to drive BOTH the rich + * witType string ([toWit], consumed by the runtime schema-graph builder + value lift) AND the + * recursive SchemaValue<->Kotlin converter codegen in [NativeRegistrationEmitter]. + * + * The full composite set is modelled here; [TypeMapper.resolve] and the emitter's converters are + * extended kind-by-kind across increments. + */ +sealed class TypeDesc { + abstract fun toWit(): String + + /** A WIT primitive (`s32`, `string`, `bool`, ...). */ + data class Prim(val wit: String) : TypeDesc() { + override fun toWit(): String = wit + } + + /** `kotlin.Unit` — a method with no return value. */ + object UnitT : TypeDesc() { + override fun toWit(): String = "()" + } + + /** A Kotlin data class -> WIT record. [kotlinFqn] is the fully-qualified class name (for ctor calls). */ + data class Record(val kotlinFqn: String, val fields: List) : TypeDesc() { + override fun toWit(): String = "record<" + fields.joinToString(",") { "${it.name}:${it.type.toWit()}" } + ">" + } + + /** Kotlin `List` -> WIT list. */ + data class ListT(val elem: TypeDesc) : TypeDesc() { + override fun toWit(): String = "list<${elem.toWit()}>" + } + + /** Kotlin `T?` -> WIT option. */ + data class OptionT(val inner: TypeDesc) : TypeDesc() { + override fun toWit(): String = "option<${inner.toWit()}>" + } + + /** Kotlin enum class -> WIT enum. [cases] are the entry names in declaration order. */ + data class EnumT(val kotlinFqn: String, val cases: List) : TypeDesc() { + override fun toWit(): String = "enum<" + cases.joinToString(",") + ">" + } + + /** Kotlin sealed class/interface -> WIT variant. */ + data class VariantT(val kotlinFqn: String, val cases: List) : TypeDesc() { + override fun toWit(): String = "variant<" + cases.joinToString(",") { "${it.name}:${it.payload?.toWit() ?: "_"}" } + ">" + } + + /** Kotlin `Map` -> WIT map. */ + data class MapT(val key: TypeDesc, val value: TypeDesc) : TypeDesc() { + override fun toWit(): String = "map<${key.toWit()},${value.toWit()}>" + } + + /** Kotlin `Pair`/`Triple` -> WIT tuple. [kotlinFqn] is the tuple class (for ctor calls). */ + data class TupleT(val kotlinFqn: String, val elems: List) : TypeDesc() { + override fun toWit(): String = "tuple<" + elems.joinToString(",") { it.toWit() } + ">" + } + + /** + * Kotlin `Either` -> WIT `result` (`Right` = ok, `Left` = err). A `Unit` arm becomes + * `_` (WIT's unit ok/err marker). + */ + data class ResultT(val ok: TypeDesc, val err: TypeDesc) : TypeDesc() { + override fun toWit(): String = "result<${arm(ok)},${arm(err)}>" + private fun arm(t: TypeDesc): String = if (t is UnitT) "_" else t.toWit() + } + + /** Kotlin `cloud.golem.Datetime` -> WIT `datetime`. */ + object DatetimeT : TypeDesc() { + override fun toWit(): String = "datetime" + } +} + +/** A record field: its name + type. */ +data class Field(val name: String, val type: TypeDesc) + +/** A variant case: its name, the Kotlin subclass FQN, and its payload type (null for a no-payload case). */ +data class VariantCase(val name: String, val kotlinFqn: String, val payload: TypeDesc?) diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/TypeMapper.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/TypeMapper.kt new file mode 100644 index 0000000000..be5d06cbde --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/TypeMapper.kt @@ -0,0 +1,154 @@ +package cloud.golem.ksp + +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.Modifier + +/** + * Maps Kotlin types to WIT type strings and back. + * + * Each Kotlin primitive maps to a distinct WIT width (Int<->s32, Short<->s16, Byte<->s8, + * UInt<->u32, ..., Float/Double<->f32/f64), plus String<->string, Boolean<->bool and + * Unit<->(). `fqnToWit` and `witToFqn` are exact inverses, so every supported primitive + * survives a Kotlin -> WIT -> Kotlin round-trip (verified by the round-trip consistency test). + */ +object TypeMapper { + + /** Map a resolved KSP type to its WIT type string (derived from its [resolve]d [TypeDesc]). */ + fun toWit(type: KSType): String = resolve(type).toWit() + + private val primitives = mapOf( + "kotlin.Int" to "s32", "kotlin.Long" to "s64", "kotlin.Short" to "s16", "kotlin.Byte" to "s8", + "kotlin.UInt" to "u32", "kotlin.ULong" to "u64", "kotlin.UShort" to "u16", "kotlin.UByte" to "u8", + "kotlin.Float" to "f32", "kotlin.Double" to "f64", "kotlin.Boolean" to "bool", "kotlin.String" to "string", + ) + + /** + * Resolves a Kotlin [KSType] to a [TypeDesc]. Supports primitives, `Unit`, `T?` (option), + * `List`, `Map`, `Pair`/`Triple` (tuple), enum classes, sealed classes (variant), data classes + * (record), `Datetime`, and `Either` (result) -- recursing into type arguments/fields. + */ + fun resolve(type: KSType): TypeDesc { + val decl = type.declaration + val fqn = decl.qualifiedName?.asString() ?: error("Cannot resolve type for WIT mapping: $type") + + // Nullable T? -> option (wraps whatever the non-null form resolves to). + if (type.isMarkedNullable) { + return TypeDesc.OptionT(resolve(type.makeNotNullable())) + } + + primitives[fqn]?.let { return TypeDesc.Prim(it) } + if (fqn == "kotlin.Unit") return TypeDesc.UnitT + + // List -> list. + if (fqn == "kotlin.collections.List") { + val elem = type.arguments.single().type?.resolve() + ?: error("Cannot resolve List element type of $type") + return TypeDesc.ListT(resolve(elem)) + } + + // Map -> map. + if (fqn == "kotlin.collections.Map") { + val k = type.arguments[0].type?.resolve() ?: error("Cannot resolve Map key type of $type") + val v = type.arguments[1].type?.resolve() ?: error("Cannot resolve Map value type of $type") + return TypeDesc.MapT(resolve(k), resolve(v)) + } + + // Pair / Triple -> tuple<...>. + if (fqn == "kotlin.Pair" || fqn == "kotlin.Triple") { + val elems = type.arguments.map { + resolve(it.type?.resolve() ?: error("Cannot resolve tuple element type of $type")) + } + return TypeDesc.TupleT(fqn, elems) + } + + // cloud.golem.Datetime -> datetime. + if (fqn == "cloud.golem.Datetime") return TypeDesc.DatetimeT + + // cloud.golem.runtime.Either -> result (Right = ok, Left = err). + if (fqn == "cloud.golem.runtime.Either") { + val l = type.arguments[0].type?.resolve() ?: error("Cannot resolve Either Left type of $type") + val r = type.arguments[1].type?.resolve() ?: error("Cannot resolve Either Right type of $type") + return TypeDesc.ResultT(ok = resolve(r), err = resolve(l)) + } + + if (decl is KSClassDeclaration) { + // enum class -> enum (entry names in declaration order). + if (decl.classKind == ClassKind.ENUM_CLASS) { + val cases = decl.declarations.filterIsInstance() + .filter { it.classKind == ClassKind.ENUM_ENTRY } + .map { it.simpleName.asString() } + .toList() + return TypeDesc.EnumT(fqn, cases) + } + // sealed class/interface -> variant. Each direct subclass is a + // case: an object subclass has no payload; a subclass with constructor params carries a + // record of those params. Case order is fixed here and reused for decode/encode. + if (Modifier.SEALED in decl.modifiers) { + val cases = decl.getSealedSubclasses().map { sub -> + val subFqn = sub.qualifiedName?.asString() ?: error("sealed subclass of $fqn has no qualified name") + val hasParams = sub.classKind != ClassKind.OBJECT && (sub.primaryConstructor?.parameters?.isNotEmpty() == true) + VariantCase(sub.simpleName.asString(), subFqn, if (hasParams) recordOf(sub, subFqn) else null) + }.toList() + require(cases.isNotEmpty()) { "sealed type $fqn has no subclasses" } + return TypeDesc.VariantT(fqn, cases) + } + // data class -> record. + if (Modifier.DATA in decl.modifiers) return recordOf(decl, fqn) + } + + error("Unsupported Kotlin type for WIT mapping: $fqn (composite kind not yet supported)") + } + + /** Builds a `record` TypeDesc from [decl]'s primary constructor params (recursing each). */ + private fun recordOf(decl: KSClassDeclaration, fqn: String): TypeDesc.Record { + val ctor = decl.primaryConstructor ?: error("$fqn has no primary constructor") + return TypeDesc.Record( + fqn, + ctor.parameters.map { p -> + Field(p.name?.asString() ?: error("Unnamed field in $fqn"), resolve(p.type.resolve())) + }, + ) + } + + /** Overload for callers that already have the qualified name (e.g. tests). */ + fun fqnToWit(fqn: String): String = when (fqn) { + "kotlin.Int" -> "s32" + "kotlin.Long" -> "s64" + "kotlin.Short" -> "s16" + "kotlin.Byte" -> "s8" + "kotlin.UInt" -> "u32" + "kotlin.ULong" -> "u64" + "kotlin.UShort" -> "u16" + "kotlin.UByte" -> "u8" + "kotlin.Float" -> "f32" + "kotlin.Double" -> "f64" + "kotlin.Boolean" -> "bool" + "kotlin.String" -> "string" + "kotlin.Unit" -> "()" + else -> error("Unsupported Kotlin type for WIT mapping: $fqn") + } + + /** + * Reverse: WIT type string -> Kotlin qualified name. The exact inverse of [fqnToWit] + * over every supported primitive (each WIT width reconstructs its distinct Kotlin type). + * Used by the round-trip consistency test. + */ + fun witToFqn(wit: String): String = when (wit) { + "s32" -> "kotlin.Int" + "s64" -> "kotlin.Long" + "s16" -> "kotlin.Short" + "s8" -> "kotlin.Byte" + "u32" -> "kotlin.UInt" + "u64" -> "kotlin.ULong" + "u16" -> "kotlin.UShort" + "u8" -> "kotlin.UByte" + "f32" -> "kotlin.Float" + "f64" -> "kotlin.Double" + "bool" -> "kotlin.Boolean" + "string" -> "kotlin.String" + "()" -> "kotlin.Unit" + else -> error("Unsupported WIT type for Kotlin mapping: $wit") + } +} diff --git a/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/WitEmitter.kt b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/WitEmitter.kt new file mode 100644 index 0000000000..c9100bb91d --- /dev/null +++ b/sdks/kotlin/ksp/src/main/kotlin/cloud/golem/ksp/WitEmitter.kt @@ -0,0 +1,80 @@ +package cloud.golem.ksp + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies + +/** + * Generates `-agent.wit`: a world that includes the Golem agent-guest + * interface, plus agent-metadata comments tooling (e.g. wit-bindgen-kotlin) can read. + */ +class WitEmitter(private val codeGenerator: CodeGenerator) { + + fun emit(model: AgentModel) { + val componentName = toKebabCase( + model.className.removeSuffix("Agent").ifEmpty { model.className }, + ) + val packageName = "kotlin:$componentName" + val worldName = "$componentName-agent" + val version = "0.1.0" + + val wit = buildWit(model, packageName, worldName, version) + + codeGenerator.createNewFile( + dependencies = Dependencies.ALL_FILES, + packageName = "wit", // KSP treats this as a sub-directory segment + fileName = worldName, + extensionName = "wit", + ).bufferedWriter().use { it.write(wit) } + } + + private fun buildWit( + model: AgentModel, + packageName: String, + worldName: String, + version: String, + ): String = buildString { + appendLine("package $packageName@$version;") + appendLine() + appendLine("world $worldName {") + appendLine(" include golem:agent/agent-guest@2.0.0;") + appendLine("}") + appendLine() + appendLine("// --- Agent metadata (for tooling — not parsed by wasm-tools) ---") + appendLine("//") + appendLine("// agent ${model.className} {") + appendLine("// mount: \"${sanitizeComment(model.mountPath)}\"") + appendLine("// description: \"${sanitizeComment(model.classDescription)}\"") + if (model.constructorParams.isNotEmpty()) { + val params = model.constructorParams.joinToString(", ") { "${it.name}: ${it.witType}" } + appendLine("// constructor($params)") + } else { + appendLine("// constructor()") + } + model.methods.forEach { m -> + val params = if (m.inputParams.isEmpty()) { + "" + } else { + m.inputParams.joinToString(", ") { "${it.name}: ${it.witType}" } + } + val returnPart = if (m.outputWitType == "()") "" else " -> ${m.outputWitType}" + val endpointParts = m.httpEndpoints.joinToString(", ") { + "${it.verb.lowercase()}: \"${sanitizeComment(it.path)}\"" + } + val promptPart = if (m.promptHint.isNotEmpty()) ", prompt: \"${sanitizeComment(m.promptHint)}\"" else "" + val meta = if (endpointParts.isNotEmpty() || promptPart.isNotEmpty()) { + " { $endpointParts$promptPart }" + } else { + "" + } + appendLine("// method ${m.name}($params)$returnPart$meta") + } + appendLine("// }") + } + + /** "CounterAgent" -> "counter", "MyFooAgent" -> "my-foo" */ + private fun toKebabCase(name: String): String = name.replace(Regex("([a-z])([A-Z])"), "$1-$2").lowercase() + + // W1: strip embedded newlines from values interpolated into // comment lines + // so a value containing \n doesn't break the comment into un-commented code. + private fun sanitizeComment(s: String): String = s.replace('\n', ' ').replace('\r', ' ') +} diff --git a/sdks/kotlin/ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/sdks/kotlin/ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..47b058e52a --- /dev/null +++ b/sdks/kotlin/ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +cloud.golem.ksp.GolemAgentProcessorProvider diff --git a/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/ConverterCodegenTest.kt b/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/ConverterCodegenTest.kt new file mode 100644 index 0000000000..790ea8a016 --- /dev/null +++ b/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/ConverterCodegenTest.kt @@ -0,0 +1,59 @@ +package cloud.golem.ksp + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Unit-tests the [ConverterCodegen] output for the `result` (Kotlin `Either`) and `datetime` + * type descriptors — pure string codegen, so no compile-testing needed. + */ +class ConverterCodegenTest { + + private val s32 = TypeDesc.Prim("s32") + private val str = TypeDesc.Prim("string") + + @Test + fun result_toWit_maps_either_arms_and_unit() { + assertEquals("result", TypeDesc.ResultT(ok = s32, err = str).toWit()) + assertEquals("result", TypeDesc.ResultT(ok = s32, err = TypeDesc.UnitT).toWit()) + assertEquals("datetime", TypeDesc.DatetimeT.toWit()) + } + + @Test + fun result_decode_maps_ok_to_right_err_to_left() { + assertEquals( + "(x as SchemaValue.ResultVal).let { if (it.ok) cloud.golem.runtime.Either.Right((it.inner!! as SchemaValue.S32).v) " + + "else cloud.golem.runtime.Either.Left((it.inner!! as SchemaValue.Str).v) }", + ConverterCodegen.decode(TypeDesc.ResultT(ok = s32, err = str), "x"), + ) + } + + @Test + fun result_encode_maps_right_to_ok_left_to_err() { + assertEquals( + "(k).let { when (it) { " + + "is cloud.golem.runtime.Either.Right -> SchemaValue.ResultVal(true, SchemaValue.S32(it.value)); " + + "is cloud.golem.runtime.Either.Left -> SchemaValue.ResultVal(false, SchemaValue.Str(it.value)) } }", + ConverterCodegen.encode(TypeDesc.ResultT(ok = s32, err = str), "k"), + ) + } + + @Test + fun result_unit_arm_uses_Unit_and_null() { + val td = TypeDesc.ResultT(ok = TypeDesc.UnitT, err = str) + assertEquals(true, ConverterCodegen.decode(td, "x").contains("cloud.golem.runtime.Either.Right(Unit)")) + assertEquals(true, ConverterCodegen.encode(td, "k").contains("SchemaValue.ResultVal(true, null)")) + } + + @Test + fun datetime_decode_and_encode() { + assertEquals( + "(x as SchemaValue.DatetimeVal).let { cloud.golem.Datetime(it.seconds, it.nanoseconds) }", + ConverterCodegen.decode(TypeDesc.DatetimeT, "x"), + ) + assertEquals( + "SchemaValue.DatetimeVal((k).seconds, (k).nanoseconds)", + ConverterCodegen.encode(TypeDesc.DatetimeT, "k"), + ) + } +} diff --git a/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/GolemAgentProcessorTest.kt b/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/GolemAgentProcessorTest.kt new file mode 100644 index 0000000000..2d6540ae36 --- /dev/null +++ b/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/GolemAgentProcessorTest.kt @@ -0,0 +1,525 @@ +package cloud.golem.ksp + +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.configureKsp +import com.tschuchort.compiletesting.kspSourcesDir +import java.io.File +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Verifies the processor's GENERATED SOURCE TEXT and, for the dedicated compilation test, that + * the generated registration + entry point compile against a JVM stub of the native SDK's + * `cloud.golem.runtime` surface. + * + * Most tests use withCompilation=false because the generated code targets Kotlin/Wasm + * (`kotlin.wasm.WasmExport`, which does not exist on the JVM classpath) and cannot be compiled by + * the JVM test compiler as-is. The dedicated withCompilation=true test instead provides a + * JVM-compatible stub of `cloud.golem.runtime` (NativeAgentDescriptor/SchemaValue/etc.) plus a + * stub `@WasmExport` annotation, so compilation succeeds — this catches unresolved-helper + * regressions that text-only assertions miss. + */ +class GolemAgentProcessorTest { + + private fun annotationStubs() = SourceFile.kotlin( + "Annotations.kt", + """ + package cloud.golem.annotations + + annotation class Agent(val mount: String = "", val description: String = "") + annotation class Endpoint( + val post: String = "", + val get: String = "", + val put: String = "", + val delete: String = "", + val path: String = "" + ) + annotation class Prompt(val hint: String = "") + annotation class Description(val text: String = "") + """.trimIndent(), + ) + + private fun baseAgentStub() = SourceFile.kotlin( + "BaseAgent.kt", + """ + package cloud.golem + abstract class BaseAgent + interface Snapshotted { + var state: S + } + """.trimIndent(), + ) + + private fun runProcessor(vararg agents: SourceFile): KotlinCompilation { + val compilation = KotlinCompilation().apply { + sources = listOf(annotationStubs(), baseAgentStub(), *agents) + inheritClassPath = true + messageOutputStream = System.out + } + compilation.configureKsp { + symbolProcessorProviders += GolemAgentProcessorProvider() + // Run KSP but do NOT compile the generated sources: the generated registration + // targets Kotlin/Wasm (kotlin.wasm.WasmExport) and won't compile on the JVM test + // compiler without stubs. We assert on the generated source text instead. + withCompilation = false + } + compilation.compile() + return compilation + } + + private fun generated(compilation: KotlinCompilation, fileName: String): String { + val file = compilation.kspSourcesDir.walkTopDown().firstOrNull { it.name == fileName } + assertNotNull(file, "$fileName was not generated under ${compilation.kspSourcesDir}") + return file.readText() + } + + private fun generatedWit(compilation: KotlinCompilation): File? = compilation.kspSourcesDir.walkTopDown().firstOrNull { it.extension == "wit" } + + @Test + fun `generates native Registration, entry point, and WIT for CounterAgent`() { + val agent = SourceFile.kotlin( + "CounterAgent.kt", + """ + package counter + + import cloud.golem.annotations.Agent + import cloud.golem.annotations.Endpoint + import cloud.golem.annotations.Prompt + import cloud.golem.annotations.Description + import cloud.golem.BaseAgent + + @Agent(mount = "/counters/{name}", description = "A durable counter agent") + class CounterAgent(val name: String) : BaseAgent() { + private var value = 0 + + @Prompt("Increase the count by one") + @Endpoint(post = "/increment") + fun increment(): Int { value++; return value } + + @Prompt("Get the current count") + @Endpoint(get = "/value") + fun getValue(): Int = value + } + """.trimIndent(), + ) + val compilation = runProcessor(agent) + + val reg = generated(compilation, "CounterAgentRegistration.kt") + assertContains(reg, "AUTO-GENERATED by golem-kotlin-ksp") + assertContains(reg, "fun registerCounterAgent()") + assertContains(reg, "import cloud.golem.runtime.NativeAgentRuntime") + assertContains(reg, "NativeAgentRuntime.registerAgent(") + assertContains(reg, "NativeAgentDescriptor(") + assertContains(reg, "\"CounterAgent\"") + assertContains(reg, "\"A durable counter agent\"") + assertContains(reg, "\"/counters/{name}\"") + assertContains(reg, "listOf(NativeParamSchema(\"name\", \"string\"))") + assertContains(reg, "NativeMethodDescriptor(\"increment\", \"s32\", emptyList(), listOf(NativeHttpEndpoint(\"POST\", \"/increment\", false, emptyList())), \"Increase the count by one\", null)") + assertContains(reg, "SchemaValue.S32((instance as CounterAgent).increment())") + assertContains(reg, "SchemaValue.S32((instance as CounterAgent).getValue())") + assertContains(reg, "CounterAgent((input[0] as SchemaValue.Str).v)") + + val entry = generated(compilation, "GolemGeneratedGuest.kt") + assertContains(entry, "fun registerAllAgents()") + assertContains(entry, "counter.registerCounterAgent()") + assertContains(entry, "@WasmExport(\"golem:agent/guest@2.0.0#initialize\")") + assertContains(entry, "@WasmExport(\"golem:agent/guest@2.0.0#invoke\")") + assertContains(entry, "@WasmExport(\"golem:agent/guest@2.0.0#get-definition\")") + assertContains(entry, "@WasmExport(\"golem:agent/guest@2.0.0#discover-agent-types\")") + assertContains(entry, "@WasmExport(\"cabi_post_golem:agent/guest@2.0.0#initialize\")") + assertContains(entry, "cloud.golem.runtime.initialize(argsPtr)") + assertContains(entry, "@WasmExport(\"golem:api/save-snapshot@1.5.0#save\")") + assertContains(entry, "@WasmExport(\"cabi_post_golem:api/save-snapshot@1.5.0#save\")") + assertContains(entry, "@WasmExport(\"golem:api/load-snapshot@1.5.0#load\")") + assertContains(entry, "@WasmExport(\"cabi_post_golem:api/load-snapshot@1.5.0#load\")") + assertContains(entry, "cloud.golem.runtime.saveSnapshot()") + assertContains(entry, "cloud.golem.runtime.cabiPostSaveSnapshot(resultPtr)") + assertContains(entry, "fun golemLoadSnapshot(payloadPtr: Int, payloadLen: Int, mimeTypePtr: Int, mimeTypeLen: Int): Int") + assertContains(entry, "cloud.golem.runtime.loadSnapshotFlat(payloadPtr, payloadLen, mimeTypePtr, mimeTypeLen)") + assertContains(entry, "cloud.golem.runtime.cabiPostLoadSnapshot(resultPtr)") + + val wit = generatedWit(compilation) + assertNotNull(wit, "counter-agent.wit was not generated") + val witSrc = wit.readText() + assertContains(witSrc, "package kotlin:counter@0.1.0") + assertContains(witSrc, "world counter-agent {") + assertContains(witSrc, "include golem:agent/agent-guest@2.0.0;") + assertContains(witSrc, "// agent CounterAgent {") + assertContains(witSrc, "// mount: \"/counters/{name}\"") + assertContains(witSrc, "// constructor(name: string)") + assertContains(witSrc, "// method increment() -> s32") + assertContains(witSrc, "// method getValue() -> s32") + assertContains(witSrc, "post: \"/increment\"") + assertContains(witSrc, "prompt: \"Increase the count by one\"") + } + + @Test + fun `handles agent with no methods`() { + val agent = SourceFile.kotlin( + "EmptyAgent.kt", + """ + package example + + import cloud.golem.annotations.Agent + import cloud.golem.BaseAgent + + @Agent(mount = "/empty") + class EmptyAgent : BaseAgent() + """.trimIndent(), + ) + val compilation = runProcessor(agent) + val reg = generated(compilation, "EmptyAgentRegistration.kt") + assertContains(reg, "fun registerEmptyAgent()") + assertContains(reg, "emptyList()") // empty constructorParams + methods + assertContains(reg, "{ _ -> EmptyAgent() }") // no-arg factory + } + + @Test + fun `handles void Unit return type`() { + val agent = SourceFile.kotlin( + "ResetAgent.kt", + """ + package example + + import cloud.golem.annotations.Agent + import cloud.golem.annotations.Endpoint + import cloud.golem.BaseAgent + + @Agent(mount = "/reset") + class ResetAgent : BaseAgent() { + @Endpoint(post = "/reset") + fun reset() {} + } + """.trimIndent(), + ) + val compilation = runProcessor(agent) + val reg = generated(compilation, "ResetAgentRegistration.kt") + assertContains(reg, "(instance as ResetAgent).reset(); SchemaValue.Unit_") + assertTrue(generatedWit(compilation) != null) + } + + // Proves the generated registration + entry point actually compile against a matching + // native-runtime boundary (JVM-stubbed). Would catch unresolved-helper regressions. + // Only the Kotlin language itself may declare anything in the `kotlin.*` package (the + // compiler rejects user code doing so), so `kotlin.wasm.WasmExport` cannot be stubbed for a + // JVM test -- meaning GolemGeneratedGuest.kt (which references it) can never compile on the + // JVM test compiler, structurally. So: run KSP once (withCompilation=false) to capture the + // generated REGISTRATION file's text, then compile ONLY that captured source (a plain + // second pass, no KSP, no @WasmExport-referencing file involved) against a JVM stub of + // cloud.golem.runtime. This still catches unresolved-helper regressions in the emitter. + @Test + fun `generated registration compiles against JVM stub native runtime`() { + val sdkStub = SourceFile.kotlin( + "GolemRuntimeStub.kt", + """ + package cloud.golem.runtime + + // JVM-compilable stub mirroring the native SDK's cloud.golem.runtime surface the + // generated registration code uses (NOT the @WasmExport entry point -- see above). + sealed class SchemaValue { + data class S32(val v: Int) : SchemaValue() + data class Str(val v: String) : SchemaValue() + data class Record(val fields: List) : SchemaValue() + object Unit_ : SchemaValue() + } + + class NativeParamSchema(val name: String, val witType: String) + class NativeHttpEndpoint( + val verb: String, + val path: String, + val auth: Boolean = false, + val cors: List = emptyList() + ) + class NativeMethodDescriptor( + val name: String, + val outputWitType: String, + val inputParams: List, + val httpEndpoints: List, + val promptHint: String = "", + val readOnlyCache: String? = null, + val handler: (Any, List) -> SchemaValue + ) + class NativeAgentDescriptor( + val typeName: String, + val description: String, + val mountPath: String, + val constructorParams: List, + val methods: List, + val factory: (List) -> Any, + val mountAuth: Boolean = false, + val mountCors: List = emptyList(), + val mode: String = "durable", + val snapshotting: String = "disabled" + ) + object NativeAgentRuntime { + fun registerAgent(descriptor: NativeAgentDescriptor) {} + } + """.trimIndent(), + ) + + val agent = SourceFile.kotlin( + "CounterAgentCompile.kt", + """ + package counter + + import cloud.golem.annotations.Agent + import cloud.golem.annotations.Endpoint + import cloud.golem.BaseAgent + + @Agent(mount = "/counters/{name}", description = "A durable counter agent") + class CounterAgentCompile(val name: String) : BaseAgent() { + private var value = 0 + + @Endpoint(post = "/increment") + fun increment(): Int { value++; return value } + } + """.trimIndent(), + ) + + // Pass 1: run KSP (no compilation) to capture the generated registration file's text. + val kspCompilation = runProcessor(agent) + val registrationSource = generated(kspCompilation, "CounterAgentCompileRegistration.kt") + + // Pass 2: compile the ORIGINAL agent source + the captured registration text + the JVM + // stub together (plain compilation, no KSP, no @WasmExport-referencing file involved). + val compilation = KotlinCompilation().apply { + sources = listOf( + annotationStubs(), + baseAgentStub(), + sdkStub, + agent, + SourceFile.kotlin("CounterAgentCompileRegistration.kt", registrationSource), + ) + inheritClassPath = true + messageOutputStream = System.out + } + val result = compilation.compile() + assertTrue( + result.exitCode == KotlinCompilation.ExitCode.OK, + "Generated registration did not compile: ${result.messages}", + ) + } + + @Test + fun `derives SnapshotCodec for an agent implementing Snapshotted with a data class state`() { + val state = SourceFile.kotlin( + "CounterState.kt", + """ + package snap + data class CounterState(val value: Int, val label: String) + """.trimIndent(), + ) + val agent = SourceFile.kotlin( + "SnapAgent.kt", + """ + package snap + + import cloud.golem.annotations.Agent + import cloud.golem.annotations.Endpoint + import cloud.golem.BaseAgent + import cloud.golem.Snapshotted + + @Agent(mount = "/snap") + class SnapAgent : BaseAgent(), Snapshotted { + override var state = CounterState(0, "") + + @Endpoint(post = "/inc") + fun inc(): Int { state = state.copy(value = state.value + 1); return state.value } + } + """.trimIndent(), + ) + val reg = generated(runProcessor(state, agent), "SnapAgentRegistration.kt") + // The snapshot codec argument is spliced after the snapshotting argument. + assertContains(reg, "snapshotCodec = cloud.golem.runtime.SnapshotCodec(") + assertContains(reg, "save = { inst -> cloud.golem.runtime.SchemaValueBytes.encode(") + assertContains(reg, "load = { inst, bytes -> (inst as SnapAgent).state =") + // Round-trips through the record encoder for the data-class state. + assertContains(reg, "(inst as SnapAgent).state") + assertContains(reg, "cloud.golem.runtime.SchemaValueBytes.decode(bytes)") + } + + // Proves the derived SnapshotCodec compiles against a JVM stub of the native runtime surface + // (mirrors the registration-compile test, extended with SnapshotCodec/SchemaValueBytes and the + // snapshotCodec descriptor param). Catches unresolved-helper / mis-spliced-expression regressions + // in the generated save/load bodies that text-only assertions miss. + @Test + fun `derived SnapshotCodec compiles against JVM stub native runtime`() { + val sdkStub = SourceFile.kotlin( + "GolemRuntimeStubSnap.kt", + """ + package cloud.golem.runtime + + sealed class SchemaValue { + data class S32(val v: Int) : SchemaValue() + data class Str(val v: String) : SchemaValue() + data class Record(val fields: List) : SchemaValue() + object Unit_ : SchemaValue() + } + + object SchemaValueBytes { + fun encode(v: SchemaValue): ByteArray = ByteArray(0) + fun decode(bytes: ByteArray): SchemaValue = SchemaValue.Unit_ + } + class SnapshotCodec( + val save: (Any) -> ByteArray, + val load: (Any, ByteArray) -> Unit, + ) + + class NativeParamSchema(val name: String, val witType: String) + class NativeHttpEndpoint( + val verb: String, + val path: String, + val auth: Boolean = false, + val cors: List = emptyList() + ) + class NativeMethodDescriptor( + val name: String, + val outputWitType: String, + val inputParams: List, + val httpEndpoints: List, + val promptHint: String = "", + val readOnlyCache: String? = null, + val handler: (Any, List) -> SchemaValue + ) + class NativeAgentDescriptor( + val typeName: String, + val description: String, + val mountPath: String, + val constructorParams: List, + val methods: List, + val factory: (List) -> Any, + val mountAuth: Boolean = false, + val mountCors: List = emptyList(), + val mode: String = "durable", + val snapshotting: String = "disabled", + val snapshotCodec: SnapshotCodec? = null + ) + object NativeAgentRuntime { + fun registerAgent(descriptor: NativeAgentDescriptor) {} + } + """.trimIndent(), + ) + val state = SourceFile.kotlin( + "CounterStateCompile.kt", + """ + package snap + data class CounterStateCompile(val value: Int, val label: String) + """.trimIndent(), + ) + val agent = SourceFile.kotlin( + "SnapAgentCompile.kt", + """ + package snap + + import cloud.golem.annotations.Agent + import cloud.golem.annotations.Endpoint + import cloud.golem.BaseAgent + import cloud.golem.Snapshotted + + @Agent(mount = "/snap") + class SnapAgentCompile : BaseAgent(), Snapshotted { + override var state = CounterStateCompile(0, "") + + @Endpoint(post = "/inc") + fun inc(): Int = state.value + } + """.trimIndent(), + ) + + // Pass 1: run KSP (no compilation) to capture the generated registration file's text. + val kspCompilation = runProcessor(state, agent) + val registrationSource = generated(kspCompilation, "SnapAgentCompileRegistration.kt") + + // Pass 2: compile the agent + captured registration + JVM stub together (no KSP). + val compilation = KotlinCompilation().apply { + sources = listOf( + annotationStubs(), + baseAgentStub(), + sdkStub, + state, + agent, + SourceFile.kotlin("SnapAgentCompileRegistration.kt", registrationSource), + ) + inheritClassPath = true + messageOutputStream = System.out + } + val result = compilation.compile() + assertTrue( + result.exitCode == KotlinCompilation.ExitCode.OK, + "Generated SnapshotCodec did not compile: ${result.messages}", + ) + } + + @Test + fun `generates asInt and Str wrap for Int param and String return`() { + val agent = SourceFile.kotlin( + "GreetAgent.kt", + """ + package greet + + import cloud.golem.annotations.Agent + import cloud.golem.annotations.Endpoint + import cloud.golem.BaseAgent + + @Agent(mount = "/greet", description = "Greet agent") + class GreetAgent : BaseAgent() { + @Endpoint(post = "/hello") + fun hello(times: Int): String = "hello".repeat(times) + } + """.trimIndent(), + ) + val compilation = runProcessor(agent) + val reg = generated(compilation, "GreetAgentRegistration.kt") + assertContains(reg, "(input[0] as SchemaValue.S32).v") + assertContains(reg, "SchemaValue.Str((instance as GreetAgent).hello(") + } + + @Test + fun `resolves Datetime to datetime and Either to result`() { + val datetime = SourceFile.kotlin( + "Datetime.kt", + """ + package cloud.golem + data class Datetime(val seconds: Long, val nanoseconds: Int) + """.trimIndent(), + ) + val either = SourceFile.kotlin( + "Either.kt", + """ + package cloud.golem.runtime + sealed class Either { + data class Left(val value: L) : Either() + data class Right(val value: R) : Either() + } + """.trimIndent(), + ) + val agent = SourceFile.kotlin( + "EventAgent.kt", + """ + package events + + import cloud.golem.annotations.Agent + import cloud.golem.annotations.Endpoint + import cloud.golem.BaseAgent + import cloud.golem.Datetime + import cloud.golem.runtime.Either + + @Agent(mount = "/events/{name}") + class EventAgent(val name: String) : BaseAgent() { + @Endpoint(post = "/at") + fun at(t: Datetime): Either = Either.Right(t.nanoseconds) + } + """.trimIndent(), + ) + val reg = generated(runProcessor(datetime, either, agent), "EventAgentRegistration.kt") + // Datetime param -> "datetime"; Either return -> result (Right=ok). + assertContains(reg, "NativeParamSchema(\"t\", \"datetime\")") + assertContains(reg, "\"result\"") + // param decode builds a Datetime; return encode branches on Either. + assertContains(reg, "cloud.golem.Datetime(") + assertContains(reg, "cloud.golem.runtime.Either.Right ->") + } +} diff --git a/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/HttpValidationTest.kt b/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/HttpValidationTest.kt new file mode 100644 index 0000000000..77ea2c53aa --- /dev/null +++ b/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/HttpValidationTest.kt @@ -0,0 +1,124 @@ +package cloud.golem.ksp + +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class HttpValidationTest { + + private fun param(name: String) = ParamModel(name, TypeDesc.Prim("string")) + + private fun method( + name: String, + params: List = emptyList(), + endpoints: List = emptyList(), + ) = MethodModel( + name = name, + description = "", + promptHint = "", + inputParams = params.map { param(it) }, + outputTypeDesc = TypeDesc.Prim("s32"), + httpEndpoints = endpoints, + ) + + private fun model( + mount: String, + ctorParams: List = emptyList(), + methods: List = emptyList(), + ) = AgentModel( + className = "CounterAgent", + qualifiedName = "counter.CounterAgent", + packageName = "counter", + mountPath = mount, + classDescription = "", + mountAuth = false, + mountCors = emptyList(), + mode = "durable", + snapshotting = "disabled", + constructorParams = ctorParams.map { param(it) }, + methods = methods, + ) + + @Test + fun `the counter template validates`() { + val m = model( + mount = "/counters/{name}", + ctorParams = listOf("name"), + methods = listOf( + method("increment", endpoints = listOf(HttpEndpointModel("POST", "/increment"))), + method("getValue", endpoints = listOf(HttpEndpointModel("GET", "/value"))), + ), + ) + assertNull(HttpValidation.validate(m)) + } + + @Test + fun `an unmounted agent with no endpoints is valid`() { + assertNull(HttpValidation.validate(model(mount = "", ctorParams = listOf("name")))) + } + + @Test + fun `catch-all in the mount path is rejected`() { + val err = HttpValidation.validate(model(mount = "/files/{+rest}", ctorParams = listOf("rest"))) + assertTrue(err != null && err.contains("catch-all"), "got: $err") + } + + @Test + fun `mount variable without a matching constructor param is rejected`() { + val err = HttpValidation.validate(model(mount = "/counters/{id}", ctorParams = listOf("name"))) + assertTrue(err != null && err.contains("'{id}'") && err.contains("constructor parameter"), "got: $err") + } + + @Test + fun `constructor param not provided by the mount path is rejected`() { + val err = HttpValidation.validate(model(mount = "/counters/{name}", ctorParams = listOf("name", "region"))) + assertTrue(err != null && err.contains("region") && err.contains("not provided"), "got: $err") + } + + @Test + fun `endpoint on an unmounted agent is rejected`() { + val m = model( + mount = "", + methods = listOf(method("increment", endpoints = listOf(HttpEndpointModel("POST", "/increment")))), + ) + val err = HttpValidation.validate(m) + assertTrue(err != null && err.contains("no HTTP mount"), "got: $err") + } + + @Test + fun `endpoint path variable must be a method parameter`() { + val m = model( + mount = "/orders/{name}", + ctorParams = listOf("name"), + methods = listOf( + method("item", params = listOf("sku"), endpoints = listOf(HttpEndpointModel("GET", "/items/{itemId}"))), + ), + ) + val err = HttpValidation.validate(m) + assertTrue(err != null && err.contains("'{itemId}'") && err.contains("item"), "got: $err") + } + + @Test + fun `endpoint path variable bound to a method parameter is valid`() { + val m = model( + mount = "/orders/{name}", + ctorParams = listOf("name"), + methods = listOf( + method("item", params = listOf("itemId"), endpoints = listOf(HttpEndpointModel("GET", "/items/{itemId}"))), + ), + ) + assertNull(HttpValidation.validate(m)) + } + + @Test + fun `catch-all is allowed in an endpoint suffix when bound to a method parameter`() { + val m = model( + mount = "/fs/{name}", + ctorParams = listOf("name"), + methods = listOf( + method("read", params = listOf("rest"), endpoints = listOf(HttpEndpointModel("GET", "/read/{+rest}"))), + ), + ) + assertNull(HttpValidation.validate(m)) + } +} diff --git a/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/TypeMapperTest.kt b/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/TypeMapperTest.kt new file mode 100644 index 0000000000..6fadf0c4ac --- /dev/null +++ b/sdks/kotlin/ksp/src/test/kotlin/cloud/golem/ksp/TypeMapperTest.kt @@ -0,0 +1,47 @@ +package cloud.golem.ksp + +import kotlin.test.Test +import kotlin.test.assertEquals + +class TypeMapperTest { + + @Test + fun `fqnToWit maps Int to s32`() = assertEquals("s32", TypeMapper.fqnToWit("kotlin.Int")) + + @Test + fun `fqnToWit maps Long to s64`() = assertEquals("s64", TypeMapper.fqnToWit("kotlin.Long")) + + @Test + fun `fqnToWit maps String to string`() = assertEquals("string", TypeMapper.fqnToWit("kotlin.String")) + + @Test + fun `fqnToWit maps Boolean to bool`() = assertEquals("bool", TypeMapper.fqnToWit("kotlin.Boolean")) + + @Test + fun `fqnToWit maps Unit to unit`() = assertEquals("()", TypeMapper.fqnToWit("kotlin.Unit")) + + @Test + fun `round-trip Int s32`() { + val wit = TypeMapper.fqnToWit("kotlin.Int") + assertEquals("kotlin.Int", TypeMapper.witToFqn(wit)) + } + + @Test + fun `round-trip String string`() { + val wit = TypeMapper.fqnToWit("kotlin.String") + assertEquals("kotlin.String", TypeMapper.witToFqn(wit)) + } + + /** + * Round-trip invariant (the part expressible at the type level): the Kotlin->WIT + * mapping is the inverse of the WIT->Kotlin mapping for the types the counter + * uses. The mapping is Int<->s32 and String<->string, so a value the user writes survives the round trip. + * (WIT widths that collapse to Int are excluded by design.) + */ + @Test + fun `counter types round-trip exactly`() { + for (fqn in listOf("kotlin.Int", "kotlin.String", "kotlin.Unit")) { + assertEquals(fqn, TypeMapper.witToFqn(TypeMapper.fqnToWit(fqn))) + } + } +} From 4e46da37cda556a62019f87c1c42450f88fb1c46 Mon Sep 17 00:00:00 2001 From: JohnSColeman Date: Mon, 13 Jul 2026 00:58:49 +0700 Subject: [PATCH 05/11] feat(kotlin): wit-native definitions and gradle build plugin --- sdks/kotlin/gradle-plugin/.gitignore | 3 + sdks/kotlin/gradle-plugin/build.gradle.kts | 66 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + sdks/kotlin/gradle-plugin/gradlew | 248 +++++ sdks/kotlin/gradle-plugin/gradlew.bat | 93 ++ sdks/kotlin/gradle-plugin/settings.gradle.kts | 1 + .../cloud/golem/gradle/NativeComponentTask.kt | 168 ++++ .../golem/gradle/WasmComponentExtension.kt | 48 + .../cloud/golem/gradle/WasmComponentPlugin.kt | 53 + .../golem/gradle/NativeComponentTaskTest.kt | 66 ++ .../golem/gradle/WasmComponentPluginTest.kt | 61 ++ .../wit-native/deps/blobstore/blobstore.wit | 29 + .../wit-native/deps/blobstore/container.wit | 68 ++ .../wit-native/deps/blobstore/types.wit | 77 ++ .../wit-native/deps/blobstore/world.wit | 5 + sdks/kotlin/wit-native/deps/cli/command.wit | 10 + .../wit-native/deps/cli/environment.wit | 22 + sdks/kotlin/wit-native/deps/cli/exit.wit | 17 + sdks/kotlin/wit-native/deps/cli/imports.wit | 36 + sdks/kotlin/wit-native/deps/cli/run.wit | 6 + sdks/kotlin/wit-native/deps/cli/stdio.wit | 26 + sdks/kotlin/wit-native/deps/cli/terminal.wit | 62 ++ .../deps/clocks/monotonic-clock.wit | 50 + .../wit-native/deps/clocks/timezone.wit | 55 ++ .../wit-native/deps/clocks/wall-clock.wit | 46 + sdks/kotlin/wit-native/deps/clocks/world.wit | 11 + sdks/kotlin/wit-native/deps/config/store.wit | 30 + sdks/kotlin/wit-native/deps/config/world.wit | 6 + .../wit-native/deps/filesystem/preopens.wit | 11 + .../wit-native/deps/filesystem/types.wit | 672 +++++++++++++ .../wit-native/deps/filesystem/world.wit | 9 + .../deps/golem-1.x/golem-context.wit | 93 ++ .../wit-native/deps/golem-1.x/golem-host.wit | 344 +++++++ .../deps/golem-1.x/golem-oplog-processor.wit | 27 + .../wit-native/deps/golem-1.x/golem-oplog.wit | 910 ++++++++++++++++++ .../wit-native/deps/golem-1.x/golem-retry.wit | 165 ++++ .../wit-native/deps/golem-agent/common.wit | 237 +++++ .../wit-native/deps/golem-agent/guest.wit | 37 + .../wit-native/deps/golem-agent/host.wit | 113 +++ .../deps/golem-core-v2/golem-core-v2.wit | 587 +++++++++++ .../golem-durability/golem-durability.wit | 92 ++ .../wit-native/deps/golem-quota/types.wit | 76 ++ .../wit-native/deps/golem-rdbms/ignite.wit | 81 ++ .../wit-native/deps/golem-rdbms/mysql.wit | 138 +++ .../wit-native/deps/golem-rdbms/postgres.wit | 293 ++++++ .../wit-native/deps/golem-rdbms/types.wit | 47 + .../wit-native/deps/golem-rdbms/world.wit | 7 + .../wit-native/deps/golem-secrets/reveal.wit | 31 + .../wit-native/deps/golem-secrets/types.wit | 73 ++ .../wit-native/deps/golem-tool/common.wit | 380 ++++++++ .../wit-native/deps/golem-tool/guest.wit | 59 ++ .../wit-native/deps/golem-tool/host.wit | 88 ++ .../deps/golem-websocket/websocket.wit | 51 + sdks/kotlin/wit-native/deps/http/handler.wit | 49 + sdks/kotlin/wit-native/deps/http/proxy.wit | 50 + sdks/kotlin/wit-native/deps/http/types.wit | 673 +++++++++++++ sdks/kotlin/wit-native/deps/io/error.wit | 34 + sdks/kotlin/wit-native/deps/io/poll.wit | 47 + sdks/kotlin/wit-native/deps/io/streams.wit | 290 ++++++ sdks/kotlin/wit-native/deps/io/world.wit | 10 + .../wit-native/deps/keyvalue/atomic.wit | 31 + .../wit-native/deps/keyvalue/caching.wit | 98 ++ .../kotlin/wit-native/deps/keyvalue/error.wit | 20 + .../deps/keyvalue/eventual-batch.wit | 81 ++ .../wit-native/deps/keyvalue/eventual.wit | 56 ++ .../wit-native/deps/keyvalue/handle-watch.wit | 17 + .../kotlin/wit-native/deps/keyvalue/types.wit | 72 ++ .../kotlin/wit-native/deps/keyvalue/world.wit | 26 + .../wit-native/deps/logging/logging.wit | 37 + .../wit-native/deps/random/insecure-seed.wit | 27 + .../wit-native/deps/random/insecure.wit | 25 + sdks/kotlin/wit-native/deps/random/random.wit | 29 + sdks/kotlin/wit-native/deps/random/world.wit | 13 + .../deps/sockets/instance-network.wit | 11 + .../deps/sockets/ip-name-lookup.wit | 56 ++ .../wit-native/deps/sockets/network.wit | 169 ++++ .../deps/sockets/tcp-create-socket.wit | 30 + sdks/kotlin/wit-native/deps/sockets/tcp.wit | 387 ++++++++ .../deps/sockets/udp-create-socket.wit | 30 + sdks/kotlin/wit-native/deps/sockets/udp.wit | 288 ++++++ sdks/kotlin/wit-native/deps/sockets/world.wit | 19 + sdks/kotlin/wit-native/main.wit | 86 ++ 83 files changed, 8752 insertions(+) create mode 100644 sdks/kotlin/gradle-plugin/.gitignore create mode 100644 sdks/kotlin/gradle-plugin/build.gradle.kts create mode 100644 sdks/kotlin/gradle-plugin/gradle/wrapper/gradle-wrapper.jar create mode 100644 sdks/kotlin/gradle-plugin/gradle/wrapper/gradle-wrapper.properties create mode 100755 sdks/kotlin/gradle-plugin/gradlew create mode 100644 sdks/kotlin/gradle-plugin/gradlew.bat create mode 100644 sdks/kotlin/gradle-plugin/settings.gradle.kts create mode 100644 sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/NativeComponentTask.kt create mode 100644 sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/WasmComponentExtension.kt create mode 100644 sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/WasmComponentPlugin.kt create mode 100644 sdks/kotlin/gradle-plugin/src/test/kotlin/cloud/golem/gradle/NativeComponentTaskTest.kt create mode 100644 sdks/kotlin/gradle-plugin/src/test/kotlin/cloud/golem/gradle/WasmComponentPluginTest.kt create mode 100644 sdks/kotlin/wit-native/deps/blobstore/blobstore.wit create mode 100644 sdks/kotlin/wit-native/deps/blobstore/container.wit create mode 100644 sdks/kotlin/wit-native/deps/blobstore/types.wit create mode 100644 sdks/kotlin/wit-native/deps/blobstore/world.wit create mode 100644 sdks/kotlin/wit-native/deps/cli/command.wit create mode 100644 sdks/kotlin/wit-native/deps/cli/environment.wit create mode 100644 sdks/kotlin/wit-native/deps/cli/exit.wit create mode 100644 sdks/kotlin/wit-native/deps/cli/imports.wit create mode 100644 sdks/kotlin/wit-native/deps/cli/run.wit create mode 100644 sdks/kotlin/wit-native/deps/cli/stdio.wit create mode 100644 sdks/kotlin/wit-native/deps/cli/terminal.wit create mode 100644 sdks/kotlin/wit-native/deps/clocks/monotonic-clock.wit create mode 100644 sdks/kotlin/wit-native/deps/clocks/timezone.wit create mode 100644 sdks/kotlin/wit-native/deps/clocks/wall-clock.wit create mode 100644 sdks/kotlin/wit-native/deps/clocks/world.wit create mode 100644 sdks/kotlin/wit-native/deps/config/store.wit create mode 100644 sdks/kotlin/wit-native/deps/config/world.wit create mode 100644 sdks/kotlin/wit-native/deps/filesystem/preopens.wit create mode 100644 sdks/kotlin/wit-native/deps/filesystem/types.wit create mode 100644 sdks/kotlin/wit-native/deps/filesystem/world.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-1.x/golem-context.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-1.x/golem-host.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-1.x/golem-oplog-processor.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-1.x/golem-oplog.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-1.x/golem-retry.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-agent/common.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-agent/guest.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-agent/host.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-core-v2/golem-core-v2.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-durability/golem-durability.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-quota/types.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-rdbms/ignite.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-rdbms/mysql.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-rdbms/postgres.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-rdbms/types.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-rdbms/world.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-secrets/reveal.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-secrets/types.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-tool/common.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-tool/guest.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-tool/host.wit create mode 100644 sdks/kotlin/wit-native/deps/golem-websocket/websocket.wit create mode 100644 sdks/kotlin/wit-native/deps/http/handler.wit create mode 100644 sdks/kotlin/wit-native/deps/http/proxy.wit create mode 100644 sdks/kotlin/wit-native/deps/http/types.wit create mode 100644 sdks/kotlin/wit-native/deps/io/error.wit create mode 100644 sdks/kotlin/wit-native/deps/io/poll.wit create mode 100644 sdks/kotlin/wit-native/deps/io/streams.wit create mode 100644 sdks/kotlin/wit-native/deps/io/world.wit create mode 100644 sdks/kotlin/wit-native/deps/keyvalue/atomic.wit create mode 100644 sdks/kotlin/wit-native/deps/keyvalue/caching.wit create mode 100644 sdks/kotlin/wit-native/deps/keyvalue/error.wit create mode 100644 sdks/kotlin/wit-native/deps/keyvalue/eventual-batch.wit create mode 100644 sdks/kotlin/wit-native/deps/keyvalue/eventual.wit create mode 100644 sdks/kotlin/wit-native/deps/keyvalue/handle-watch.wit create mode 100644 sdks/kotlin/wit-native/deps/keyvalue/types.wit create mode 100644 sdks/kotlin/wit-native/deps/keyvalue/world.wit create mode 100644 sdks/kotlin/wit-native/deps/logging/logging.wit create mode 100644 sdks/kotlin/wit-native/deps/random/insecure-seed.wit create mode 100644 sdks/kotlin/wit-native/deps/random/insecure.wit create mode 100644 sdks/kotlin/wit-native/deps/random/random.wit create mode 100644 sdks/kotlin/wit-native/deps/random/world.wit create mode 100644 sdks/kotlin/wit-native/deps/sockets/instance-network.wit create mode 100644 sdks/kotlin/wit-native/deps/sockets/ip-name-lookup.wit create mode 100644 sdks/kotlin/wit-native/deps/sockets/network.wit create mode 100644 sdks/kotlin/wit-native/deps/sockets/tcp-create-socket.wit create mode 100644 sdks/kotlin/wit-native/deps/sockets/tcp.wit create mode 100644 sdks/kotlin/wit-native/deps/sockets/udp-create-socket.wit create mode 100644 sdks/kotlin/wit-native/deps/sockets/udp.wit create mode 100644 sdks/kotlin/wit-native/deps/sockets/world.wit create mode 100644 sdks/kotlin/wit-native/main.wit diff --git a/sdks/kotlin/gradle-plugin/.gitignore b/sdks/kotlin/gradle-plugin/.gitignore new file mode 100644 index 0000000000..9f5f768c41 --- /dev/null +++ b/sdks/kotlin/gradle-plugin/.gitignore @@ -0,0 +1,3 @@ +.gradle/ +build/ +.kotlin/ diff --git a/sdks/kotlin/gradle-plugin/build.gradle.kts b/sdks/kotlin/gradle-plugin/build.gradle.kts new file mode 100644 index 0000000000..26997813ea --- /dev/null +++ b/sdks/kotlin/gradle-plugin/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + kotlin("jvm") version "2.4.0" + `java-gradle-plugin` + `maven-publish` + id("org.jlleitschuh.gradle.ktlint") version "14.2.0" +} + +group = "cloud.golem" +version = "0.0.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + // ProjectBuilder (org.gradle.testfixtures) comes from gradleApi(), which java-gradle-plugin + // already puts on the classpath; these add the JUnit Platform test runtime + kotlin.test. + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} + +gradlePlugin { + plugins { + create("wasmComponent") { + id = "cloud.golem.wasm-component" + implementationClass = "cloud.golem.gradle.WasmComponentPlugin" + displayName = "Golem Kotlin native wasm-component plugin" + description = "Builds a Golem Wasm Component directly from Kotlin/Wasm (WasmGC): " + + "KSP -> Kotlin/Wasm (wasmWasi) -> wasm-tools component embed -> " + + "wasm-tools component new (WASI p1->p2 adapter). No JS, no QuickJS." + } + } +} + +publishing { + repositories { mavenLocal() } +} + +// Bundle the canonical sdks/kotlin/wit-native/ (single source of truth, also used directly by +// sdks/kotlin/example/) as a single resource inside the plugin jar, so a `golem new`-scaffolded +// project anywhere on disk doesn't need its own copy: NativeComponentTask extracts this at build +// time when the consuming project doesn't set wasmComponent.witNativeDir explicitly. +val witNativeSrc = layout.projectDirectory.dir("../wit-native") +val witNativeResourcesDir = layout.buildDirectory.dir("generated-resources") + +val zipWitNative by tasks.registering(Zip::class) { + from(witNativeSrc) + archiveFileName.set("wit-native.zip") + destinationDirectory.set(witNativeResourcesDir) + includeEmptyDirs = false +} + +sourceSets { + main { + resources.srcDir(witNativeResourcesDir) + } +} + +tasks.processResources { + dependsOn(zipWitNative) +} diff --git a/sdks/kotlin/gradle-plugin/gradle/wrapper/gradle-wrapper.jar b/sdks/kotlin/gradle-plugin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 zcma&NW0WmQwk%w>ZQHhO+qUi6W!pA(xoVef+k2O7+pkXd9rt^$@9p#T8Y9=Q^(R-x zjL3*NQ$ZRS1O)&B0s;U4fbe_$e;)(@NB~(;6+v1_IWc+}NnuerWl>cXPyoQcezKvZ z?Yzc@<~LK@Yhh-7jwvSDadFw~t7KfJ%AUfU*p0wc+3m9#p=Zo4`H`aA_wBL6 z9Q`7!;Ok~8YhZ^Vt#N97bt5aZ#mQc8r~hs3;R?H6V4(!oxSADTK|DR2PL6SQ3v6jM<>eLMh9 zAsd(APyxHNFK|G4hA_zi+YV?J+3K_*DIrdla>calRjaE)4(?YnX+AMqEM!Y|ED{^2 zI5gZ%nG-1qAVtl==8o0&F1N+aPj`Oo99RfDNP#ZHw}}UKV)zw6yy%~8Se#sKr;3?g zJGOkV2luy~HgMlEJB+L<_$@9sUXM7@bI)>-K!}JQUCUwuMdq@68q*dV+{L#Vc?r<( z?Wf1HbqxnI6=(Aw!Vv*Z1H_SoPtQTiy^bDVD8L=rRZ`IoIh@}a`!hY>VN&316I#k} z1Sg~_3ApcIFaoZ+d}>rz0Z8DL*zGq%zU1vF1z1D^YDnQrG3^QourmO6;_SrGg3?qWd9R1GMnKV>0++L*NTt>aF2*kcZ;WaudfBhTaqikS(+iNzDggUqvhh?g ziJCF8kA+V@7zi30n=b(3>X0X^lcCCKT(CI)fz-wfOA1P()V)1OciPu4b_B5ORPq&l zchP6l3u9{2on%uTwo>b-v0sIrRwPOzG;Wcq8mstd&?Pgb9rRqF#Yol1d|Q6 z7O20!+zXL(B%tC}@3QOs&T8B=I*k{!Y74nv#{M<0_g4BCf1)-f)6~`;(P-= zPqqH2%j0LDX2k5|_)zavpD{L1BW?<+s$>F&1VNb3T+gu!Dgd{W+na9(yV`M7UaCBuJZg1Y)y6{U}0=LTvxBDApz@r>dGt(m^v|jy&aLA zdsOeJcquuj3G^NkH)g)z@gTzgpr!zpE$0>$aT^{((&VA>+(nQB!M(NnPvEP}ZRz+6 zE!=UW!r7sbX3>{1{XW1?hSDNsur6cNeYxE{$bFwZzZ597{pDqjr%ag85sIns_Xz%= zqY{h#z8J6GA~vfLQ2-jWWcloE5LA62jta=C*1KxAL}jugoPqj4el4R4g3zC4nE#2-NeS{c3#!2tIS|1h8*|kpw2VSH9OcIQZx0Yh!8~P&p}fI$4Bj9Z zr5Yv?i-PfO#<}clM>mO(D0wHniZZdv8pOuJFW z+-u}BH84PQCgT~VWBM88vtCly1y$uEGJ<7vnW%!2yV>l>dxA0X0q{cN6y3u$8R-*f z-4^OlZ1HmxCv`dFW%quP<7xzAbtiFxvY0M1&2ng&A}QXAVR=prc_5m(D+_?hv#$M^ zG#MQ#fHMc!+S%HgU^Qv7Z9eu6eNqpSr3e8(;No*YfovbJ;60LjCzv9O~^>gFKO>t zGZg9`a5;$hksp*fHp{7&RE@DM&Pa@a>Kwk%*F7UGO|}^Z0ho1U$THOgX9jtCW6N$v zLOm}xcMBtw)CC(;LLX!R9jp|UsBWGfs@HaMiosA3#hFee7(4vLY}IrhD++}>pY zo+=_h+uJ;j^CP*OGQ9$0q+%}UB`4`5c766d#)*Czs<91wxw)jI^IdvyjT%<8OqI=i zNn0OUqW#POg^4ma)e2b?*Xv;dri*N0SJ7_{&0>;S!)!YV1TQuiT1C3ZFDvThe}yTCmErx#6yyQ4X@OAbHhdEV!K2%;7J>tiUZF)>Z|eRVDwtDC~=J z*M8|WEgzsyNH@-5lJE+P6HrurgY!PqtWk z^69SOHZ*}xn|j2FDVg`qRT}ob*1XiGo=x8MDEX)duljcVO}oJjuAbB$Z+f&!{z3k< zO6+{@O#2^s4qT`6k}Nw?DKV1DU~}0jVA)(kNz$c-p`*FNG#Gb&o?ko70F||R^y*hD z6HD|hJzF)G&^K=vuN$@b2fIfHVFw@hC_-0hPnB!1{=Nn~ran4VeTMM(Xx2A3h95U} z&J#Kw4>*V(LHOA<3Dy{sbW-9k5M2<%yDw~ce0+aez8 z04skG8@QEESIL;m-@Mf_hY!)KkEUowHu(>)Inz(pM`@pkxz z1_K#Qs6$E^c$7w=JLy>nSY)>aY;x2z`LW-$$rnY0!suTZSG)^0ZMeT#$0_oER zfZ1Hf>#TP|;J^rzn3V^2)Dy!goj6roAho>c=?28yjzQ>N-yU)XduKq8Lb3+ZA|#-{ z?34)Ml8%)3F1}oF;q9XFxoM}Zn{~2>kr%X_=WMen%b>n))hx6kHWNoKUBAz?($h(m(l;U*Gq7;p5J{B;kfO^C%C9HhtW!=O3-h>$U zI2=uaEymeK^h#QuB8a?1Qr0Gn;ZZ@;otg2l>gf= z$_mO!iis+#(8-GZw`ZiCnt}>qKmghHCb)`6U!8qS*DhBANfGj|U2C->7>*Bqe5h<% zF+9uy>$;#cZB>?Wdz3mqi2Y>+6-#!Dd56@$WF{_^P2?6kNNfaw!r74>MZUNkFAt*H zvS@2hNmT%xnXp}_1gixv9!5#YI3ftgFXG20Vt1IQ(~+HmryrZI+r0(y2Scl+y=G^* zxt$Vvn&S=Vul-rgOlYNio7%ST_3!t`_`N@SCv$ppCqok(Q+i_?OL}2@TU$dr6B$c8 zQ$Z(lS6fp%7f}ymQwJAIdpkN~8$)O3|K7Z;{FD?hBSP-#pJgq0C_SFT;^sBc#da0M z;^UuXXq{!hEwQpp(o9+)jPM6ru1P$u0evVO(NJ;%0FgmMNlJ+BJ zf^`a|U*ab?uN*Ue>tHJ$Pl~chCwRnxi3%X06NxwlIAKa*KReLL^y1B^nuy|^SPj3} z5X|?1divh3@zci;648jb2qEOm!_8Tjh3gi;H%2`d`~Q(IL{Wcl1C18+&P>tU&0!nO z&+7mpvr2SsTj=@sX zxG=;T^f7Rg=c=V*u8X(fo)4;RYax^+=quviOJ{>r6{wgf)g){I&qe`=HL}6J>i6Ne zSZ*h9f&JG>Y`@Bg5Pb&>4&UqFp9I<8o`n4W_V=4AugM`RqUeS-!`OyNLyKMqa_Ct| zON-hyk#-}{lZZx>B1F@dF^8S>x|C*QAjKqn&Ej9H#z@Q#KA*ckBX@^;gIP&?aK15l z*EY@kG57oUcm(d{NyXg6$Kj#xR5XdZ1EBCT+Zy!gyXwN&b_zI&$$>7R#{ zh8U@H8NY-cA*CBfH$OCs^priPwtwrzFjDO}DBn#mgbI~hn}cp2U{yv@S)iy|jR9+E zgd(hF|1cyC#te0P;iFGqpNBqc(k<{p^1>wHE_c8Tr4|&NV4mzpzFe;Cr)C~qpVNjl z^u(^s5=kj{QBae)Y*#^A39jT4`!NuIUQzD#DOyfa!R=PrX6oS@x@kJV)Cn$!xTK9A&VI#F-Slt8I4|=$bcjaC5h=9E{51g8X5q1Qfg~~G>qAgy*7h4-WuqE zlIEx?Hu*%99?$6TheLAD4NIMO=Q@*;gaXDl6yLLXfFX0*1-9KQm42c%WX*AXFo$it z?FwnWn2tBHY&Qj6=PV?ergU$VKzu+`(5pCRqX}IoSFo?P!`sff%u1?N+(KsoL+K={ zi*JGl%_jiuB;&YW+n%1o^%5@!HB9}OlIdQZ*XzQ%vu!8p2gnKW+!X>@oC{gp3lNx^ z82|5Jdg9-B<1j|y(@3J;$D-lqdnf0Q6T~q7;#O}EMPV3k(bi$DpZwj9(UhU%_l&nN zR}8tN_NhDMhs)gtG*76~+W2yQ{!kDTE@X4gft2?W;S$BLp9X z;sh2jpm!mkfPX>Vuqxyt76<@f4fyY%&iuDfS1@#PHgzHqG;=X^`X}t2|Alr^lx^ja z1rhvG(PH(a0THitc?4hk=P*#IS;-`fjOKqJ4kgo@dAD@ob*))H)=)6s3cthp&4Q55 z4dQRdG0EveK*(ZUCFcCjILgS#$@%y=8leYxN-%zQaky@H?kjhyBrLYA!cv>kV5;i1 zZ^w&U7s&K8fNr4Pfy9GyTK2Tiay4Y_PsPWoWW5YA8nfUkoyjU)i@nKj@4rY13sxO6 z_NzYdG=Vr<@08Xi#8rnX&^d{Bl`oHXO6Y3!v2U~ZV>I*30X3X&4@zqqVO~RyF)6?a zD(<+33_9TqeHL)#Y?($m4_zZvaJXWXppZ4?wo?$wF)%M6rEVk2gM=l9k+=*Q+((fI zIUBH6)}M?ahSxD4lgmJ30ygk#4d!O@?%WNEONommx`ZK81ZV)mJpKB`PgQ}F>NGdV zkV|>^}oWQd6@Ay7$&)6!% zOu_p~TZ3A#G_UqiJ85&*$!(+!V*+*{&-JXb53gtc9n3>8)T$jUVXe+M6n$m633Mi? zlh5{_+6iZ<%gMWMrtHyDl(u-hMl^DViUDc50UD;0g_l$F`Hb(F=o+?94B0fjb;|?Q5c~TWX>t8i1RP@>Ccgm z?2=z0coeb?uvn44moKFb^+(#pAdHE7{EW(DxJE=@Z0^Am`dpm98e`*S+-~*zmhdQ7 zCNig0!yUu5U#>KKocrg-xMjQoNzQ`th0f{!0`ammp_KMFh?_zF4#YhF35bPE&Fq~_ z#VnniU6fso{!3Z^1C57q?0i!ok(a zL;-f$YlDk%qi%n637_$=Gw=bBY}8#meS~+#X}Oz~ZKd%q(UE>f%!qca?(u}) z!tLTuQadlAN;a#^A?!@V=T?oeJ1f7yRy)H1zn_+wARewYIYr`zD=^v+D|ObvH4rOB zT@duqF>$Dk6&i|pZh?%Wq-7_kyP4l)-nqBz#G0lqo3J2D%zmbU)>3)5e?sTZy8|~B zPC7!`eD+deR?L6$6 z-e{!ihef=f<4HPZ9rSt&yb=5Q)BFAXWPR^~a&Zru?8146wvlm;<)ugbd|!}O6aE0t z6`#KqcH#S#*yz-K90+!Fhv+ zKH+?!_0yl|gWXSaASLcB9a8g7i%qz*vbO)YW`Q@Nxpp*6TZ*OO8Z|5-UWihd@CUXF zY!aTAZ$c^?4hiaq34=s2il}#Pxu=#c2^=(PbHNAyUqy__kR+n?twKrQe^8l6rk=orf}Mk80viC1NZ^1q zeF~g*iGp0=jKncK%s@#jZcn6=EiR<8S#)yiEOuwbG;SV$4lB^R?7sxOf8)oq$sT)) zA&nBCFJxsnci+)owdCHV#cjP2|1j22xIRsxHrLLBk3GI|OppUv3%r>#;J|26!W>xC z9gq@NQWJ`|gH}F{-QG#R6xlT<;=43amaDT>VaG*;GfPZJ&W*rO8WAQQc^JGw-fz-| zzAe&RAnC(gAP#FoJtt~ynR3Z<)m_<9Oo)XW}CWd50^eI4!1p4}s(zLhBIDi5r zr{UH>YIz2!+&Cy(RI(;ja_>SUC2Q`ohWPlI+sK-6IU}*nIsT)vLnuVPFM%~gdel}S zUlY%>H$?-rQRGTdUM^p^FEkqnwC{^BGl|gM)h9zkXplL90;yOcgt(8&LJwOj!5Qgy zu$@^*k%9JoAzwj@iSB^SNu#YVl@&*g$uYxxsJBvIQ>bfuS97JccQcS7&a z)`1m2^@5c9pD`P$VqH*O*fxkvFRtH-@Pd0@3y2!jW>i=jabBCJ+bW@wwUkWjwx_WR zHH5*XR4hbQ1`D@4@unmyEX)!?^~_}~JQNvP4jO&F)CH9srkFhf8h*=P z;X1&vs_&v03#BGc`|#@!ZONxVj9Ssb#_d63jxA6dX_RBt(s;ig3#s(YU3P3klF;mc z%%@^IJUAlGE=cnsTH+(qb1SxN@HzfAjYcUCb(VU)JV^3ZC;#k!t?XjaC!|68eLE zU_hlvOSNj7Qlr{x)y$S$l^2DPCMA=pzapcSkjfk*r!iWU%T{?<3#Hw6s1ux1^Ao6o zR@5DIfo-|c9AaFw848Y!BVG-+vURe;I29F#hLu$9o}oSa9&2sgG#;lj@@)9|2Z3 zon?%NV&AYSVnd~eW~v0yoF$X^1FR@i2kin0mFLG8-aA>hYK;B%TJ~7%P4?_{Bu<0t zvmI)Uk-MRncVb)A890>OqnYf=wu-J5A~^%4jpK~*xp)=h0BZB4*5uWrP>iRV+|kMX zv+BEskY~(P-K)-!JSHR`$brY)HFI|L@YyrxheT3cgHu}KtF%s%k3B`X)E_lA=E>M4 z2VV3M{c0*)`qZAsJ==)F#D~2Ndzm@hKhSBL_Sf3{ctckh-rB`gkfC?Dp6FdM?p;vv z#UlQMp3H5*)8o#Ys@-aj7O#brUfgQ7BjG`7 ztoE7v-tH2%KVC$xKYf%uvZD!_uf3x>h?8r!zYHkcc7$Gdn(6cDmYL&p3pCfaSfY4$ zG|yuujr6!Wl0}V%* zQ;nY##kEdvo8YY=SVDb)M>^Ub9e#4c$O&urD$uaRtxm-UH=6_s0m^^5y^_+F^Q?;8 z+Fd?+De}er^2EmFNn&e8SyS*`*`e;KFIG&+x5iWCsrEyH*0SFBCMx?`m5~hl1BrT> zr8W3*3}Fwsx@%UOuxNoCSoL%AM{Uj|v@>l{pYYI&D$j`&**;?X`cuOOk~?;U{~xvDUjaiH^d`A+gQL#Z?*lm)x_n6R-S% zf6*=Q1m>mq5|Niefl8s=5F={ncn5S;6~&Ns2)yGZ@wt&u4c+)Sk?hdfI^b77@K-=y zM_k=j5hp&u`2nkJK+2Lw`uLypr4dO?Bm3BTZdtWnQa5unCoTKIiG81t4bG`epBU5| zG{toT`)LE}&j{P+AFj`YZrjF-^>k+`zCM`QcQz^Ba4BEte@S}j=Q_Opx14jq|DB}& zNB44BOJ`?GJM({v`gh9pzbg8-%Un=E@uLfJwGkagLEM^!`ct3s5@-xqq*xd+2C@eu z*1ge`retZK)=bPO<`>@62cLN?^S%v#EsiPQF`cg&I7{}l?)}O$!^wNJp4Zd;1yBbQ zv@_7x7d6aXJvGHkNNcOg?A};m_Nq7H=(+zqf9)e3&yP^EU63Ew!NW4CYj_!=OTVb* z-ijSrv0M)u=MF=@+`3ldT-hzOn$Ng><)WL0vqQ&jH>W7EmLLQY+c?%i9~f_x&{OYX z{?kyyNZ&gT*m$(%-OeDAJeC^c)X!k${D*c;c}9)0_7iWMbfu)!j3+{*!Dj|?C`sGz z2xWha)#`9@p*{-X2MN2a;%FM-WqB2h)GTqQH$ZsGD#Wi`;+$i?fk;23fLpYI^3TT3 z5+Zn3cu-_2Ck*@%3^L3}JpVN`5ZJ;gmKn>gm(Z)b%!v|RYf(qrmGL#0$WHQFw4mJqQ85w=$tn^7(z|eJ$3R0} z2k9^EU<^-$ygq!ZR+7wT0KViK8qkAO7xs*e@1dq{=M3haulHwA0~BYNytr7k2K*(W z755P9a^;Hdl2X;K{c}yWr|QH?PEuh6x)9n{^3m2QUfC_Q*BW&<9#^ZVwOolx@6y9- z-YF=S;mEypj68yxNxfJ56x%ES`z-5$M${V1HX(@#R>%$X`67*Ab8vC6UzvoDOY*P= zFbPXany0%>rqH1gi7d>e`=PWZTG>^=#PQf&iJjJ0&2dO(4b8) zCl%8xJg1mg4__!?t|y_roExn~%u@Eu|p9YFb`8_qP@v#KW#kFs4eVetJ+Q+s|Y0?#D z@?dt_BA7C4tGpjOB~*LFu0!5oU(_xj7xA$meN)Z;q4Z_Rb7jY1rJBzJPr0V=(y99F zh=V-NbK+64rd#ltw~7X-%kP$R896DxRuj)p7Zj@8&>IlP&}ME3s9eV2R>SpUnSxeg zmpm?HQJ^u1T;pvwvlc4F_)>3P~jlTch4+u6;o{@PtpnJcn~p0v_6Po%*KkTXV#2AGc) zv)jvvC?l#s$yvyy=>=7D3pkmV24xhd7<5}f_u5!8gmOU|4555dv`I=rLWW!W!Uxg| zFGXpH3~)9!C2|Y6oB~$gz(;$CTnw&R&psa+E!KNgrE1+WkLM6SOf$>sGW+Y{>u?Fw zTc!xG{pa3c#y@d$d0e7a9~e_xjGcaw5f6Fk>lg$Jm}cFd%BO_YT(9s+_Q;ft%1*k$ z_cXkf&QHkaQr9U?*Gr$r6|bCV>2S)Cedfk3rO?JbyabY zgqxm#BM7Sg6s-`5%(p@SxBJzR6w`O6`+Kuo36wwBzwf6K{0HENVz^^w|E$r zdZM%T0oy8OK|>>2vSzw5rqoqEroCZ%(^OmOSFN84B2-8Z?R1)Pn9|5Xkui(fQRl^zA35EH^(JbuQd@Uh z2FJ6C(5FDD(++_NLOG)1H<+X~pt68d@JiB8iUQSZ+?qc;Jr+aJ8bKF3z`K&zSl&C7 zEgl&!h?sc=}K7 ziEC(3IrY?h7|d= zVjh{@BGW^AaNcdRceoiKmQI+F$ITdcM$YigXtH)6<-7d@5DyyWw}s!`72j`A{QC~e ze-u0a6A;QSPT$vqf3f(kO1j^%GYap*vfWQ@X=n{lR9%HX^R~t+HoeaT5%L7XSTNn` zCzo})tF@DMZ$|t6$KTx+WQqu~PXPa9FL&shBGx3C>FlGz}7gjfv}(NKvjR#r5PL$a1>%asaylWA8^g!KJ=$}_UccHmi zAZd5c{I&Ywpi3a1#27C6TC~zm3y8D>_1an8XHGNgL?uT$p+a<5AdWLR6w9jdhUt9U zz?)93=1p$x;Qiq!CYbX&S}+IITWLkfu%T6X5(pk9-fs8lh9z8h?9+>GlFeFcs*Z>u zJSaL!2?L8LbOu_Ye!=4~ZKL?643lcsNn8>qUT|q&Rv+(z>Z9=tyG&5}zZK&Q?S!nG zR;Ui^<406=jLYA>zl!a-OXH#J-pP4A`=)r%9HV5m1qGZ1m*t^wi>3$JRcH)3Q(LQz z(3}~y3=QsUu!PN$$N~#yBP@=aJ+Bkp_hx8^x1Ou6+(Kk9l1CXr4p~IQvq@AUePuAj zcq5>YDr(JTmrAuLwn6sgohTR-vc^y^#I{grF7 zg}8?&5!^$|{X`C;YrZ7?rKH#`=n0zck(q37+5%U;Hmds2w+dLmm9|@`HqQ<5CUEz{I1eNIL?X~rd{f71y z>_<94#1G+j`d5|fKK@>QDK6|HRR|9UZvO6HdB1afJvuwUf8bw>_Fha)Ii8I}Gqw}p zdS~e^K4j{d%y+A#OBa1C4i0)sM=}tjd8fZ9#uY}{#G7rJp{t6?*5*A^KKhim06i{}OJ%eA@M~zIfA`h_gJ_o%w;FaFQMnVkBT|_ z(`m9r+11~EPh9f7>S=$F7|ibj=4Pt>WVzk6NfGRvI_aG66RHig-(S%WKRLP%_h0He``xT))N^RI@6!ADl=*vsqVb|7 zr~Lwl6qn|u!%is<{YA`Mde2Z${@EAHC^t>4`X;F9za=RC{{$4OcGmw%9+{$i@!cCn z;7w~r8HY->M@3OzYh+L7Z2Lc8AcP*FZbl6VVN*_sp}K zQP|=g@aFthq}*?|+Gm4@wbs_?Fx-HD2%)_UDJ);X88~7ch~d0cJ!<7;mv>iv!RS$a z;(-cYTW=K=|F0gIg3EW0%u2CSr(Kx}yLoki|KSIt$#P(O!=UjBGRzb3L3-?NGr7!! z^VC7_Q(GhT;C*(bLivfhlRDVdz7=h%ABuLA2g$qy)A}U@Kj_L-Jd|--fy#-*ESRo| zgu?*?jGEgs9y>1`t}|^Ucd1I=1N=mOo{8Ph zwZS(F%G?nfI{#%sGayNItK9J5P)Qk+^4$ZoXZJ0G1}hwcckJ0g-QJ<)3%`bF8}(ahYIjKFYMtg3X;e7J18ZvDkV@N=nxvDl zo?}lXoT3pZY;4$QKI`~GFuQKv;G6b<8;o89Hd2yu+|%sU(9C=h8ibwZ zARqZ#lk@kp4*#URe-YmpRc&=-b&QP>5b{9{(tH*)(@ZPKfOslBgwCPx6d*{XMX|Q{y0F!5a^ScCE;h8bQmTJR3*}A>aGcDF0?tU)Tnml z#DgruwAva-fiU3s*POY_ZHiJyW%v+733X`&ocwHz$uqJCOhrM;#u*V2eK$D5HiN(` zII{BEg(PV6#_Nv3rZBUyd+TI!>L72KW_Oml6L=pNv#aOl( zgpYxAH^@2aJQu3urlrCeanwSpHHD_Cxb+=cm49{ZU5Z@;{^{okEJ6&fpDD31w~$`% zcz@_REsC~Vq>3YF7yJ41ZEPBW&%|OwlnfG|QNpiX;fGR0f^3?PEf|-33P&LFGe`8^ zaX3M+*h+?6;s|=$j*d|S-r6PSHnmLqm9oshPNpGzlxV21cFrxcQLidd2%h>n%Mc4{ z|JWBvtbb;(-nhWpPO95hR>(e(H$n%*pCh0k4xE#I%xu=#B)zXSaH+azwCI;0@bY<*-10-Qyaq%5NxSlq_@YJUUwy z*d;qPjW^cuKxdXiOWwP}5FN6SZW~NqB%4?|WifPNZr&XNVkzF0n#Y)pbaEodqNO4F z2Bq#^Gr^Ji3!T9`_!D;a1lW$?!LQ-iYV_A{FQ~^C-Jp`_5uOC)6+mzBr4Nl3fHly% zcXeU3x-?#J`=p$6c~$T~V^!C0Bk_3#WYrtoFCx9_5quCQ*4*?XG0n_9%l_!n`M85^ z7}~Clj~ocls6)V&sWGs?B<`{Ob>vnbXZwdda%ipwbzOJ(V`W>KBF5zdCTE8;mc&xU z^clCzd0(T#8*(})tSYSNP1N{FnNVAU^M1S_pq4VEQ*#5nv`CoYSALMEB zf6egyuRMzK2?r^M0hCD*sU;On6c0^Vh|#tRG*n1p5R)QyVw%Va37nMSV%9&uq^hp| zCHeu}y{m=NsA=naDy;q`fd9t)I$Qd-A1Il$#0KyDc>X)hKJViqNB{HnQyf5D(ZJ*J z{-oGB-%Q|QZ%Pqu34>fCy)Asi}IY7luNR9ebgH4DAjCVvSWfa%PE16 zkC7EIuEK}?IR!jgP%eX%dcxk4%N!zIjW4wYMfIq@s%GetDs^g!^p}DH46EP`Nh_wD z4Rwc4ezh1U$Mc)Fe6ii6eD^*iB2MFp-B-HhGTR0tC2?bq$#^J!v1r+Z0y+& znVub*k=*^0yP(c#mEvX}@Abx%&}!W(1olcWEHAVgskbBrzx(f2v&}4~WkVN?af#yi z4IE-(_^)?4e3(d{F@0<~NV5|e0eaB!?(g%l&Hq$UqzC_Enuest?CL+IrSD`tv8|{C z=79vnL=P6ne+}6X1&cd$kam=jCcv`~^y#R{doTh?6D?H)^M7-P+=D@?H;bt$*V+)K z?+?Ex3Z@8JE3c4eHDYItB^tSot;@2p_fuZ8mW^i^a(L;Xn6K+1GuG0n$v(38;+<78 zC?eMzbQCW2%&;U>j}b>YEH5>RkP44$QlG6k(KwXtq{e#13wnx5Jh=uH?lQIl8%Qxr zq%pDC)mYYKa?N>%aF%YwA}CzV@IOV9&a81d9eiU-6F&lGvz68~%{&4LuwV_5{#km3(tf`fejjs%`{Y`|0p!6|-U z8XQA9Sl=*kM|(2KA!LWOCY3Qq4sZ7r&}__rR*Sj(9W8R1_RxI&4TI+_7RSJF&-363 zJvczH?1(`Jb+RDJL9$Whnj8qJRI+Mz9=Qjvubb=Lz8nWVXG{Te;$%s9-D#$)-!{~w zIM(vkr#OM>2F7W$$Lq%fEYl%e|Tsc>9rB9c8 zQoi4nXomx3&sBI9AwaHkoOp%SMDf2@T#73Bi?|!r!Q?wc(^b_u4ranezYx~=aRV-a zD|_WPK^iJh&=)~h{t<>_$VMXsee;{r-|`#H|1?DZgWvuc*!&C2*(yv(4G5s{8ZRzt zZMC~5gjiU@6fPGMN%X~pL};Q`|IfPfs0m9;RV}xSxjb)*gmvGO1`CQb~W1M1{KwXBLyPz0JQG=JkVX zlPq&zNZS59gf-?*5Z0IFitTX4T$1Oo#_~V%4q2vI?Y@UkSHh}H9xZ1va}^oBrCY{+ z3wwj*FHCsS2}GdSG7W(|k+MWu9h1Qs6cft~RH)n*!;)5HmPX1DqrJ3-Cs%i4q^{$N zC&skM7#8f{&S!9Eq-WqyY$u?uTgrSDt#NU%{3bQZtUSkUof4`Z1P8aLOKJ+^dKh%n zfEfQ zO|P*J>;{=`9@D)qpnt`#NH>}sir*&oFC+W!HR)ecHcPwjF-|)}8+tR#@A+~CLl+Ab zCqp+=Cuc(&VGC1ZYg4CxIXYL>33p^wjIWJSh6R=oq)jD52q3~KVGt=w_z(arS!gx^ zSd|?!rzDu1$>0o0Y0+!iZU=ew^Hr+cq(I(C>9}^sBc++0+S#I;js@_NLD9>MH(tN3 zE5F+J_bYdPfYm5%7-e=lm?!-xlvX~nDkBqu!Zf0ra65JD&@tYDW+c@P3W-YyWe4^6 zhW?FUJ;c{^?b`N)03>!@#JI)r2&!6An27q?*^wyUx3T4uyeIl4*(4CV5OTK#RSnYt zq<+RKCdrYIJtdmNC-NtfH)K&pytbM^Mi6JWjkzJo0TdX>HOjJaIQmQ?Q;l2)8oN@d zVyT=%y@TihQaJX7#B2wY#_ufuaF55-sWO{OwUx$2zRyW$YM(CFBs4Y;YmBk(4u&u- zEf@rIR~4#}IMeq$?T%z3s3RAR7m%M?8No;a=1HXKP?ia#uwy!`4v0GFSjZiMii@ib z#xRmA-v~CSVl8z9cEWVEk;9_BKPS6Y2|bk#PAb|}gPxHs-dt*k`5tU#FZL)FLodY8 zmb!m`DagEJ#q1VKwO~%zmw7;LESf5u!KJNm829pbY_w$P2}16`Bb?0uoL3~V71;_U z`B~wKOB7Bp!Vn!M@o?RHydmah!dHPaT`&idV83kQPxA>E=~YgJC<)rdM1#B$JIgnq z0V{p|Cm3eeMaO58Wrv^9-kAOJ+*HR!;;A9z&>78VsYmF9$U^*ZE=K%d7=MZ~G?~Hz zSHlKWK!Us^%?uE6`E|_XI+nC354jkbUPvedHbh(DkKGkquYf}=-EEB1g>RC{O9ORL371y8V*CR5EW z@lmFq%MWEBdeHR7%(Rpf!Yg52vX%D7#@*^M`fy7Srb z^Ta9wcwf$89uL61@qeg2vc&TAGKSLV>YKI3#5lfs#q5Zm`~Ogef!!CoWWyiA=J;js z%X_n!njeF2MZgaVoMh@S@8%lR)AsYyzmqkj+C8ghxI4G6O7ovK$udULO!2$(|__`2~6JjuoERet}kenJ%I0pU_O@tU*Fsd4gm&hV?p%Y{!;r}{S^Fv z_4EJbVjFv7>+dE9{rBS@8&_vbx9>4!8&g4JV^e2mSwlNR^Z&ujriy)b3jzqfYb35o z!;J+c>%LY+?P!IticwSrP;x2|k>j3Sxg2X%E2%57

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/sdks/kotlin/gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/sdks/kotlin/gradle-plugin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..94113f200e --- /dev/null +++ b/sdks/kotlin/gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/sdks/kotlin/gradle-plugin/gradlew b/sdks/kotlin/gradle-plugin/gradlew new file mode 100755 index 0000000000..739907dfd1 --- /dev/null +++ b/sdks/kotlin/gradle-plugin/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/sdks/kotlin/gradle-plugin/gradlew.bat b/sdks/kotlin/gradle-plugin/gradlew.bat new file mode 100644 index 0000000000..c4bdd3ab8e --- /dev/null +++ b/sdks/kotlin/gradle-plugin/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sdks/kotlin/gradle-plugin/settings.gradle.kts b/sdks/kotlin/gradle-plugin/settings.gradle.kts new file mode 100644 index 0000000000..528c27a1c2 --- /dev/null +++ b/sdks/kotlin/gradle-plugin/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "golem-wasm-component-plugin" diff --git a/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/NativeComponentTask.kt b/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/NativeComponentTask.kt new file mode 100644 index 0000000000..be794dbd09 --- /dev/null +++ b/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/NativeComponentTask.kt @@ -0,0 +1,168 @@ +package cloud.golem.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import java.io.File +import java.util.zip.ZipInputStream +import javax.inject.Inject + +/** + * Componentizes a Kotlin/Wasm (wasmWasi) core module directly into a Golem-deployable Wasm + * Component -- no JS, no QuickJS, no wasm-rquickjs. Two `wasm-tools` invocations: + * + * 1. `wasm-tools component embed --world -o /embed.wasm` + * attaches the component-type information (the exports the core module's `@WasmExport` + * names satisfy, per `golem:agent/guest@2.0.0`). + * 2. `wasm-tools component new /embed.wasm --adapt wasi_snapshot_preview1= -o ` + * turns the embedded module into an actual component, adapting Kotlin/Wasm's WASI Preview 1 + * imports to Preview 2 (the reactor adapter from a `wit-bindgen` checkout, or any compatible + * `wasi_snapshot_preview1.reactor.wasm`). + */ +abstract class NativeComponentTask @Inject constructor( + private val exec: ExecOperations, +) : DefaultTask() { + + /** + * The directory Kotlin/Wasm compiles the core module into + * (`build/compileSync/wasmWasi/main/productionExecutable/kotlin/`). Its file name is always + * the Gradle root project's name, which a `golem new`-scaffolded app's `settings.gradle.kts` + * does not control (that file lives in the "common" template layer, which is rendered once + * per app -- `component_name` substitution only applies to the per-component layer). So + * rather than requiring an exact match, this task globs the directory for its one `.wasm` + * file. + */ + @get:InputDirectory + abstract val coreWasmDir: DirectoryProperty + + /** + * The wit-native root (`main.wit` + `deps/`). Optional -- if unset, the plugin's own bundled + * copy (`wit-native.zip`, packaged from the canonical `sdks/kotlin/wit-native/`) is extracted + * into the task's temp dir and used instead, so a `golem new`-scaffolded project doesn't need + * its own copy of the WIT. + */ + @get:Input + @get:Optional + abstract val witNativeDir: Property + + @get:Input + abstract val worldName: Property + + /** Absolute path to the WASI p1->p2 adapter. Empty/unset triggers auto-discovery. */ + @get:Input + @get:Optional + abstract val wasiAdapterPath: Property + + @get:Input + abstract val wasmTools: Property + + @get:OutputFile + abstract val outputWasm: RegularFileProperty + + @TaskAction + fun componentize() { + val dir = coreWasmDir.get().asFile + val candidates = dir.listFiles { f -> f.isFile && f.name.endsWith(".wasm") }?.toList() ?: emptyList() + require(candidates.isNotEmpty()) { + "No compiled Kotlin/Wasm core module (*.wasm) found in $dir -- did the wasmWasi prod compile run?" + } + require(candidates.size == 1) { + "Expected exactly one *.wasm file in $dir, found ${candidates.size}: ${candidates.map { it.name }}" + } + val core = candidates.single() + + val wit = witNativeDir.orNull?.let { File(it) } ?: extractBundledWitNative() + require(wit.resolve("main.wit").exists()) { "wit-native root has no main.wit: $wit" } + + val adapter = resolveAdapter() + require(adapter.exists()) { + "WASI preview1->preview2 adapter not found: $adapter -- set wasmComponent.wasiAdapterPath, " + + "or install a wit-bindgen checkout providing wasi_snapshot_preview1.reactor.wasm" + } + + val out = outputWasm.get().asFile + out.parentFile.mkdirs() + val tmpDir = temporaryDir + val embedWasm = File(tmpDir, "embed.wasm") + + exec.exec { + it.commandLine( + wasmTools.get(), "component", "embed", + wit.absolutePath, core.absolutePath, + "--world", worldName.get(), + "-o", embedWasm.absolutePath, + ) + } + require(embedWasm.exists()) { "wasm-tools component embed did not produce $embedWasm" } + + exec.exec { + it.commandLine( + wasmTools.get(), + "component", + "new", + embedWasm.absolutePath, + "--adapt", + "wasi_snapshot_preview1=${adapter.absolutePath}", + "-o", + out.absolutePath, + ) + } + require(out.exists()) { "wasm-tools component new did not produce $out" } + + exec.exec { + it.commandLine( + wasmTools.get(), + "validate", + out.absolutePath, + "--features", + "component-model,gc,function-references,exceptions", + ) + } + + logger.lifecycle("golem: native component -> $out (${out.length()} bytes)") + } + + /** Extracts the plugin-bundled `wit-native.zip` resource into the task's temp dir. */ + private fun extractBundledWitNative(): File { + val dest = File(temporaryDir, "wit-native") + if (dest.resolve("main.wit").exists()) return dest + val resource = javaClass.classLoader.getResourceAsStream("wit-native.zip") + ?: error("wasmComponent.witNativeDir was not set and no bundled wit-native.zip was found on the plugin classpath") + ZipInputStream(resource).use { zip -> + var entry = zip.nextEntry + while (entry != null) { + val outFile = File(dest, entry.name) + if (entry.isDirectory) { + outFile.mkdirs() + } else { + outFile.parentFile.mkdirs() + outFile.outputStream().use { zip.copyTo(it) } + } + entry = zip.nextEntry + } + } + return dest + } + + private fun resolveAdapter(): File { + wasiAdapterPath.orNull?.let { return File(it) } + val home = System.getProperty("user.home") + val checkouts = File(home, ".cargo/git/checkouts") + val found = checkouts.listFiles { f -> f.isDirectory && f.name.startsWith("wit-bindgen-") } + ?.flatMap { it.listFiles()?.toList() ?: emptyList() } + ?.flatMap { checkoutRev -> + File(checkoutRev, "tests") + .listFiles { f -> f.name == "wasi_snapshot_preview1.reactor.wasm" } + ?.toList() ?: emptyList() + } + ?.firstOrNull() + return found ?: File(home, ".cargo/git/checkouts/wit-bindgen-NOT-FOUND/wasi_snapshot_preview1.reactor.wasm") + } +} diff --git a/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/WasmComponentExtension.kt b/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/WasmComponentExtension.kt new file mode 100644 index 0000000000..8b3e752411 --- /dev/null +++ b/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/WasmComponentExtension.kt @@ -0,0 +1,48 @@ +package cloud.golem.gradle + +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property + +/** + * Configuration for the `cloud.golem.wasm-component` plugin (native path: no JS/QuickJS). + * + * Example: + * ``` + * wasmComponent { + * moduleName.set("counter-agent") + * witNativeDir.set(file("../wit-native")) + * outputWasm.set(layout.buildDirectory.file("golem/counter-agent.wasm")) + * } + * ``` + */ +abstract class WasmComponentExtension { + /** The component's name, used for the default output file name. */ + abstract val moduleName: Property + + /** + * The wit-native root (a `main.wit` + `deps/` directory including + * `golem:agent/agent-guest@2.0.0`). No default -- the consuming project must point this at + * its own copy (e.g. `sdks/kotlin/wit-native/`, or a `golem new`-scaffolded project's own + * bundled copy). + */ + abstract val witNativeDir: DirectoryProperty + + /** The WIT world to embed (must be defined in [witNativeDir]). Default: "kotlin-agent". */ + abstract val worldName: Property + + /** Where to write the final componentized, validated wasm. */ + abstract val outputWasm: RegularFileProperty + + /** + * The WASI preview1->preview2 adapter module (`wasi_snapshot_preview1.reactor.wasm`), + * needed because Kotlin/Wasm's wasmWasi target emits WASI Preview 1 imports. No default -- + * `NativeComponentTask` falls back to auto-discovering it under a cargo `wit-bindgen` + * checkout if this is left unset (matching `docs/spikes/compile-to-wasm-poc`'s convention), + * but an explicit path is recommended for reproducible builds. + */ + abstract val wasiAdapterPath: RegularFileProperty + + /** `wasm-tools` binary (PATH name or absolute path). Default: "wasm-tools". */ + abstract val wasmTools: Property +} diff --git a/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/WasmComponentPlugin.kt b/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/WasmComponentPlugin.kt new file mode 100644 index 0000000000..f161386470 --- /dev/null +++ b/sdks/kotlin/gradle-plugin/src/main/kotlin/cloud/golem/gradle/WasmComponentPlugin.kt @@ -0,0 +1,53 @@ +package cloud.golem.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * `cloud.golem.wasm-component` — native path (no JS/QuickJS). + * + * Applied to a Kotlin/Wasm (wasmWasi) agent project, it wires a `nativeComponent` task chaining: + * compileProductionExecutableKotlinWasmWasi (KSP runs as part of it) + * -> wasm-tools component embed (attach the golem:agent/guest@2.0.0 component type) + * -> wasm-tools component new --adapt (WASI p1->p2, produces the deployable component) + * -> wasm-tools validate + * + * The Kotlin/Wasm + KSP plugins are expected to be applied by the agent project itself. + */ +class WasmComponentPlugin : Plugin { + override fun apply(project: Project) { + val ext = project.extensions.create("wasmComponent", WasmComponentExtension::class.java) + ext.worldName.convention("kotlin-agent") + ext.wasmTools.convention("wasm-tools") + + val buildDir = project.layout.buildDirectory + ext.outputWasm.convention(ext.moduleName.flatMap { buildDir.file("golem/$it.wasm") }) + + // Kotlin/Wasm IR emits exactly one *.wasm file into this directory for a + // binaries.executable() wasmWasi target; NativeComponentTask globs for it (its name is + // always the Gradle root project's name, not necessarily ext.moduleName). + val coreWasmDir = buildDir.dir("compileSync/wasmWasi/main/productionExecutable/kotlin") + + val nativeComponent = project.tasks.register("nativeComponent", NativeComponentTask::class.java) { t -> + t.group = "golem" + t.description = "Componentize the Kotlin/Wasm agent directly into a Golem Wasm Component (no JS)." + t.coreWasmDir.set(coreWasmDir) + t.witNativeDir.set( + ext.witNativeDir.map { it.asFile.absolutePath }.orElse(project.provider { null }), + ) + t.worldName.set(ext.worldName) + t.wasiAdapterPath.set( + ext.wasiAdapterPath.map { it.asFile.absolutePath }.orElse(project.provider { null }), + ) + t.wasmTools.set(ext.wasmTools) + t.outputWasm.set(ext.outputWasm) + t.dependsOn("compileProductionExecutableKotlinWasmWasi") + } + + project.tasks.register("wasmComponent") { t -> + t.group = "golem" + t.description = "Alias for nativeComponent: build a validated Golem Wasm Component." + t.dependsOn(nativeComponent) + } + } +} diff --git a/sdks/kotlin/gradle-plugin/src/test/kotlin/cloud/golem/gradle/NativeComponentTaskTest.kt b/sdks/kotlin/gradle-plugin/src/test/kotlin/cloud/golem/gradle/NativeComponentTaskTest.kt new file mode 100644 index 0000000000..ec427b6ddd --- /dev/null +++ b/sdks/kotlin/gradle-plugin/src/test/kotlin/cloud/golem/gradle/NativeComponentTaskTest.kt @@ -0,0 +1,66 @@ +package cloud.golem.gradle + +import org.gradle.testfixtures.ProjectBuilder +import java.io.File +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** + * Input-validation tests for [NativeComponentTask]. Each drives the `componentize` action + * directly with controlled inputs and asserts the `require` failure -- all of which fire + * before any wasm-tools invocation, so no external toolchain is needed. + */ +class NativeComponentTaskTest { + + /** + * Builds a task with the mandatory inputs set and an empty `coreWasmDir`, then lets the + * caller stage files / override inputs via [configure] (receives the task and its coreWasmDir). + */ + private fun task(configure: (NativeComponentTask, File) -> Unit): NativeComponentTask { + val project = ProjectBuilder.builder().build() + val coreDir = File(project.projectDir, "core").apply { mkdirs() } + val task = project.tasks.register("nc", NativeComponentTask::class.java).get() + task.coreWasmDir.set(coreDir) + task.worldName.set("kotlin-agent") + task.wasmTools.set("wasm-tools") + task.outputWasm.set(File(project.projectDir, "out.wasm")) + configure(task, coreDir) + return task + } + + @Test + fun `fails when no core wasm is present`() { + // coreWasmDir is left empty -- no *.wasm staged. + val task = task { _, _ -> } + val ex = assertFailsWith { task.componentize() } + assertTrue( + ex.message!!.contains("No compiled Kotlin/Wasm core module"), + "message was: ${ex.message}", + ) + } + + @Test + fun `fails when multiple core wasm files are present`() { + val task = task { _, coreDir -> + File(coreDir, "a.wasm").writeText("") + File(coreDir, "b.wasm").writeText("") + } + val ex = assertFailsWith { task.componentize() } + assertTrue(ex.message!!.contains("Expected exactly one"), "message was: ${ex.message}") + } + + @Test + fun `fails when the wit-native root has no main wit`() { + val task = task { task, coreDir -> + File(coreDir, "core.wasm").writeText("") + val witDir = File(coreDir.parentFile, "wit").apply { mkdirs() } + task.witNativeDir.set(witDir.absolutePath) + } + val ex = assertFailsWith { task.componentize() } + assertTrue( + ex.message!!.contains("wit-native root has no main.wit"), + "message was: ${ex.message}", + ) + } +} diff --git a/sdks/kotlin/gradle-plugin/src/test/kotlin/cloud/golem/gradle/WasmComponentPluginTest.kt b/sdks/kotlin/gradle-plugin/src/test/kotlin/cloud/golem/gradle/WasmComponentPluginTest.kt new file mode 100644 index 0000000000..fb372e0610 --- /dev/null +++ b/sdks/kotlin/gradle-plugin/src/test/kotlin/cloud/golem/gradle/WasmComponentPluginTest.kt @@ -0,0 +1,61 @@ +package cloud.golem.gradle + +import org.gradle.testfixtures.ProjectBuilder +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Wiring/convention tests for [WasmComponentPlugin], using Gradle's in-process + * [ProjectBuilder] -- no wasm-tools, no Kotlin/Wasm toolchain, no network. + */ +class WasmComponentPluginTest { + + private fun appliedProject() = ProjectBuilder.builder().build().also { it.plugins.apply(WasmComponentPlugin::class.java) } + + private fun extension(project: org.gradle.api.Project) = project.extensions.getByType(WasmComponentExtension::class.java) + + @Test + fun `creates the wasmComponent extension`() { + val ext = appliedProject().extensions.findByName("wasmComponent") + assertNotNull(ext, "wasmComponent extension should be created") + assertIs(ext) + } + + @Test + fun `applies default conventions for worldName and wasmTools`() { + val ext = extension(appliedProject()) + assertEquals("kotlin-agent", ext.worldName.get()) + assertEquals("wasm-tools", ext.wasmTools.get()) + } + + @Test + fun `outputWasm defaults from moduleName`() { + val ext = extension(appliedProject()) + ext.moduleName.set("foo") + val path = ext.outputWasm.get().asFile.invariantSeparatorsPath + assertTrue(path.endsWith("build/golem/foo.wasm"), "outputWasm default was: $path") + } + + @Test + fun `registers the nativeComponent and wasmComponent tasks in the golem group`() { + val project = appliedProject() + + val native = project.tasks.getByName("nativeComponent") + assertIs(native) + assertEquals("golem", native.group) + + val alias = project.tasks.getByName("wasmComponent") + assertEquals("golem", alias.group) + } + + @Test + fun `wasmComponent alias depends on nativeComponent`() { + val project = appliedProject() + val alias = project.tasks.getByName("wasmComponent") + val deps = alias.taskDependencies.getDependencies(alias).map { it.name } + assertTrue("nativeComponent" in deps, "wasmComponent should depend on nativeComponent, got: $deps") + } +} diff --git a/sdks/kotlin/wit-native/deps/blobstore/blobstore.wit b/sdks/kotlin/wit-native/deps/blobstore/blobstore.wit new file mode 100644 index 0000000000..cc52516a57 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/blobstore/blobstore.wit @@ -0,0 +1,29 @@ +package wasi:blobstore; + +// wasi-cloud Blobstore service definition +interface blobstore { + use container.{container}; + use types.{error, container-name, object-id}; + + // creates a new empty container + create-container: func(name: container-name) -> result; + + // retrieves a container by name + get-container: func(name: container-name) -> result; + + // deletes a container and all objects within it + delete-container: func(name: container-name) -> result<_, error>; + + // returns true if the container exists + container-exists: func(name: container-name) -> result; + + // copies (duplicates) an object, to the same or a different container. + // returns an error if the target container does not exist. + // overwrites destination object if it already existed. + copy-object: func(src: object-id, dest: object-id) -> result<_, error>; + + // moves or renames an object, to the same or a different container + // returns an error if the destination container does not exist. + // overwrites destination object if it already existed. + move-object: func(src:object-id, dest: object-id) -> result<_, error>; +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/blobstore/container.wit b/sdks/kotlin/wit-native/deps/blobstore/container.wit new file mode 100644 index 0000000000..0652def81d --- /dev/null +++ b/sdks/kotlin/wit-native/deps/blobstore/container.wit @@ -0,0 +1,68 @@ +package wasi:blobstore; + +// a Container is a collection of objects +interface container { + use wasi:io/streams@0.2.3.{ + input-stream, + output-stream, + }; + + use types.{ + container-metadata, + error, + incoming-value, + object-metadata, + object-name, + outgoing-value, + }; + + // this defines the `container` resource + resource container { + // returns container name + name: func() -> result; + + // returns container metadata + info: func() -> result; + + // retrieves an object or portion of an object, as a resource. + // Start and end offsets are inclusive. + // Once a data-blob resource has been created, the underlying bytes are held by the blobstore service for the lifetime + // of the data-blob resource, even if the object they came from is later deleted. + get-data: func(name: object-name, start: u64, end: u64) -> result; + + // creates or replaces an object with the data blob. + write-data: func(name: object-name, data: borrow) -> result<_, error>; + + // returns list of objects in the container. Order is undefined. + list-objects: func() -> result; + + // deletes object. + // does not return error if object did not exist. + delete-object: func(name: object-name) -> result<_, error>; + + // deletes multiple objects in the container + delete-objects: func(names: list) -> result<_, error>; + + // returns true if the object exists in this container + has-object: func(name: object-name) -> result; + + // returns metadata for the object + object-info: func(name: object-name) -> result; + + // removes all objects within the container, leaving the container empty. + clear: func() -> result<_, error>; + } + + // this defines the `stream-object-names` resource which is a representation of stream + resource stream-object-names { + // reads the next number of objects from the stream + // + // This function returns the list of objects read, and a boolean indicating if the end of the stream was reached. + read-stream-object-names: func(len: u64) -> result, bool>, error>; + + // skip the next number of objects in the stream + // + // This function returns the number of objects skipped, and a boolean indicating if the end of the stream was reached. + skip-stream-object-names: func(num: u64) -> result, error>; + } +} diff --git a/sdks/kotlin/wit-native/deps/blobstore/types.wit b/sdks/kotlin/wit-native/deps/blobstore/types.wit new file mode 100644 index 0000000000..42cfc95278 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/blobstore/types.wit @@ -0,0 +1,77 @@ +package wasi:blobstore; + +// Types used by blobstore +interface types { + use wasi:io/streams@0.2.3.{input-stream, output-stream}; + + // name of a container, a collection of objects. + // The container name may be any valid UTF-8 string. + type container-name = string; + + // name of an object within a container + // The object name may be any valid UTF-8 string. + type object-name = string; + + // TODO: define timestamp to include seconds since + // Unix epoch and nanoseconds + // https://github.com/WebAssembly/wasi-blob-store/issues/7 + type timestamp = u64; + + // size of an object, in bytes + type object-size = u64; + + type error = string; + + // information about a container + record container-metadata { + // the container's name + name: container-name, + // date and time container was created + created-at: timestamp, + } + + // information about an object + record object-metadata { + // the object's name + name: object-name, + // the object's parent container + container: container-name, + // date and time the object was created + created-at: timestamp, + // size of the object, in bytes + size: object-size, + } + + // identifier for an object that includes its container name + record object-id { + container: container-name, + object: object-name + } + + /// A data is the data stored in a data blob. The value can be of any type + /// that can be represented in a byte array. It provides a way to write the value + /// to the output-stream defined in the `wasi-io` interface. + // Soon: switch to `resource value { ... }` + resource outgoing-value { + new-outgoing-value: static func() -> outgoing-value; + outgoing-value-write-body: func() -> result; + } + + /// A incoming-value is a wrapper around a value. It provides a way to read the value + /// from the input-stream defined in the `wasi-io` interface. + /// + /// The incoming-value provides two ways to consume the value: + /// 1. `incoming-value-consume-sync` consumes the value synchronously and returns the + /// value as a list of bytes. + /// 2. `incoming-value-consume-async` consumes the value asynchronously and returns the + /// value as an input-stream. + // Soon: switch to `resource incoming-value { ... }` + resource incoming-value { + incoming-value-consume-sync: func() -> result; + incoming-value-consume-async: func() -> result; + size: func() -> u64; + } + + type incoming-value-async-body = input-stream; + type incoming-value-sync-body = list; +} diff --git a/sdks/kotlin/wit-native/deps/blobstore/world.wit b/sdks/kotlin/wit-native/deps/blobstore/world.wit new file mode 100644 index 0000000000..4391d68b87 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/blobstore/world.wit @@ -0,0 +1,5 @@ +package wasi:blobstore; + +world blob-store { + import blobstore; +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/cli/command.wit b/sdks/kotlin/wit-native/deps/cli/command.wit new file mode 100644 index 0000000000..3a81766d64 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/cli/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.2.3; + +@since(version = 0.2.0) +world command { + @since(version = 0.2.0) + include imports; + + @since(version = 0.2.0) + export run; +} diff --git a/sdks/kotlin/wit-native/deps/cli/environment.wit b/sdks/kotlin/wit-native/deps/cli/environment.wit new file mode 100644 index 0000000000..2f449bd7c1 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/cli/environment.wit @@ -0,0 +1,22 @@ +@since(version = 0.2.0) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.2.0) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.2.0) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.2.0) + initial-cwd: func() -> option; +} diff --git a/sdks/kotlin/wit-native/deps/cli/exit.wit b/sdks/kotlin/wit-native/deps/cli/exit.wit new file mode 100644 index 0000000000..427935c8d0 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/cli/exit.wit @@ -0,0 +1,17 @@ +@since(version = 0.2.0) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.2.0) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} diff --git a/sdks/kotlin/wit-native/deps/cli/imports.wit b/sdks/kotlin/wit-native/deps/cli/imports.wit new file mode 100644 index 0000000000..8b4e3975ec --- /dev/null +++ b/sdks/kotlin/wit-native/deps/cli/imports.wit @@ -0,0 +1,36 @@ +package wasi:cli@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + include wasi:clocks/imports@0.2.3; + @since(version = 0.2.0) + include wasi:filesystem/imports@0.2.3; + @since(version = 0.2.0) + include wasi:sockets/imports@0.2.3; + @since(version = 0.2.0) + include wasi:random/imports@0.2.3; + @since(version = 0.2.0) + include wasi:io/imports@0.2.3; + + @since(version = 0.2.0) + import environment; + @since(version = 0.2.0) + import exit; + @since(version = 0.2.0) + import stdin; + @since(version = 0.2.0) + import stdout; + @since(version = 0.2.0) + import stderr; + @since(version = 0.2.0) + import terminal-input; + @since(version = 0.2.0) + import terminal-output; + @since(version = 0.2.0) + import terminal-stdin; + @since(version = 0.2.0) + import terminal-stdout; + @since(version = 0.2.0) + import terminal-stderr; +} diff --git a/sdks/kotlin/wit-native/deps/cli/run.wit b/sdks/kotlin/wit-native/deps/cli/run.wit new file mode 100644 index 0000000000..655346efb6 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/cli/run.wit @@ -0,0 +1,6 @@ +@since(version = 0.2.0) +interface run { + /// Run the program. + @since(version = 0.2.0) + run: func() -> result; +} diff --git a/sdks/kotlin/wit-native/deps/cli/stdio.wit b/sdks/kotlin/wit-native/deps/cli/stdio.wit new file mode 100644 index 0000000000..1b54f5318a --- /dev/null +++ b/sdks/kotlin/wit-native/deps/cli/stdio.wit @@ -0,0 +1,26 @@ +@since(version = 0.2.0) +interface stdin { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{input-stream}; + + @since(version = 0.2.0) + get-stdin: func() -> input-stream; +} + +@since(version = 0.2.0) +interface stdout { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{output-stream}; + + @since(version = 0.2.0) + get-stdout: func() -> output-stream; +} + +@since(version = 0.2.0) +interface stderr { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{output-stream}; + + @since(version = 0.2.0) + get-stderr: func() -> output-stream; +} diff --git a/sdks/kotlin/wit-native/deps/cli/terminal.wit b/sdks/kotlin/wit-native/deps/cli/terminal.wit new file mode 100644 index 0000000000..d305498c64 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/cli/terminal.wit @@ -0,0 +1,62 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.2.0) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.2.0) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.2.0) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.2.0) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stdin { + @since(version = 0.2.0) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stdout { + @since(version = 0.2.0) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stderr { + @since(version = 0.2.0) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stderr: func() -> option; +} diff --git a/sdks/kotlin/wit-native/deps/clocks/monotonic-clock.wit b/sdks/kotlin/wit-native/deps/clocks/monotonic-clock.wit new file mode 100644 index 0000000000..c676fb84d8 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/clocks/monotonic-clock.wit @@ -0,0 +1,50 @@ +package wasi:clocks@0.2.3; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.2.0) +interface monotonic-clock { + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.2.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.2.0) + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + @since(version = 0.2.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.2.0) + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// has occurred. + @since(version = 0.2.0) + subscribe-instant: func( + when: instant, + ) -> pollable; + + /// Create a `pollable` that will resolve after the specified duration has + /// elapsed from the time this function is invoked. + @since(version = 0.2.0) + subscribe-duration: func( + when: duration, + ) -> pollable; +} diff --git a/sdks/kotlin/wit-native/deps/clocks/timezone.wit b/sdks/kotlin/wit-native/deps/clocks/timezone.wit new file mode 100644 index 0000000000..b43e93b233 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/clocks/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.2.3; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/sdks/kotlin/wit-native/deps/clocks/wall-clock.wit b/sdks/kotlin/wit-native/deps/clocks/wall-clock.wit new file mode 100644 index 0000000000..e00ce08933 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/clocks/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.2.3; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.2.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.2.0) + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.2.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.2.0) + resolution: func() -> datetime; +} diff --git a/sdks/kotlin/wit-native/deps/clocks/world.wit b/sdks/kotlin/wit-native/deps/clocks/world.wit new file mode 100644 index 0000000000..05f04f797d --- /dev/null +++ b/sdks/kotlin/wit-native/deps/clocks/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import monotonic-clock; + @since(version = 0.2.0) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/sdks/kotlin/wit-native/deps/config/store.wit b/sdks/kotlin/wit-native/deps/config/store.wit new file mode 100644 index 0000000000..794379a754 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/config/store.wit @@ -0,0 +1,30 @@ +interface store { + /// An error type that encapsulates the different errors that can occur fetching configuration values. + variant error { + /// This indicates an error from an "upstream" config source. + /// As this could be almost _anything_ (such as Vault, Kubernetes ConfigMaps, KeyValue buckets, etc), + /// the error message is a string. + upstream(string), + /// This indicates an error from an I/O operation. + /// As this could be almost _anything_ (such as a file read, network connection, etc), + /// the error message is a string. + /// Depending on how this ends up being consumed, + /// we may consider moving this to use the `wasi:io/error` type instead. + /// For simplicity right now in supporting multiple implementations, it is being left as a string. + io(string), + } + + /// Gets a configuration value of type `string` associated with the `key`. + /// + /// The value is returned as an `option`. If the key is not found, + /// `Ok(none)` is returned. If an error occurs, an `Err(error)` is returned. + get: func( + /// A string key to fetch + key: string + ) -> result, error>; + + /// Gets a list of configuration key-value pairs of type `string`. + /// + /// If an error occurs, an `Err(error)` is returned. + get-all: func() -> result>, error>; +} diff --git a/sdks/kotlin/wit-native/deps/config/world.wit b/sdks/kotlin/wit-native/deps/config/world.wit new file mode 100644 index 0000000000..f92f080a57 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/config/world.wit @@ -0,0 +1,6 @@ +package wasi:config@0.2.0-draft; + +world imports { + /// The interface for wasi:config/store + import store; +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/filesystem/preopens.wit b/sdks/kotlin/wit-native/deps/filesystem/preopens.wit new file mode 100644 index 0000000000..cea97495b5 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/filesystem/preopens.wit @@ -0,0 +1,11 @@ +package wasi:filesystem@0.2.3; + +@since(version = 0.2.0) +interface preopens { + @since(version = 0.2.0) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.2.0) + get-directories: func() -> list>; +} diff --git a/sdks/kotlin/wit-native/deps/filesystem/types.wit b/sdks/kotlin/wit-native/deps/filesystem/types.wit new file mode 100644 index 0000000000..d229a21f48 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/filesystem/types.wit @@ -0,0 +1,672 @@ +package wasi:filesystem@0.2.3; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.2.0) +interface types { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{input-stream, output-stream, error}; + @since(version = 0.2.0) + use wasi:clocks/wall-clock@0.2.3.{datetime}; + + /// File size or length of a region within a file. + @since(version = 0.2.0) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.2.0) + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.2.0) + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.2.0) + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.2.0) + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + @since(version = 0.2.0) + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + @since(version = 0.2.0) + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.2.0) + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + @since(version = 0.2.0) + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + @since(version = 0.2.0) + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + @since(version = 0.2.0) + resource descriptor { + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + @since(version = 0.2.0) + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> result; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + @since(version = 0.2.0) + write-via-stream: func( + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in POSIX. + @since(version = 0.2.0) + append-via-stream: func() -> result; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.2.0) + advise: func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.2.0) + sync-data: func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) + get-flags: func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) + get-type: func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.2.0) + set-size: func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.2.0) + set-times: func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code>; + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.2.0) + write: func( + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + @since(version = 0.2.0) + read-directory: func() -> result; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.2.0) + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.2.0) + create-directory-at: func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) + stat: func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) + stat-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + @since(version = 0.2.0) + set-times-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.2.0) + link-at: func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.2.0) + open-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.2.0) + readlink-at: func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.2.0) + remove-directory-at: func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.2.0) + rename-at: func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.2.0) + symlink-at: func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + @since(version = 0.2.0) + unlink-file-at: func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + @since(version = 0.2.0) + is-same-object: func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.2.0) + metadata-hash: func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.2.0) + metadata-hash-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } + + /// A stream of directory entries. + @since(version = 0.2.0) + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + @since(version = 0.2.0) + read-directory-entry: func() -> result, error-code>; + } + + /// Attempts to extract a filesystem-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// filesystem-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are filesystem-related errors. + @since(version = 0.2.0) + filesystem-error-code: func(err: borrow) -> option; +} diff --git a/sdks/kotlin/wit-native/deps/filesystem/world.wit b/sdks/kotlin/wit-native/deps/filesystem/world.wit new file mode 100644 index 0000000000..29405bc2cc --- /dev/null +++ b/sdks/kotlin/wit-native/deps/filesystem/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import types; + @since(version = 0.2.0) + import preopens; +} diff --git a/sdks/kotlin/wit-native/deps/golem-1.x/golem-context.wit b/sdks/kotlin/wit-native/deps/golem-1.x/golem-context.wit new file mode 100644 index 0000000000..7d97eed940 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-1.x/golem-context.wit @@ -0,0 +1,93 @@ +package golem:api@1.5.0; + +/// Invocation context support +interface context { + use wasi:clocks/wall-clock@0.2.3.{datetime}; + + /// Starts a new `span` with the given name, as a child of the current invocation context + start-span: func(name: string) -> span; + + /// Gets the current invocation context + /// + /// The function call captures the current context; if new spans are started, the returned `invocation-context` instance will not + /// reflect that. + current-context: func() -> invocation-context; + + /// Allows or disallows forwarding of trace context headers in outgoing HTTP requests + /// + /// Returns the previous value of the setting + allow-forwarding-trace-context-headers: func(allow: bool) -> bool; + + /// Represents a unit of work or operation + resource span { + /// Gets the starting time of the span + started-at: func() -> datetime; + + /// Set an attribute on the span + set-attribute: func(name: string, value: attribute-value); + + /// Set multiple attributes on the span + set-attributes: func(attributes: list); + + /// Early finishes the span; otherwise it will be finished when the resource is dropped + finish: func(); + } + + /// Represents an invocation context wich allows querying the stack of attributes + /// created by automatic and user-defined spans. + resource invocation-context { + /// Gets the current trace id + trace-id: func() -> trace-id; + + /// Gets the current span id + span-id: func() -> span-id; + + /// Gets the parent context, if any; allows recursive processing of the invocation context. + /// + /// Alternatively, the attribute query methods can return inherited values without having to + /// traverse the stack manually. + parent: func() -> option; + + /// Gets the value of an attribute `key`. If `inherited` is true, the value is searched in the stack of spans, + /// otherwise only in the current span. + get-attribute: func(key: string, inherited: bool) -> option; + + /// Gets all attributes of the current invocation context. If `inherited` is true, it returns the merged set of attributes, each + /// key associated with the latest value found in the stack of spans. + get-attributes: func(inherited: bool) -> list; + + /// Gets the chain of attribute values associated with the given `key`. If the key does not exist in any of the + /// spans in the invocation context, the list is empty. The chain's first element contains the most recent (innermost) value. + get-attribute-chain: func(key: string) -> list; + + /// Gets all values of all attributes of the current invocation context. + get-attribute-chains: func() -> list; + + /// Gets the W3C Trace Context headers associated with the current invocation context + trace-context-headers: func() -> list>; + } + + /// An attribute of a span + record attribute { + key: string, + value: attribute-value + } + + /// A chain of attribute values, the first element representing the most recent value + record attribute-chain { + key: string, + values: list + } + + /// Possible span attribute value types + variant attribute-value { + /// A string value + %string(string) + } + + /// The trace represented by a 16 bytes hexadecimal string + type trace-id = string; + + /// The span represented by a 8 bytes hexadecimal string + type span-id = string; +} diff --git a/sdks/kotlin/wit-native/deps/golem-1.x/golem-host.wit b/sdks/kotlin/wit-native/deps/golem-1.x/golem-host.wit new file mode 100644 index 0000000000..c0e9789376 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-1.x/golem-host.wit @@ -0,0 +1,344 @@ +package golem:api@1.5.0; + +/// The Golem host API provides low level access to Golem specific features such as promises and control over +/// the durability and transactional guarantees the executor provides. +interface host { + use golem:core/types@2.0.0.{component-id, card-id, uuid, agent-id, promise-id, oplog-index}; + use wasi:io/poll@0.2.3.{pollable}; + + /// Represents a Golem component's version + type component-revision = u64; + + /// Represents a Golem environment + record environment-id { + uuid: uuid, + } + + /// Configurable persistence level for agents + variant persistence-level { + persist-nothing, + persist-remote-side-effects, + smart + } + + /// Describes how to update an agent to a different component version + enum update-mode { + /// Automatic update tries to recover the agent using the new component version + /// and may fail if there is a divergence. + automatic, + + /// Manual, snapshot-based update uses a user-defined implementation of the `save-snapshot` interface + /// to store the agent's state, and a user-defined implementation of the `load-snapshot` interface to + /// load it into the new version. + snapshot-based + } + + /// Operators used in filtering enumerated agents + enum filter-comparator { + equal, + not-equal, + greater-equal, + greater, + less-equal, + less + } + + /// Operators used on strings in filtering enumerated agents + enum string-filter-comparator { + equal, + not-equal, + like, + not-like, + starts-with + } + + /// The current status of an agent + enum agent-status { + /// The agent is running an invoked function + running, + /// The agent is ready to run an invoked function + idle, + /// An invocation is active but waiting for something (sleeping, waiting for a promise) + suspended, + /// The last invocation was interrupted but will be resumed + interrupted, + /// The last invocation failed and a retry was scheduled + retrying, + /// The last invocation failed and the agent can no longer be used + failed, + /// The agent exited after a successful invocation and can no longer be invoked + exited, + } + + /// Describes a filter condition on agent IDs when enumerating agents + record agent-name-filter { + comparator: string-filter-comparator, + value: string + } + + /// Describes a filter condition on the agent status when enumerating agents + record agent-status-filter { + comparator: filter-comparator, + value: agent-status + } + + /// Describes a filter condition on the component version when enumerating agents + record agent-version-filter { + comparator: filter-comparator, + value: u64 + } + + /// Describes a filter condition on the agent's creation time when enumerating agents + record agent-created-at-filter { + comparator: filter-comparator, + value: u64 + } + + /// Describes a filter condition on the agent's environment variables when enumerating agents + record agent-env-filter { + name: string, + comparator: string-filter-comparator, + value: string + } + + /// Describes a filter condition on the agent's configuration variables when enumerating agents + record agent-config-vars-filter { + name: string, + comparator: string-filter-comparator, + value: string + } + + /// Describes one filter condition for enumerating agents + variant agent-property-filter { + name(agent-name-filter), + status(agent-status-filter), + version(agent-version-filter), + created-at(agent-created-at-filter), + env(agent-env-filter), + config(agent-config-vars-filter) + } + + /// Combines multiple filter conditions with an `AND` relationship for enumerating agents + record agent-all-filter { + filters: list + } + + /// Combines multiple groups of filter conditions with an `OR` relationship for enumerating agents + record agent-any-filter { + filters: list + } + + /// Metadata about an agent + record agent-metadata { + /// The agent ID, consists of the component ID, agent type and agent parameters + agent-id: agent-id, + /// Command line arguments seen by the agent + args: list, + /// Environment variables seen by the agent + env: list>, + /// Configuration variables seen by the agent + config: list>, + /// The current agent status + status: agent-status, + /// The component version the agent is running with + component-revision: u64, + /// The agent's current retry count + retry-count: u64, + /// The environment the agent belongs to + environment-id: environment-id + } + + /// Creates an agent enumeration + resource get-agents { + /// Creates an agent enumeration request. It is going to enumerate all agents of all the agent types + /// defined in `component-id`, filtered by the conditions given by `filter`. If `precise` is true, + /// the server will calculate the most recent state of all the returned agents, otherwise the returned + /// metadata will be not guaranteed to be up-to-date. + constructor(component-id: component-id, filter: option, precise: bool); + + /// Retrieves the next batch of agent metadata. + get-next: func() -> option>; + } + + /// Target parameter for the `revert-agent` operation + variant revert-agent-target { + /// Revert to a specific oplog index. The given index will be the last one to be kept. + revert-to-oplog-index(oplog-index), + /// Revert the last N invocations. + revert-last-invocations(u64) + } + + /// Details about the fork result + record fork-details { + forked-phantom-id: uuid, + } + + /// Indicates which agent the code is running on after `fork`. + /// The parameter contains details about the fork result, such as the phantom-ID of the newly + /// created agent. + variant fork-result { + /// The original agent that called `fork` + original(fork-details), + /// The new agent + forked(fork-details) + } + + resource get-promise-result { + /// Returns a pollable that can be used to wait for the promise to become ready.j + subscribe: func() -> pollable; + /// Poll the result of the promise, returning none if it is not yet ready. + get: func() -> option>; + } + + /// Plain-data handle to a permission card visible to the guest. + record card { + card-id: card-id, + } + + enum card-install-error { + revoked, + not-found, + not-permitted, + } + + /// Create a new promise + create-promise: func() -> promise-id; + + /// Gets a handle to the result of the promise. Can only be called in the same agent that orignally created the promise. + get-promise: func(promise-id: promise-id) -> get-promise-result; + + /// Completes the given promise with the given payload. Returns true if the promise was completed, false + /// if the promise was already completed. The payload is passed to the agent that is awaiting the promise. + complete-promise: func(promise-id: promise-id, data: list) -> bool; + + /// Returns the current position in the persistent op log + get-oplog-index: func() -> oplog-index; + + /// Makes the current agent travel back in time and continue execution from the given position in the persistent + /// op log. + set-oplog-index: func(oplog-idx: oplog-index); + + /// Blocks the execution until the oplog has been written to at least the specified number of replicas, + /// or the maximum number of replicas if the requested number is higher. + oplog-commit: func(replicas: u8); + + /// Marks the beginning of an atomic operation. + /// In case of a failure within the region selected by `mark-begin-operation` and `mark-end-operation` + /// the whole region will be reexecuted on retry. + /// The end of the region is when `mark-end-operation` is called with the returned oplog-index. + mark-begin-operation: func() -> oplog-index; + + /// Commits this atomic operation. After `mark-end-operation` is called for a given index, further calls + /// with the same parameter will do nothing. + mark-end-operation: func(begin: oplog-index); + + /// Unconditionally traps the current invocation with the given reason. + /// + /// This call never returns: it surfaces as an uncatchable wasm trap on the host side + /// and the worker enters the standard trap-recovery flow. SDK guard helpers use this + /// to guarantee that a failed atomic region always leads to a trap, regardless of + /// whether the guest language could otherwise catch the failure. + trap: func(reason: string); + + /// Gets the agent's current persistence level. + get-oplog-persistence-level: func() -> persistence-level; + + /// Sets the agent's current persistence level. This can increase the performance of execution in cases where durable + /// execution is not required. + set-oplog-persistence-level: func(new-persistence-level: persistence-level); + + /// Gets the current idempotence mode. See `set-idempotence-mode` for details. + get-idempotence-mode: func() -> bool; + + /// Sets the current idempotence mode. The default is true. + /// True means side-effects are treated idempotent and Golem guarantees at-least-once semantics. + /// In case of false the executor provides at-most-once semantics, failing the agent in case it is + /// not known if the side effect was already executed. + set-idempotence-mode: func(idempotent: bool); + + /// Generates an idempotency key. This operation will never be replayed — + /// i.e. not only is this key generated, but it is persisted and committed, such that the key can be used in third-party systems (e.g. payment processing) + /// to introduce idempotence. + generate-idempotency-key: func() -> uuid; + + /// Returns one permission card currently installed in this agent's wallet, if any. + self-card: func() -> option; + + /// Derives a card handle from an installed card. This minimal test-facing form keeps the card as plain data. + derive-card: func(card: card) -> result; + + /// Installs a card into this agent's own wallet. + install-card: func(card: card) -> result<_, card-install-error>; + + /// Initiates an update attempt for the given agent. The function returns immediately once the request has been processed, + /// not waiting for the agent to get updated. + update-agent: func(agent-id: agent-id, target-revision: component-revision, mode: update-mode); + + /// Get the current agent's metadata + get-self-metadata: func() -> agent-metadata; + + /// Get agent metadata + get-agent-metadata: func(agent-id: agent-id) -> option; + + /// Fork an agent to another agent at a given oplog index + fork-agent: func(source-agent-id: agent-id, target-agent-id: agent-id, oplog-idx-cut-off: oplog-index); + + /// Revert an agent to a previous state + revert-agent: func(agent-id: agent-id, revert-target: revert-agent-target); + + /// Get the component-id for a given component reference. + /// Returns none when no component with the specified reference exists. + /// The syntax of the component reference is implementation dependent. + /// + /// Golem OSS: "{component_name}" + /// Golem Cloud: + /// 1: "{component_name}" -> will resolve in current account and project + /// 2: "{project_name}/{component_name}" -> will resolve in current account + /// 3: "{account_id}/{project_name}/{component_name}" + resolve-component-id: func(component-reference: string) -> option; + + /// Get the agent-id for a given component and agent name. + /// Returns none when no component for the specified reference exists. + resolve-agent-id: func(component-reference: string, agent-name: string) -> option; + + /// Get the agent-id for a given component and agent-name. + /// Returns none when no component for the specified component-reference or no agent with the specified agent-name exists. + resolve-agent-id-strict: func(component-reference: string, agent-name: string) -> option; + + /// Forks the current agent at the current execution point. The new agent gets the same base agent ID but + /// with a new unique phantom ID. The phantom ID of the forked agent is returned in `fork-result` on + /// both sides. The newly created agent continues running from the same point, but the return value is + /// going to be different in this agent and the forked agent. + fork: func() -> fork-result; + + /// Snapshot payload + record snapshot { + payload: list, + mime-type: string + } +} + +/// Interface providing user-defined snapshotting capability. This can be used to perform manual update of agents +/// when the new component incompatible with the old one. +interface save-snapshot { + use host.{snapshot}; + + /// Saves the component's state into a user-defined snapshot + save: func() -> snapshot; +} + +/// Interface providing user-defined snapshotting capability. This can be used to perform manual update of agents +/// when the new component incompatible with the old one. +interface load-snapshot { + use host.{snapshot}; + + /// Tries to load a user-defined snapshot, setting up the agent's state based on it. + /// The function can return with a failure to indicate that the update is not possible. + load: func(snapshot: snapshot) -> result<_, string>; +} + +world golem-host { + import host; + import save-snapshot; + import load-snapshot; +} diff --git a/sdks/kotlin/wit-native/deps/golem-1.x/golem-oplog-processor.wit b/sdks/kotlin/wit-native/deps/golem-1.x/golem-oplog-processor.wit new file mode 100644 index 0000000000..874e1d748e --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-1.x/golem-oplog-processor.wit @@ -0,0 +1,27 @@ +package golem:api@1.5.0; + +interface oplog-processor { + use wasi:clocks/wall-clock@0.2.3.{datetime}; + use golem:core/types@2.0.0.{account-id, component-id, agent-id}; + + use host.{oplog-index, agent-metadata}; + use oplog.{oplog-entry}; + + record account-info { + account-id: account-id + } + + /// Called when one of the agents the plugin is activated on has written new entries to its oplog. + /// + /// There are no guarantees for the number of processors running at the same time, and different entries from the same agent + /// may be sent to different processor instances. + /// + /// The `account-info` parameters contains details of the account the installation belongs to. + /// The `config` parameter contains the configuration parameters for the plugin, as specified in the plugin installation + /// The `component-id` parameter contains the identifier of the component the plugin was installed to. + /// The `agent-id` parameter identifies the agent. + /// The `metadata` parameter contains the latest metadata of the agent. + /// The `first-entry-index` parameter contains the index of the first entry in the list of `entries`. + /// The `entries` parameter always contains at least one element. + process: func(account-info: account-info, config: list>, component-id: component-id, agent-id: agent-id, metadata: agent-metadata, first-entry-index: oplog-index, entries: list) -> result<_, string>; +} diff --git a/sdks/kotlin/wit-native/deps/golem-1.x/golem-oplog.wit b/sdks/kotlin/wit-native/deps/golem-1.x/golem-oplog.wit new file mode 100644 index 0000000000..416ee38c3f --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-1.x/golem-oplog.wit @@ -0,0 +1,910 @@ +package golem:api@1.5.0; + +/// Host interface for enumerating and searching for agent oplogs +interface oplog { + use wasi:clocks/wall-clock@0.2.3.{datetime}; + use golem:core/types@2.0.0.{account-id, card-id, schema-value-tree, typed-schema-value}; + + use host.{component-revision, oplog-index, persistence-level, environment-id, uuid, agent-id, snapshot}; + use wasi:clocks/monotonic-clock@0.2.3.{duration}; + + /// Whether an agent is durable (persistent oplog) or ephemeral. + /// This mirrors the agent-mode enum in golem:agent/common@1.5.0; it is duplicated here to avoid + /// a circular WIT package dependency between golem:api and golem:agent. + enum agent-mode { + durable, + ephemeral + } + + use context.{attribute, attribute-value, span-id, trace-id}; + use retry.{retry-predicate, retry-policy, named-retry-policy}; + + /// Index into a retry-policy-state's node list + type state-node-index = s32; + + /// Persistent state of a retry policy across retry attempts. + /// Root is nodes[0]. Children referenced by state-node-index. + record retry-policy-state { + nodes: list, + } + + variant state-node { + /// Counter-based state (e.g. periodic, exponential, fibonacci). + counter(u32), + /// Terminal state — policy has given up. + terminal, + /// Wrapper state delegating to an inner policy state. + wrapper(state-node-index), + /// Count-box state with attempt tracking. + count-box(count-box-state), + /// And-then sequential composition state. + and-then(and-then-state), + /// Pair state for union/intersect composition. + pair(pair-state), + } + + record count-box-state { + attempts: u32, + inner: state-node-index, + } + + record and-then-state { + left: state-node-index, + right: state-node-index, + on-right: bool, + } + + record pair-state { + left: state-node-index, + right: state-node-index, + } + + record environment-plugin-grant-id { + uuid: uuid, + } + + variant wrapped-function-type { + /// The side-effect reads from the agent's local state (for example local file system, + /// random generator, etc.) + read-local, + /// The side-effect writes to the agent's local state (for example local file system) + write-local, + /// The side-effect reads from external state (for example a key-value store) + read-remote, + /// The side-effect manipulates external state (for example an RPC call) + write-remote, + /// The side-effect manipulates external state through multiple invoked functions (for example + /// a HTTP request where reading the response involves multiple host function calls) + /// + /// On the first invocation of the batch, the parameter should be `None` - this triggers + /// writing a scope `Start` entry in the oplog. Followup invocations should contain this + /// entry's index as the parameter so their host-call `Start` entries can point back to the + /// scope. In batched remote writes it is the caller's responsibility to manually write the + /// matching scope `End` entry (using `end_function`) when the operation is completed. + write-remote-batched(option), + write-remote-transaction(option) + } + + record plugin-installation-description { + environment-plugin-grant-id: environment-plugin-grant-id, + plugin-priority: s32, + plugin-name: string, + plugin-version: string, + parameters: list> + } + + record raw-local-agent-config-entry { + path: list, + value: schema-value-tree + } + + record local-agent-config-entry { + path: list, + value: typed-schema-value + } + + record create-parameters { + timestamp: datetime, + agent-id: agent-id, + agent-mode: agent-mode, + component-revision: component-revision, + env: list>, + created-by: account-id, + environment-id: environment-id, + parent: option, + component-size: u64, + initial-total-linear-memory-size: u64, + initial-active-plugins: list, + local-agent-config: list, + original-phantom-id: option, + instance-id: uuid + } + + record start-parameters { + timestamp: datetime, + parent-start-index: option, + function-name: string, + request: option, + durable-function-type: wrapped-function-type, + } + + record end-parameters { + timestamp: datetime, + start-index: oplog-index, + response: option, + forced-commit: bool, + } + + record cancelled-parameters { + timestamp: datetime, + start-index: oplog-index, + partial: option, + } + + record local-span-data { + span-id: span-id, + start: datetime, + parent: option, + /// Optionally an index of the invocation-context field within agent-invocation + linked-context: option, + attributes: list, + inherited: bool + } + + record external-span-data { + span-id: span-id + } + + variant span-data { + local-span(local-span-data), + external-span(external-span-data) + } + + record agent-invocation-started-parameters { + timestamp: datetime, + invocation: agent-invocation, + } + + record agent-invocation-finished-parameters { + timestamp: datetime, + %result: agent-invocation-result, + method-name: option, + consumed-fuel: s64, + component-revision: u64, + } + + record error-parameters { + timestamp: datetime, + error: string, + retry-from: oplog-index, + inside-atomic-region: bool, + /// Persistent retry policy state, if a semantic retry policy is active. + retry-policy-state: option, + } + + record oplog-region { + start: oplog-index, + end: oplog-index + } + + record jump-parameters { + timestamp: datetime, + jump: oplog-region + } + + /// Parameters for a set-retry-policy oplog entry. + record set-retry-policy-parameters { + timestamp: datetime, + policy: named-retry-policy, + } + + /// Parameters for a remove-retry-policy oplog entry. + record remove-retry-policy-parameters { + timestamp: datetime, + name: string, + } + + record queued-card-event-install { + card-id: card-id, + } + + record queued-card-event-revoke { + card-id: card-id, + } + + variant queued-card-event { + install(queued-card-event-install), + revoke(queued-card-event-revoke), + } + + /// Parameters for a card-event-queued oplog entry. + record card-event-queued-parameters { + timestamp: datetime, + event: queued-card-event, + } + + /// Parameters for a card-installed oplog entry. + record card-installed-parameters { + timestamp: datetime, + queued-event-index: option, + card-id: card-id, + } + + /// Raw parameters for a card-installed oplog entry. + record raw-card-installed-parameters { + timestamp: datetime, + queued-event-index: option, + card: list, + } + + enum card-install-failure { + card-revoked, + not-found, + recipient-mismatch, + not-permitted, + } + + /// Parameters for a card-install-failed oplog entry. + record card-install-failed-parameters { + timestamp: datetime, + queued-event-index: oplog-index, + card-id: card-id, + reason: card-install-failure, + } + + /// Parameters for a card-revoked oplog entry. + record card-revoked-parameters { + timestamp: datetime, + queued-event-index: oplog-index, + card-id: card-id, + } + + /// Parameters for a card-expired oplog entry. + record card-expired-parameters { + timestamp: datetime, + card-id: card-id, + } + + record end-atomic-region-parameters { + timestamp: datetime, + begin-index: oplog-index + } + + record agent-initialization-parameters { + idempotency-key: string, + constructor-parameters: typed-schema-value, + trace-id: string, + trace-states: list, + invocation-context: list>, + } + + record agent-method-invocation-parameters { + idempotency-key: string, + method-name: string, + function-input: typed-schema-value, + trace-id: string, + trace-states: list, + invocation-context: list>, + } + + record load-snapshot-parameters { + snapshot: snapshot-data, + } + + record process-oplog-entries-parameters { + idempotency-key: string, + } + + record manual-update-parameters { + target-revision: component-revision, + } + + variant agent-invocation { + agent-initialization(agent-initialization-parameters), + agent-method-invocation(agent-method-invocation-parameters), + save-snapshot, + load-snapshot(load-snapshot-parameters), + process-oplog-entries(process-oplog-entries-parameters), + manual-update(manual-update-parameters), + } + + variant agent-invocation-result { + agent-initialization(agent-invocation-output-parameters), + agent-method(agent-invocation-output-parameters), + manual-update, + load-snapshot(fallible-result-parameters), + save-snapshot(save-snapshot-result-parameters), + process-oplog-entries(fallible-result-parameters), + } + + record agent-invocation-output-parameters { + output: typed-schema-value, + } + + record fallible-result-parameters { + error: option, + } + + record save-snapshot-result-parameters { + snapshot: snapshot-data, + } + + record pending-agent-invocation-parameters { + timestamp: datetime, + invocation: agent-invocation + } + + variant update-description { + /// Automatic update by replaying the oplog on the new version + auto-update, + /// Custom update by loading a given snapshot on the new version + snapshot-based(snapshot) + } + + record pending-update-parameters { + timestamp: datetime, + target-revision: component-revision, + description: update-description + } + + record successful-update-parameters { + timestamp: datetime, + target-revision: component-revision, + new-component-size: u64, + new-active-plugins: list + } + + record failed-update-parameters { + timestamp: datetime, + target-revision: component-revision, + details: option + } + + record grow-memory-parameters { + timestamp: datetime, + delta: u64 + } + + record filesystem-storage-usage-update-parameters { + timestamp: datetime, + delta: s64 + } + + type agent-resource-id = u64; + + record create-resource-parameters { + timestamp: datetime, + id: agent-resource-id, + name: string, + owner: string + } + + record drop-resource-parameters { + timestamp: datetime, + id: agent-resource-id, + name: string, + owner: string + } + + enum log-level { + stdout, + stderr, + trace, + debug, + info, + warn, + error, + critical + } + + record log-parameters { + timestamp: datetime, + level: log-level, + context: string, + message: string + } + + record activate-plugin-parameters { + timestamp: datetime, + plugin: plugin-installation-description + } + + record deactivate-plugin-parameters { + timestamp: datetime, + plugin: plugin-installation-description + } + + record revert-parameters { + timestamp: datetime, + dropped-region: oplog-region + } + + record cancel-pending-invocation-parameters { + timestamp: datetime, + idempotency-key: string + } + + record start-span-parameters { + timestamp: datetime, + span-id: span-id, + parent: option, + linked-context-id: option, + attributes: list, + } + + record finish-span-parameters { + timestamp: datetime, + span-id: span-id + } + + record set-span-attribute-parameters { + timestamp: datetime, + span-id: span-id, + key: string, + value: attribute-value + } + + record change-persistence-level-parameters { + timestamp: datetime, + persistence-level: persistence-level + } + + record begin-remote-transaction-parameters { + timestamp: datetime, + transaction-id: string + } + + record remote-transaction-parameters { + timestamp: datetime, + begin-index: oplog-index + } + + record snapshot-data { + data: list, + mime-type: string, + } + + record snapshot-parameters { + timestamp: datetime, + data: snapshot-data, + } + + record oplog-processor-checkpoint-parameters { + timestamp: datetime, + plugin: plugin-installation-description, + target-agent-id: agent-id, + confirmed-up-to: oplog-index, + sending-up-to: oplog-index, + last-batch-start: oplog-index, + } + + record timestamp { + timestamp: datetime + } + + /// Opaque oplog payload, which can either be serialized inline or stored externally + variant oplog-payload { + inline(list), + external(oplog-external-payload) + } + + record oplog-external-payload { + payload-id: uuid, + md5-hash: list + } + + record agent-terminated-by-quota-error { + environment-id: environment-id, + resource-name: string + } + + record ephemeral-sleep-too-long { + requested-nanos: u64, + max-nanos: u64 + } + + record ephemeral-fuel-exhausted { + overdraft-limit: u64 + } + + record ephemeral-cannot-suspend { + reason: string + } + + record read-only-violation { + method: string, + host-function: string + } + + /// Describes the error that occurred in the agent + variant worker-error { + unknown(string), + invalid-request(string), + stack-overflow, + out-of-memory, + exceeded-memory-limit, + internal-error(string), + deterministic-trap(string), + transient-error(string), + permanent-error(string), + exceeded-table-limit, + exceeded-http-call-limit, + exceeded-rpc-call-limit, + node-out-of-filesystem-storage, + agent-exceeded-filesystem-storage-limit, + agent-terminated-by-quota(agent-terminated-by-quota-error), + ephemeral-sleep-too-long(ephemeral-sleep-too-long), + ephemeral-fuel-exhausted(ephemeral-fuel-exhausted), + ephemeral-cannot-suspend(ephemeral-cannot-suspend), + read-only-violation(read-only-violation) + } + + record raw-create-parameters { + timestamp: datetime, + agent-id: agent-id, + agent-mode: agent-mode, + component-revision: component-revision, + env: list>, + environment-id: environment-id, + created-by: account-id, + parent: option, + component-size: u64, + initial-total-linear-memory-size: u64, + initial-active-plugins: list, + local-agent-config: list, + original-phantom-id: option, + instance-id: uuid + } + + record raw-start-parameters { + timestamp: datetime, + parent-start-index: option, + function-name: string, + request: option, + durable-function-type: wrapped-function-type, + } + + record raw-end-parameters { + timestamp: datetime, + start-index: oplog-index, + response: option, + forced-commit: bool, + } + + record raw-cancelled-parameters { + timestamp: datetime, + start-index: oplog-index, + partial: option, + } + + record raw-agent-invocation-started-parameters { + timestamp: datetime, + idempotency-key: string, + payload: oplog-payload, + trace-id: string, + trace-states: list, + invocation-context: list, + } + + record raw-agent-invocation-finished-parameters { + timestamp: datetime, + %result: oplog-payload, + method-name: option, + consumed-fuel: s64, + component-revision: u64, + } + + record raw-error-parameters { + timestamp: datetime, + error: worker-error, + retry-from: oplog-index, + inside-atomic-region: bool, + /// Persistent retry policy state, if a semantic retry policy is active. + retry-policy-state: option, + } + + record raw-pending-agent-invocation-parameters { + timestamp: datetime, + idempotency-key: string, + payload: oplog-payload, + trace-id: string, + trace-states: list, + invocation-context: list, + } + + /// Raw update description used in oplog entries + variant raw-update-description { + /// Automatic update by replaying the oplog on the new version + automatic(component-revision), + /// Custom update by loading a given snapshot on the new version + snapshot-based(raw-snapshot-based-update) + } + + record raw-snapshot-based-update { + target-revision: component-revision, + payload: oplog-payload, + mime-type: string + } + + record raw-pending-update-parameters { + timestamp: datetime, + description: raw-update-description + } + + record raw-successful-update-parameters { + timestamp: datetime, + target-revision: component-revision, + new-component-size: u64, + new-active-plugins: list + } + + record resource-type-id { + name: string, + owner: string, + } + + record raw-create-resource-parameters { + timestamp: datetime, + id: agent-resource-id, + resource-type-id: resource-type-id, + } + + record raw-drop-resource-parameters { + timestamp: datetime, + id: agent-resource-id, + resource-type-id: resource-type-id, + } + + record raw-activate-plugin-parameters { + timestamp: datetime, + plugin-grant-id: environment-plugin-grant-id + } + + record raw-deactivate-plugin-parameters { + timestamp: datetime, + plugin-grant-id: environment-plugin-grant-id + } + + record raw-begin-remote-transaction-parameters { + timestamp: datetime, + transaction-id: string, + original-begin-index: option + } + + record raw-snapshot-parameters { + timestamp: datetime, + data: oplog-payload, + mime-type: string + } + + record raw-oplog-processor-checkpoint-parameters { + timestamp: datetime, + plugin-grant-id: environment-plugin-grant-id, + target-agent-id: agent-id, + confirmed-up-to: oplog-index, + sending-up-to: oplog-index, + last-batch-start: oplog-index, + } + + variant oplog-entry { + /// The initial agent oplog entry + create(raw-create-parameters), + /// Marks the start of a durable host call (or scope such as a batched-write). + start(raw-start-parameters), + /// Marks the successful completion of a durable host call (or scope) started by a matching `Start`. + end(raw-end-parameters), + /// Marks that a durable host call started by a matching `Start` was cancelled + /// (e.g. dropped from a `select!`) before producing a final response. + cancelled(raw-cancelled-parameters), + /// The agent has been invoked + agent-invocation-started(raw-agent-invocation-started-parameters), + /// The agent has completed an invocation + agent-invocation-finished(raw-agent-invocation-finished-parameters), + /// Agent suspended + suspend(timestamp), + /// Agent failed + error(raw-error-parameters), + /// Marker entry added when get-oplog-index is called from the agent, to make the jumping behavior + /// more predictable. + no-op(timestamp), + /// The agent needs to recover up to the given target oplog index and continue running from + /// the source oplog index from there + /// `jump` is an oplog region representing that from the end of that region we want to go back to the start and + /// ignore all recorded operations in between. + jump(jump-parameters), + /// Indicates that the agent has been interrupted at this point. + /// Only used to recompute the agent's (cached) status, has no effect on execution. + interrupted(timestamp), + /// Indicates that the agent has been exited using WASI's exit function. + exited(timestamp), + /// Begins an atomic region. All oplog entries after `BeginAtomicRegion` are to be ignored during + /// recovery except if there is a corresponding `EndAtomicRegion` entry. + begin-atomic-region(timestamp), + /// Ends an atomic region. All oplog entries between the corresponding `BeginAtomicRegion` and this + /// entry are to be considered during recovery, and the begin/end markers can be removed during oplog + /// compaction. + end-atomic-region(end-atomic-region-parameters), + /// An invocation request arrived while the agent was busy + pending-agent-invocation(raw-pending-agent-invocation-parameters), + /// An update request arrived and will be applied as soon the agent restarts + pending-update(raw-pending-update-parameters), + /// An update was successfully applied + successful-update(raw-successful-update-parameters), + /// An update failed to be applied + failed-update(failed-update-parameters), + /// Increased total linear memory size + grow-memory(grow-memory-parameters), + /// Updated filesystem usage by a signed delta + filesystem-storage-usage-update(filesystem-storage-usage-update-parameters), + /// Created a resource instance + create-resource(raw-create-resource-parameters), + /// Dropped a resource instance + drop-resource(raw-drop-resource-parameters), + /// The agent emitted a log message + log(log-parameters), + /// The agent has been restarted, forgetting all its history + restart(timestamp), + /// Activates a plugin + activate-plugin(raw-activate-plugin-parameters), + /// Deactivates a plugin + deactivate-plugin(raw-deactivate-plugin-parameters), + /// Revert an agent to a previous state + revert(revert-parameters), + /// Cancel a pending invocation + cancel-pending-invocation(cancel-pending-invocation-parameters), + /// Start a new span in the invocation context + start-span(start-span-parameters), + /// Finish an open span in the invocation context + finish-span(finish-span-parameters), + /// Set an attribute on an open span in the invocation context + set-span-attribute(set-span-attribute-parameters), + /// Change the current persistence level + change-persistence-level(change-persistence-level-parameters), + /// Begins a transaction operation + begin-remote-transaction(raw-begin-remote-transaction-parameters), + /// Pre-Commit of the transaction, indicating that the transaction will be committed + pre-commit-remote-transaction(remote-transaction-parameters), + /// Pre-Rollback of the transaction, indicating that the transaction will be rolled back + pre-rollback-remote-transaction(remote-transaction-parameters), + /// Committed transaction operation, indicating that the transaction was committed + committed-remote-transaction(remote-transaction-parameters), + /// Rolled back transaction operation, indicating that the transaction was rolled back + rolled-back-remote-transaction(remote-transaction-parameters), + /// A snapshot of the agent's state + snapshot(raw-snapshot-parameters), + /// Checkpoint for oplog processor plugin delivery tracking + oplog-processor-checkpoint(raw-oplog-processor-checkpoint-parameters), + /// Sets or overwrites a named retry policy + set-retry-policy(set-retry-policy-parameters), + /// Removes a named retry policy by name + remove-retry-policy(remove-retry-policy-parameters), + /// Durable queue entry for pending permission-card work + card-event-queued(card-event-queued-parameters), + /// Records successful installation of a permission card into the agent wallet + card-installed(raw-card-installed-parameters), + /// Records failed installation of a permission card into the agent wallet + card-install-failed(card-install-failed-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters), + /// Records that a permission card used by the agent has expired + card-expired(card-expired-parameters) + } + + variant public-oplog-entry { + /// The initial agent oplog entry + create(create-parameters), + /// Marks the start of a durable host call (or scope such as a batched-write). + start(start-parameters), + /// Marks the successful completion of a durable host call (or scope) started by a matching `Start`. + end(end-parameters), + /// Marks that a durable host call started by a matching `Start` was cancelled + /// (e.g. dropped from a `select!`) before producing a final response. + cancelled(cancelled-parameters), + /// The agent has been invoked + agent-invocation-started(agent-invocation-started-parameters), + /// The agent has completed an invocation + agent-invocation-finished(agent-invocation-finished-parameters), + /// Agent suspended + suspend(timestamp), + /// Agent failed + error(error-parameters), + /// Marker entry added when get-oplog-index is called from the agent, to make the jumping behavior + /// more predictable. + no-op(timestamp), + /// The agent needs to recover up to the given target oplog index and continue running from + /// the source oplog index from there + /// `jump` is an oplog region representing that from the end of that region we want to go back to the start and + /// ignore all recorded operations in between. + jump(jump-parameters), + /// Indicates that the agent has been interrupted at this point. + /// Only used to recompute the agent's (cached) status, has no effect on execution. + interrupted(timestamp), + /// Indicates that the agent has been exited using WASI's exit function. + exited(timestamp), + /// Begins an atomic region. All oplog entries after `BeginAtomicRegion` are to be ignored during + /// recovery except if there is a corresponding `EndAtomicRegion` entry. + begin-atomic-region(timestamp), + /// Ends an atomic region. All oplog entries between the corresponding `BeginAtomicRegion` and this + /// entry are to be considered during recovery, and the begin/end markers can be removed during oplog + /// compaction. + end-atomic-region(end-atomic-region-parameters), + /// An invocation request arrived while the agent was busy + pending-agent-invocation(pending-agent-invocation-parameters), + /// An update request arrived and will be applied as soon the agent restarts + pending-update(pending-update-parameters), + /// An update was successfully applied + successful-update(successful-update-parameters), + /// An update failed to be applied + failed-update(failed-update-parameters), + /// Increased total linear memory size + grow-memory(grow-memory-parameters), + /// Updated filesystem usage by a signed delta + filesystem-storage-usage-update(filesystem-storage-usage-update-parameters), + /// Created a resource instance + create-resource(create-resource-parameters), + /// Dropped a resource instance + drop-resource(drop-resource-parameters), + /// The agent emitted a log message + log(log-parameters), + /// The agent's has been restarted, forgetting all its history + restart(timestamp), + /// Activates a plugin + activate-plugin(activate-plugin-parameters), + /// Deactivates a plugin + deactivate-plugin(deactivate-plugin-parameters), + /// Revert an agent to a previous state + revert(revert-parameters), + /// Cancel a pending invocation + cancel-pending-invocation(cancel-pending-invocation-parameters), + /// Start a new span in the invocation context + start-span(start-span-parameters), + /// Finish an open span in the invocation context + finish-span(finish-span-parameters), + /// Set an attribute on an open span in the invocation context + set-span-attribute(set-span-attribute-parameters), + /// Change the current persistence level + change-persistence-level(change-persistence-level-parameters), + /// Begins a transaction operation + begin-remote-transaction(begin-remote-transaction-parameters), + /// Pre-Commit of the transaction, indicating that the transaction will be committed + pre-commit-remote-transaction(remote-transaction-parameters), + /// Pre-Rollback of the transaction, indicating that the transaction will be rolled back + pre-rollback-remote-transaction(remote-transaction-parameters), + /// Committed transaction operation, indicating that the transaction was committed + committed-remote-transaction(remote-transaction-parameters), + /// Rolled back transaction operation, indicating that the transaction was rolled back + rolled-back-remote-transaction(remote-transaction-parameters), + /// A snapshot of the worker's state + snapshot(snapshot-parameters), + /// Checkpoint for oplog processor plugin delivery tracking + oplog-processor-checkpoint(oplog-processor-checkpoint-parameters), + /// Sets or overwrites a named retry policy + set-retry-policy(set-retry-policy-parameters), + /// Removes a named retry policy by name + remove-retry-policy(remove-retry-policy-parameters), + /// Durable queue entry for pending permission-card work + card-event-queued(card-event-queued-parameters), + /// Records successful installation of a permission card into the agent wallet + card-installed(card-installed-parameters), + /// Records failed installation of a permission card into the agent wallet + card-install-failed(card-install-failed-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters), + /// Records that a permission card used by the agent has expired + card-expired(card-expired-parameters) + } + + /// Enriches raw oplog entries into public oplog entries by resolving oplog payloads + /// and augmenting entries with component metadata. + enrich-oplog-entries: func(environment-id: environment-id, agent-id: agent-id, entries: list>, component-revision: component-revision) -> result, string>; + + resource get-oplog { + constructor(agent-id: agent-id, start: oplog-index); + get-next: func() -> option>; + } + + resource search-oplog { + constructor(agent-id: agent-id, text: string); + get-next: func() -> option>>; + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-1.x/golem-retry.wit b/sdks/kotlin/wit-native/deps/golem-1.x/golem-retry.wit new file mode 100644 index 0000000000..1cf2fd4680 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-1.x/golem-retry.wit @@ -0,0 +1,165 @@ +package golem:api@1.5.0; + +interface retry { + use wasi:clocks/monotonic-clock@0.2.3.{duration}; + + // ── Predicate value ────────────────────────────────────────────── + + /// Dynamic value for property comparisons in retry predicates + variant predicate-value { + text(string), + integer(s64), + boolean(bool), + } + + // ── Predicate tree (flattened) ─────────────────────────────────── + + /// Index into a retry-predicate's node list + type predicate-node-index = s32; + + /// A composable predicate tree for matching retry contexts. + /// Root is nodes[0]. Children referenced by predicate-node-index. + record retry-predicate { + nodes: list, + } + + variant predicate-node { + prop-eq(property-comparison), + prop-neq(property-comparison), + prop-gt(property-comparison), + prop-gte(property-comparison), + prop-lt(property-comparison), + prop-lte(property-comparison), + prop-exists(string), + prop-in(property-set-check), + prop-matches(property-pattern), + prop-starts-with(property-pattern), + prop-contains(property-pattern), + pred-and(tuple), + pred-or(tuple), + pred-not(predicate-node-index), + pred-true, + pred-false, + } + + record property-comparison { + property-name: string, + value: predicate-value, + } + + record property-set-check { + property-name: string, + values: list, + } + + record property-pattern { + property-name: string, + pattern: string, + } + + // ── Policy tree (flattened) ────────────────────────────────────── + + /// Index into a retry-policy's node list + type policy-node-index = s32; + + /// A composable retry policy tree. + /// Root is nodes[0]. Children referenced by policy-node-index. + record retry-policy { + nodes: list, + } + + variant policy-node { + periodic(duration), + exponential(exponential-config), + fibonacci(fibonacci-config), + immediate, + never, + count-box(count-box-config), + time-box(time-box-config), + clamp-delay(clamp-config), + add-delay(add-delay-config), + jitter(jitter-config), + filtered-on(filtered-config), + and-then(tuple), + policy-union(tuple), + policy-intersect(tuple), + } + + record exponential-config { + base-delay: duration, + factor: f64, + } + + record fibonacci-config { + first: duration, + second: duration, + } + + record count-box-config { + max-retries: u32, + inner: policy-node-index, + } + + record time-box-config { + limit: duration, + inner: policy-node-index, + } + + record clamp-config { + min-delay: duration, + max-delay: duration, + inner: policy-node-index, + } + + record add-delay-config { + delay: duration, + inner: policy-node-index, + } + + record jitter-config { + factor: f64, + inner: policy-node-index, + } + + /// filtered-config embeds a full retry-predicate inline (self-contained) + record filtered-config { + predicate: retry-predicate, + inner: policy-node-index, + } + + // ── Named policy rule ──────────────────────────────────────────── + + /// A named retry policy rule: predicate selects when it applies, + /// policy defines the retry strategy, priority controls evaluation order + /// (higher = checked first). + record named-retry-policy { + name: string, + priority: u32, + predicate: retry-predicate, + policy: retry-policy, + } + + // ── Host functions ─────────────────────────────────────────────── + + /// Get all retry policies active for this agent + get-retry-policies: func() -> list; + + /// Get a specific retry policy by name + get-retry-policy-by-name: func(name: string) -> option; + + /// Resolve the matching retry policy for a given operation context. + /// Evaluates named policies in descending priority order; returns the + /// policy from the first rule whose predicate matches, or none. + resolve-retry-policy: func( + verb: string, + noun-uri: string, + properties: list>, + ) -> option; + + /// Add or overwrite a named retry policy (persisted to oplog). + /// If a policy with the same name exists, it is replaced. + set-retry-policy: func(policy: named-retry-policy); + + /// Remove a named retry policy by name (persisted to oplog). + remove-retry-policy: func(name: string); +} diff --git a/sdks/kotlin/wit-native/deps/golem-agent/common.wit b/sdks/kotlin/wit-native/deps/golem-agent/common.wit new file mode 100644 index 0000000000..782c25aabd --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-agent/common.wit @@ -0,0 +1,237 @@ +package golem:agent@2.0.0; + +interface common { + use golem:core/types@2.0.0.{schema-graph, type-node-index, typed-schema-value, + metadata-envelope, agent-id, account-id, component-id}; + use wasi:clocks/monotonic-clock@0.2.3.{duration}; + + enum agent-mode { + durable, + ephemeral + } + + /// Full agent type declaration. + /// + /// `schema` is the per-agent type-node pool. Constructor / method / config + /// schema roots below are `type-node-index` values into `schema`. The + /// `schema.root` field is a structurally-required placeholder and is not the + /// semantic root of the agent type. + record agent-type { + type-name: string, + description: string, + source-language: string, + schema: schema-graph, + %constructor: agent-constructor, + methods: list, + dependencies: list, + mode: agent-mode, + http-mount: option, + snapshotting: snapshotting, + config: list, + } + + /// Associates an agent type with a component that implements it + record registered-agent-type { + agent-type: agent-type, + implemented-by: component-id + } + + /// Dependent agent type. All schema roots inside it resolve against its own + /// `schema`, not against the parent agent-type's graph. + record agent-dependency { + type-name: string, + description: option, + schema: schema-graph, + %constructor: agent-constructor, + methods: list, + } + + /// Ordered, named input parameters with per-field source annotation. + variant input-schema { + parameters(list), + } + + record named-field { + name: string, + source: field-source, + /// Index into the owning agent-type / dependency `schema` graph. + schema: type-node-index, + metadata: metadata-envelope, + } + + variant field-source { + user-supplied, + auto-injected(auto-injected-kind), + } + + enum auto-injected-kind { + principal, + } + + /// Output is either unit (no value) or a single value of the given type. + variant output-schema { + unit, + /// Index into the owning agent-type / dependency `schema` graph. + single(type-node-index), + } + + record agent-method { + name: string, + description: string, + http-endpoint: list, + prompt-hint: option, + input-schema: input-schema, + output-schema: output-schema, + read-only: option, + } + + record read-only-config { + cache-policy: cache-policy, + uses-principal: bool, + } + + variant cache-policy { + no-cache, + until-write, + ttl(duration), + } + + record http-mount-details { + path-prefix: list, + auth-details: option, + phantom-agent: bool, + cors-options: cors-options, + webhook-suffix: list, + } + + record cors-options { + allowed-patterns: list, + } + + record http-endpoint-details { + http-method: http-method, + path-suffix: list, + header-vars: list, + query-vars: list, + auth-details: option, + cors-options: cors-options, + } + + variant http-method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + custom(string), + } + + variant path-segment { + literal(string), + system-variable(system-variable), + path-variable(path-variable), + // only allowed as the last segment + remaining-path-variable(path-variable) + } + + enum system-variable { + agent-type, + agent-version + } + + record path-variable { + variable-name: string, + } + + record header-variable { + header-name: string, + variable-name: string, + } + + record query-variable { + query-param-name: string, + variable-name: string, + } + + record auth-details { + required: bool, + } + + variant principal { + oidc(oidc-principal), + agent(agent-principal), + golem-user(golem-user-principal), + anonymous + } + + record oidc-principal { + sub: string, + issuer: string, + + email: option, + name: option, + email-verified: option, + given-name: option, + family-name: option, + picture: option, + preferred-username: option, + + claims: string, + } + + record agent-principal { + agent-id: agent-id, + } + + record golem-user-principal { + account-id: account-id + } + + record agent-constructor { + name: option, + description: string, + prompt-hint: option, + input-schema: input-schema, + } + + variant snapshotting { + disabled, + enabled(snapshotting-config) + } + + variant snapshotting-config { + default, // current default in the server + periodic(duration), + every-n-invocation(u16) + } + + enum agent-config-source { + local, + secret + } + + record agent-config-declaration { + source: agent-config-source, + path: list, + /// Index into the owning agent-type `schema` graph. + value-type: type-node-index, + } + + record typed-agent-config-value { + path: list, + value: typed-schema-value, + } + + /// Agent-level failures + variant agent-error { + invalid-input(string), + invalid-method(string), + invalid-type(string), + invalid-agent-id(string), + custom-error(typed-schema-value), + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-agent/guest.wit b/sdks/kotlin/wit-native/deps/golem-agent/guest.wit new file mode 100644 index 0000000000..13a190dbab --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-agent/guest.wit @@ -0,0 +1,37 @@ +package golem:agent@2.0.0; + +interface guest { + use golem:core/types@2.0.0.{schema-value-tree}; + use common.{agent-error, agent-type, principal}; + + /// Initializes the agent of a given type with the given constructor parameters. + /// If called a second time, it fails. + /// + /// `input` is a value tree whose root encodes the constructor's parameter + /// list (one record field per declared `named-field`, in declaration order). + /// The guest interprets it against its own constructor `input-schema`. + initialize: func(agent-type: string, input: schema-value-tree, principal: principal) -> result<_, agent-error>; + + /// Invokes an agent. If create was not called before, it fails. + /// + /// `input` is a value tree whose root encodes the method's parameter list. + /// The result is `none` when the method's `output-schema` is `unit`, and + /// `some(value)` for a `single` output. + invoke: func(method-name: string, input: schema-value-tree, principal: principal) -> result, agent-error>; + + /// Gets the agent type. If create was not called before, it fails + get-definition: func() -> agent-type; + + /// Gets the agent types defined by this component + discover-agent-types: func() -> result, agent-error>; +} + +world agent-guest { + import golem:api/host@1.5.0; + import common; + export guest; +} + +world agent-host { + export host; +} diff --git a/sdks/kotlin/wit-native/deps/golem-agent/host.wit b/sdks/kotlin/wit-native/deps/golem-agent/host.wit new file mode 100644 index 0000000000..f1ed750c9a --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-agent/host.wit @@ -0,0 +1,113 @@ +package golem:agent@2.0.0; + +interface host { + use golem:core/types@2.0.0.{component-id, uuid, promise-id, schema-graph, schema-value-tree, typed-schema-value}; + use wasi:clocks/wall-clock@0.2.3.{datetime}; + use wasi:io/poll@0.2.3.{pollable}; + use common.{agent-error, agent-type, registered-agent-type, typed-agent-config-value}; + + /// Gets all the registered agent types + get-all-agent-types: func() -> list; + + /// Get a specific registered agent type by name + get-agent-type: func(agent-type-name: string) -> option; + + /// Constructs a string agent-id from the agent type and its constructor parameters + /// and an optional phantom ID. + /// + /// `input` is a value tree whose root encodes the constructor's parameter list. + make-agent-id: func(agent-type-name: string, input: schema-value-tree, phantom-id: option) -> result; + + /// Parses an agent-id (created by `make-agent-id`) into an agent type name and its constructor parameters + /// and an optional phantom ID. + /// + /// The constructor parameters are returned as a self-contained typed value + /// (graph + value tree) so the receiver can interpret them without an + /// external schema registry. + parse-agent-id: func(agent-id: string) -> result>, agent-error>; + + /// Creates a webhook that can be used to integrate with webhook driven apis. + /// When the created url is called with a post request, the provided promise-id is completed with the body of the post request. + /// Note the following behaviours: + /// * Only agents whoose agent types are _currently_ deployed via an http api are allowed to create a webhook. Calling this function while the agent + /// is not deployed via an http api will trap. + /// * Only the agent type that created the promise is allowed to create a webhook for it. Using this host function + /// from a different agent type will trap. + create-webhook: func(promise-id: promise-id) -> string; + + /// Possible failures of an RPC call + variant rpc-error { + /// Protocol level error + protocol-error(string), + /// Access denied + denied(string), + /// Target agent or function not found + not-found(string), + /// Internal error on the remote side + remote-internal-error(string), + /// The remote endpoint returned an agent-domain error + remote-agent-error(agent-error) + } + + /// An RPC client for invoking remote agents + resource wasm-rpc { + /// Constructs the RPC client connecting to the given target agent. + /// + /// `constructor` is a value tree whose root encodes the target agent + /// constructor's parameter list. + constructor(agent-type-name: string, %constructor: schema-value-tree, phantom-id: option, agent-config: list); + + /// Invokes a remote method with the given parameters, and awaits the result. + /// + /// `input` encodes the method's parameter list; the result is `none` for + /// a `unit` output and `some(value)` for a `single` output. + invoke-and-await: func(method-name: string, input: schema-value-tree) -> result, rpc-error>; + + /// Triggers the invocation of a remote method with the given parameters, and returns immediately. + invoke: func(method-name: string, input: schema-value-tree) -> result<_, rpc-error>; + + /// Invokes a remote method with the given parameters, and returns a `future-invoke-result` value which can + /// be polled for the result. + /// + /// With this function it is possible to call multiple (different) agents simultaneously. + async-invoke-and-await: func(method-name: string, input: schema-value-tree) -> future-invoke-result; + + /// Schedule invocation for later + schedule-invocation: func(scheduled-time: datetime, method-name: string, input: schema-value-tree); + + /// Schedule invocation for later. Call cancel on the returned resource to cancel the invocation before the scheduled time. + schedule-cancelable-invocation: func(scheduled-time: datetime, method-name: string, input: schema-value-tree) -> cancellation-token; + } + + /// Represents a pollable invocation result + resource future-invoke-result { + /// Subscribes to the result of the invocation + subscribe: func() -> pollable; + /// Poll for the invocation. If the invocation has not completed yet, returns `none`. + get: func() -> option, rpc-error>>; + /// Best-effort attempt to cancel the remote invocation by idempotency key. + /// If the invocation has already started or completed, this is a no-op. + cancel: func(); + } + + /// Cancellation token for scheduled invocations + resource cancellation-token { + /// Cancel the scheduled invocation + cancel: func(); + } + + /// Get the current value of the config key. + /// + /// The expected schema is a hint to the host what type of value is expected by the guest and can be used + /// by the host to automatically migrate config values to fit the expected schema. + /// + /// Only keys that are declared by the agent-type are allowed to be accessed. Trying + /// to access an undeclared key will trap, unless the expected type is an option. In that case + /// none is returned. + /// + /// Getting a local key will get values defined as part of the current + /// component revision + overrides declared during agent creation. + /// + /// Getting a shared key will get the current value of the key in the environment. + get-config-value: func(key: list, expected: schema-graph) -> schema-value-tree; +} diff --git a/sdks/kotlin/wit-native/deps/golem-core-v2/golem-core-v2.wit b/sdks/kotlin/wit-native/deps/golem-core-v2/golem-core-v2.wit new file mode 100644 index 0000000000..bb7dda191e --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-core-v2/golem-core-v2.wit @@ -0,0 +1,587 @@ +// Language-independent value and type model used by Golem agents and the +// platform's typed I/O surfaces. +// +// Recursive schema types and values are represented flat-with-indices because +// WIT does not support recursive types. The matching in-memory forms in each +// SDK (e.g., a recursive `SchemaType` / `SchemaValue` / `SchemaGraph` in Rust) +// convert to and from this flat form mechanically. +// +// Design notes: +// - Graph-with-root: a `schema-graph` owns a `defs` list of named type +// definitions, a flat `type-nodes` list, and a root index. +// `schema-type-node::ref-type` references entries in `defs` by index. A +// `schema-graph` is always self-contained — there is no implicit external +// registry that consumers must look up. +// - `quantity-value` is a fixed-point decimal `mantissa * 10^(-scale)` with +// a unit string; mantissa fits in `s64` and scale fits in `s32`. + +package golem:core@2.0.0; + +interface types { + // ============================================================ + // Carrier indices + // ============================================================ + + /// Index into a `schema-graph`'s `type-nodes` list. + type type-node-index = s32; + + /// Index into a `schema-value-tree`'s `value-nodes` list. + type value-node-index = s32; + + /// Index into a `schema-graph`'s `defs` list. + type def-index = s32; + + /// Stable, language-independent identifier for a named type definition. + /// Must be unique within the enclosing `schema-graph`. Conventional format + /// is a dot-separated namespace path (e.g., `"myapp.users.user"`). Each + /// SDK provides a default derivation rule (typically based on the local + /// language's type name); cross-language interop requires the same + /// `type-id` on every side, which users can pin via the SDK's `named` + /// attribute. + type type-id = string; + + // ============================================================ + // Common embedded value types + // ============================================================ + + record uuid { + high-bits: u64, + low-bits: u64, + } + + /// Parses a UUID from a string + parse-uuid: func(uuid: string) -> result; + + /// Converts a UUID to a string + uuid-to-string: func(uuid: uuid) -> string; + + record datetime { + /// Seconds since the Unix epoch (UTC). + seconds: s64, + /// Nanoseconds since `seconds`, in `[0, 1_000_000_000)`. + nanoseconds: u32, + } + + record environment-id { + uuid: uuid, + } + + // ============================================================ + // Platform identifiers + // ============================================================ + // + // Platform identifiers used by the schema-native surfaces. Keeping them + // here lets migrated interfaces depend on a single core version. + + /// Represents a Golem component + record component-id { + uuid: uuid, + } + + /// Represents a Golem agent + record agent-id { + /// Identifies the component the agent belongs to + component-id: component-id, + /// String representation of the agent ID (agent type and constructor parameters) + agent-id: string, + } + + /// Represents a Golem account + record account-id { + uuid: uuid, + } + + /// Represents a Golem permission card + record card-id { + uuid: uuid + } + + /// An index into the persistent log storing all performed operations of an agent + type oplog-index = u64; + + /// A promise ID is a value that can be passed to an external Golem API to + /// complete that promise from an arbitrary external source, while Golem + /// agents can await for this completion. + record promise-id { + agent-id: agent-id, + oplog-idx: oplog-index, + } + + // ============================================================ + // Capability resources + // ============================================================ + + /// An unforgeable capability granting the right to consume a named + /// resource. The handle is opaque: a guest can only obtain one from the + /// host (e.g. via `golem:quota`) and can never inspect or fabricate its + /// internal state. When carried in a `schema-value-tree` (see + /// `schema-value-node::quota-token-handle`) the handle is transferred by + /// ownership; the host converts it to/from its internal trusted + /// representation at the boundary. + resource quota-token; + + /// An unforgeable handle to sensitive material held by the runtime. The + /// handle is opaque to guests: components can pass it through schema values + /// and reveal it only through capability-gated host interfaces. + resource secret; + + // ============================================================ + // Schema graph (self-contained type carrier) + // ============================================================ + + /// A self-contained schema graph. Anywhere a schema travels with a value + /// (typed pair, oplog `custom` payload, REST/RPC envelope, public oplog + /// rendering), the payload owns its own `schema-graph` — there is no + /// implicit external registry that consumers must look up. + record schema-graph { + /// All schema-type nodes used in this graph. Reachable from `root` + /// directly (anonymous types) or transitively via `defs`. + type-nodes: list, + /// Named type definitions in this graph. Indices into this list are + /// the targets of `schema-type-node::ref-type`. Ordering is + /// deterministic (sorted by `id`). + defs: list, + /// Index into `type-nodes` of the root schema type. + root: type-node-index, + } + + /// A named type definition inside a `schema-graph`. + /// + /// The def itself does not carry metadata; metadata lives on the + /// referenced `schema-type-node` so there is one source of truth for + /// docs / aliases / examples / deprecation / role on each type. + record schema-type-def { + /// Stable identifier; unique within the enclosing graph. + id: type-id, + /// Optional human-readable qualified name (display only). + name: option, + /// Index into the enclosing graph's `type-nodes` for this def's body. + body: type-node-index, + } + + /// Typed metadata envelope. Holds non-validation, non-rendering-critical + /// information (docs, aliases, examples, deprecation, role). Per-scalar + /// validation constraints live on the relevant scalar's typed substructure, + /// not here. + record metadata-envelope { + doc: option, + aliases: list, + /// Canonical-encoded example values (JSON strings). Empty = no examples. + /// Kept as strings so metadata is self-contained on the type side and + /// does not have to cross-reference an accompanying value tree. + examples: list, + /// Deprecation message; `none` means not deprecated. + deprecated: option, + /// Optional role annotation tagging a type with a consumer-facing intent. + role: option, + } + + /// Open registry; unknown roles fall back to structural handling. + variant role { + /// `list>` whose elements are interchangeable modalities. + multimodal, + /// `variant { inline: text, url: url }` ergonomic unstructured-text wrapper. + unstructured-text, + /// `variant { inline: binary, url: url }` ergonomic unstructured-binary wrapper. + unstructured-binary, + /// Any other producer-defined role, preserved verbatim. + other(string), + } + + // ============================================================ + // Schema type + // ============================================================ + + /// One node in a `schema-graph`. Carries the type body and a per-node + /// metadata envelope (docs, aliases, examples, deprecation, role). + /// Recursive positions reference other nodes (or named definitions) by + /// index inside the body. + record schema-type-node { + body: schema-type-body, + metadata: metadata-envelope, + } + + /// The structural body of a `schema-type-node`. + /// + /// Closed sum types come in two shapes that differ by how the decoder + /// learns which branch a value belongs to: + /// + /// - `variant-type` is a **carried-tag** sum: the value explicitly + /// carries its case (`variant-value.case` index). Zero-inference + /// decoding. Natural mapping for language-level algebraic data types + /// (Rust `enum`, Scala `sealed trait`, WIT `variant`, etc.). + /// + /// - `union-type` is an **inferred-tag** sum: the value does not carry + /// its tag. Each branch declares a `discriminator-rule` (prefix / + /// suffix / contains / regex on string-shaped bodies, or + /// field-equals / field-absent on record-shaped bodies) and the + /// decoder picks the branch whose rule matches the raw value. + /// Natural mapping for inputs where the producer writes an unadorned + /// value (a URL whose scheme picks the handler, a JSON object whose + /// `"kind"` field picks the variant, an MCP content block whose + /// `"type"` field picks the part shape, …). + variant schema-type-body { + // --- Reference to a named definition in the same `schema-graph` --- + ref-type(def-index), + + // --- Primitives --- + bool-type, + s8-type(option), + s16-type(option), + s32-type(option), + s64-type(option), + u8-type(option), + u16-type(option), + u32-type(option), + u64-type(option), + f32-type(option), + f64-type(option), + char-type, + string-type, + + // --- Structural composites --- + record-type(list), + variant-type(list), + enum-type(list), + flags-type(list), + tuple-type(list), + list-type(type-node-index), + fixed-list-type(fixed-list-spec), + map-type(map-spec), + option-type(type-node-index), + result-type(result-spec), + + // --- Rich semantic types --- + text-type(text-restrictions), + binary-type(binary-restrictions), + path-type(path-spec), + url-type(url-restrictions), + datetime-type, + duration-type, + quantity-type(quantity-spec), + + // --- Discriminated union (closed, inferred-tag) --- + union-type(union-spec), + + // --- Capability nodes --- + secret-type(secret-spec), + quota-token-type(quota-token-spec), + + // --- WASI P3 stubs (parseable only; no semantics yet) --- + future-type(option), + stream-type(option), + } + + record named-field-type { + name: string, + body: type-node-index, + metadata: metadata-envelope, + } + + record variant-case-type { + name: string, + payload: option, + metadata: metadata-envelope, + } + + record fixed-list-spec { + element: type-node-index, + length: u32, + } + + /// Map key types are restricted to primitives. Enforced at schema + /// construction time, not by the WIT type itself. + record map-spec { + key: type-node-index, + value: type-node-index, + } + + record result-spec { + ok: option, + err: option, + } + + // --- Numeric restrictions --- + + /// A numeric bound usable across every numeric representation. Float bounds + /// carry canonical IEEE-754 `f64` bits (NaN/inf rejected, -0.0 normalized); + /// comparisons decode the bits to `f64` and compare numerically. + variant numeric-bound { + signed(s64), + unsigned(u64), + float-bits(u64), + } + + /// Inline numeric refinement. `none` on a numeric type means unconstrained + /// (the common case). The empty restriction set is never encoded as `some`: + /// producers normalize it to `none`, decoders normalize a decoded empty to + /// `none`. `unit` is schema/help metadata only. + record numeric-restrictions { + min: option, + max: option, + unit: option, + } + + // --- Text / Binary restrictions --- + + record text-restrictions { + /// Optional set of allowed BCP-47 language codes. `none` = unrestricted. + languages: option>, + min-length: option, + max-length: option, + regex: option, + } + + record binary-restrictions { + /// Optional set of allowed MIME types. `none` = unrestricted. + mime-types: option>, + min-bytes: option, + max-bytes: option, + } + + // --- Path --- + + enum path-direction { + input, + output, + in-out, + } + + enum path-kind { + file, + directory, + any, + } + + record path-spec { + direction: path-direction, + kind: path-kind, + allowed-mime-types: option>, + allowed-extensions: option>, + } + + // --- URL --- + + record url-restrictions { + allowed-schemes: option>, + allowed-hosts: option>, + } + + // --- Quantity --- + + /// Fixed-point decimal value with unit: numeric value = `mantissa * 10^(-scale)`. + /// `unit` is a free-form string at the value level; the schema's + /// `quantity-spec` constrains the accepted set. + record quantity-value { + mantissa: s64, + scale: s32, + unit: string, + } + + record quantity-spec { + /// Canonical base unit (e.g., `"kg"`, `"m"`, `"s"`, `"B"`). + base-unit: string, + /// Suffixes accepted on input and rendered on output (e.g., `["kg","g","mg"]`). + allowed-suffixes: list, + /// Optional inclusive range, expressed in canonical fixed-point form. + min: option, + max: option, + } + + // --- Discriminated union --- + // + // Inferred-tag sum. Each branch declares a rule that the decoder uses to + // identify values belonging to that branch from the raw underlying value. + // + // Rules enforced at schema-construction time: + // - branch `tag`s are unique within the union, + // - string-pattern discriminators (`prefix` / `suffix` / `contains` / + // `regex`) require a string-shaped branch body (`string-type`, + // `text-type`, `url-type`, `path-type`, or a `ref` resolving to one), + // - record-shaped discriminators (`field-equals` / `field-absent`) + // require a record-shaped branch body that declares the referenced + // field (with the matching literal type, if any), + // - the discriminator set must be unambiguous: no value matches more + // than one branch. Overlap is checked structurally where decidable + // (e.g., `prefix("a")` and `prefix("ab")` overlap) and best-effort + // for `regex`. + + record union-spec { + branches: list, + } + + record union-branch { + /// Logical branch name. Carried in `union-value.tag` after the + /// decoder resolves the branch; used by renderers, codegen, docs. + tag: string, + /// Branch body type. Any schema type compatible with the + /// discriminator rule (see schema-construction validation above). + body: type-node-index, + /// Rule the decoder uses to pick this branch from a raw value. + discriminator: discriminator-rule, + metadata: metadata-envelope, + } + + /// How the decoder identifies that a value belongs to a given union branch. + variant discriminator-rule { + /// String value starts with this prefix (e.g. `"ssh://"`). + prefix(string), + /// String value ends with this suffix (e.g. `".tar.gz"`). + suffix(string), + /// String value contains this substring. + contains(string), + /// String value matches this anchored regex. + regex(string), + /// Record-shaped value where the named field is present, and — if + /// `literal` is set — has the given literal string value. The common + /// JSON-discriminated-object case (`{"kind":"circle",…}`) is + /// `field-equals { field-name: "kind", literal: some("circle") }`. + field-equals(field-discriminator), + /// Record-shaped value where the named field is absent. + field-absent(string), + } + + record field-discriminator { + field-name: string, + /// Optional required literal value for the field. + literal: option, + } + + // --- Capability nodes --- + + record secret-spec { + /// Revealed payload type carried by this secret handle. + inner: type-node-index, + /// Optional categorisation (e.g., `"api-key"`, `"oauth-token"`). + category: option, + } + + record quota-token-spec { + /// Resource name this token covers (e.g., declared in the agent manifest). + /// `none` = any resource permitted. + resource-name: option, + } + + // ============================================================ + // Schema value (always paired with a schema-graph) + // ============================================================ + + /// A flat schema-value tree. Always travels paired with a `schema-graph` + /// (see `typed-schema-value`). Indices refer to entries in `value-nodes` + /// within this same tree. + /// + /// The value tree is structurally driven by the schema: record-value + /// payload order matches the schema's field order, variant-value carries a + /// case index, enum-value carries a case index, union-value carries the + /// discriminator's literal tag. The value side does not redundantly carry + /// field names, case names, or named-ref identifiers — those come from the + /// schema. + record schema-value-tree { + value-nodes: list, + root: value-node-index, + } + + variant schema-value-node { + // Primitives + bool-value(bool), + s8-value(s8), + s16-value(s16), + s32-value(s32), + s64-value(s64), + u8-value(u8), + u16-value(u16), + u32-value(u32), + u64-value(u64), + f32-value(f32), + f64-value(f64), + char-value(char), + string-value(string), + + // Structural composites + record-value(list), + variant-value(variant-value-payload), + enum-value(u32), + flags-value(list), + tuple-value(list), + list-value(list), + fixed-list-value(list), + map-value(list), + option-value(option), + result-value(result-value-payload), + + // Rich semantic + text-value(text-value-payload), + binary-value(binary-value-payload), + path-value(string), + url-value(string), + datetime-value(datetime), + duration-value(duration-value-payload), + quantity-value-node(quantity-value), + + // Discriminated union: tag is matched against schema branches. + union-value(union-value-payload), + + // Capability nodes + secret-value(own), + quota-token-handle(own), + + // WASI P3 stubs (parseable only in the schema; no constructible values). + } + + record variant-value-payload { + case: u32, + payload: option, + } + + record map-entry { + key: value-node-index, + value: value-node-index, + } + + /// Result payload: exactly one of `ok-value` / `err-value` is set. Each + /// inner option allows `result<_, _>` cases whose ok/err type is unit (no + /// payload). + variant result-value-payload { + ok-value(option), + err-value(option), + } + + record text-value-payload { + text: string, + /// BCP-47 language tag, when known. + language: option, + } + + record binary-value-payload { + bytes: list, + mime-type: option, + } + + /// Signed duration as total nanoseconds. + record duration-value-payload { + nanoseconds: s64, + } + + record union-value-payload { + /// Tag of the branch the decoder resolved, matching one of the + /// `union-spec::branches[*].tag` values. Carried so receivers do not + /// have to re-run discriminator rules to know which branch was + /// matched; encoders must ensure it agrees with the body. + tag: string, + /// Underlying value. Its shape matches the resolved branch's body + /// type and (by construction) satisfies the branch's discriminator + /// rule. + body: value-node-index, + } + + // ============================================================ + // Wire carriers + // ============================================================ + + /// A typed value: a self-contained schema graph paired with a value tree + /// built against that schema. + record typed-schema-value { + graph: schema-graph, + value: schema-value-tree, + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-durability/golem-durability.wit b/sdks/kotlin/wit-native/deps/golem-durability/golem-durability.wit new file mode 100644 index 0000000000..68a6951c2d --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-durability/golem-durability.wit @@ -0,0 +1,92 @@ +package golem:durability@1.5.0; + +interface durability { + use golem:api/host@1.5.0.{persistence-level}; + use golem:api/oplog@1.5.0.{oplog-index, wrapped-function-type}; + use wasi:clocks/wall-clock@0.2.3.{datetime}; + use wasi:io/poll@0.2.3.{pollable}; + use golem:core/types@2.0.0.{typed-schema-value}; + + type durable-function-type = wrapped-function-type; + + /// Represents the current durable execution state + record durable-execution-state { + /// If true, the executor is in live mode, side-effects should be performed and persisted. + /// If false, the executor is in replay mode, side-effects should be replayed from the persisted data. + is-live: bool, + /// The currently active persistence level + persistence-level: persistence-level, + } + + /// Represents the oplog entry version; this is for backward compatibility and most use cases should always use + /// (and expect) the latest version. + enum oplog-entry-version { + v1, + v2 + } + + /// Represents a persisted durable function invocation. The `response` field + /// contains a value and its schema graph together, making the user-defined payload observable by external tools. + record persisted-durable-function-invocation { + /// The timestamp of the invocation. + timestamp: datetime, + /// The invoked function's unique name + function-name: string, + /// Arbitrary structured value and schema graph describing the invocation's result + response: typed-schema-value, + /// Type of the durable function invocation + function-type: durable-function-type, + /// Oplog entry version + entry-version: oplog-entry-version + } + + /// Observes a function call (produces logs and metrics) + observe-function-call: func(iface: string, function: string); + + /// Marks the beginning of a durable function. + /// + /// There must be a corresponding call to `end-durable-function` after the function has + /// performed its work (it can be ended in a different context, for example after an async + /// pollable operation has been completed) + begin-durable-function: func(function-type: durable-function-type) -> oplog-index; + + /// Marks the end of a durable function + /// + /// This is a pair of `begin-durable-function` and should be called after the durable function + /// has performed and persisted or replayed its work. The `begin-index` should be the index + /// returned by `begin-durable-function`. + /// + /// Normally commit behavior is decided by the executor based on the `function-type`. However, in special + /// cases the `forced-commit` parameter can be used to force commit the oplog in an efficient way. + end-durable-function: func(function-type: durable-function-type, begin-index: oplog-index, forced-commit: bool); + + /// Gets the current durable execution state + current-durable-execution-state: func() -> durable-execution-state; + + /// Writes a record to the agent's oplog representing a durable function invocation + /// + /// The request and response are defined as schema-carrying values, which makes it + /// self-describing for observers of oplogs. + persist-durable-function-invocation: func( + function-name: string, + request: typed-schema-value, + response: typed-schema-value, + function-type: durable-function-type, + ); + + /// Reads the next persisted durable function invocation from the oplog during replay + read-persisted-durable-function-invocation: func() -> persisted-durable-function-invocation; + + /// A pollable which can be later attached to a real `pollable`, but it can be subscribed to even + /// before that. + resource lazy-initialized-pollable { + /// Creates a `pollable` that is never ready until it gets attached to a real `pollable` implementation + /// using `set-lazy-initialized-pollable`. + constructor(); + + /// Sets the underlying `pollable` for a pollable created with `create-lazy-initialized-pollable`. + set: func(pollable: pollable); + + subscribe: func() -> pollable; + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-quota/types.wit b/sdks/kotlin/wit-native/deps/golem-quota/types.wit new file mode 100644 index 0000000000..a6acaec556 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-quota/types.wit @@ -0,0 +1,76 @@ +package golem:quota@1.5.0; + +/// Host interface for the Golem quota system. +/// +/// Agents use quota-tokens to declare intent to consume a named resource and to +/// reserve / commit actual usage. +/// +/// The `quota-token` capability itself is defined in `golem:core/types` so that +/// it can travel inside a `schema-value-tree` as an opaque, unforgeable handle. +/// This interface only exposes the operations that act on such a handle. +interface types { + use golem:core/types@2.0.0.{quota-token}; + + /// Error returned when a reservation cannot be satisfied because the + /// resource's enforcement policy is `reject`. + /// + /// The inner value is an optional estimate in nanoseconds of how long + /// the caller would need to wait for capacity (only available for + /// rate-limited resources). + record failed-reservation { + estimated-wait-nanos: option, + } + + /// A short-lived capability that represents a pending or committed + /// resource consumption. Drop without committing is equivalent to + /// committing zero usage. + resource reservation { + /// Commit actual usage, consuming the reservation. + /// + /// If `used` < reserved — unused capacity is returned to the pool. + /// If `used` > reserved — the excess is deducted from the token's + /// remaining allocation as "debt". + commit: static func(this: reservation, used: u64); + } + + /// Request a quota capability for the given resource. + /// - `resource-name` : the resource name (as declared in the manifest). + /// - `expected-use` : expected units per reservation; used to derive + /// credit rate and max-credit for fair scheduling. + new-token: func( + resource-name: string, + expected-use: u64, + ) -> quota-token; + + /// Reserve `amount` units from the token's local allocation. + /// + /// Blocks internally until capacity is available or the resource's + /// enforcement action fires. Returns a `reservation` handle that + /// must be committed (or dropped) to release unused capacity. + reserve: func( + token: borrow, + amount: u64, + ) -> result; + + /// Split off a child token with `child-expected-use` units from `token`. + /// + /// The parent's `expected-use` is reduced by `child-expected-use`. + /// Credits are divided proportionally between parent and child. + /// + /// Traps if `child-expected-use` exceeds the parent's current `expected-use`. + split: func( + token: borrow, + child-expected-use: u64, + ) -> quota-token; + + /// Merge `other` back into `token`. + /// + /// The two tokens must refer to the same resource (same resource-name + /// and environment). `other` is consumed. + /// + /// Traps if the tokens refer to different resources. + merge: func( + token: borrow, + other: quota-token, + ); +} diff --git a/sdks/kotlin/wit-native/deps/golem-rdbms/ignite.wit b/sdks/kotlin/wit-native/deps/golem-rdbms/ignite.wit new file mode 100644 index 0000000000..505988023d --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-rdbms/ignite.wit @@ -0,0 +1,81 @@ +// Copyright 2024-2025 Golem Cloud +// +// Licensed under the Golem Source License v1.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package golem:rdbms@1.5.0; + +interface ignite2 { + // ── Error ────────────────────────────────────────────────────────────────── + variant error { + connection-failure(string), + query-parameter-failure(string), + query-execution-failure(string), + query-response-failure(string), + other(string), + } + + // ── Value types (maps 1-to-1 onto ignite_client::IgniteValue) ───────────── + variant db-value { + db-null, + db-boolean(bool), + db-byte(s8), + db-short(s16), + db-int(s32), + db-long(s64), + db-float(f32), + db-double(f64), + /// 16-bit Unicode code unit (Java char). + db-char(u16), + db-string(string), + db-uuid(tuple), + /// Milliseconds since Unix epoch (UTC). + db-date(s64), + /// (milliseconds since epoch, sub-millisecond nanoseconds 0..999_999). + db-timestamp(tuple), + /// Nanoseconds since midnight. + db-time(s64), + db-decimal(string), + db-byte-array(list), + } + + // ── Metadata ─────────────────────────────────────────────────────────────── + record db-column { ordinal: u64, name: string } + record db-row { values: list } + record db-result { columns: list, rows: list } + + // ── Resources ────────────────────────────────────────────────────────────── + resource db-result-stream { + get-columns: func() -> result, error>; + get-next: func() -> result>, error>; + } + + resource db-transaction { + query: func(statement: string, params: list) -> result; + query-stream: func(statement: string, params: list) -> result; + execute: func(statement: string, params: list) -> result; + commit: func() -> result<_, error>; + rollback: func() -> result<_, error>; + } + + resource db-connection { + /// Open a connection to an Apache Ignite 2.x node. + /// + /// Address format: `ignite://[user:pass@]host:port[?pool_size=N&tls=true]` + /// Default port: 10800. + open: static func(address: string) -> result; + query: func(statement: string, params: list) -> result; + query-stream: func(statement: string, params: list) -> result; + execute: func(statement: string, params: list) -> result; + begin-transaction: func() -> result; + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-rdbms/mysql.wit b/sdks/kotlin/wit-native/deps/golem-rdbms/mysql.wit new file mode 100644 index 0000000000..49247006d9 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-rdbms/mysql.wit @@ -0,0 +1,138 @@ +package golem:rdbms@1.5.0; + +interface mysql { + use types.{date, time, timestamp}; + + variant error { + connection-failure(string), + query-parameter-failure(string), + query-execution-failure(string), + query-response-failure(string), + other(string) + } + + variant db-column-type { + boolean, + tinyint, + smallint, + mediumint, + int, + bigint, + tinyint-unsigned, + smallint-unsigned, + mediumint-unsigned, + int-unsigned, + bigint-unsigned, + float, + double, + decimal, + date, + datetime, + timestamp, + time, + year, + fixchar, + varchar, + tinytext, + text, + mediumtext, + longtext, + binary, + varbinary, + tinyblob, + blob, + mediumblob, + longblob, + enumeration, + set, + bit, + json + } + + record db-column { + ordinal: u64, + name: string, + db-type: db-column-type, + db-type-name: string + } + + /// Value descriptor for a single database value + variant db-value { + boolean(bool), + tinyint(s8), + smallint(s16), + mediumint(s32), + int(s32), + bigint(s64), + tinyint-unsigned(u8), + smallint-unsigned(u16), + mediumint-unsigned(u32), + int-unsigned(u32), + bigint-unsigned(u64), + float(f32), + double(f64), + decimal(string), + date(date), + datetime(timestamp), + timestamp(timestamp), + time(time), + year(u16), + fixchar(string), + varchar(string), + tinytext(string), + text(string), + mediumtext(string), + longtext(string), + binary(list), + varbinary(list), + tinyblob(list), + blob(list), + mediumblob(list), + longblob(list), + enumeration(string), + set(string), + bit(list), + json(string), + null + } + + /// A single row of values + record db-row { + values: list + } + + record db-result { + columns: list, + rows: list + } + + /// A potentially very large and lazy stream of rows: + resource db-result-stream { + get-columns: func() -> list; + get-next: func() -> option>; + } + + resource db-connection { + open: static func(address: string) -> result; + + query: func(statement: string, params: list) -> result; + + query-stream: func(statement: string, params: list) -> result; + + execute: func(statement: string, params: list) -> result; + + begin-transaction: func() -> result; + } + + resource db-transaction { + query: func(statement: string, params: list) -> result; + + query-stream: func(statement: string, params: list) -> result; + + execute: func(statement: string, params: list) -> result; + + commit: func() -> result<_, error>; + + rollback: func() -> result<_, error>; + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-rdbms/postgres.wit b/sdks/kotlin/wit-native/deps/golem-rdbms/postgres.wit new file mode 100644 index 0000000000..b68b7ab148 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-rdbms/postgres.wit @@ -0,0 +1,293 @@ +package golem:rdbms@1.5.0; + +interface postgres { + use types.{date, time, timetz, timestamp, timestamptz, uuid, ip-address, mac-address}; + + variant error { + connection-failure(string), + query-parameter-failure(string), + query-execution-failure(string), + query-response-failure(string), + other(string) + } + + record interval { + months: s32, + days: s32, + microseconds: s64 + } + + variant int4bound { + included(s32), + excluded(s32), + unbounded + } + + variant int8bound { + included(s64), + excluded(s64), + unbounded + } + + variant numbound { + included(string), + excluded(string), + unbounded + } + + variant tsbound { + included(timestamp), + excluded(timestamp), + unbounded + } + + variant tstzbound { + included(timestamptz), + excluded(timestamptz), + unbounded + } + + variant datebound { + included(date), + excluded(date), + unbounded + } + + record int4range { + start: int4bound, + end: int4bound + } + + record int8range { + start: int8bound, + end: int8bound + } + + record numrange { + start: numbound, + end: numbound + } + + record tsrange { + start: tsbound, + end: tsbound + } + + record tstzrange { + start: tstzbound, + end: tstzbound + } + + record daterange { + start: datebound, + end: datebound + } + + record enumeration-type { + name: string + } + + record enumeration { + name: string, + value: string + } + + record composite-type { + name: string, + attributes: list> + } + + record composite { + name: string, + values: list + } + + record domain-type { + name: string, + base-type: lazy-db-column-type + } + + record domain { + name: string, + value: lazy-db-value + } + + record range-type { + name: string, + base-type: lazy-db-column-type + } + + variant value-bound { + included(lazy-db-value), + excluded(lazy-db-value), + unbounded + } + + record values-range { + start: value-bound, + end: value-bound + } + + record range { + name: string, + value: values-range + } + + record sparse-vec { + dim: s32, + indices: list, + values: list + } + + variant db-column-type { + character, + int2, + int4, + int8, + float4, + float8, + numeric, + boolean, + text, + varchar, + bpchar, + timestamp, + timestamptz, + date, + time, + timetz, + interval, + bytea, + uuid, + xml, + json, + jsonb, + jsonpath, + inet, + cidr, + macaddr, + bit, + varbit, + int4range, + int8range, + numrange, + tsrange, + tstzrange, + daterange, + money, + oid, + enumeration(enumeration-type), + composite(composite-type), + domain(domain-type), + array(lazy-db-column-type), + range(range-type), + vector, + halfvec, + sparsevec + } + + variant db-value { + character(s8), + int2(s16), + int4(s32), + int8(s64), + float4(f32), + float8(f64), + numeric(string), + boolean(bool), + text(string), + varchar(string), + bpchar(string), + timestamp(timestamp), + timestamptz(timestamptz), + date(date), + time(time), + timetz(timetz), + interval(interval), + bytea(list), + json(string), + jsonb(string), + jsonpath(string), + xml(string), + uuid(uuid), + inet(ip-address), + cidr(ip-address), + macaddr(mac-address), + bit(list), + varbit(list), + int4range(int4range), + int8range(int8range), + numrange(numrange), + tsrange(tsrange), + tstzrange(tstzrange), + daterange(daterange), + money(s64), + oid(u32), + enumeration(enumeration), + composite(composite), + domain(domain), + array(list), + range(range), + null, + vector(list), + halfvec(list), + sparsevec(sparse-vec) + } + + resource lazy-db-value { + constructor(value: db-value); + get: func() -> db-value; + } + + resource lazy-db-column-type { + constructor(value: db-column-type); + get: func() -> db-column-type; + } + + record db-column { + ordinal: u64, + name: string, + db-type: db-column-type, + db-type-name: string + } + + /// A single row of values + record db-row { + values: list + } + + record db-result { + columns: list, + rows: list + } + + /// A potentially very large and lazy stream of rows: + resource db-result-stream { + get-columns: func() -> list; + get-next: func() -> option>; + } + + resource db-connection { + open: static func(address: string) -> result; + + query: func(statement: string, params: list) -> result; + + query-stream: func(statement: string, params: list) -> result; + + execute: func(statement: string, params: list) -> result; + + begin-transaction: func() -> result; + } + + resource db-transaction { + query: func(statement: string, params: list) -> result; + + query-stream: func(statement: string, params: list) -> result; + + execute: func(statement: string, params: list) -> result; + + commit: func() -> result<_, error>; + + rollback: func() -> result<_, error>; + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-rdbms/types.wit b/sdks/kotlin/wit-native/deps/golem-rdbms/types.wit new file mode 100644 index 0000000000..36af8b1fde --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-rdbms/types.wit @@ -0,0 +1,47 @@ +package golem:rdbms@1.5.0; + +interface types { + + record uuid { + high-bits: u64, + low-bits: u64 + } + + variant ip-address { + ipv4(tuple), + ipv6(tuple), + } + + record mac-address { + octets: tuple + } + + record date { + year: s32, + month: u8, + day: u8 + } + + record time { + hour: u8, + minute: u8, + second: u8, + nanosecond: u32 + } + + record timestamp { + date: date, + time: time + } + + record timestamptz { + timestamp: timestamp, + offset: s32 + } + + record timetz { + time: time, + offset: s32 + } + +} diff --git a/sdks/kotlin/wit-native/deps/golem-rdbms/world.wit b/sdks/kotlin/wit-native/deps/golem-rdbms/world.wit new file mode 100644 index 0000000000..f8702cbf16 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-rdbms/world.wit @@ -0,0 +1,7 @@ +package golem:rdbms@1.5.0; + +world imports { + import postgres; + import mysql; + import ignite2; +} diff --git a/sdks/kotlin/wit-native/deps/golem-secrets/reveal.wit b/sdks/kotlin/wit-native/deps/golem-secrets/reveal.wit new file mode 100644 index 0000000000..76a81735f6 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-secrets/reveal.wit @@ -0,0 +1,31 @@ +package golem:secrets@0.1.0; + +/// Capability-gated escape hatch: convert a secret resource back +/// to plaintext. The capability is the import — components that +/// do not import this interface cannot reveal secrets. v1 grants +/// reveal at the import-or-not level only; v2 introduces +/// per-(agent, tool, tool-middleware) manifest binding axes that narrow +/// reveal even when imported (§5.6). +/// +/// Every successful reveal is recorded in the calling agent's +/// oplog as `(calling-agent, secret-id, timestamp)`. The +/// plaintext bytes are not part of the audit record. +/// +/// Tools that consume secrets at the wire boundary (HTTP +/// authorization headers, signing operations, encryption) SHOULD +/// prefer host-mediated substitution over reveal — host +/// capabilities accepting `borrow` directly let the +/// runtime substitute plaintext at the syscall boundary, never +/// crossing into guest linear memory at all. Reveal is the +/// fallback for genuinely custom protocols the host doesn't +/// natively support; its use is loud by design. +interface reveal { + use types.{secret-error}; + use golem:core/types@2.0.0.{secret, schema-graph, schema-value-tree}; + + /// Unpack a secret resource to its inner typed value. `expected` is the + /// guest's inner-type graph; the host validates it against the secret's + /// pinned inner type and returns the stored value as a schema-value-tree. + reveal: func(s: borrow, expected: schema-graph) + -> result; +} diff --git a/sdks/kotlin/wit-native/deps/golem-secrets/types.wit b/sdks/kotlin/wit-native/deps/golem-secrets/types.wit new file mode 100644 index 0000000000..5d2e93e759 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-secrets/types.wit @@ -0,0 +1,73 @@ +package golem:secrets@0.1.0; + +/// The secret resource, plus the metadata records other interfaces +/// reference. Components that only need to *receive and pass* +/// secrets — most tools, audit middleware, MCP-import bridges — +/// import only this interface. +interface types { + use wasi:clocks/wall-clock@0.2.3.{datetime}; + use golem:core/types@2.0.0.{secret}; + + /// The opaque handle to a sensitive value is defined in + /// `golem:core/types` so it can travel inside a `schema-value-tree` as an + /// unforgeable owned handle. This interface exposes only safe metadata + /// operations over that handle. + /// + /// Resource handles are scoped to a component instance's + /// resource table. When a secret is passed across a component + /// boundary (e.g., agent → tool via `tool-rpc.invoke`'s + /// `value-tree`), the runtime issues a fresh handle in the + /// receiving instance's table that points at the same + /// host-side state. The wire form carries only the + /// `secret-id`, never plaintext. + + /// Stable, opaque identifier scoped to the deployment. + record secret-id { + bytes: list, + } + + record secret-metadata { + /// Config-key path the secret resolved from, when applicable. + /// `none` for secrets returned in tool-result position. + config-key: option>, + /// Pinned version captured at resolve-time. `none` for + /// dynamic-origin secrets (e.g. returned from a tool that + /// didn't itself have a versioned source). + version: option, + /// Time of resolution. + resolved-at: datetime, + /// The semantic category declared on the secret's schema type, when + /// known. Mirrors `secret-spec.category` in `golem:core/types`. + category: option, + } + + /// A secret-store-assigned, monotonic version identifier. The + /// secret-store backend defines the ordering and the format; + /// the type-tree treats it as opaque. + record secret-version { + bytes: list, + } + + /// Stable opaque identifier. Comparing equal across two `secret` handles + /// means "same secret material, same version" — useful for caching and audit + /// correlation. Safe to log, oplog, and emit in traces. + id: func(s: borrow) -> secret-id; + + /// Immutable metadata captured at resolve-time. Includes the config-key + /// path, the resolved version, and the resolution timestamp. No plaintext. + metadata: func(s: borrow) -> secret-metadata; + + /// Errors common to operations on secrets. + variant secret-error { + /// The secret was bound but its current resolution failed + /// (e.g., the secret-store entry was deleted between binding + /// and reveal, or the network to the store is partitioned). + unavailable(string), + /// The secret was version-pinned and the pinned version no + /// longer exists (administratively destroyed). + version-not-found(secret-version), + /// Internal runtime error (carries an opaque message; never + /// includes plaintext). + internal(string), + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-tool/common.wit b/sdks/kotlin/wit-native/deps/golem-tool/common.wit new file mode 100644 index 0000000000..f28d894d44 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-tool/common.wit @@ -0,0 +1,380 @@ +package golem:tool@0.1.0; + +/// Data model for Golem tool metadata. +/// +/// A "tool" is a callable unit declared from a single piece of +/// metadata. From that metadata the SDKs project two delivered +/// surfaces — typed function signatures in TypeScript / Rust / +/// Python / Scala / MoonBit for programmatic invocation, and +/// help-text rendering for any node in the command tree — and the +/// same metadata is sufficient to drive a future full-CLI +/// projection (parseable args, shell completions, exit codes, +/// terminal-runnable as a Unix utility) without further authoring. +/// The CLI projection is a future possibility enabled by the model; +/// it is not part of the current specification's deliverable. +/// +/// The model is CLI-native: commands, subcommands, options, flags, and +/// positionals are primary — not a generic data schema with CLI mappings +/// layered on top. Constraint-by-construction is preferred over runtime +/// validation: variadic-only-at-tail is structural; mutual exclusion is +/// expressed by subcommand choice or by an explicit `mutex-groups` +/// constraint; co-occurrence is structural via sub-records or via +/// `all-or-none`. +/// +/// Types and values are not modeled by this package: every input/output +/// type and every metadata-time or runtime value is expressed with the +/// shared `golem:core/types@2.0.0` schema model. A tool owns a single +/// `schema-graph` type-node pool (the `tool.schema` field); command bodies +/// reference entries in it by `type-node-index`, exactly the same way the +/// agent model (`golem:agent/common`) references its per-agent +/// `schema-graph`. Metadata-time values (option/positional defaults, the +/// literal side of `value-is` constraint refs) are `schema-value-tree`s +/// interpreted against the referenced type node in `tool.schema`; runtime +/// invocation inputs, results, and custom-error payloads are self-contained +/// `typed-schema-value`s. +/// +/// The remaining tool-specific recursion site is the command tree: a +/// flattened command hierarchy with the root at index 0 and children +/// referenced by `command-index`. +/// +/// Construction invariants (validated by the producer; the WIT shape +/// alone does not enforce them): +/// +/// • All identifier-like strings (command names, option/flag long +/// names, positional names, error names, formatter names) match +/// `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`. +/// • Subcommand names + aliases are pairwise unique among siblings. +/// • Within a `command-body`: option long-names + flag long-names + +/// positional names + aliases + short forms are pairwise unique, +/// AND unique against globals inherited from any ancestor command. +/// • Constraint `ref` names resolve against body-declared options / +/// flags / positionals AND globals inherited from any ancestor. +/// • For `ref::value-is(name, lit)`, the literal must be a valid value +/// for the declared type node of `name` in `tool.schema`. +/// • `default-formatter` resolves to a name in `formatters`. +/// • `tail-positional`: if `verbatim` is true, `separator` must be +/// `some`; `separator` with `min = 0` is legal (the separator alone, +/// no items, is valid). +/// • A `positional` / `option` / `result` / `error` `type-node-index` +/// resolves to a node in `tool.schema`. +/// • A `repeatable-list` option's `default`, if present, is a `list` +/// whose elements are values of the `repeatable-list-shape.item-type` +/// node. A `repeatable-map` option's `default`, if present, is a `map` +/// value of the `repeatable-map-shape.map-type` node. +/// • A `value-is` ref naming a `repeatable-list` option, tail positional, +/// or otherwise list-shaped target means "any occurrence / element +/// equals this literal"; the literal is a value of the element type. For +/// a `repeatable-map` option the literal is a value of the map's value +/// type (any entry's value equals this literal). +/// • The tool's identity is its root command name +/// (`commands.nodes[0].name`); `get-tool(name)` and +/// `guest.invoke(tool-name, …)` match against it. `commands.nodes` +/// is always non-empty. +/// +/// Capability scoping (WASI preopens, env masking, outbound-socket +/// filters, subprocess-exec capability, and `golem:agent/host`'s +/// `get-config-value` resolution) is performed by the host by inspecting +/// the component's WIT imports — what a component *can* do is already +/// declared structurally by which interfaces it imports — not by reading +/// a declarative metadata record. +interface common { + use golem:core/types@2.0.0.{schema-graph, type-node-index, schema-value-tree, + typed-schema-value}; + use wasi:io/streams@0.2.3.{output-stream}; + + // Top level + record tool { + version: string, + commands: command-tree, + /// Self-contained type-node pool holding every type referenced from + /// this tool's commands. Command bodies reference entries by + /// `type-node-index`. `schema.root` is a structurally-required + /// placeholder and is not the semantic root of the tool (mirrors + /// `golem:agent/common`'s `agent-type.schema`). + schema: schema-graph, + } + + // Command tree + type command-index = s32; + + record command-tree { + /// Always non-empty; the root command is at index 0. + nodes: list, + } + + /// A command may dispatch to subcommands, run its own body, or both. + /// Globals declared here apply to this command's own body and to + /// every descendant subcommand body (recursive globals — "this level + /// and downward"). + record command-node { + name: string, + aliases: list, + doc: doc, + globals: globals, + subcommands: list, + body: option, + } + + record globals { + options: list, + %flags: list, + } + + record command-body { + positionals: positionals, + options: list, + %flags: list, + constraints: list, + stdin: option, + stdout: option, + %result: option, + errors: list, + annotations: option, + } + + /// Behavioral hints surfaced to MCP and other LLM-facing + /// surfaces. All four follow the MCP convention. When absent + /// the surface treats them as untrusted defaults + /// (`destructive: true`, `open-world: true`, `read-only: false`, + /// `idempotent: false`), per MCP guidance. + record command-annotations { + /// Tool performs no destructive updates; safe to call freely. + read-only: bool, + /// Tool may delete or overwrite (default true per MCP semantics). + destructive: bool, + /// Repeated calls with the same input have the same effect. + idempotent: bool, + /// Tool reaches outside the host's controlled environment + /// (network calls, external APIs, the open world). + open-world: bool, + } + + // Positionals (variadic only at the tail, structurally) + record positionals { + fixed: list, + tail: option, + } + + record positional { + name: string, + doc: doc, + value-name: option, + /// Index into `tool.schema`. + %type: type-node-index, + /// Default value, interpreted against `%type` in `tool.schema`. + default: option, + required: bool, + /// If true, the positional's value may be read from standard input + /// (e.g. grep's trailing `files` accepting piped input). + accepts-stdio: bool, + } + + record tail-positional { + name: string, + doc: doc, + value-name: option, + /// Index into `tool.schema`. + item-type: type-node-index, + min: u32, + max: option, + /// Token required before tail items (e.g. "--" for `git log -- `). + separator: option, + /// If true, tokens after `separator` are not flag-parsed (for + /// `kubectl exec -- CMD ARGS...`). + verbatim: bool, + /// If true, the tail items may be read from standard input + /// (e.g. grep's trailing `files` accepting piped input). + accepts-stdio: bool, + } + + // Options and flags + record option-spec { + long: string, + short: option, + aliases: list, + doc: doc, + value-name: option, + shape: option-shape, + /// Default value, interpreted against the option's type node in + /// `tool.schema`. + default: option, + required: bool, + env-var: option, + } + + variant option-shape { + /// Required value: --opt VALUE or --opt=VALUE. Index into `tool.schema`. + scalar(type-node-index), + /// Bare presence collapses to `default`; with value parses normally + /// (--decorate, --signed[=mode], --force-with-lease[=ref]). Index into + /// `tool.schema`. + optional-scalar(type-node-index), + /// Repeatable scalar option (`-e a -e b`); the collected value is a + /// `list` of the element type. + repeatable-list(repeatable-list-shape), + /// Repeatable key-value option (`-c a=1 -c b=2`); the collected value is a + /// `golem:core` `map` node, never a `list`. + repeatable-map(repeatable-map-shape), + } + + record repeatable-list-shape { + repetition: repetition, + /// Index into `tool.schema`; the element type of the collected `list`. + item-type: type-node-index, + } + + record repeatable-map-shape { + repetition: repetition, + /// Index into `tool.schema`; a `golem:core` `map` (key + value) node. The + /// collected value is this map. + map-type: type-node-index, + /// What happens when the same key is supplied more than once. + duplicate-key-policy: duplicate-key-policy, + } + + /// Resolution policy for a repeated key in a `repeatable-map` option. + enum duplicate-key-policy { + /// A repeated key is a usage error. + reject, + /// A repeated key takes the last supplied value. + last-wins, + } + + variant repetition { + /// --inc a --inc b + repeated, + /// --inc=a,b + delimited(char), + /// Both surface forms accepted. + either(char), + } + + record flag-spec { + long: string, + short: option, + aliases: list, + doc: doc, + shape: flag-shape, + env-var: option, + } + + variant flag-shape { + bool-flag(bool-flag-shape), + /// Counted flag (-vvv); optional max count. + count-flag(option), + } + + record bool-flag-shape { + default: bool, + /// If true, --no- is auto-synthesized. + negatable: bool, + } + + // Constraints + variant ref { + present(string), + value-is(value-is-ref), + } + + record value-is-ref { + name: string, + /// Literal value, interpreted against the declared type node of `name` + /// in `tool.schema`. + value: schema-value-tree, + } + + variant constraint { + requires-all(list), + all-or-none(list), + requires-any(list), + mutex-groups(list), + implies(implies-c), + forbids(forbids-c), + } + + record ref-group { + refs: list, + } + + record implies-c { + lhs-quant: quantifier, + lhs: list, + rhs-quant: quantifier, + rhs: list, + } + + record forbids-c { + lhs-quant: quantifier, + lhs: list, + rhs: list, + } + + enum quantifier { all, any } + + // Streams, structured results, errors + record stream-spec { + doc: doc, + mime: list, + required: bool, + } + + record result-spec { + /// Index into `tool.schema`. + %type: type-node-index, + doc: doc, + formatters: list, + default-formatter: string, + } + + record formatter { + name: string, + doc: doc, + } + + record error-case { + name: string, + doc: doc, + kind: error-kind, + exit-code: u8, + /// Index into `tool.schema`. + payload: option, + } + + enum error-kind { usage-error, runtime-error } + + // Documentation, examples + record doc { + summary: string, + description: string, + examples: list, + } + + record example { + title: string, + body: string, + } + + // Invocation contract — shared between guest and host + variant tool-error { + invalid-tool-name(string), + invalid-command-path(list), + invalid-input(string), + constraint-violation(string), + /// Returned `invocation-result` does not match the body's + /// declared `result-spec` (e.g., the returned value's root type + /// does not match the body's declared result schema; see §6.1 + /// transparency invariant). + invalid-result(string), + /// Tool-defined failure. Mirrors `golem:agent/common`'s + /// `agent-error::custom-error`: the payload is a self-contained + /// `typed-schema-value` carrying the error value. Producers SHOULD + /// shape it so its root type matches one of the body's declared + /// `error-case` payload types. + custom-error(typed-schema-value), + } + + record invocation-result { + %result: option, + stdout: option, + } +} diff --git a/sdks/kotlin/wit-native/deps/golem-tool/guest.wit b/sdks/kotlin/wit-native/deps/golem-tool/guest.wit new file mode 100644 index 0000000000..05f2d53114 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-tool/guest.wit @@ -0,0 +1,59 @@ +package golem:tool@0.1.0; + +/// Interface exported by a component that provides tools. The component +/// declares which tools it exposes, supplies their metadata, and accepts +/// invocations against a chosen leaf command of any tool. +/// +/// Tools are stateless from the host's perspective: each invocation is +/// independent. State accumulated by the underlying agent (file-system +/// writes, config-store updates, etc.) persists per the agent's normal +/// rules and is independent of the tool calling convention. +interface guest { + use common.{tool, tool-error, invocation-result}; + use golem:core/types@2.0.0.{typed-schema-value}; + use golem:agent/common@2.0.0.{principal}; + use wasi:io/streams@0.2.3.{input-stream}; + + /// Enumerate the tools this component exposes. The returned metadata + /// is complete (full command tree and schema graph). + discover-tools: func() -> result, tool-error>; + + /// Look up a single tool by name. + get-tool: func(name: string) -> result; + + /// Invoke a command of a tool. + /// + /// `command-path` selects the command body to execute. An empty list + /// targets the root command's body; non-empty lists walk the + /// `subcommands` field, each segment matching a `command-node.name` + /// or alias. + /// + /// `input` is a self-contained `typed-schema-value` whose root must + /// structurally match the selected body's input schema — a record with + /// one field per positional, option, flag, and inherited global declared + /// on or above the body, each field typed by the matching type node in + /// the body's schema. + /// + /// `stdin` is supplied when the selected body declared a stdin + /// `stream-spec`. Stream ownership: the `stdin` resource handle is moved + /// into the callee for the duration of the call. + /// + /// `principal` carries the caller's authenticated identity for + /// authorization and audit, identical in semantics to the parameter + /// of the same name in `golem:agent/guest.invoke`. + invoke: func( + tool-name: string, + command-path: list, + input: typed-schema-value, + stdin: option, + principal: principal, + ) -> result; +} + +world tool-guest { + import golem:api/host@1.5.0; + import golem:agent/host@2.0.0; + import host; + import common; + export guest; +} diff --git a/sdks/kotlin/wit-native/deps/golem-tool/host.wit b/sdks/kotlin/wit-native/deps/golem-tool/host.wit new file mode 100644 index 0000000000..4f9da477bb --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-tool/host.wit @@ -0,0 +1,88 @@ +package golem:tool@0.1.0; + +/// Interface the runtime exposes to agents and tools for discovering and +/// invoking ambient tools — tools registered by other components in the +/// same Golem environment. +/// +/// Mirrors the structure of `golem:agent/host`, but keyed on tool name +/// (rather than agent-id), and without the agent-instance constructor +/// step (tools are stateless invocables). +interface host { + use common.{tool, tool-error, invocation-result}; + use golem:core/types@2.0.0.{typed-schema-value, component-id}; + use wasi:io/streams@0.2.3.{input-stream}; + use wasi:io/poll@0.2.3.{pollable}; + + /// A tool registered in the environment, addressable by name from + /// any agent or other tool. `definition` carries the full metadata; + /// `implemented-by` identifies the component that registers the + /// tool with the runtime — a Golem component exporting + /// `golem:tool/guest` for native tools, the runtime-internal + /// MCP-import bridge component for tools projected from + /// `mcp.imports` (§5.7.2), or the runtime itself (a synthesized + /// component-id) for host-implemented privileged tools (§4.6). + record registered-tool { + definition: tool, + implemented-by: component-id, + } + + /// Returns every tool **the calling agent has access to** in + /// the current environment, per the manifest's per-env and + /// per-agent binding rules (§5.6 / §6.4.3). The set returned + /// is exactly the set the calling agent could `tool-rpc.invoke` + /// against; tools the calling agent has no binding for are + /// excluded. Mirrors the *function shape* of + /// `golem:agent/host`'s `get-all-agent-types`, with the + /// addition of per-caller access filtering. Order is + /// unspecified; callers that + /// want a stable ordering should sort by + /// `definition.commands.nodes[0].name`. + get-all-tools: func() -> list; + + /// Returns the registered tool with the given name iff the + /// calling agent has access to it (per the same per-env and + /// per-agent binding rules as `get-all-tools`). Returns `none` + /// either if the tool is not registered or if the calling agent + /// has no binding for it; the two cases are not distinguished. + get-tool: func(name: string) -> option; + + variant rpc-error { + protocol-error(string), + denied(string), + not-found(string), + remote-internal-error(string), + remote-tool-error(tool-error), + } + + resource tool-rpc { + constructor(tool-name: string); + + invoke-and-await: func( + command-path: list, + input: typed-schema-value, + stdin: option, + ) -> result; + + invoke: func( + command-path: list, + input: typed-schema-value, + stdin: option, + ) -> result<_, rpc-error>; + + async-invoke-and-await: func( + command-path: list, + input: typed-schema-value, + stdin: option, + ) -> future-invoke-result; + } + + resource future-invoke-result { + subscribe: func() -> pollable; + get: func() -> option>; + cancel: func(); + } +} + +world tool-host { + export host; +} diff --git a/sdks/kotlin/wit-native/deps/golem-websocket/websocket.wit b/sdks/kotlin/wit-native/deps/golem-websocket/websocket.wit new file mode 100644 index 0000000000..b5d86b7ec5 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/golem-websocket/websocket.wit @@ -0,0 +1,51 @@ +package golem:websocket@1.5.0; + +interface client { + use wasi:io/poll@0.2.3.{pollable}; + + variant error { + connection-failure(string), + send-failure(string), + receive-failure(string), + protocol-error(string), + closed(option), + other(string), + } + + record close-info { + code: u16, + reason: string, + } + + /// A WebSocket message — text or binary + variant message { + text(string), + binary(list), + } + + /// A WebSocket connection resource + resource websocket-connection { + /// Connect to a WebSocket server at the given URL (ws:// or wss://) + /// Optional headers for auth, subprotocols, etc. + connect: static func( + url: string, + headers: option>> + ) -> result; + + /// Send a message (text or binary) + send: func(message: message) -> result<_, error>; + + /// Receive the next message (blocks until available) + receive: func() -> result; + + /// Receive the next message with a timeout in milliseconds. + /// Returns none if the timeout expires before a message arrives. + receive-with-timeout: func(timeout-ms: u64) -> result, error>; + + /// Send a close frame with optional code and reason + close: func(code: option, reason: option) -> result<_, error>; + + /// Returns a pollable that resolves when a message is available to read + subscribe: func() -> pollable; + } +} diff --git a/sdks/kotlin/wit-native/deps/http/handler.wit b/sdks/kotlin/wit-native/deps/http/handler.wit new file mode 100644 index 0000000000..6a6c62966f --- /dev/null +++ b/sdks/kotlin/wit-native/deps/http/handler.wit @@ -0,0 +1,49 @@ +/// This interface defines a handler of incoming HTTP Requests. It should +/// be exported by components which can respond to HTTP Requests. +@since(version = 0.2.0) +interface incoming-handler { + @since(version = 0.2.0) + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + @since(version = 0.2.0) + handle: func( + request: incoming-request, + response-out: response-outparam + ); +} + +/// This interface defines a handler of outgoing HTTP Requests. It should be +/// imported by components which wish to make HTTP Requests. +@since(version = 0.2.0) +interface outgoing-handler { + @since(version = 0.2.0) + use types.{ + outgoing-request, request-options, future-incoming-response, error-code + }; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + @since(version = 0.2.0) + handle: func( + request: outgoing-request, + options: option + ) -> result; +} diff --git a/sdks/kotlin/wit-native/deps/http/proxy.wit b/sdks/kotlin/wit-native/deps/http/proxy.wit new file mode 100644 index 0000000000..de3bbe8ae0 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/http/proxy.wit @@ -0,0 +1,50 @@ +package wasi:http@0.2.3; + +/// The `wasi:http/imports` world imports all the APIs for HTTP proxies. +/// It is intended to be `include`d in other worlds. +@since(version = 0.2.0) +world imports { + /// HTTP proxies have access to time and randomness. + @since(version = 0.2.0) + import wasi:clocks/monotonic-clock@0.2.3; + @since(version = 0.2.0) + import wasi:clocks/wall-clock@0.2.3; + @since(version = 0.2.0) + import wasi:random/random@0.2.3; + + /// Proxies have standard output and error streams which are expected to + /// terminate in a developer-facing console provided by the host. + @since(version = 0.2.0) + import wasi:cli/stdout@0.2.3; + @since(version = 0.2.0) + import wasi:cli/stderr@0.2.3; + + /// TODO: this is a temporary workaround until component tooling is able to + /// gracefully handle the absence of stdin. Hosts must return an eof stream + /// for this import, which is what wasi-libc + tooling will do automatically + /// when this import is properly removed. + @since(version = 0.2.0) + import wasi:cli/stdin@0.2.3; + + /// This is the default handler to use when user code simply wants to make an + /// HTTP request (e.g., via `fetch()`). + @since(version = 0.2.0) + import outgoing-handler; +} + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +@since(version = 0.2.0) +world proxy { + @since(version = 0.2.0) + include imports; + + /// The host delivers incoming HTTP requests to a component by calling the + /// `handle` function of this exported interface. A host may arbitrarily reuse + /// or not reuse component instance when delivering incoming HTTP requests and + /// thus a component must be able to handle 0..N calls to `handle`. + @since(version = 0.2.0) + export incoming-handler; +} diff --git a/sdks/kotlin/wit-native/deps/http/types.wit b/sdks/kotlin/wit-native/deps/http/types.wit new file mode 100644 index 0000000000..2498f180ad --- /dev/null +++ b/sdks/kotlin/wit-native/deps/http/types.wit @@ -0,0 +1,673 @@ +/// This interface defines all of the types and methods for implementing +/// HTTP Requests and Responses, both incoming and outgoing, as well as +/// their headers, trailers, and bodies. +@since(version = 0.2.0) +interface types { + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.3.{duration}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/error@0.2.3.{error as io-error}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + + /// This type corresponds to HTTP standard Methods. + @since(version = 0.2.0) + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.2.0) + variant scheme { + HTTP, + HTTPS, + other(string) + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// + @since(version = 0.2.0) + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option) + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.2.0) + record DNS-error-payload { + rcode: option, + info-code: option + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.2.0) + record TLS-alert-received-payload { + alert-id: option, + alert-message: option + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.2.0) + record field-size-payload { + field-name: option, + field-size: option + } + + /// Attempts to extract a http-related `error` from the wasi:io `error` + /// provided. + /// + /// Stream operations which return + /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of + /// type `wasi:io/error/error` with more information about the operation + /// that failed. This payload can be passed through to this function to see + /// if there's http-related information about the error to return. + /// + /// Note that this function is fallible because not all io-errors are + /// http-related errors. + @since(version = 0.2.0) + http-error-code: func(err: borrow) -> option; + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + @since(version = 0.2.0) + variant header-error { + /// This error indicates that a `field-name` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + + /// This error indicates that a forbidden `field-name` was used when trying + /// to set a header in a `fields`. + forbidden, + + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + @since(version = 0.2.1) + type field-name = field-key; + + /// Field keys are always strings. + /// + /// Field keys should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + /// + /// # Deprecation + /// + /// This type has been deprecated in favor of the `field-name` type. + @since(version = 0.2.0) + @deprecated(version = 0.2.2) + type field-key = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + @since(version = 0.2.0) + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `incoming-request.headers`, `outgoing-request.headers`) might be be + /// immutable. In an immutable fields, the `set`, `append`, and `delete` + /// operations will fail with `header-error.immutable`. + @since(version = 0.2.0) + resource fields { + + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + @since(version = 0.2.0) + constructor(); + + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. + /// + /// An error result will be returned if any `field-name` or `field-value` is + /// syntactically invalid, or if a field is forbidden. + @since(version = 0.2.0) + from-list: static func( + entries: list> + ) -> result; + + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields` or is syntactically invalid, an empty list is returned. + /// However, if the name is present but empty, this is represented by a list + /// with one or more empty field-values present. + @since(version = 0.2.0) + get: func(name: field-name) -> list; + + /// Returns `true` when the name is present in this `fields`. If the name is + /// syntactically invalid, `false` is returned. + @since(version = 0.2.0) + has: func(name: field-name) -> bool; + + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` or any of + /// the `field-value`s are syntactically invalid. + @since(version = 0.2.0) + set: func(name: field-name, value: list) -> result<_, header-error>; + + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` is + /// syntactically invalid. + @since(version = 0.2.0) + delete: func(name: field-name) -> result<_, header-error>; + + /// Append a value for a name. Does not change or delete any existing + /// values for that name. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` or + /// `field-value` are syntactically invalid. + @since(version = 0.2.0) + append: func(name: field-name, value: field-value) -> result<_, header-error>; + + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. + /// + /// The outer list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + @since(version = 0.2.0) + entries: func() -> list>; + + /// Make a deep copy of the Fields. Equivalent in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + @since(version = 0.2.0) + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + @since(version = 0.2.0) + type headers = fields; + + /// Trailers is an alias for Fields. + @since(version = 0.2.0) + type trailers = fields; + + /// Represents an incoming HTTP Request. + @since(version = 0.2.0) + resource incoming-request { + + /// Returns the method of the incoming request. + @since(version = 0.2.0) + method: func() -> method; + + /// Returns the path with query parameters from the request, as a string. + @since(version = 0.2.0) + path-with-query: func() -> option; + + /// Returns the protocol scheme from the request. + @since(version = 0.2.0) + scheme: func() -> option; + + /// Returns the authority of the Request's target URI, if present. + @since(version = 0.2.0) + authority: func() -> option; + + /// Get the `headers` associated with the request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// The `headers` returned are a child resource: it must be dropped before + /// the parent `incoming-request` is dropped. Dropping this + /// `incoming-request` before all children are dropped will trap. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Gives the `incoming-body` associated with this request. Will only + /// return success at most once, and subsequent calls will return error. + @since(version = 0.2.0) + consume: func() -> result; + } + + /// Represents an outgoing HTTP Request. + @since(version = 0.2.0) + resource outgoing-request { + + /// Construct a new `outgoing-request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. + @since(version = 0.2.0) + constructor( + headers: headers + ); + + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-request` can be retrieved at most once. Subsequent + /// calls will return error. + @since(version = 0.2.0) + body: func() -> result; + + /// Get the Method for the Request. + @since(version = 0.2.0) + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + @since(version = 0.2.0) + set-method: func(method: method) -> result; + + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + @since(version = 0.2.0) + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + @since(version = 0.2.0) + set-path-with-query: func(path-with-query: option) -> result; + + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + @since(version = 0.2.0) + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + @since(version = 0.2.0) + set-scheme: func(scheme: option) -> result; + + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. + @since(version = 0.2.0) + authority: func() -> option; + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid URI authority. + @since(version = 0.2.0) + set-authority: func(authority: option) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transferred to + /// another component by e.g. `outgoing-handler.handle`. + @since(version = 0.2.0) + headers: func() -> headers; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound a + /// blocking call to `wasi:io/poll.poll`. + @since(version = 0.2.0) + resource request-options { + /// Construct a default `request-options` value. + @since(version = 0.2.0) + constructor(); + + /// The timeout for the initial connect to the HTTP Server. + @since(version = 0.2.0) + connect-timeout: func() -> option; + + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported. + @since(version = 0.2.0) + set-connect-timeout: func(duration: option) -> result; + + /// The timeout for receiving the first byte of the Response body. + @since(version = 0.2.0) + first-byte-timeout: func() -> option; + + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported. + @since(version = 0.2.0) + set-first-byte-timeout: func(duration: option) -> result; + + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + @since(version = 0.2.0) + between-bytes-timeout: func() -> option; + + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported. + @since(version = 0.2.0) + set-between-bytes-timeout: func(duration: option) -> result; + } + + /// Represents the ability to send an HTTP Response. + /// + /// This resource is used by the `wasi:http/incoming-handler` interface to + /// allow a Response to be sent corresponding to the Request provided as the + /// other argument to `incoming-handler.handle`. + @since(version = 0.2.0) + resource response-outparam { + + /// Set the value of the `response-outparam` to either send a response, + /// or indicate an error. + /// + /// This method consumes the `response-outparam` to ensure that it is + /// called at most once. If it is never called, the implementation + /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. + @since(version = 0.2.0) + set: static func( + param: response-outparam, + response: result, + ); + } + + /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.2.0) + type status-code = u16; + + /// Represents an incoming HTTP Response. + @since(version = 0.2.0) + resource incoming-response { + + /// Returns the status code from the incoming response. + @since(version = 0.2.0) + status: func() -> status-code; + + /// Returns the headers from the incoming response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `incoming-response` is dropped. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Returns the incoming body. May be called at most once. Returns error + /// if called additional times. + @since(version = 0.2.0) + consume: func() -> result; + } + + /// Represents an incoming HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, indicating that the full contents of the + /// body have been received. This resource represents the contents as + /// an `input-stream` and the delivery of trailers as a `future-trailers`, + /// and ensures that the user of this interface may only be consuming either + /// the body contents or waiting on trailers at any given time. + @since(version = 0.2.0) + resource incoming-body { + + /// Returns the contents of the body, as a stream of bytes. + /// + /// Returns success on first call: the stream representing the contents + /// can be retrieved at most once. Subsequent calls will return error. + /// + /// The returned `input-stream` resource is a child: it must be dropped + /// before the parent `incoming-body` is dropped, or consumed by + /// `incoming-body.finish`. + /// + /// This invariant ensures that the implementation can determine whether + /// the user is consuming the contents of the body, waiting on the + /// `future-trailers` to be ready, or neither. This allows for network + /// backpressure is to be applied when the user is consuming the body, + /// and for that backpressure to not inhibit delivery of the trailers if + /// the user does not read the entire body. + @since(version = 0.2.0) + %stream: func() -> result; + + /// Takes ownership of `incoming-body`, and returns a `future-trailers`. + /// This function will trap if the `input-stream` child is still alive. + @since(version = 0.2.0) + finish: static func(this: incoming-body) -> future-trailers; + } + + /// Represents a future which may eventually return trailers, or an error. + /// + /// In the case that the incoming HTTP Request or Response did not have any + /// trailers, this future will resolve to the empty set of trailers once the + /// complete Request or Response body has been received. + @since(version = 0.2.0) + resource future-trailers { + + /// Returns a pollable which becomes ready when either the trailers have + /// been received, or an error has occurred. When this pollable is ready, + /// the `get` method will return `some`. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Returns the contents of the trailers, or an error which occurred, + /// once the future is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the trailers or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the HTTP Request or Response + /// body, as well as any trailers, were received successfully, or that an + /// error occurred receiving them. The optional `trailers` indicates whether + /// or not trailers were present in the body. + /// + /// When some `trailers` are returned by this method, the `trailers` + /// resource is immutable, and a child. Use of the `set`, `append`, or + /// `delete` methods will return an error, and the resource must be + /// dropped before the parent `future-trailers` is dropped. + @since(version = 0.2.0) + get: func() -> option, error-code>>>; + } + + /// Represents an outgoing HTTP Response. + @since(version = 0.2.0) + resource outgoing-response { + + /// Construct an `outgoing-response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// * `headers` is the HTTP Headers for the Response. + @since(version = 0.2.0) + constructor(headers: headers); + + /// Get the HTTP Status Code for the Response. + @since(version = 0.2.0) + status-code: func() -> status-code; + + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + @since(version = 0.2.0) + set-status-code: func(status-code: status-code) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transferred to + /// another component by e.g. `outgoing-handler.handle`. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Returns the resource corresponding to the outgoing Body for this Response. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + @since(version = 0.2.0) + body: func() -> result; + } + + /// Represents an outgoing HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, inducating the full contents of the body + /// have been sent. This resource represents the contents as an + /// `output-stream` child resource, and the completion of the body (with + /// optional trailers) with a static function that consumes the + /// `outgoing-body` resource, and ensures that the user of this interface + /// may not write to the body contents after the body has been finished. + /// + /// If the user code drops this resource, as opposed to calling the static + /// method `finish`, the implementation should treat the body as incomplete, + /// and that an error has occurred. The implementation should propagate this + /// error to the HTTP protocol by whatever means it has available, + /// including: corrupting the body on the wire, aborting the associated + /// Request, or sending a late status code for the Response. + @since(version = 0.2.0) + resource outgoing-body { + + /// Returns a stream for writing the body contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-body` resource is dropped (or finished), + /// otherwise the `outgoing-body` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-body` may be retrieved at most once. Subsequent calls + /// will return error. + @since(version = 0.2.0) + write: func() -> result; + + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` + /// is dropped without calling `outgoing-body.finalize`, the implementation + /// should treat the body as corrupted. + /// + /// Fails if the body's `outgoing-request` or `outgoing-response` was + /// constructed with a Content-Length header, and the contents written + /// to the body (via `write`) does not match the value given in the + /// Content-Length. + @since(version = 0.2.0) + finish: static func( + this: outgoing-body, + trailers: option + ) -> result<_, error-code>; + } + + /// Represents a future which may eventually return an incoming HTTP + /// Response, or an error. + /// + /// This resource is returned by the `wasi:http/outgoing-handler` interface to + /// provide the HTTP Response corresponding to the sent Request. + @since(version = 0.2.0) + resource future-incoming-response { + /// Returns a pollable which becomes ready when either the Response has + /// been received, or an error has occurred. When this pollable is ready, + /// the `get` method will return `some`. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Returns the incoming HTTP Response, or an error, once one is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the response or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the incoming HTTP Response + /// status and headers have received successfully, or that an error + /// occurred. Errors may also occur while consuming the response body, + /// but those will be reported by the `incoming-body` and its + /// `output-stream` child. + @since(version = 0.2.0) + get: func() -> option>>; + } +} diff --git a/sdks/kotlin/wit-native/deps/io/error.wit b/sdks/kotlin/wit-native/deps/io/error.wit new file mode 100644 index 0000000000..97c6068779 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/sdks/kotlin/wit-native/deps/io/poll.wit b/sdks/kotlin/wit-native/deps/io/poll.wit new file mode 100644 index 0000000000..9bcbe8e036 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.3; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/sdks/kotlin/wit-native/deps/io/streams.wit b/sdks/kotlin/wit-native/deps/io/streams.wit new file mode 100644 index 0000000000..0de0846293 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.3; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/sdks/kotlin/wit-native/deps/io/world.wit b/sdks/kotlin/wit-native/deps/io/world.wit new file mode 100644 index 0000000000..f1d2102dca --- /dev/null +++ b/sdks/kotlin/wit-native/deps/io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/sdks/kotlin/wit-native/deps/keyvalue/atomic.wit b/sdks/kotlin/wit-native/deps/keyvalue/atomic.wit new file mode 100644 index 0000000000..1d32b7e32a --- /dev/null +++ b/sdks/kotlin/wit-native/deps/keyvalue/atomic.wit @@ -0,0 +1,31 @@ +/// A keyvalue interface that provides atomic operations. +/// +/// Atomic operations are single, indivisible operations. When a fault causes +/// an atomic operation to fail, it will appear to the invoker of the atomic +/// operation that the action either completed successfully or did nothing +/// at all. +interface atomic { + /// A keyvalue interface that provides atomic operations. + use types.{bucket, error, key}; + + /// Atomically increment the value associated with the key in the bucket by the + /// given delta. It returns the new value. + /// + /// If the key does not exist in the bucket, it creates a new key-value pair + /// with the value set to the given delta. + /// + /// If any other error occurs, it returns an `Err(error)`. + increment: func(bucket: borrow, key: key, delta: u64) -> result; + + /// Compare-and-swap (CAS) atomically updates the value associated with the key + /// in the bucket if the value matches the old value. This operation returns + /// `Ok(true)` if the swap was successful, `Ok(false)` if the value did not match, + /// + /// A successful CAS operation means the current value matched the `old` value + /// and was replaced with the `new` value. + /// + /// If the key does not exist in the bucket, it returns `Ok(false)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + compare-and-swap: func(bucket: borrow, key: key, old: u64, new: u64) -> result; +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/keyvalue/caching.wit b/sdks/kotlin/wit-native/deps/keyvalue/caching.wit new file mode 100644 index 0000000000..5880362a8c --- /dev/null +++ b/sdks/kotlin/wit-native/deps/keyvalue/caching.wit @@ -0,0 +1,98 @@ +// The `wasi:keyvalue/cache` interface defines the operations of a single +// instance of a "cache", which is a non-durable, weakly-consistent key-value +// store. "Non-durable" means that caches are allowed and expected to +// arbitrarily discard key-value entries. "Weakly-consistent" means that there +// are essentially no guarantees that operations will agree on their results: a +// get following a set may not observe the set value; multiple gets may observe +// different previous set values; etc. The only guarantee is that values are +// not materialized "out of thin air": if a `get` returns a value, that value +// was passed to a `set` operation at some point in time in the past. +// Additionally, caches MUST make a best effort to respect the supplied +// Time-to-Live values (within the usual limitations around time in a +// distributed setting). +interface cache { + use wasi:io/poll@0.2.3.{pollable}; + use types.{key, incoming-value, outgoing-value, error}; + + // The `get` operation returns the value passed by a previous `set` for the + // same key within the given TTL or none if there is no such value. + get: func(k: key) -> future-get-result; + + // This block defines a special resource type used by `get` to emulate + // `future,error>>`. In the return value + // of the `get` method, the outer `option` returns `none` when the pollable + // is not yet ready and the inner `option` returns `none` when the + // requested key wasn't present. + resource future-get-result { + future-get-result-get: func() -> option, error>>; + listen-to-future-get-result: func() -> pollable; + } + + // The `exists` operation returns whether a value was previously `set` for + // the given key within the TTL. + exists: func(k: key) -> future-exists-result; + + // This block defines a special resource type used by `exists` to emulate + // `future>`. + resource future-exists-result { + future-exists-result-get: func() -> option>; + listen-to-future-exists-result: func() -> pollable; + } + + // The `set` operation sets the given value for the given key for the given + // time-to-live (TTL) duration, if supplied, specified in milliseconds. If + // a TTL is not supplied, the key may be kept indefinitely (as-if a very + // large TTL were used). If the key is already present in the cache, the + // value is updated in-place. In the common case of computing and caching a + // value if the given key is not already in the cache, consider using + // `get-or-set` (below) intead of separate `get` and `set` operations. + set: func(k: key, v: borrow, TTL-ms: option) -> future-result; + + // This block defines a special resource type used by `set` and `delete` to + // emulate `future>`. + resource future-result { + future-result-get: func() -> option>; + listen-to-future-result: func() -> pollable; + } + + // The `get-or-set` operation asynchronously returns one of two cases + // enumerated by `get-or-set-entry`: in the `occupied` case, the given key + // already has a value present in the cache; in the `vacant` case, there + // was no value and the caller should write a value into the returned + // `vacancy`. This operation allows multiple concurrent `get-or-set` + // invocations to rendezvous such that only one invocation receives the + // `vacant` result while all other invocations wait until the vacancy is + // filled before receiving an `occupied` result. Implementations are not + // required to implement this rendezvous or to rendezvous in all possible + // cases. + variant get-or-set-entry { + occupied(incoming-value), + vacant(vacancy) + } + get-or-set: func(k: key) -> future-get-or-set-result; + + // This block defines a special resource type used by `get-or-set` to + // emulate `future>`. + resource future-get-or-set-result { + future-get-or-set-result-get: func() -> option>; + listen-to-future-get-or-set-result: func() -> pollable; + } + + // The following block defines the `vacancy` resource type. (When resource + // types are added, the `u32` type aliases can be replaced by proper + // `resource` types.) When the caller of `get-or-set` receives a `vacancy`, + // they must either call the `fill` method or drop the `vacancy` to + // indicate an error that prevents calling `fill`. An implementation MAY + // have a timeout that drops a vacancy that hasn't been filled in order + // to unblock other waiting `get-or-set` callers. + resource vacancy { + vacancy-fill: func(TTL-ms: option) -> outgoing-value; + } + + // The `delete` operation removes any value with the given key from the + // cache. Like all cache operations, `delete` is weakly ordered and thus + // concurrent `get` calls may still see deleted keys for a period of time. + // Additionally, due to weak ordering, concurrent `set` calls for the same + // key may or may not get deleted. + delete: func(k: key) -> future-result; +} diff --git a/sdks/kotlin/wit-native/deps/keyvalue/error.wit b/sdks/kotlin/wit-native/deps/keyvalue/error.wit new file mode 100644 index 0000000000..cd244f6fff --- /dev/null +++ b/sdks/kotlin/wit-native/deps/keyvalue/error.wit @@ -0,0 +1,20 @@ +interface wasi-keyvalue-error { + /// An error resource type for keyvalue operations. + /// + /// Common errors: + /// - Connectivity errors (e.g. network errors): when the client cannot establish + /// a connection to the keyvalue service. + /// - Authentication and Authorization errors: when the client fails to authenticate + /// or does not have the required permissions to perform the operation. + /// - Data errors: when the client sends incompatible or corrupted data. + /// - Resource errors: when the system runs out of resources (e.g. memory). + /// - Internal errors: unexpected errors on the server side. + /// + /// Currently, this provides only one function to return a string representation + /// of the error. In the future, this will be extended to provide more information + /// about the error. + // Soon: switch to `resource error { ... }` + resource error { + trace: func() -> string; + } +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/keyvalue/eventual-batch.wit b/sdks/kotlin/wit-native/deps/keyvalue/eventual-batch.wit new file mode 100644 index 0000000000..080999eeaa --- /dev/null +++ b/sdks/kotlin/wit-native/deps/keyvalue/eventual-batch.wit @@ -0,0 +1,81 @@ +/// A keyvalue interface that provides eventually consistent batch operations. +/// +/// A batch operation is an operation that operates on multiple keys at once. +/// +/// Batch operations are useful for reducing network round-trip time. For example, +/// if you want to get the values associated with 100 keys, you can either do 100 get +/// operations or you can do 1 batch get operation. The batch operation is +/// faster because it only needs to make 1 network call instead of 100. +/// +/// A batch operation does not guarantee atomicity, meaning that if the batch +/// operation fails, some of the keys may have been modified and some may not. +/// Transactional operations are being worked on and will be added in the future to +/// provide atomicity. +/// +/// Data consistency in a key value store refers to the gaurantee that once a +/// write operation completes, all subsequent read operations will return the +/// value that was written. +/// +/// The level of consistency in batch operations is **eventual consistency**, the same +/// with the readwrite interface. This interface does not guarantee strong consistency, +/// meaning that if a write operation completes, subsequent read operations may not return +/// the value that was written. +interface eventual-batch { + /// A keyvalue interface that provides batch get operations. + use types.{bucket, error, key, incoming-value, outgoing-value}; + + /// Get the values associated with the keys in the bucket. It returns a list of + /// incoming-value that can be consumed to get the value associated with the key. + /// + /// If any of the keys do not exist in the bucket, it returns a `none` value for + /// that key in the list. + /// + /// Note that the key-value pairs are guaranteed to be returned in the same order + /// + /// MAY show an out-of-date value if there are concurrent writes to the bucket. + /// + /// If any other error occurs, it returns an `Err(error)`. + get-many: func(bucket: borrow, keys: list) -> result>, error>; + + /// Get all the keys in the bucket. It returns a list of keys. + /// + /// Note that the keys are not guaranteed to be returned in any particular order. + /// + /// If the bucket is empty, it returns an empty list. + /// + /// MAY show an out-of-date list of keys if there are concurrent writes to the bucket. + /// + /// If any error occurs, it returns an `Err(error)`. + keys: func(bucket: borrow) -> result, error>; + + /// Set the values associated with the keys in the bucket. If the key already + /// exists in the bucket, it overwrites the value. + /// + /// Note that the key-value pairs are not guaranteed to be set in the order + /// they are provided. + /// + /// If any of the keys do not exist in the bucket, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it + /// does not rollback the key-value pairs that were already set. Thus, this batch operation + /// does not guarantee atomicity, implying that some key-value pairs could be + /// set while others might fail. + /// + /// Other concurrent operations may also be able to see the partial results. + set-many: func(bucket: borrow, key-values: list>>) -> result<_, error>; + + /// Delete the key-value pairs associated with the keys in the bucket. + /// + /// Note that the key-value pairs are not guaranteed to be deleted in the order + /// they are provided. + /// + /// If any of the keys do not exist in the bucket, it skips the key. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it + /// does not rollback the key-value pairs that were already deleted. Thus, this batch operation + /// does not guarantee atomicity, implying that some key-value pairs could be + /// deleted while others might fail. + /// + /// Other concurrent operations may also be able to see the partial results. + delete-many: func(bucket: borrow, keys: list) -> result<_, error>; +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/keyvalue/eventual.wit b/sdks/kotlin/wit-native/deps/keyvalue/eventual.wit new file mode 100644 index 0000000000..e6e33cfe74 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/keyvalue/eventual.wit @@ -0,0 +1,56 @@ +/// A keyvalue interface that provides eventually consistent CRUD operations. +/// +/// A CRUD operation is an operation that acts on a single key-value pair. +/// +/// The value in the key-value pair is defined as a `u8` byte array and the intention +/// is that it is the common denominator for all data types defined by different +/// key-value stores to handle data, ensuring compatibility between different +/// key-value stores. Note: the clients will be expecting serialization/deserialization overhead +/// to be handled by the key-value store. The value could be a serialized object from +/// JSON, HTML or vendor-specific data types like AWS S3 objects. +/// +/// Data consistency in a key value store refers to the gaurantee that once a +/// write operation completes, all subsequent read operations will return the +/// value that was written. +/// +/// The level of consistency in readwrite interfaces is **eventual consistency**, +/// which means that if a write operation completes successfully, all subsequent +/// read operations will eventually return the value that was written. In other words, +/// if we pause the updates to the system, the system eventually will return +/// the last updated value for read. +interface eventual { + /// A keyvalue interface that provides simple read and write operations. + use types.{bucket, error, incoming-value, key, outgoing-value}; + + /// Get the value associated with the key in the bucket. + /// + /// The value is returned as an option. If the key-value pair exists in the + /// bucket, it returns `Ok(value)`. If the key does not exist in the + /// bucket, it returns `Ok(none)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + get: func(bucket: borrow, key: key) -> result, error>; + + /// Set the value associated with the key in the bucket. If the key already + /// exists in the bucket, it overwrites the value. + /// + /// If the key does not exist in the bucket, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. + set: func(bucket: borrow, key: key, outgoing-value: borrow) -> result<_, error>; + + /// Delete the key-value pair associated with the key in the bucket. + /// + /// If the key does not exist in the bucket, it does nothing. + /// + /// If any other error occurs, it returns an `Err(error)`. + delete: func(bucket: borrow, key: key) -> result<_, error>; + + /// Check if the key exists in the bucket. + /// + /// If the key exists in the bucket, it returns `Ok(true)`. If the key does + /// not exist in the bucket, it returns `Ok(false)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + exists: func(bucket: borrow, key: key) -> result; +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/keyvalue/handle-watch.wit b/sdks/kotlin/wit-native/deps/keyvalue/handle-watch.wit new file mode 100644 index 0000000000..0ca7b37728 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/keyvalue/handle-watch.wit @@ -0,0 +1,17 @@ +/// A keyvalue interface that provides handle-watch operations. +/// +/// This interface is used to provide event-driven mechanisms to handle +/// keyvalue changes. +interface handle-watch { + /// A keyvalue interface that provides handle-watch operations. + use types.{bucket, key, incoming-value}; + + /// Handle the `set` event for the given bucket and key. + /// It returns a `incoming-value` that represents the new value being set. + /// The new value can be consumed by the handler. + on-set: func(bucket: bucket, key: key, incoming-value: borrow); + + /// Handle the `delete` event for the given bucket and key. + /// It returns a `key` that represents the key being deleted. + on-delete: func(bucket: bucket, key: key); +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/keyvalue/types.wit b/sdks/kotlin/wit-native/deps/keyvalue/types.wit new file mode 100644 index 0000000000..81ee803c0f --- /dev/null +++ b/sdks/kotlin/wit-native/deps/keyvalue/types.wit @@ -0,0 +1,72 @@ +// A generic keyvalue interface for WASI. +interface types { + /// A bucket is a collection of key-value pairs. Each key-value pair is stored + /// as a entry in the bucket, and the bucket itself acts as a collection of all + /// these entries. + /// + /// It is worth noting that the exact terminology for bucket in key-value stores + /// can very depending on the specific implementation. For example, + /// 1. Amazon DynamoDB calls a collection of key-value pairs a table + /// 2. Redis has hashes, sets, and sorted sets as different types of collections + /// 3. Cassandra calls a collection of key-value pairs a column family + /// 4. MongoDB calls a collection of key-value pairs a collection + /// 5. Riak calls a collection of key-value pairs a bucket + /// 6. Memcached calls a collection of key-value pairs a slab + /// 7. Azure Cosmos DB calls a collection of key-value pairs a container + /// + /// In this interface, we use the term `bucket` to refer to a collection of key-value + // Soon: switch to `resource bucket { ... }` + resource bucket { + /// Opens a bucket with the given name. + /// + /// If any error occurs, including if the bucket does not exist, it returns an `Err(error)`. + open-bucket: static func(name: string) -> result; + } + /// A key is a unique identifier for a value in a bucket. The key is used to + /// retrieve the value from the bucket. + type key = string; + + use wasi:io/streams@0.2.3.{input-stream, output-stream}; + use wasi-keyvalue-error.{ error }; + /// A value is the data stored in a key-value pair. The value can be of any type + /// that can be represented in a byte array. It provides a way to write the value + /// to the output-stream defined in the `wasi-io` interface. + // Soon: switch to `resource value { ... }` + resource outgoing-value { + new-outgoing-value: static func() -> outgoing-value; + /// Writes the value to the output-stream asynchronously. + /// If any other error occurs, it returns an `Err(error)`. + outgoing-value-write-body-async: func() -> result; + /// Writes the value to the output-stream synchronously. + /// If any other error occurs, it returns an `Err(error)`. + outgoing-value-write-body-sync: func(value: outgoing-value-body-sync) -> result<_, error>; + } + type outgoing-value-body-async = output-stream; + type outgoing-value-body-sync = list; + + /// A incoming-value is a wrapper around a value. It provides a way to read the value + /// from the `input-stream` defined in the `wasi-io` interface. + /// + /// The incoming-value provides two ways to consume the value: + /// 1. `incoming-value-consume-sync` consumes the value synchronously and returns the + /// value as a `list`. + /// 2. `incoming-value-consume-async` consumes the value asynchronously and returns the + /// value as an `input-stream`. + /// In addition, it provides a `incoming-value-size` function to get the size of the value. + /// This is useful when the value is large and the caller wants to allocate a buffer of + /// the right size to consume the value. + // Soon: switch to `resource incoming-value { ... }` + resource incoming-value { + /// Consumes the value synchronously and returns the value as a list of bytes. + /// If any other error occurs, it returns an `Err(error)`. + incoming-value-consume-sync: func() -> result; + /// Consumes the value asynchronously and returns the value as an `input-stream`. + /// If any other error occurs, it returns an `Err(error)`. + incoming-value-consume-async: func() -> result; + /// The size of the value in bytes. + /// If the size is unknown or unavailable, this function returns an `Err(error)`. + incoming-value-size: func() -> result; + } + type incoming-value-async-body = input-stream; + type incoming-value-sync-body = list; +} diff --git a/sdks/kotlin/wit-native/deps/keyvalue/world.wit b/sdks/kotlin/wit-native/deps/keyvalue/world.wit new file mode 100644 index 0000000000..ea64fe5eed --- /dev/null +++ b/sdks/kotlin/wit-native/deps/keyvalue/world.wit @@ -0,0 +1,26 @@ +package wasi:keyvalue@0.1.0; + +/// The `wasi:keyvalue/imports` world provides common APIs for interacting +/// with key-value stores. Components targeting this world will be able to +/// do +/// 1. CRUD (create, read, update, delete) operations on key-value stores. +/// 2. Atomic `increment` and CAS (compare-and-swap) operations. +/// 3. Batch operations that can reduce the number of round trips to the network. +world imports { + /// The `eventual` capability allows the component to perform + /// eventually consistent CRUD operations on the key-value store. + import eventual; + + /// The `atomic` capability allows the component to perform atomic + /// `increment` and CAS (compare-and-swap) operations. + import atomic; + + /// The `eventual-batch` capability allows the component to perform eventually + /// consistent batch operations that can reduce the number of round trips to the network. + import eventual-batch; +} + +world keyvalue-handle-watch { + include imports; + export handle-watch; +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/logging/logging.wit b/sdks/kotlin/wit-native/deps/logging/logging.wit new file mode 100644 index 0000000000..adb5ee5fb2 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/logging/logging.wit @@ -0,0 +1,37 @@ +package wasi:logging; + +/// WASI Logging is a logging API intended to let users emit log messages with +/// simple priority levels and context values. +interface logging { + /// A log level, describing a kind of message. + enum level { + /// Describes messages about the values of variables and the flow of + /// control within a program. + trace, + + /// Describes messages likely to be of interest to someone debugging a + /// program. + debug, + + /// Describes messages likely to be of interest to someone monitoring a + /// program. + info, + + /// Describes messages indicating hazardous situations. + warn, + + /// Describes messages indicating serious errors. + error, + + /// Describes messages indicating fatal errors. + critical, + } + + /// Emit a log message. + /// + /// A log message has a `level` describing what kind of message is being + /// sent, a context, which is an uninterpreted string meant to help + /// consumers group similar messages, and a string containing the message + /// text. + log: func(level: level, context: string, message: string); +} \ No newline at end of file diff --git a/sdks/kotlin/wit-native/deps/random/insecure-seed.wit b/sdks/kotlin/wit-native/deps/random/insecure-seed.wit new file mode 100644 index 0000000000..67d024d5bf --- /dev/null +++ b/sdks/kotlin/wit-native/deps/random/insecure-seed.wit @@ -0,0 +1,27 @@ +package wasi:random@0.2.3; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.2.0) + insecure-seed: func() -> tuple; +} diff --git a/sdks/kotlin/wit-native/deps/random/insecure.wit b/sdks/kotlin/wit-native/deps/random/insecure.wit new file mode 100644 index 0000000000..a07dfab327 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/random/insecure.wit @@ -0,0 +1,25 @@ +package wasi:random@0.2.3; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + @since(version = 0.2.0) + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.2.0) + get-insecure-random-u64: func() -> u64; +} diff --git a/sdks/kotlin/wit-native/deps/random/random.wit b/sdks/kotlin/wit-native/deps/random/random.wit new file mode 100644 index 0000000000..91957e6330 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/random/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.2.3; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.2.0) + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.2.0) + get-random-u64: func() -> u64; +} diff --git a/sdks/kotlin/wit-native/deps/random/world.wit b/sdks/kotlin/wit-native/deps/random/world.wit new file mode 100644 index 0000000000..0c1218f36e --- /dev/null +++ b/sdks/kotlin/wit-native/deps/random/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import random; + + @since(version = 0.2.0) + import insecure; + + @since(version = 0.2.0) + import insecure-seed; +} diff --git a/sdks/kotlin/wit-native/deps/sockets/instance-network.wit b/sdks/kotlin/wit-native/deps/sockets/instance-network.wit new file mode 100644 index 0000000000..5f6e6c1cc9 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/sockets/instance-network.wit @@ -0,0 +1,11 @@ + +/// This interface provides a value-export of the default network handle.. +@since(version = 0.2.0) +interface instance-network { + @since(version = 0.2.0) + use network.{network}; + + /// Get a handle to the default network. + @since(version = 0.2.0) + instance-network: func() -> network; +} diff --git a/sdks/kotlin/wit-native/deps/sockets/ip-name-lookup.wit b/sdks/kotlin/wit-native/deps/sockets/ip-name-lookup.wit new file mode 100644 index 0000000000..c1d8a47c16 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,56 @@ +@since(version = 0.2.0) +interface ip-name-lookup { + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-address}; + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// This function never blocks. It either immediately fails or immediately + /// returns successfully with a `resolve-address-stream` that can be used + /// to (asynchronously) fetch the results. + /// + /// # Typical errors + /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + resolve-addresses: func(network: borrow, name: string) -> result; + + @since(version = 0.2.0) + resource resolve-address-stream { + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + @since(version = 0.2.0) + resolve-next-address: func() -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/sdks/kotlin/wit-native/deps/sockets/network.wit b/sdks/kotlin/wit-native/deps/sockets/network.wit new file mode 100644 index 0000000000..f3f60a3709 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/sockets/network.wit @@ -0,0 +1,169 @@ +@since(version = 0.2.0) +interface network { + @unstable(feature = network-error-code) + use wasi:io/error@0.2.3.{error}; + + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + @since(version = 0.2.0) + resource network; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// - `concurrency-conflict` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.2.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + /// Attempts to extract a network-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// network-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are network-related errors. + @unstable(feature = network-error-code) + network-error-code: func(err: borrow) -> option; + + @since(version = 0.2.0) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.2.0) + type ipv4-address = tuple; + @since(version = 0.2.0) + type ipv6-address = tuple; + + @since(version = 0.2.0) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.2.0) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.2.0) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.2.0) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } +} diff --git a/sdks/kotlin/wit-native/deps/sockets/tcp-create-socket.wit b/sdks/kotlin/wit-native/deps/sockets/tcp-create-socket.wit new file mode 100644 index 0000000000..eedbd30768 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/sockets/tcp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface tcp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use tcp.{tcp-socket}; + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-tcp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/sdks/kotlin/wit-native/deps/sockets/tcp.wit b/sdks/kotlin/wit-native/deps/sockets/tcp.wit new file mode 100644 index 0000000000..b4cd87fcef --- /dev/null +++ b/sdks/kotlin/wit-native/deps/sockets/tcp.wit @@ -0,0 +1,387 @@ +@since(version = 0.2.0) +interface tcp { + @since(version = 0.2.0) + use wasi:io/streams@0.2.3.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.3.{duration}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + @since(version = 0.2.0) + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bind-in-progress` + /// - `bound` (See note below) + /// - `listen-in-progress` + /// - `listening` + /// - `connect-in-progress` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `network::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.2.0) + resource tcp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the `connected` state. + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A connect operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. + /// Because all WASI sockets are non-blocking this is expected to return + /// EINPROGRESS, which should be translated to `ok()` in WASI. + /// + /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` + /// with a timeout of 0 on the socket descriptor. Followed by a check for + /// the `SO_ERROR` socket option, in case the poll signaled readiness. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-connect: func() -> result, error-code>; + + /// Start listening for new connections. + /// + /// Transitions the socket into the `listening` state. + /// + /// Unlike POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A listen operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the listen operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `listen` as part of either `start-listen` or `finish-listen`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-listen: func() -> result<_, error-code>; + @since(version = 0.2.0) + finish-listen: func() -> result<_, error-code>; + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + accept: func() -> result, error-code>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.2.0) + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + @since(version = 0.2.0) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.2.0) + keep-alive-enabled: func() -> result; + @since(version = 0.2.0) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-idle-time: func() -> result; + @since(version = 0.2.0) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-interval: func() -> result; + @since(version = 0.2.0) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-count: func() -> result; + @since(version = 0.2.0) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + hop-limit: func() -> result; + @since(version = 0.2.0) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which can be used to poll for, or block on, + /// completion of any of the asynchronous operations of this socket. + /// + /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` + /// return `error(would-block)`, this pollable can be used to wait for + /// their success or failure, after which the method can be retried. + /// + /// The pollable is not limited to the async operation that happens to be + /// in progress at the time of calling `subscribe` (if any). Theoretically, + /// `subscribe` only has to be called once per socket and can then be + /// (re)used for the remainder of the socket's lifetime. + /// + /// See + /// for more information. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Initiate a graceful shutdown. + /// + /// - `receive`: The socket is not expecting to receive any data from + /// the peer. The `input-stream` associated with this socket will be + /// closed. Any data still in the receive queue at time of calling + /// this method will be discarded. + /// - `send`: The socket has no more data to send to the peer. The `output-stream` + /// associated with this socket will be closed and a FIN packet will be sent. + /// - `both`: Same effect as `receive` & `send` combined. + /// + /// This function is idempotent; shutting down a direction more than once + /// has no effect and returns `ok`. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} diff --git a/sdks/kotlin/wit-native/deps/sockets/udp-create-socket.wit b/sdks/kotlin/wit-native/deps/sockets/udp-create-socket.wit new file mode 100644 index 0000000000..e8eeacbfef --- /dev/null +++ b/sdks/kotlin/wit-native/deps/sockets/udp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface udp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use udp.{udp-socket}; + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-udp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/sdks/kotlin/wit-native/deps/sockets/udp.wit b/sdks/kotlin/wit-native/deps/sockets/udp.wit new file mode 100644 index 0000000000..01901ca27f --- /dev/null +++ b/sdks/kotlin/wit-native/deps/sockets/udp.wit @@ -0,0 +1,288 @@ +@since(version = 0.2.0) +interface udp { + @since(version = 0.2.0) + use wasi:io/poll@0.2.3.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + /// A received datagram. + @since(version = 0.2.0) + record incoming-datagram { + /// The payload. + /// + /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + data: list, + + /// The source address. + /// + /// This field is guaranteed to match the remote address the stream was initialized with, if any. + /// + /// Equivalent to the `src_addr` out parameter of `recvfrom`. + remote-address: ip-socket-address, + } + + /// A datagram to be sent out. + @since(version = 0.2.0) + record outgoing-datagram { + /// The payload. + data: list, + + /// The destination address. + /// + /// The requirements on this field depend on how the stream was initialized: + /// - with a remote address: this field must be None or match the stream's remote address exactly. + /// - without a remote address: this field is required. + /// + /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. + remote-address: option, + } + + /// A UDP socket handle. + @since(version = 0.2.0) + resource udp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Set up inbound & outbound communication channels, optionally to a specific peer. + /// + /// This function only changes the local socket configuration and does not generate any network traffic. + /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, + /// based on the best network path to `remote-address`. + /// + /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// This method may be called multiple times on the same socket to change its association, but + /// only the most recently returned pair of streams will be operational. Implementations may trap if + /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. + /// + /// The POSIX equivalent in pseudo-code is: + /// ```text + /// if (was previously connected) { + /// connect(s, AF_UNSPEC) + /// } + /// if (remote_address is Some) { + /// connect(s, remote_address) + /// } + /// ``` + /// + /// Unlike in POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-state`: The socket is not bound. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + %stream: func(remote-address: option) -> result, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the address the socket is currently streaming to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + unicast-hop-limit: func() -> result; + @since(version = 0.2.0) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource incoming-datagram-stream { + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// + /// This function returns successfully with an empty list when either: + /// - `max-results` is 0, or: + /// - `max-results` is greater than 0, but no results are immediately available. + /// This function never returns `error(would-block)`. + /// + /// # Typical errors + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + receive: func(max-results: u64) -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready to receive again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource outgoing-datagram-stream { + /// Check readiness for sending. This function never blocks. + /// + /// Returns the number of datagrams permitted for the next call to `send`, + /// or an error. Calling `send` with more datagrams than this function has + /// permitted will trap. + /// + /// When this function returns ok(0), the `subscribe` pollable will + /// become ready when this function will report at least ok(1), or an + /// error. + /// + /// Never returns `would-block`. + check-send: func() -> result; + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). This function never + /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// + /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if + /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + send: func(datagrams: list) -> result; + + /// Create a `pollable` which will resolve once the stream is ready to send again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/sdks/kotlin/wit-native/deps/sockets/world.wit b/sdks/kotlin/wit-native/deps/sockets/world.wit new file mode 100644 index 0000000000..2f0ad0d7c9 --- /dev/null +++ b/sdks/kotlin/wit-native/deps/sockets/world.wit @@ -0,0 +1,19 @@ +package wasi:sockets@0.2.3; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import instance-network; + @since(version = 0.2.0) + import network; + @since(version = 0.2.0) + import udp; + @since(version = 0.2.0) + import udp-create-socket; + @since(version = 0.2.0) + import tcp; + @since(version = 0.2.0) + import tcp-create-socket; + @since(version = 0.2.0) + import ip-name-lookup; +} diff --git a/sdks/kotlin/wit-native/main.wit b/sdks/kotlin/wit-native/main.wit new file mode 100644 index 0000000000..1626c72a96 --- /dev/null +++ b/sdks/kotlin/wit-native/main.wit @@ -0,0 +1,86 @@ +package kotlin:agent-native@0.1.0; + +world kotlin-agent { + include golem:agent/agent-guest@2.0.0; + include golem:tool/tool-guest@0.1.0; + + // Not pulled in transitively by agent-guest or tool-guest (verified: golem-host.wit only + // *imports* these interfaces into its world -- the component guest must export them so the + // host can call back into the component during a manual snapshot-based update). + // Needed for SnapshotApi's save/load hooks (Task 7). + export golem:api/save-snapshot@1.5.0; + export golem:api/load-snapshot@1.5.0; + + // Not pulled in transitively by agent-guest (which only imports golem:api/host@1.5.0 and + // `common` -- verified in wit/deps/golem-agent/guest.wit). Needed for parse-agent-id + // (Task N4C.2 increment 3). + import golem:agent/host@2.0.0; + + // Not pulled in transitively by anything else in this world (verified: no reference to + // "durability" anywhere under wit/deps/golem-agent or wit/deps/golem-tool). Needed for + // DurabilityApi's core oplog/durable-function primitives (Task N4C.3 increment 1). + import golem:durability/durability@1.5.0; + + // Not pulled in transitively (verified: no reference to golem:api/retry anywhere already + // imported). Needed for RetryApi's retry-policy/retry-predicate tree marshalling (Task N4C.3). + import golem:api/retry@1.5.0; + + // Not pulled in transitively: DurabilityApi's `import golem:durability` only makes a type-only + // `use` of golem:api/oplog's {oplog-index, wrapped-function-type}, which does NOT bring the + // oplog interface's own functions/resources. Needed for OplogApi's get-oplog/search-oplog + // resources (Task N4C.3). + import golem:api/oplog@1.5.0; + + // Not pulled in transitively (verified: no real "context" import anywhere under + // wit/deps/golem-agent, wit/deps/golem-tool, or wit/deps/golem-durability -- only an + // unrelated doc-comment match). Needed for ContextApi's span/invocation-context resources + // (Task N4C.3). + import golem:api/context@1.5.0; + + // Not pulled in transitively (golem:core/types declares the `secret` resource but not the + // capability-gated `reveal` interface). Needed for SecretApi.reveal. + import golem:secrets/reveal@0.1.0; + + // Not pulled in transitively (verified: no reference to "wasi:cli/environment" or + // "wasi:logging" anywhere under wit/deps/golem-agent, wit/deps/golem-tool, + // wit/deps/golem-durability, or wit/deps/golem-1.x). Needed for Environment.kt/Logging.kt + // (Task N4C.4). + import wasi:cli/environment@0.2.3; + import wasi:logging/logging; + + // Not pulled in transitively (same check as above). Needed for Config.kt (Task N4C.4). + import wasi:config/store@0.2.0-draft; + + // Not pulled in transitively (same check as above). Needed for KeyValue.kt (Task N4C.4): + // the types/eventual/eventual-batch interfaces it wraps, plus wasi-keyvalue-error (its own + // `error` resource, whose [method]/[resource-drop] we call directly, not just a type-only + // reference). + import wasi:keyvalue/types@0.1.0; + import wasi:keyvalue/eventual@0.1.0; + import wasi:keyvalue/eventual-batch@0.1.0; + import wasi:keyvalue/wasi-keyvalue-error@0.1.0; + + // Not pulled in transitively (same check as above). Needed for Blobstore.kt (Task N4C.4). + // Unversioned package, like wasi:logging. wasi:io/streams@0.2.3 and wasi:io/error@0.2.3 + // (also used by Blobstore.kt, for the output-stream write path) are NOT added here -- they + // were already part of this world's import surface before this SDK added any custom host + // API. + import wasi:blobstore/types; + import wasi:blobstore/container; + import wasi:blobstore/blobstore; + + // Not pulled in transitively (same check as above). Needed for QuotaApi.kt (Task N4C.4). + import golem:quota/types@1.5.0; + + // Not pulled in transitively (same check as above). Needed for Rdbms.kt's Postgres port + // (Task N4C.4). + import golem:rdbms/postgres@1.5.0; + + // Not pulled in transitively (same check as above). Needed for Rdbms.kt's MySQL port + // (Task N4C.4). + import golem:rdbms/mysql@1.5.0; + + // Not pulled in transitively (same check as above). Needed for Rdbms.kt's Ignite port + // (Task N4C.4) -- the last of the three RDBMS backends. + import golem:rdbms/ignite2@1.5.0; +} From 15c0eb55b5ebccda799d8dc8dcfad55bd46b5bd5 Mon Sep 17 00:00:00 2001 From: JohnSColeman Date: Mon, 13 Jul 2026 00:58:49 +0700 Subject: [PATCH 06/11] docs(kotlin): README and API documentation --- sdks/kotlin/README.md | 283 +++++++++++ sdks/kotlin/docs/api/agent-model.md | 380 +++++++++++++++ sdks/kotlin/docs/api/context.md | 174 +++++++ sdks/kotlin/docs/api/durability.md | 165 +++++++ sdks/kotlin/docs/api/guards-checkpoint.md | 244 ++++++++++ sdks/kotlin/docs/api/host-api.md | 280 +++++++++++ sdks/kotlin/docs/api/middleware.md | 170 +++++++ sdks/kotlin/docs/api/oplog.md | 328 +++++++++++++ sdks/kotlin/docs/api/quota.md | 202 ++++++++ sdks/kotlin/docs/api/rdbms.md | 556 ++++++++++++++++++++++ sdks/kotlin/docs/api/retry.md | 325 +++++++++++++ sdks/kotlin/docs/api/rpc.md | 319 +++++++++++++ sdks/kotlin/docs/api/secrets.md | 97 ++++ sdks/kotlin/docs/api/tools.md | 217 +++++++++ sdks/kotlin/docs/api/transactions.md | 244 ++++++++++ sdks/kotlin/docs/api/types.md | 236 +++++++++ sdks/kotlin/docs/api/wasi.md | 244 ++++++++++ 17 files changed, 4464 insertions(+) create mode 100644 sdks/kotlin/README.md create mode 100644 sdks/kotlin/docs/api/agent-model.md create mode 100644 sdks/kotlin/docs/api/context.md create mode 100644 sdks/kotlin/docs/api/durability.md create mode 100644 sdks/kotlin/docs/api/guards-checkpoint.md create mode 100644 sdks/kotlin/docs/api/host-api.md create mode 100644 sdks/kotlin/docs/api/middleware.md create mode 100644 sdks/kotlin/docs/api/oplog.md create mode 100644 sdks/kotlin/docs/api/quota.md create mode 100644 sdks/kotlin/docs/api/rdbms.md create mode 100644 sdks/kotlin/docs/api/retry.md create mode 100644 sdks/kotlin/docs/api/rpc.md create mode 100644 sdks/kotlin/docs/api/secrets.md create mode 100644 sdks/kotlin/docs/api/tools.md create mode 100644 sdks/kotlin/docs/api/transactions.md create mode 100644 sdks/kotlin/docs/api/types.md create mode 100644 sdks/kotlin/docs/api/wasi.md diff --git a/sdks/kotlin/README.md b/sdks/kotlin/README.md new file mode 100644 index 0000000000..99398fa371 --- /dev/null +++ b/sdks/kotlin/README.md @@ -0,0 +1,283 @@ +# Golem Kotlin SDK + +**Write durable, distributed [Golem](https://golem.cloud) agents in idiomatic Kotlin — compiled +natively to a WebAssembly Component.** No JavaScript, no QuickJS, no bundling: your Kotlin source +compiles straight to Wasm (WasmGC) and links the Golem host interfaces directly. + +> **Status:** the SDK covers the full agent programming model plus the large majority of Golem's +> host capabilities (durability, oplog, retries, transactions, RPC, RDBMS, the WASI capability set, +> and more). See the [Capability matrix](#capability-matrix) for exactly what is complete, partial, +> and not-yet-started. + +--- + +## Why native compile-to-Wasm + +Golem runs agents as WebAssembly Components against a set of WIT-defined host interfaces +(`golem:agent`, `golem:api/*`, `golem:durability`, `wasi:*`, …). Most language SDKs reach Wasm by +compiling to JavaScript first and then embedding a JS engine (QuickJS via `componentize-js`). + +This Kotlin SDK takes the **native path** instead: Kotlin/Wasm (the WasmGC IR backend) compiles your +agent — and the SDK itself — directly into one Wasm module, which `wasm-tools` turns into a Component +that links the Golem host imports through the raw canonical ABI. + +| | JS-embedding path | **This SDK (native)** | +|---|---|---| +| Runtime engine | QuickJS interpreter inside the component | none — your code *is* Wasm | +| Bundle | JS bundle + engine | single Kotlin/Wasm module | +| Host calls | marshalled through the JS shim | direct canonical-ABI `@WasmImport`/`@WasmExport` | +| Typical size | large (engine included) | small (a counter agent is ~250 KB) | + +The result is a smaller, faster component with no interpreter overhead, programmed in ordinary +Kotlin — data classes, sealed classes, enums, `kotlin.time.Duration`, and so on map straight onto +Golem's value model. + +--- + +## Quickstart + +**Requirements:** JDK 17+, the `golem` CLI, and `wasm-tools` on `PATH`. The Gradle plugin drives the +Kotlin/Wasm compile and the `wasm-tools embed → new → validate` pipeline for you. + +```bash +# 1. Scaffold a project (a durable counter agent, mounted at /counters/{name}) +golem new --template kotlin --component-name example:counter --yes app +cd app + +# 2. Build → a validated Wasm Component (runs `gradle nativeComponent` under the hood: +# Kotlin/Wasm compile → wasm-tools component embed → new --adapt (WASI p1→p2) → validate) +golem build + +# 3. Deploy to a running Golem +golem server run # local server, in another terminal +golem deploy --yes + +# 4a. Invoke via the CLI / REPL +golem agent invoke 'CounterAgent("c1")' increment # -> 1 +golem agent invoke 'CounterAgent("c1")' getValue # -> 1 + +# 4b. Invoke over HTTP (the @Endpoint-exposed routes) +curl -X POST http://localhost:9006/counters/c1/increment # -> 2 +curl http://localhost:9006/counters/c1/value # -> 2 +``` + +The agent that scaffolds: + +```kotlin +@Agent(mount = "/counters/{name}", description = "A durable counter agent") +class CounterAgent(val name: String) : BaseAgent() { + + private var value: Int = 0 + + @Prompt("Increase the count by one") + @Description("Increments the counter and returns the new value") + @Endpoint(post = "/increment") + fun increment(): Int { + value++ + return value + } + + @Prompt("Get the current counter value") + @Description("Returns the current value without modifying it") + @Endpoint(get = "/value") + fun getValue(): Int = value +} +``` + +`value` is ordinary agent state — Golem persists it durably. Each distinct `{name}` is its own agent +instance with independent, crash-proof state. A KSP processor reads the annotations at compile time +and generates the real `@WasmExport("golem:agent/guest@2.0.0#…")` entry points. + +See **[the agent model](docs/api/agent-model.md)** for the full programming surface. + +--- + +## Capability matrix + +Each capability links to its dedicated API doc with signatures and worked examples. Status is +cross-referenced against the Scala reference SDK — **15 complete, 3 partial** (each with a single +scoped remainder), and nothing outstanding that is the SDK's own to build. + +### ✅ Complete + +| Capability | What it gives you | Docs | +|---|---|---| +| **Agent model** | `@Agent` / `@Endpoint` / `@Prompt` / `@Description` / `@ReadOnly`, `BaseAgent` (incl. caller `principal`), mounting, HTTP exposure + compile-time route validation, KSP registration | [agent-model.md](docs/api/agent-model.md) | +| **Type mapping** | Kotlin ⇄ WIT: primitives, data classes→record, sealed→variant, enums, `List`/`Map`/`Pair`/`Triple`, nullable→option, `Datetime`, `Either`→`result`, arbitrarily nested | [types.md](docs/api/types.md) | +| **Host API** (`golem:api/host`) | agent metadata & registry, ids, lifecycle (update/fork/revert), oplog/idempotence primitives | [host-api.md](docs/api/host-api.md) | +| **Oplog** (`golem:api/oplog`) | read/search the operation log; full 46-case `PublicOplogEntry` decode | [oplog.md](docs/api/oplog.md) | +| **Retry + Retry DSL** (`golem:api/retry`) | policy/predicate host binding + an ergonomic Kotlin DSL (`Duration`, infix `and`/`or`, `Props.x eq y`) | [retry.md](docs/api/retry.md) | +| **Transactions** | saga / compensation with an `Either` result model | [transactions.md](docs/api/transactions.md) | +| **Context** (`golem:api/context`) | spans & invocation-context for distributed tracing | [context.md](docs/api/context.md) | +| **RPC** (`golem:agent/host` wasm-rpc) | `WasmRpc` + `@RemoteAgent` KSP-generated typed clients; sync / async / scheduled invocations | [rpc.md](docs/api/rpc.md) | +| **RDBMS** (`golem:rdbms`) | Postgres, MySQL & Ignite: connections, transactions, parameterized queries, full value coverage | [rdbms.md](docs/api/rdbms.md) | +| **Quota** (`golem:quota`) | quota reservation / token model | [quota.md](docs/api/quota.md) | +| **WASI capabilities** | KeyValue, Blobstore, Config, Logging, Environment | [wasi.md](docs/api/wasi.md) | +| **Guards & Checkpoint** | scoped persistence / idempotence / atomic / retry-policy guards; crash-consistent checkpoints & `revert` | [guards-checkpoint.md](docs/api/guards-checkpoint.md) | +| **Secrets** (`golem:secrets`) | reveal a `secret` handle to its inner typed value | [secrets.md](docs/api/secrets.md) | +| **Snapshotting** (`Snapshotted`) | opt-in typed agent-state save/restore: KSP auto-derives a byte codec from `S`, wrapped in a principal-carrying envelope; drives the guest `save-snapshot`/`load-snapshot` a manual (snapshot-based) update invokes, reconstructing the agent from its id on load | [agent-model.md](docs/api/agent-model.md#state-snapshotting) | +| **Utility types** | caller `Principal` (via `BaseAgent.principal`), `Uuid`, `Datetime`, unsigned integers, and `Either`→`result` | [types.md](docs/api/types.md) | + +### 🟡 Partial + +| Capability | State | Docs | +|---|---|---| +| **Durability** | `persist`/`read` done (composite payloads supported); only `lazy-initialized-pollable` deferred | [durability.md](docs/api/durability.md) | +| **Tools** (`golem:tool`) | expose + discovery + all invoke forms (`invoke` / `invokeAndAwait` / `asyncInvokeAndAwait`, composite payloads); only streamed stdin is a follow-up | [tools.md](docs/api/tools.md) | +| **Middleware** (auth / CORS) | metadata threads into the agent-type + compile-time route validation; request-time HTTP enforcement is host-side (deferred) | [middleware.md](docs/api/middleware.md) | + +### ❌ Not started + +- **Request-time HTTP routing/enforcement** — host-side; the SDK's metadata + compile-time + validation are done. +- **Bridge SDK generation** — `golem-cli` can generate a typed REST client library for calling + agents from *outside* Golem, but only for Rust/TypeScript/Scala/MoonBit; `golem-cli build` + currently errors *"Bridge generation is not yet supported for Kotlin"*. This is a `golem-cli` + code-generator gap, not an SDK-runtime one, and does **not** affect in-Golem agent-to-agent calls + (see **RPC** above — `@RemoteAgent`, complete). + +Everything else above is complete or partial. + +--- + +## Use cases + +- **Durable stateful services.** A counter, a shopping cart, a workflow, a game session — hold state + as ordinary Kotlin fields and let Golem persist it. Survives crashes, restarts, and redeploys with + no external database. +- **Long-running / reliable workflows.** Compose multi-step business processes with + [transactions](docs/api/transactions.md) (automatic compensation on failure) and tune failure + handling with the [retry DSL](docs/api/retry.md). +- **Agent meshes.** Have agents call each other with typed [RPC](docs/api/rpc.md) clients — blocking, + async (futures), or scheduled/cancelable. +- **HTTP-exposed microservices.** Annotate methods with `@Endpoint(get/post/…)` and Golem serves them + as REST routes under the agent's mount path. +- **Data-backed agents.** Talk to [Postgres / MySQL / Ignite](docs/api/rdbms.md), object storage and + key-value stores ([WASI capabilities](docs/api/wasi.md)) directly from the host. +- **Auditable systems.** Read your own [oplog](docs/api/oplog.md) for introspection, debugging, or + event-sourcing-style replay. + +--- + +## Kotlin-specific notes + +- **Kotlin/Wasm (WasmGC), `wasmWasi` target.** The SDK is a normal Kotlin/Wasm dependency compiled + into your agent's module. Requires a Kotlin/Wasm-capable toolchain (JDK 17+); Golem's engine runs + it with the `wasm_gc`, `function_references`, and `exceptions` proposals enabled. +- **Idiomatic types map directly.** Data classes become records, sealed hierarchies become variants, + enums become enums, `List`/`Map`/`Pair`/`Triple`/`T?` map to their WIT equivalents — arbitrarily + nested — as agent constructor params, method params, and return types. See + [types.md](docs/api/types.md). +- **Integer widths round-trip through WIT.** `Int`→`s32`, `Long`→`s64`, `UInt`→`u32`, etc. — pick the + Kotlin width that matches the value you mean. +- **Resource handles are `AutoCloseable`-style.** Host resources (oplog readers, spans, DB + connections, `WasmRpc` clients, …) hold a Wasm handle you must `close()` when done; the docs mark + each one. Prefer `use { }` where the type supports it. +- **Synchronous host model.** Golem's host calls are synchronous at the ABI boundary, so APIs that are + `Future`-based in other SDKs (e.g. transactions) are ported as straight-line Kotlin. Async only + appears where the host itself is async (RPC futures, pollables). +- **Compile-time codegen via KSP.** The `golem-kotlin-ksp` processor generates the guest exports, the + agent-type metadata, and `@RemoteAgent` typed RPC clients. It is a normal `kspWasmWasi` dependency. +- **One package per app (for now).** All `@Agent` classes in a project must currently share a single + Kotlin package — the generated registration entry point references each `register()` without + per-package import plumbing. Multiple packages produce a KSP build error; multi-package support is + not yet available. + +--- + +## Project layout + +``` +sdks/kotlin/ +├── sdk/ # the SDK — commonMain (annotations, BaseAgent) + wasmWasiMain (runtime, host bindings) +├── ksp/ # the KSP processor: annotations → guest exports, agent-type, RPC clients +├── gradle-plugin/ # `cloud.golem.wasm-component` plugin: Kotlin/Wasm compile → wasm-tools → validate +├── wit-native/ # the minimal WIT world the agent component targets (the ABI contract) +├── example/ # the reference counter agent +├── scripts/ # optional dev/CI verification tooling (not required to build or ship) +└── docs/api/ # the per-API documentation linked above +``` + +`scripts/` is **optional** — `native-e2e.sh` reproduces the full `golem new → build → deploy → +invoke` pipeline for verification, but nothing in the build, the plugin, or a scaffolded project +depends on it. `native-contract-tests.sh` is a contract-test harness: one probe per capability +proving the compiled-Kotlin ⇄ host ABI boundary against a locally built server (see +`scripts/contract-tests/README.md`) — for CI/regression use. + +A scaffolded project depends on the SDK and processor as ordinary Gradle artifacts: + +```kotlin +plugins { + kotlin("multiplatform") version "2.4.0" + id("com.google.devtools.ksp") version "2.3.9" + id("cloud.golem.wasm-component") version "0.0.0-SNAPSHOT" +} + +kotlin { + wasmWasi { binaries.executable(); nodejs() } + sourceSets { + val wasmWasiMain by getting { + dependencies { implementation("cloud.golem:golem-kotlin-sdk:0.0.0-SNAPSHOT") } + } + } +} +dependencies { add("kspWasmWasi", "cloud.golem:golem-kotlin-ksp:0.0.0-SNAPSHOT") } +``` + +### Dependency layers + +The four modules are **independent Gradle builds** (each with its own `settings.gradle.kts`), +published to mavenLocal at `cloud.golem:*:0.0.0-SNAPSHOT`. None depends on another at build time — +the only edges point *from a consumer* into all three. The runtime SDK carries **zero external +library dependencies** (every host capability is a raw canonical-ABI `@WasmImport`/`@WasmExport`), +which keeps the compiled agent module small. + +``` +┌─ BUILD-TIME (JVM · run on the toolchain, not shipped) ───────────────┐ +│ ksp → com.google.devtools.ksp:symbol-processing-api │ +│ (test: kotlin-test, dev.zacsweers.kctfork:ksp) │ +│ gradle-plugin → Gradle API + java-gradle-plugin only │ +│ · bundles wit-native/ as wit-native.zip │ +│ · shells out to the wasm-tools CLI │ +└──────────────────────────────────────────────────────────────────────┘ + ▲ all three published to mavenLocal, pulled in by a consumer ▼ +┌─ RUNTIME (Kotlin/Wasm · compiled INTO the agent module) ─────────────┐ +│ sdk → NO external runtime deps — Kotlin stdlib only │ +│ (test: kotlin-test on wasmWasiTest) │ +└──────────────────────────────────────────────────────────────────────┘ +┌─ CONTRACT (data · not a Gradle module, not compiled) ────────────────┐ +│ wit-native/ → WIT interfaces (golem:*, wasi:*) — the ABI │ +│ contract the SDK bindings + KSP codegen target │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +Required external tools (on `PATH`, not Gradle dependencies): JDK 17+, Gradle, the `wasm-tools` +and `golem` CLIs, and a WASI p1→p2 reactor adapter. + +--- + +## Full documentation index + +**Core** +- [Agent model](docs/api/agent-model.md) — `@Agent`, `BaseAgent`, endpoints, lifecycle +- [Type mapping](docs/api/types.md) — Kotlin ⇄ WIT / schema values + +**Durability & reliability** +- [Host API](docs/api/host-api.md) +- [Durability](docs/api/durability.md) +- [Oplog](docs/api/oplog.md) +- [Retry & Retry DSL](docs/api/retry.md) +- [Transactions](docs/api/transactions.md) +- [Guards & Checkpoint](docs/api/guards-checkpoint.md) +- [Secrets](docs/api/secrets.md) +- [Context / tracing](docs/api/context.md) + +**Distribution & data** +- [RPC / agent-to-agent](docs/api/rpc.md) +- [RDBMS (Postgres / MySQL / Ignite)](docs/api/rdbms.md) +- [Quota](docs/api/quota.md) +- [WASI capabilities (KeyValue / Blobstore / Config / Logging / Environment)](docs/api/wasi.md) + +**Extending agents** +- [Tools](docs/api/tools.md) +- [Middleware (auth / CORS)](docs/api/middleware.md) diff --git a/sdks/kotlin/docs/api/agent-model.md b/sdks/kotlin/docs/api/agent-model.md new file mode 100644 index 0000000000..3130a246dc --- /dev/null +++ b/sdks/kotlin/docs/api/agent-model.md @@ -0,0 +1,380 @@ +# Agent Model + +> The core programming model of the Golem Kotlin SDK: `@Agent`, `BaseAgent`, `@Endpoint`, `@Prompt`, `@Description`, and how KSP turns an annotated class into a registered, durable, HTTP-exposed agent compiled natively to Wasm. **Status:** Complete (per capability ledger). + +## Overview + +The Golem Kotlin SDK lets you write **durable agents** as ordinary Kotlin classes and +compile them *natively* to a WebAssembly Component (WasmGC) — no JavaScript, no QuickJS. +You annotate a class with `@Agent`, extend [`BaseAgent`](#baseagent), and annotate methods +with `@Endpoint`. At compile time a KSP processor +(`cloud.golem.ksp.GolemAgentProcessor`) reads those annotations and generates: + +- the agent-type registration (constructor params + method signatures as Golem schema types), +- the real `@WasmExport("golem:agent/guest@2.0.0#...")` guest functions the Golem host calls, +- the WIT surface, and +- HTTP endpoint wiring for the declared routes. + +Each agent instance is identified by its constructor parameters and gets **independent, +host-managed persistent state**. The runtime persists and replays your agent so that its +in-memory fields (like a counter's `value`) survive restarts, upgrades, and failures. + +See [Types](types.md) for how Kotlin constructor/method parameter and return types map to +Golem's WIT/schema value model, and the [SDK README](../../README.md) for build/deploy flow. + +## `@Agent` + +Marks a class as a Golem agent. Applied to the class; retained at runtime. + +```kotlin +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Agent( + val mount: String = "", + val description: String = "", + /** If true, the mount's `http-mount-details.auth-details` requires authentication. */ + val auth: Boolean = false, + /** Allowed CORS origin patterns for the mount, e.g. `["*"]`. Empty = no CORS headers. */ + val cors: Array = [], + /** + * `"durable"` (default) or `"ephemeral"` -- mirrors `golem:agent/common@2.0.0`'s + * `agent-mode` enum and Scala's `@agentDefinition(mode = DurabilityMode....)`. + */ + val mode: String = "durable", + /** + * Snapshotting cadence, using the same DSL as Scala's `@agentDefinition(snapshotting = ...)`: + * `"disabled"` (default), `"enabled"` (server default cadence), `"periodic()"` + * (periodic snapshots every `` nanoseconds), or `"every()"` (every `` + * invocations, `` must fit a u16: 0..65535). + */ + val snapshotting: String = "disabled" +) +``` + +| Parameter | Type | Default | Meaning | +|-----------|------|---------|---------| +| `mount` | `String` | `""` | HTTP mount path for the agent, with `{param}` segments bound to constructor parameters, e.g. `"/counters/{name}"`. Empty = no HTTP mount. | +| `description` | `String` | `""` | Human-readable agent description. A class-level [`@Description`](#description) overrides this if present. | +| `auth` | `Boolean` | `false` | If `true`, the mount requires authentication (`http-mount-details.auth-details`). | +| `cors` | `Array` | `[]` | Allowed CORS origin patterns for the mount, e.g. `["*"]`. Empty means no CORS headers. | +| `mode` | `String` | `"durable"` | `"durable"` or `"ephemeral"`; mirrors `golem:agent/common@2.0.0`'s `agent-mode` enum. | +| `snapshotting` | `String` | `"disabled"` | Snapshot cadence DSL: `"disabled"`, `"enabled"`, `"periodic()"`, or `"every()"` (count must fit a u16, `0..65535`). | + +Notes on how these are consumed by KSP (`GolemAgentProcessor.buildAgentModel`): + +- `mount` → the agent's `mountPath`. +- `description` is the primary source, but a **class-level `@Description(text = ...)` wins** + when present. +- `auth`/`cors` populate the mount's auth/CORS details; `cors` is read as a `List`. +- `mode` defaults to `"durable"`, `snapshotting` to `"disabled"` when unset. + +## `BaseAgent` + +Every agent class must extend `BaseAgent`. It exposes the agent's self-identity, read from +the Golem host at call time (host-backed — real values only appear inside the Golem Wasm +runtime). + +```kotlin +abstract class BaseAgent { + /** Canonical string agent ID: component + agent type + constructor parameters. */ + val agentId: String + + /** Agent type name (best-effort — see the SDK host bindings). */ + val agentType: String + + /** Agent name / primary constructor parameter (best-effort). */ + val agentName: String + + /** The authenticated identity of the caller of the current invocation. */ + val principal: Principal +} +``` + +- `agentId` — canonical string ID: component + agent type + constructor parameters. +- `agentType` — the agent type name (best-effort). +- `agentName` — the agent name / primary constructor parameter (best-effort). +- `principal` — **who invoked the current method.** The Golem host passes an authenticated + identity to every `initialize`/`invoke`; the SDK decodes it and exposes it here. It is a sealed + type: + + ```kotlin + sealed class Principal { + data class Oidc(val sub: String, val issuer: String, val email: String?, /* …name, claims, … */) : Principal() + data class Agent(val agentId: String) : Principal() // another Golem agent + data class GolemUser(val accountId: Uuid) : Principal() // a Golem user account + object Anonymous : Principal() // no authenticated caller + } + ``` + + ```kotlin + @Endpoint(post = "/admin") + fun adminAction(): String = when (val p = principal) { + is Principal.Oidc -> "hello ${p.email ?: p.sub}" + is Principal.GolemUser -> "user ${p.accountId}" + is Principal.Agent -> "agent ${p.agentId}" + Principal.Anonymous -> throw IllegalStateException("authentication required") + } + ``` + +`agentId`/`agentType`/`agentName` are backed by internal `expect` functions resolved per platform; +outside the Golem runtime they do not return meaningful values. `principal` reflects the identity +of the in-flight invocation (`Principal.Anonymous` outside one). + +## `@Endpoint` + +Marks a method as an invocable agent method and, when an HTTP verb is set, exposes it as an +HTTP route. Applied to functions; retained at runtime. + +```kotlin +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Endpoint( + val post: String = "", + val get: String = "", + val put: String = "", + val delete: String = "", + val path: String = "", + /** If true, the endpoint's `http-endpoint-details.auth-details` requires authentication. */ + val auth: Boolean = false, + /** Allowed CORS origin patterns for the endpoint, e.g. `["*"]`. Empty = no CORS headers. */ + val cors: Array = [] +) +``` + +| Parameter | Type | Default | Meaning | +|-----------|------|---------|---------| +| `post` | `String` | `""` | POST route (relative to the agent mount), e.g. `"/increment"`. | +| `get` | `String` | `""` | GET route, e.g. `"/value"`. | +| `put` | `String` | `""` | PUT route. | +| `delete` | `String` | `""` | DELETE route. | +| `path` | `String` | `""` | Endpoint path (used where a verb is not the route carrier). | +| `auth` | `Boolean` | `false` | If `true`, the endpoint requires authentication. | +| `cors` | `Array` | `[]` | Allowed CORS origin patterns for this endpoint. Empty = no CORS headers. | + +How KSP maps verbs to routes (`GolemAgentProcessor.buildMethodModel`): each **non-empty** +verb string produces its own HTTP endpoint (`GET`/`POST`/`PUT`/`DELETE`), all sharing the +method's `auth` and `cors`. A single method may therefore expose more than one verb/route. +Only **declared** functions carrying `@Endpoint` become agent methods (inherited members are +excluded and duplicates are deduped by name). + +The method's parameter and return types are resolved through +[`TypeMapper`](types.md) — see [Types](types.md) for what's supported. + +## `@Prompt` + +Attaches an LLM-facing prompt hint to a method, describing what invoking it does. + +```kotlin +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Prompt(val hint: String = "") +``` + +The `hint` string is lowered into the method's `agent-method.prompt-hint` in the generated +agent-type metadata (empty → `none`). + +## `@ReadOnly` + +Marks a method as **read-only** — it does not mutate agent state, so Golem may cache its result. + +```kotlin +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class ReadOnly(val cache: String = "until-write") +``` + +`cache` is a DSL string (same style as `@Agent(snapshotting = ...)`): + +| Value | Meaning | +|---|---| +| `"until-write"` (default) | cache until the next state-mutating call | +| `"no-cache"` | never cache | +| `"ttl()"` | cache for a fixed duration in nanoseconds, e.g. `"ttl(5000000000)"` | + +It lowers to `agent-method.read-only = some(read-only-config { cache-policy, uses-principal })`. +`uses-principal` is currently always `false` (the SDK has no `Principal`-typed parameters yet). + +```kotlin +@Prompt("Get the current counter value") +@ReadOnly("ttl(1000000000)") // safe to cache for 1s — never mutates state +@Endpoint(get = "/value") +fun getValue(): Int = value +``` + +## `@Description` + +Human-readable description, usable on **both a class and a method**. + +```kotlin +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Description(val text: String = "") +``` + +- On a **class**: overrides `@Agent(description = ...)` when present. +- On a **method**: supplies that method's description in the generated model. + +## State snapshotting + +Opt an agent into snapshot-based (manual) updates by mixing in `Snapshotted` alongside +`BaseAgent`, with `S` as your state type: + +```kotlin +data class CounterState(val value: Int) + +@Agent(mount = "/counters/{name}", description = "A durable counter agent", snapshotting = "every(1)") +class CounterAgent(val name: String) : BaseAgent(), Snapshotted { + override var state = CounterState(0) + + @Endpoint(post = "/increment") + fun increment(): Int { state = CounterState(state.value + 1); return state.value } + + @Endpoint(get = "/value") + fun getValue(): Int = state.value +} +``` + +- **KSP derives the codec.** At compile time KSP resolves `S`'s `TypeDesc` and generates a + byte-level save/load codec from it — you never write serialization. `S` **must** be a + WIT-mappable type (data class, `List`/`Map`/`Pair`/`Triple`, enum, sealed class, primitive, + `Datetime`, `Either`); a non-mappable `S` is a **compile-time error**, never a silent empty + snapshot. This mirrors Scala's `Snapshotted[S]`, minus `stateSchema` (KSP derives it). +- **Opt-out is the default.** An agent that does not mix in `Snapshotted` produces an empty + snapshot (no-op). The guest `save-snapshot`/`load-snapshot` exports are always present, like + `initialize`/`invoke`. +- **Caller identity survives.** The runtime auto-serializes `state` and wraps it in a + principal-carrying envelope, so the identity captured at `initialize` is restored on load. +- **`@Agent(snapshotting = …)` is independent.** It advertises the snapshot *cadence* to the + host (see [`@Agent`](#agent)); mixing in `Snapshotted` is what provides the *state* the host + saves and restores. + +On a manual (snapshot-based) update the host invokes `save-snapshot` on the old component +revision and `load-snapshot` on the new one. Because the host recovers a snapshot-updated worker +without replaying the original `initialize`, the SDK reconstructs the agent from its own agent-id +(the constructor parameters encoded in it) inside `load-snapshot`, then restores `state` — so typed +state survives both a revision bump and a worker restart. + +## How KSP builds the agent + +`GolemAgentProcessor.process` runs at compile time and, for every `@Agent` class: + +1. **Builds an agent model** (`buildAgentModel`): reads `mount`, `description` (with the + class `@Description` override), `auth`, `cors`, `mode`, `snapshotting`; resolves each + primary-constructor parameter's type; and collects declared `@Endpoint` methods. +2. **Builds each method model** (`buildMethodModel`): reads the `@Prompt` hint, method + `@Description`, endpoint `auth`/`cors`, expands each non-empty HTTP verb into an endpoint, + and resolves parameter + return types. +3. **Emits code**: `NativeRegistrationEmitter` (registration + real + `@WasmExport golem:agent/guest@2.0.0` functions + `SchemaValue` converters), + `WitEmitter` (the WIT surface). +4. **Emits one entry point** (`emitEntryPoint`) that registers every `@Agent`. Registration + is triggered from this generated entry point, not from your `main()`. + +**Constraint:** all `@Agent` classes must currently share a single package, so the generated +native entry point can reference each `register()` without per-package import +plumbing. Multiple packages produce a KSP error. Multi-package support is not yet available. + +## Examples + +### The counter agent (canonical) + +```kotlin +package counter + +import cloud.golem.BaseAgent +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Description +import cloud.golem.annotations.Endpoint +import cloud.golem.annotations.Prompt + +@Agent(mount = "/counters/{name}", description = "A durable counter agent") +class CounterAgent(val name: String) : BaseAgent() { + + private var value: Int = 0 + + @Prompt("Increase the count by one") + @Description("Increments the counter and returns the new value") + @Endpoint(post = "/increment") + fun increment(): Int { + value++ + return value + } + + @Prompt("Get the current counter value") + @Description("Returns the current value without modifying it") + @Endpoint(get = "/value") + fun getValue(): Int = value +} +``` + +`POST /counters/alice/increment` and `GET /counters/alice/value` operate on the `alice` +instance; `bob` gets an independent, separately persisted counter. + +### A richer agent: auth, CORS, ephemeral mode, snapshotting, multiple verbs + +```kotlin +package shop + +import cloud.golem.BaseAgent +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Description +import cloud.golem.annotations.Endpoint +import cloud.golem.annotations.Prompt + +@Agent( + mount = "/carts/{userId}", + description = "A shopping cart scoped to a user", + auth = true, + cors = ["https://shop.example.com"], + mode = "durable", + snapshotting = "every(50)" +) +class CartAgent(val userId: String) : BaseAgent() { + + private val items = mutableMapOf() + + @Prompt("Add a quantity of a product to the cart") + @Description("Adds `qty` of `sku` and returns the new total item count") + @Endpoint(post = "/items", auth = true) + fun addItem(sku: String, qty: Int): Int { + items[sku] = (items[sku] ?: 0) + qty + return items.values.sum() + } + + @Prompt("List the current cart contents") + @Description("Returns the SKU -> quantity map") + @Endpoint(get = "/items") + fun listItems(): Map = items.toMap() + + @Prompt("Remove a product from the cart entirely") + @Description("Removes `sku`; returns true if it was present") + @Endpoint(delete = "/items/{sku}") + fun removeItem(sku: String): Boolean = items.remove(sku) != null + + @Prompt("Report who owns this cart") + @Description("Returns the host-backed agent id") + @Endpoint(get = "/whoami") + fun whoAmI(): String = agentId // from BaseAgent +} +``` + +This agent shows: a mount with a `{userId}` segment bound to the constructor parameter; +mount-level `auth`/`cors`; `mode = "durable"` with `snapshotting = "every(50)"`; per-endpoint +`auth`; multiple verbs (`POST`, `GET`, `DELETE`); composite return types (`Map`, +`Boolean`) resolved via [Types](types.md); and use of `BaseAgent.agentId`. + +## Notes + +- **Native path.** Agents compile natively to Wasm (WasmGC). There is no JS/QuickJS layer on + this path. +- **Single package.** Until multi-package support lands, keep all `@Agent` classes in one + package or KSP will error. +- **Description precedence.** A class-level `@Description` overrides `@Agent(description=...)`. + A method's description comes from a method-level `@Description`. +- **Verbs are independent.** Every non-empty verb on `@Endpoint` yields its own route; set + several to expose one method under multiple verbs. +- **Registration is generated.** You do not call any register function yourself — the KSP + entry point does it. Just annotate and extend `BaseAgent`. +- **`BaseAgent` identity is host-backed.** `agentId`/`agentType`/`agentName` only return real + values inside the Golem runtime; `agentType`/`agentName` are best-effort. +- See [Types](types.md) for supported parameter/return types and the WIT strings they produce. diff --git a/sdks/kotlin/docs/api/context.md b/sdks/kotlin/docs/api/context.md new file mode 100644 index 0000000000..4331b09701 --- /dev/null +++ b/sdks/kotlin/docs/api/context.md @@ -0,0 +1,174 @@ +# Context API + +> `ContextApi` — the native Kotlin/Wasm binding to Golem's invocation-context / tracing interface `golem:api/context@1.5.0`. **Status:** 🟢 Available. + +## Overview + +`ContextApi` (`cloud.golem.runtime.host.ContextApi`) exposes Golem's tracing model: **spans** +(units of work you open and close) and the **invocation context** (the stack of attributes +accumulated by automatic and user-defined spans, plus trace/span ids and trace-context +headers). It mirrors OpenTelemetry-style tracing but is backed entirely by the Golem host. + +Both `span` and `invocation-context` are component-model **resources**, so `Span` and +`InvocationContext` wrap raw host handles that are **not** tied to Kotlin/Wasm GC — each must be +`close()`d. For `Span`, dropping without an explicit `finish()` is the *normal* lifecycle: the +host finishes the span automatically at drop time, so `finish()` is only for an early finish. + +Use `ContextApi` from inside an [`@Agent`](agent-model.md) method to add structured tracing +around operations, or to read trace ids for correlating logs. See the SDK overview in +[`../../README.md`](../../README.md). + +## API reference + +### Types + +```kotlin +/** wasi:clocks/wall-clock@0.2.3's datetime record. */ +data class ContextDateTime(val seconds: Long, val nanoseconds: Int) + +/** golem:api/context@1.5.0's attribute-value variant (currently one case: a string). */ +sealed class AttributeValue { + data class StringValue(val value: String) : AttributeValue() +} + +data class Attribute(val key: String, val value: AttributeValue) +data class AttributeChain(val key: String, val values: List) +``` + +### Entry point + +```kotlin +object ContextApi { + /** Start a new span with the given name, as a child of the current invocation context. */ + fun startSpan(name: String): Span + + /** The current invocation context. */ + fun currentContext(): InvocationContext + + /** Allow/disallow forwarding trace-context headers in outgoing HTTP; returns the previous value. */ + fun allowForwardingTraceContextHeaders(allow: Boolean): Boolean +} +``` + +### `Span` + +```kotlin +/** + * A unit of work (golem:api/context@1.5.0's span resource). MUST be close()d when done. + * Dropping without finish() is the normal lifecycle — the host finishes it at drop time. + */ +class Span { + /** When the span was started. */ + fun startedAt(): ContextDateTime + + /** Set a single attribute on the span. */ + fun setAttribute(name: String, value: AttributeValue) + + /** Set several attributes at once. */ + fun setAttributes(attributes: List) + + /** Early-finish the span; otherwise it finishes automatically when close()d. */ + fun finish() + + fun close() +} +``` + +### `InvocationContext` + +```kotlin +/** + * Query the stack of attributes created by automatic and user-defined spans + * (golem:api/context@1.5.0's invocation-context resource). MUST be close()d when done. + */ +class InvocationContext { + fun traceId(): String + fun spanId(): String + + /** The parent context, if any. The returned InvocationContext MUST also be close()d. */ + fun parent(): InvocationContext? + + fun getAttribute(key: String, inherited: Boolean): AttributeValue? + fun getAttributes(inherited: Boolean): List + + fun getAttributeChain(key: String): List + fun getAttributeChains(): List + + /** W3C trace-context headers to forward on outgoing requests. */ + fun traceContextHeaders(): List> + + fun close() +} +``` + +- `getAttribute` / `getAttributes` take `inherited: Boolean` — when `true`, attributes from + parent spans are included, not only those set on the current context. +- `getAttributeChain(key)` returns every value recorded for `key` up the span stack; + `getAttributeChains()` returns all keys' chains at once. + +## Examples + +### Tracing an operation with a span + +```kotlin +@Agent +class OrderAgent : BaseAgent() { + + @Endpoint + fun placeOrder(sku: String, qty: Int): String { + val span = ContextApi.startSpan("place-order") + try { + span.setAttributes( + listOf( + Attribute("sku", AttributeValue.StringValue(sku)), + Attribute("qty", AttributeValue.StringValue(qty.toString())), + ) + ) + + val result = reserveInventory(sku, qty) // ... real work ... + + span.setAttribute("result", AttributeValue.StringValue(result)) + return result + } finally { + // close() finishes the span (no explicit finish() needed for the normal path). + span.close() + } + } +} +``` + +### Reading trace ids and inherited attributes + +```kotlin +val ctx = ContextApi.currentContext() +try { + val traceId = ctx.traceId() + val spanId = ctx.spanId() + val tenant = ctx.getAttribute("tenant", inherited = true) + log("trace=$traceId span=$spanId tenant=$tenant") +} finally { + ctx.close() +} +``` + +### Forwarding trace-context headers downstream + +```kotlin +val previous = ContextApi.allowForwardingTraceContextHeaders(true) +// ... make outgoing HTTP calls; W3C traceparent/tracestate headers are now propagated ... +ContextApi.allowForwardingTraceContextHeaders(previous) // restore +``` + +## Notes + +- Binds `golem:api/context@1.5.0` (as declared verbatim in the source `@WasmImport` bindings). +- `Span` and `InvocationContext` are resource-backed and **must** be `close()`d — an unclosed + handle leaks in the host's resource table until the whole component instance tears down. + `parent()` returns a fresh `InvocationContext` that must be closed too. +- Every `Span` / `InvocationContext` method throws if called after `close()`. +- `attribute-value` currently models a single WIT case (`StringValue`); the `sealed class` + leaves room for future cases without a breaking change. +- Unlike `golem:api/host`'s `get-agents`, `span` and `invocation-context` have no WIT + `constructor`: handles are obtained only via `startSpan` / `currentContext`. +- See also the runtime primitives on [`HostApi`](host-api.md) and the durability model in + [Durability API](durability.md). diff --git a/sdks/kotlin/docs/api/durability.md b/sdks/kotlin/docs/api/durability.md new file mode 100644 index 0000000000..2c699ca91e --- /dev/null +++ b/sdks/kotlin/docs/api/durability.md @@ -0,0 +1,165 @@ +# Durability API + +> `DurabilityApi` — the native Kotlin/Wasm binding to Golem's durable-execution interface `golem:durability/durability@1.5.0`. **Status:** 🟡 Partial (persist + read of durable invocations work for arbitrary composite payloads; only `lazy-initialized-pollable` is deferred). + +## Overview + +`DurabilityApi` (`cloud.golem.runtime.host.DurabilityApi`) exposes the low-level durability +primitives Golem uses to make external side effects replay-safe. During the first execution of +an agent invocation the runtime is *live* and calls really happen; on replay the runtime +returns the persisted results instead of re-executing. This interface is how the SDK observes +those calls, delimits durable-function regions, and persists / reads back the recorded +request/response pairs. + +Most application code does not call `DurabilityApi` directly — it underpins the higher-level +transaction and guard machinery. Reach for it when you are wrapping a custom external side +effect and need it to participate in Golem's replay model. It complements the oplog primitives +on [`HostApi`](host-api.md); see the SDK overview in [`../../README.md`](../../README.md). + +## API reference + +### Types + +```kotlin +/** + * golem:api/oplog@1.5.0's wrapped-function-type (aliased durable-function-type in + * golem:durability@1.5.0). Case order and payload shape preserved. + */ +sealed class DurableFunctionType { + object ReadLocal : DurableFunctionType() + object WriteLocal : DurableFunctionType() + object ReadRemote : DurableFunctionType() + object WriteRemote : DurableFunctionType() + data class WriteRemoteBatched(val begin: Long?) : DurableFunctionType() + data class WriteRemoteTransaction(val begin: Long?) : DurableFunctionType() +} + +/** golem:durability@1.5.0's oplog-entry-version enum. */ +enum class OplogEntryVersion { V1, V2 } + +/** golem:durability@1.5.0's durable-execution-state record. */ +data class DurableExecutionState( + val isLive: Boolean, + val persistenceLevel: HostApi.PersistenceLevel, +) + +/** A persisted durable function invocation, read back during replay. */ +data class PersistedDurableFunctionInvocation( + val timestampSeconds: Long, + val timestampNanoseconds: Int, + val functionName: String, + val response: TypedSchemaValue, + val functionType: DurableFunctionType, + val entryVersion: OplogEntryVersion, +) +``` + +### Observation & region markers + +```kotlin +object DurabilityApi { + /** Record that an (interface, function) host call is about to occur. */ + fun observeFunctionCall(iface: String, function: String) + + /** Open a durable-function region; returns the begin oplog index. */ + fun beginDurableFunction(functionType: DurableFunctionType): Long + + /** Close a durable-function region opened by beginDurableFunction. */ + fun endDurableFunction( + functionType: DurableFunctionType, + beginIndex: Long, + forcedCommit: Boolean, + ) + + /** Whether execution is currently live (vs. replaying) and the active persistence level. */ + fun currentDurableExecutionState(): DurableExecutionState +} +``` + +- `beginDurableFunction` / `endDurableFunction` bracket a durable operation; pass the `Long` + returned by `begin` as `beginIndex` to `end`. +- `currentDurableExecutionState().isLive` is `true` on the original run and `false` during + replay — the standard way to guard "only do this once" logic. + +### Persist & read durable invocations + +```kotlin +/** + * Write a durable-function-invocation record to the agent's oplog. request/response are + * self-describing typed-schema-values (any composite payload supported). + */ +fun persistDurableFunctionInvocation( + functionName: String, + request: TypedSchemaValue, + response: TypedSchemaValue, + functionType: DurableFunctionType, +) + +/** Read the next persisted durable-function invocation from the oplog during replay. */ +fun readPersistedDurableFunctionInvocation(): PersistedDurableFunctionInvocation +``` + +`request` and `response` are `TypedSchemaValue`s — a schema graph paired with a value — encoded +and decoded via the SDK's typed-schema-value support, which handles **arbitrary composite +payloads** (records, variants, enums, lists, options, tuples, maps, results — nested). + +## Examples + +### Making a custom side effect replay-safe + +```kotlin +@Agent +class QuoteAgent : BaseAgent() { + + @Endpoint + fun currentPrice(symbol: String): Long { + val state = DurabilityApi.currentDurableExecutionState() + + val begin = DurabilityApi.beginDurableFunction(DurableFunctionType.ReadRemote) + val price: Long = if (state.isLive) { + val fetched = fetchPriceFromExternalApi(symbol) // real call, first run only + DurabilityApi.persistDurableFunctionInvocation( + functionName = "quote.currentPrice", + request = TypedSchemaValue("string", SchemaValue.Str(symbol)), + response = TypedSchemaValue("s64", SchemaValue.S64(fetched)), + functionType = DurableFunctionType.ReadRemote, + ) + fetched + } else { + // Replay: return the recorded result instead of hitting the network again. + val persisted = DurabilityApi.readPersistedDurableFunctionInvocation() + (persisted.response.value as SchemaValue.S64).v + } + DurabilityApi.endDurableFunction(DurableFunctionType.ReadRemote, begin, forcedCommit = false) + return price + } +} +``` + +A `TypedSchemaValue` pairs a WIT type string with the matching `SchemaValue` — a primitive +(`"s64"` ↔ `SchemaValue.S64`) or a composite (`"record"` ↔ +`SchemaValue.Record(listOf(SchemaValue.S32(…), SchemaValue.Str(…)))`). On read-back, +pattern-match or cast `response.value` to the expected variant. + +### Observing a host call + +```kotlin +DurabilityApi.observeFunctionCall("golem:api/host@1.5.0", "generate-idempotency-key") +``` + +## Notes + +- **Status: 🟡 Partial.** `persistDurableFunctionInvocation` / + `readPersistedDurableFunctionInvocation` are implemented and verified, including **arbitrary + composite** `typed-schema-value` payloads (round-trip-tested for records, variants, enums, + lists, options, tuples, maps, results, and nested combinations). +- **Deferred:** `lazy-initialized-pollable` (a WIT `resource` on this interface) is not yet + bound. It's an async-pollable primitive with no consumer in the SDK's synchronous model — + grouped with the deferred `wasi:io/poll` workstream, not a durability-specific gap. +- `golem:durability/durability@1.5.0` is not pulled in transitively by anything else, so the + native world imports it explicitly. +- `DurableFunctionType` mirrors `golem:api/oplog@1.5.0`'s `wrapped-function-type` exactly, + including the `option` payload on the two batched/transaction cases. +- These primitives underpin, and are usually reached through, the SDK's transaction and guard + helpers rather than being called directly. See also the oplog primitives on + [`HostApi`](host-api.md). diff --git a/sdks/kotlin/docs/api/guards-checkpoint.md b/sdks/kotlin/docs/api/guards-checkpoint.md new file mode 100644 index 0000000000..5e03ce2f22 --- /dev/null +++ b/sdks/kotlin/docs/api/guards-checkpoint.md @@ -0,0 +1,244 @@ +# Guards & Checkpoint + +> Scoped runtime controls (`Guards`) and oplog-index revert points (`Checkpoint`), built on the existing `HostApi` oplog/persistence/idempotence primitives — no new WIT surface. **Status:** Complete (`Guards` covers persistence, idempotence, atomic-operation, and retry-policy guards; `Checkpoint` complete). + +## Overview + +Both types are native ports of the Scala SDK (`Guards.scala`, `Checkpoint.scala`), recast as +**synchronous** Kotlin. The Scala versions wrap each call in a `Future`, but that is a +Scala.js environment artifact — the underlying `HostApi` calls have no async host boundary, +so the faithful native translation is plain synchronous code (no coroutines, no `suspend`). + +- **`Guards`** applies a scoped change to the runtime (persistence level, idempotence mode, or + an atomic region) and guarantees it is restored/closed afterward. +- **`Checkpoint`** captures the current Golem oplog index so execution can be *reverted* back + to that point — the building block for "try this, and if it doesn't work out, rewind." + +## `Guards` API reference + +Every `use*` method applies a change and returns a [`Guard`](#guard) that restores the +previous value when [`Guard.drop`](#guard) (or `close()`) is called. Every `with*` method +applies the change for the duration of a `block` and guarantees restoration afterward (even on +exception). + +### Persistence level + +```kotlin +fun usePersistenceLevel(level: HostApi.PersistenceLevel): PersistenceLevelGuard +fun withPersistenceLevel(level: HostApi.PersistenceLevel, block: () -> A): A +``` + +Sets the oplog persistence level, restoring the previous level when the guard is dropped / +the block returns. + +### Idempotence mode + +```kotlin +fun useIdempotenceMode(flag: Boolean): IdempotenceModeGuard +fun withIdempotenceMode(flag: Boolean, block: () -> A): A +``` + +Enables or disables idempotence mode for the scope, then restores the previous setting. + +### Atomic operation + +```kotlin +fun markAtomicOperation(): AtomicOperationGuard +fun atomically(block: () -> A): A +``` + +`markAtomicOperation` opens an atomic region (via `HostApi.markBeginOperation`) and returns a +guard whose `drop()` commits it (`markEndOperation`). `atomically` runs `block` atomically: +on success the region is committed; on failure it calls the host `trap` function, which +surfaces as an **uncatchable** wasm trap (so caller code cannot observe the failure via +`try`/`catch`). The atomic region is intentionally left open on trap — Golem's replay-time +fallback in `markBeginOperation` deletes the partial inner side effects and re-executes the +block. + +### Retry policy + +```kotlin +fun useRetryPolicy(policy: NamedRetryPolicy): RetryPolicyGuard +fun useRetryPolicy(policy: NamedPolicy): RetryPolicyGuard // Retry-DSL overload +fun withRetryPolicy(policy: NamedRetryPolicy, block: () -> A): A +fun withRetryPolicy(policy: NamedPolicy, block: () -> A): A // Retry-DSL overload +``` + +Registers `policy` as the current agent's retry policy for the scope, then restores the policy +previously registered under the same name when the guard is dropped / the block returns — +removing it if none existed. The `NamedPolicy` overloads accept a policy built with the +[Retry DSL](./retry.md); both delegate to [`RetryApi`](./retry.md). + +```kotlin +import cloud.golem.runtime.Guards +import cloud.golem.runtime.host.* // Retry DSL: Policy, Props, NamedPolicy +import kotlin.time.Duration.Companion.milliseconds + +val aggressive = NamedPolicy( + name = "aggressive", + policy = Policy.exponential(baseDelay = 50.milliseconds, factor = 2.0).maxRetries(10), + predicate = Props.statusCode.oneOf(502L, 503L), +) + +Guards.withRetryPolicy(aggressive) { + callFlakyDownstream() // runs under the aggressive policy; prior policy restored afterward +} +``` + +### `Guard` + +```kotlin +sealed class Guard(private val release: () -> Unit) : AutoCloseable { + final override fun close() // = drop() + fun drop() // idempotent: releases once, then no-ops +} + +class PersistenceLevelGuard : Guard +class IdempotenceModeGuard : Guard +class AtomicOperationGuard : Guard +class RetryPolicyGuard : Guard +``` + +`Guard` implements `AutoCloseable`, so a `use*` guard works with Kotlin's `use { }`. `drop()` +is idempotent — releasing an already-released guard does nothing. + +## `Checkpoint` API reference + +```kotlin +class Checkpoint private constructor(private val oplogIndex: Long) +``` + +Captures the current oplog index and can revert execution back to it. Construct one with the +[`Checkpoint()` factory](#companion-factories) (`operator fun invoke`) rather than the private +constructor. Reverting uses [`Either`](./transactions.md#either) from the `Transactions` +module. + +### Instance members + +```kotlin +fun revert(): Nothing +fun unwrapOrRevert(result: Either<*, T>): T +fun runOrRevert(fn: () -> Either<*, T>): T +fun tryOrRevert(fn: () -> T): T +fun assertOrRevert(condition: Boolean) +``` + +- `revert()` — resets the oplog index to the captured point and never returns normally. +- `unwrapOrRevert(result)` — returns the value of an `Either.Right`, or reverts on + `Either.Left`. +- `runOrRevert(fn)` — runs `fn`, reverting if it returns `Either.Left`. +- `tryOrRevert(fn)` — runs `fn`, reverting if it **throws**. +- `assertOrRevert(condition)` — reverts if `condition` is `false`. + +### Companion factories + +```kotlin +operator fun invoke(): Checkpoint +fun withCheckpoint(fn: (Checkpoint) -> Either<*, T>): T +fun withCheckpointTry(fn: (Checkpoint) -> T): T +``` + +- `Checkpoint()` — creates a checkpoint at the current oplog index. +- `withCheckpoint(fn)` — creates a checkpoint, runs `fn`, and reverts if it returns + `Either.Left`. +- `withCheckpointTry(fn)` — creates a checkpoint, runs `fn`, and reverts if it throws. + +## Examples + +### Turning off persistence for a noisy, non-durable side effect + +```kotlin +import cloud.golem.runtime.Guards +import cloud.golem.runtime.HostApi + +@Agent +class MetricsAgent { + @Endpoint + fun recordAndReturn(value: Int): Int = + // Metrics emission shouldn't bloat the oplog; restore the level afterward. + Guards.withPersistenceLevel(HostApi.PersistenceLevel.PERSIST_NOTHING) { + metrics.emit("value", value) + value * 2 + } +} +``` + +### Atomic multi-step effect + +```kotlin +import cloud.golem.runtime.Guards + +@Endpoint +fun transfer(from: String, to: String, amount: Long) { + Guards.atomically { + ledger.debit(from, amount) + ledger.credit(to, amount) + // If either line traps, the partial effects are discarded and the block re-runs on replay. + } +} +``` + +### Manual guard with `use { }` + +```kotlin +import cloud.golem.runtime.Guards + +@Endpoint +fun withIdempotentScope() { + Guards.useIdempotenceMode(true).use { // AutoCloseable -> restored at end of block + doSomethingThatMayReplay() + } +} +``` + +### Checkpoint — rewind on a failed validation + +```kotlin +import cloud.golem.runtime.Checkpoint +import cloud.golem.runtime.Either + +@Endpoint +fun applyChange(input: String): String = + Checkpoint.withCheckpoint { _ -> + val staged = stage(input) // side effects recorded in the oplog + if (staged.isValid) Either.Right(staged.summary) + else Either.Left("invalid") // reverts to before stage(...) + } +``` + +### Checkpoint — explicit assertion / try variants + +```kotlin +import cloud.golem.runtime.Checkpoint + +@Endpoint +fun guardedWork(): String { + val cp = Checkpoint() + val result = doRiskyWork() + cp.assertOrRevert(result.isNotEmpty()) // rewind if the postcondition fails + return result +} + +@Endpoint +fun tryWork(): String = + Checkpoint.withCheckpointTry { _ -> + callThatMightThrow() // any throw rewinds to the checkpoint + } +``` + +## Notes + +- `Guards` and `Checkpoint` share the same oplog-index mechanism that + [`Transactions`](./transactions.md) uses; `Transactions` is in fact built on + `Guards.markAtomicOperation`. +- `atomically` and `Checkpoint.revert` never return normally on their failure paths — a trap + or an oplog rewind, respectively — so treat them as terminal in control flow. +- `Guard.drop()` is safe to call more than once; `with*` helpers always call it in a `finally`. +- To change retry behaviour, use the [Retry DSL](./retry.md) — the retry guards from the + Scala SDK are intentionally not part of `Guards` here. + +## See also + +- [Transactions](./transactions.md) — saga/compensation built on these guards. +- [Retry](./retry.md) — declarative retry policies (replaces the omitted retry guards). +- [Kotlin SDK README](../../README.md) diff --git a/sdks/kotlin/docs/api/host-api.md b/sdks/kotlin/docs/api/host-api.md new file mode 100644 index 0000000000..af89a2cdd2 --- /dev/null +++ b/sdks/kotlin/docs/api/host-api.md @@ -0,0 +1,280 @@ +# Host API + +> `HostApi` — the native Kotlin/Wasm binding to Golem's runtime host interface `golem:api/host@1.5.0` (plus `golem:agent/host@2.0.0` for agent-id parsing). **Status:** 🟢 Available. + +## Overview + +`HostApi` is a Kotlin `object` (`cloud.golem.runtime.HostApi`) that exposes Golem's runtime +host API directly over the WebAssembly Component Model canonical ABI — no JavaScript layer. +Every function below is a thin wrapper over a raw `@WasmImport` binding whose signature was +verified against the WIT in `wit-native/deps/golem-1.x/`. + +It groups four capability families: + +- **Oplog / atomic regions / persistence / idempotence** — the low-level primitives the + durability, transaction, and guard machinery build on (see [Durability API](durability.md)). +- **Agent metadata** — read the current agent's metadata or another agent's, and resolve + component/agent references into ids. +- **Agent id parsing & the type registry** — parse an agent-id string, and enumerate or look + up registered agent types. +- **Agent lifecycle** — update, fork, revert, and fork-at-point. + +Reach for `HostApi` from inside an [`@Agent`](agent-model.md) method when you need runtime +identity, deterministic idempotency keys, atomic oplog regions, or agent lifecycle control. +For the SDK overview see [`../../README.md`](../../README.md). + +Two supporting types recur throughout: + +```kotlin +/** A Golem UUID (two u64 halves, matching golem:core/types@2.0.0's uuid record). */ +data class Uuid(val highBits: Long, val lowBits: Long) + +/** golem:core/types@2.0.0's component-id record: {uuid: uuid}. */ +data class ComponentId(val uuid: Uuid) + +/** golem:core/types@2.0.0's environment-id record: {uuid: uuid}. */ +data class EnvironmentId(val uuid: Uuid) + +/** golem:core/types@2.0.0's agent-id: {component-id, agent-id: string}. */ +data class AgentId(val componentId: ComponentId, val agentId: String) +``` + +## API reference + +### Oplog, atomic regions, persistence & idempotence + +```kotlin +object HostApi { + enum class PersistenceLevel { PERSIST_NOTHING, PERSIST_REMOTE_SIDE_EFFECTS, SMART } + + fun getOplogIndex(): Long + fun setOplogIndex(index: Long) + + fun markBeginOperation(): Long + fun markEndOperation(begin: Long) + fun oplogCommit(replicas: Int) + + fun getOplogPersistenceLevel(): PersistenceLevel + fun setOplogPersistenceLevel(level: PersistenceLevel) + + fun getIdempotenceMode(): Boolean + fun setIdempotenceMode(flag: Boolean) + + fun generateIdempotencyKey(): Uuid + + /** Unconditionally traps the current invocation; never returns. */ + fun trap(reason: String): Nothing +} +``` + +- `markBeginOperation()` returns a begin marker (an oplog index) to pass to + `markEndOperation(begin)`, delimiting an atomic region. +- `generateIdempotencyKey()` returns a deterministic, replay-stable `Uuid`. +- `trap(reason)` surfaces as an uncatchable wasm trap on the host side and drives the standard + trap-recovery flow; its return type is `Nothing`. + +### Agent metadata + +```kotlin +/** golem:api/host@1.5.0's agent-status enum (case order preserved). */ +enum class AgentStatus { RUNNING, IDLE, SUSPENDED, INTERRUPTED, RETRYING, FAILED, EXITED } + +/** golem:api/host@1.5.0's agent-metadata record. */ +data class AgentMetadata( + val agentId: AgentId, + val args: List, + val env: List>, + val config: List>, + val status: AgentStatus, + val componentRevision: Long, + val retryCount: Long, + val environmentId: EnvironmentId, +) +``` + +```kotlin +/** The current agent's own metadata, including its full agent-id string. */ +fun getSelfMetadata(): AgentMetadata + +/** Another agent's metadata, or null if it does not exist. */ +fun getAgentMetadata(agentId: AgentId): AgentMetadata? + +/** Resolve a component reference string into a component-id, or null. */ +fun resolveComponentId(componentReference: String): ComponentId? + +/** Resolve a component reference + agent name into an agent-id, or null. */ +fun resolveAgentId(componentReference: String, agentName: String): AgentId? + +/** Strict variant of resolveAgentId (see golem-host.wit), or null. */ +fun resolveAgentIdStrict(componentReference: String, agentName: String): AgentId? +``` + +### Agent id parsing & the type registry + +```kotlin +/** Result of parseAgentId: the agent-type name plus an optional phantom UUID. */ +data class ParsedAgentId(val agentTypeName: String, val phantom: Uuid?) + +sealed class ParseAgentIdResult { + data class Ok(val value: ParsedAgentId) : ParseAgentIdResult() + data class Err(val error: AgentError) : ParseAgentIdResult() +} + +/** golem:agent@2.0.0's agent-error variant. */ +sealed class AgentError { + data class InvalidInput(val message: String) : AgentError() + data class InvalidMethod(val message: String) : AgentError() + data class InvalidType(val message: String) : AgentError() + data class InvalidAgentId(val message: String) : AgentError() + object CustomError : AgentError() +} + +/** A registered agent type: its name and the component that implements it. */ +data class RegisteredAgentType(val typeName: String, val implementedBy: ComponentId) +``` + +```kotlin +/** Parse an agent-id string into its agent-type name and optional phantom UUID. */ +fun parseAgentId(agentId: String): ParseAgentIdResult + +/** Every agent type currently registered with the Golem host. */ +fun getAllAgentTypes(): List + +/** Look up a single registered agent type by name, or null. */ +fun registeredAgentType(typeName: String): RegisteredAgentType? +``` + +`parseAgentId` binds `golem:agent/host@2.0.0`'s `parse-agent-id`. It deliberately does **not** +expose the constructor parameters that the underlying WIT result also carries — decoding that +`typed-schema-value` (an arbitrary recursive `schema-graph`) is out of scope, and Scala's own +`HostApi.parseAgentId` drops it for the same reason. Likewise `RegisteredAgentType` projects +only the two fields Scala's public `RegisteredAgentType` exposes, discarding the richer +`agent-type` schema/methods/http-mount detail. + +### Agent lifecycle + +```kotlin +/** golem:api/host@1.5.0's update-mode enum. */ +enum class UpdateMode { AUTOMATIC, SNAPSHOT_BASED } + +/** golem:api/host@1.5.0's revert-agent-target variant. */ +sealed class RevertAgentTarget { + data class RevertToOplogIndex(val oplogIndex: Long) : RevertAgentTarget() + data class RevertLastInvocations(val count: Long) : RevertAgentTarget() +} + +/** golem:api/host@1.5.0's fork-result variant. */ +sealed class ForkResult { + data class Original(val forkedPhantomId: Uuid) : ForkResult() + data class Forked(val forkedPhantomId: Uuid) : ForkResult() +} +``` + +```kotlin +fun updateAgent(agentId: AgentId, targetRevision: Long, mode: UpdateMode) + +fun forkAgent(sourceAgentId: AgentId, targetAgentId: AgentId, cutOff: Long) + +fun revertAgent(agentId: AgentId, target: RevertAgentTarget) + +/** Forks the current agent at the current execution point. */ +fun fork(): ForkResult +``` + +`fork()` returns `ForkResult.Original` in the parent execution and `ForkResult.Forked` in the +new fork — branch on it to run divergent logic in each. + +### Agent enumeration (resource handle) + +```kotlin +/** Handle to an in-progress agent enumeration. MUST be close()d when done. */ +class GetAgentsHandle { + /** The next batch of agent metadata, or null when the enumeration is exhausted. */ + fun getNext(): List? + fun close() +} + +/** Start enumerating every agent of every agent type in the given component. */ +fun getAgents(componentId: ComponentId): GetAgentsHandle +``` + +`getAgents` wraps a raw component-model `resource` handle, which is **not** tied to Kotlin/Wasm +GC — an unclosed handle leaks in the host's resource table until the whole component instance +tears down. Always `close()` it (a `try`/`finally` is the idiomatic form). Filtering is not yet +supported: the enumeration always covers every agent of every type in the component. + +## Examples + +### Deterministic idempotency key inside an atomic region + +```kotlin +@Agent +class PaymentAgent : BaseAgent() { + + @Endpoint + fun charge(amountCents: Long): String { + // Stable across replays — safe to send to an external payment provider. + val key = HostApi.generateIdempotencyKey() + + val begin = HostApi.markBeginOperation() + try { + // ... perform the side-effecting work here ... + return "charged $amountCents (key=${key.highBits}:${key.lowBits})" + } finally { + HostApi.markEndOperation(begin) + } + } +} +``` + +### Reading self-identity and the agent registry + +```kotlin +@Agent +class DiagnosticsAgent : BaseAgent() { + + @Endpoint + fun whoAmI(): String { + val self = HostApi.getSelfMetadata() + val parsed = when (val r = HostApi.parseAgentId(self.agentId.agentId)) { + is ParseAgentIdResult.Ok -> r.value.agentTypeName + is ParseAgentIdResult.Err -> "unknown (${r.error})" + } + val registered = HostApi.getAllAgentTypes().map { it.typeName } + return "type=$parsed status=${self.status} registered=$registered" + } +} +``` + +### Enumerating agents, closing the handle + +```kotlin +val handle = HostApi.getAgents(componentId) +try { + val all = buildList { + while (true) { + val batch = handle.getNext() ?: break + addAll(batch) + } + } + // ... use `all` ... +} finally { + handle.close() +} +``` + +## Notes + +- `HostApi` is only meaningful inside the Golem wasm runtime; the host imports have no + behaviour outside it. +- [`BaseAgent`](agent-model.md) surfaces the host-backed identity properties `agentId` and + `agentType`. `BaseAgent.agentType` is derived via `parseAgentId`. `BaseAgent.agentName` + remains best-effort/unwired on the native path: there is no well-defined WIT-level way to + derive an "agent name" (Scala's value comes from a JS-shim-only field with no WIT + equivalent). +- `parseAgentId`, `getAllAgentTypes`, and `registeredAgentType` bind + `golem:agent/host@2.0.0`; every other function binds `golem:api/host@1.5.0`. +- Constructor parameters (from `parse-agent-id`) and the full `agent-type` schema are + intentionally not decoded — they require lifting an arbitrary recursive `schema-graph`, + matching the projection Scala's own public API makes. +- `GetAgentsHandle` filtering (`agent-any-filter`) is a deferred follow-up. diff --git a/sdks/kotlin/docs/api/middleware.md b/sdks/kotlin/docs/api/middleware.md new file mode 100644 index 0000000000..4065f97f9c --- /dev/null +++ b/sdks/kotlin/docs/api/middleware.md @@ -0,0 +1,170 @@ +# Auth / CORS Middleware + +> Declaring authentication and CORS metadata on an agent's HTTP mount (`@Agent`) and its +> individual endpoints (`@Endpoint`), threaded into the `golem:agent/common@2.0.0` agent-type. +> **Status:** 🟡 Partial — the metadata threads through to the agent-type (jco-verified) and +> route templates are validated at compile time; request-time HTTP enforcement of auth/CORS is +> host-side and not yet wired. + +## Overview + +An [`@Agent`](agent-model.md) can expose an HTTP mount, and each method can expose one or more +[`@Endpoint`](agent-model.md)s under it. Both carry two pieces of middleware metadata: + +- **`auth`** — a `Boolean`. When true, the corresponding `auth-details.required` flag is set in + the agent-type. +- **`cors`** — an array/list of allowed origin patterns (e.g. `["*"]`). Empty means no CORS + headers are declared. + +At build time the KSP-generated descriptor carries these fields, and +`AgentTypeModel.lowerAgentType` lowers them into the canonical-ABI `agent-type` record that +Golem reads via `get-definition()`. The KSP processor also **validates the route templates at +compile time** (see [Compile-time route validation](#compile-time-route-validation)) so +malformed mounts/endpoints fail the build rather than surfacing at deploy time. What remains +deferred is *request-time* enforcement: the auth/CORS values are emitted into the agent-type +(and round-trip through jco), but Golem's HTTP gateway — not the SDK — is responsible for +enforcing the auth requirement and applying the CORS policy on each request, and that +integration is not yet wired. Treat the annotations as declarative intent until it lands. + +## Compile-time route validation + +The KSP processor (`cloud.golem.ksp.HttpValidation`) checks every `@Agent(mount=...)` / +`@Endpoint` route template against the agent's constructor and method parameters, failing the +build (via `logger.error`) on a violation. Ported from the Scala SDK's `HttpValidation`, +restricted to the path-variable subset the Kotlin surface exposes and using the runtime's +segment convention (`{name}` = path variable, `{+name}` = catch-all). The rules: + +- a mount path may not contain a catch-all (`{+name}`) variable; +- every mount path variable must name a constructor parameter; +- every constructor parameter must be provided by the mount path (a mounted agent's identity + must be fully addressable from its URL); +- a method may not declare HTTP endpoints on an agent with no mount; +- every endpoint path variable must name a parameter of that method. + +Header/query-variable and `Principal`-rejection checks from the Scala version are intentionally not +ported: the Kotlin surface has no header/query-variable annotations, and while a caller +[`Principal`](agent-model.md#baseagent) now exists (read via `BaseAgent.principal`), it is **not** +an agent-surface *parameter* type — so there is no Principal-typed path/mount variable for those +checks to reject. + +For the SDK overview see [`../../README.md`](../../README.md). + +## `@Agent` — mount-level auth / CORS + +`cloud.golem.annotations.Agent`. `auth` and `cors` apply to the agent's HTTP mount +(`http-mount-details`): + +```kotlin +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Agent( + val mount: String = "", + val description: String = "", + /** If true, the mount's `http-mount-details.auth-details` requires authentication. */ + val auth: Boolean = false, + /** Allowed CORS origin patterns for the mount, e.g. `["*"]`. Empty = no CORS headers. */ + val cors: Array = [], + val mode: String = "durable", + val snapshotting: String = "disabled" +) +``` + +The mount's auth/CORS are only emitted when the agent declares a non-empty `mount`. + +## `@Endpoint` — endpoint-level auth / CORS + +`cloud.golem.annotations.Endpoint`. `auth` and `cors` apply to that single endpoint +(`http-endpoint-details`): + +```kotlin +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Endpoint( + val post: String = "", + val get: String = "", + val put: String = "", + val delete: String = "", + val path: String = "", + /** If true, the endpoint's `http-endpoint-details.auth-details` requires authentication. */ + val auth: Boolean = false, + /** Allowed CORS origin patterns for the endpoint, e.g. `["*"]`. Empty = no CORS headers. */ + val cors: Array = [] +) +``` + +## How it threads into the agent-type + +The annotations feed the native descriptor (`cloud.golem.runtime.NativeAgentDescriptor`), whose +relevant fields are: + +```kotlin +data class NativeAgentDescriptor( + // ... + val mountPath: String, + /** From `@Agent(auth=..., cors=...)` — the mount's auth-details / cors-options. */ + val mountAuth: Boolean = false, + val mountCors: List = emptyList(), + // ... +) + +data class NativeHttpEndpoint( + val verb: String, + val path: String, + val auth: Boolean = false, // from @Endpoint(auth = ...) + val cors: List = emptyList() // from @Endpoint(cors = ...) +) +``` + +`AgentTypeModel.lowerAgentType` then lowers these into the canonical ABI: + +- **auth** → `option`: `none` when `auth = false`, otherwise + `some({ required: true })`. Applied to both the mount (`http-mount-details.auth-details`) and + each endpoint (`http-endpoint-details.auth-details`). +- **cors** → `cors-options { allowed-patterns: list }`: an empty list when no patterns + are declared, otherwise the declared origin patterns. Applied to the mount + (`http-mount-details.cors-options`) and each endpoint (`http-endpoint-details.cors-options`). + +## Examples + +An agent whose mount requires auth and allows one CORS origin, with a mix of protected and +public endpoints: + +```kotlin +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Endpoint + +@Agent( + mount = "/counter", + description = "A durable counter", + auth = true, + cors = ["https://app.example.com"] +) +class CounterAgent { + private var count: Int = 0 + + // Inherits the mount's auth requirement; adds a wildcard CORS policy for this endpoint. + @Endpoint(post = "/increment", auth = true, cors = ["*"]) + fun increment(by: Int): Int { + count += by + return count + } + + // A public read endpoint: no auth required, no CORS headers declared. + @Endpoint(get = "/value") + fun value(): Int = count +} +``` + +## Notes + +- **Metadata only, for now.** Auth/CORS are lowered into the agent-type and verified via jco, + but the HTTP gateway does not yet enforce the auth requirement or apply the CORS policy at + request time. Treat these annotations as declarative intent until enforcement lands. +- **`auth` is a plain boolean** — it maps to `auth-details.required`. There is no scope / + scheme / audience modelling yet. +- **`cors` is a list of origin patterns**, lowered verbatim into `cors-options.allowed-patterns`. + An empty array means no CORS headers are declared (not "deny all" enforcement — see above). +- Mount-level auth/CORS are only emitted when `@Agent(mount = ...)` is non-empty. + +See also: [Agent model](agent-model.md) · [Tools](tools.md) · [WASI](wasi.md) · +[SDK README](../../README.md). diff --git a/sdks/kotlin/docs/api/oplog.md b/sdks/kotlin/docs/api/oplog.md new file mode 100644 index 0000000000..094e649506 --- /dev/null +++ b/sdks/kotlin/docs/api/oplog.md @@ -0,0 +1,328 @@ +# Oplog API + +> `OplogApi` — the native Kotlin/Wasm binding to Golem's oplog **read** surface `golem:api/oplog@1.5.0` (`get-oplog` / `search-oplog`). **Status:** ✅ Complete. + +## Overview + +Every Golem agent is backed by an append-only *operation log* (the "oplog"): the durable +record of everything the agent did — invocations, host calls, spans, plugin activity, updates, +snapshots, retries. The oplog is what makes an agent replayable and durable. This API exposes +the two read primitives Golem offers over an agent's oplog: + +- [`GetOplog`](#getoplog) — read the oplog forward from a given index, in batches. +- [`SearchOplog`](#searchoplog) — full-text search across an oplog, returning matching + `(oplog-index, entry)` pairs. + +Both classes are thin wrappers over the corresponding `golem:api/oplog@1.5.0` resources (they +mirror the Scala SDK's `OplogApi.GetOplog` / `OplogApi.SearchOplog`). Each yields +[`PublicOplogEntry`](#publicoplogentry) values — a sealed hierarchy with one Kotlin data class +per WIT `public-oplog-entry` variant case (all 46 cases are typed). + +These are lower-level introspection tools: use them for auditing, debugging, building an +activity feed over an agent's history, or reacting to specific past operations. They complement +the durable-execution primitives on [`HostApi`](host-api.md) and +[`DurabilityApi`](durability.md); see the SDK overview in [`../../README.md`](../../README.md). + +Both resources hold a host-side handle that is **not** tied to Kotlin/Wasm GC — always call +`close()` when finished (a `try`/`finally` is idiomatic). + +Types live in `cloud.golem.runtime.host`. The `AgentId` / `ComponentId` / `EnvironmentId` / +`Uuid` / `TypedSchemaValue` / `HostApi.PersistenceLevel` types referenced below are documented +in [`host-api.md`](host-api.md) and [`types.md`](types.md). + +## API reference + +### `GetOplog` + +Reads an agent's oplog forward from a starting index. + +```kotlin +class GetOplog(agentId: AgentId, start: Long) { + /** The next batch of entries, or null once the oplog is exhausted. */ + fun getNext(): List? + + /** Releases the get-oplog handle's guest-side handle-table entry. */ + fun close() +} +``` + +`getNext()` returns entries in oplog order and paginates internally: keep calling it until it +returns `null`, which signals the oplog is exhausted. + +### `SearchOplog` + +Full-text search over an agent's oplog. Each result pairs the matching entry's oplog index with +the entry itself. + +```kotlin +class SearchOplog(agentId: AgentId, text: String) { + /** The next batch of (oplog-index, entry) matches, or null once exhausted. */ + fun getNext(): List>? + + /** Releases the search-oplog handle's guest-side handle-table entry. */ + fun close() +} +``` + +### `PublicOplogEntry` + +A sealed hierarchy over the WIT `public-oplog-entry` variant. Every case carries a +[`Timestamp`](#supporting-types) plus its case-specific fields. There are 46 cases; rather than +enumerate all of them, the shape is: + +```kotlin +sealed class PublicOplogEntry { + // ── Timestamp-only lifecycle markers ────────────────────────────── + data class Suspend(val timestamp: Timestamp) : PublicOplogEntry() + data class NoOp(val timestamp: Timestamp) : PublicOplogEntry() + data class Interrupted(val timestamp: Timestamp) : PublicOplogEntry() + data class Exited(val timestamp: Timestamp) : PublicOplogEntry() + data class BeginAtomicRegion(val timestamp: Timestamp) : PublicOplogEntry() + data class Restart(val timestamp: Timestamp) : PublicOplogEntry() + + // ── Control-flow / regions ──────────────────────────────────────── + data class Jump(val timestamp: Timestamp, val jump: OplogRegion) : PublicOplogEntry() + data class EndAtomicRegion(val timestamp: Timestamp, val beginIndex: Long) : PublicOplogEntry() + data class Revert(val timestamp: Timestamp, val droppedRegion: OplogRegion) : PublicOplogEntry() + + // ── Resource lifecycle ──────────────────────────────────────────── + data class CreateResource(val timestamp: Timestamp, val id: Long, val name: String, val owner: String) : PublicOplogEntry() + data class DropResource(val timestamp: Timestamp, val id: Long, val name: String, val owner: String) : PublicOplogEntry() + + // ── Durable host calls (request/response ride TypedSchemaValue) ──── + data class Start( + val timestamp: Timestamp, + val parentStartIndex: Long?, + val functionName: String, + val request: TypedSchemaValue?, + val durableFunctionType: DurableFunctionType + ) : PublicOplogEntry() + data class End(val timestamp: Timestamp, val startIndex: Long, val response: TypedSchemaValue?, val forcedCommit: Boolean) : PublicOplogEntry() + data class Cancelled(val timestamp: Timestamp, val startIndex: Long, val partial: TypedSchemaValue?) : PublicOplogEntry() + + // ── Errors, logging, retry policy ───────────────────────────────── + data class Error( + val timestamp: Timestamp, + val error: String, + val retryFrom: Long, + val insideAtomicRegion: Boolean, + val retryPolicyState: RetryPolicyState? + ) : PublicOplogEntry() + data class Log(val timestamp: Timestamp, val level: LogLevel, val context: String, val message: String) : PublicOplogEntry() + data class SetRetryPolicy(val timestamp: Timestamp, val policy: NamedRetryPolicy) : PublicOplogEntry() + data class RemoveRetryPolicy(val timestamp: Timestamp, val name: String) : PublicOplogEntry() + + // ── Invocation-context spans ────────────────────────────────────── + data class StartSpan( + val timestamp: Timestamp, + val spanId: String, + val parent: String?, + val linkedContextId: String?, + val attributes: List + ) : PublicOplogEntry() + data class FinishSpan(val timestamp: Timestamp, val spanId: String) : PublicOplogEntry() + data class SetSpanAttribute(val timestamp: Timestamp, val spanId: String, val key: String, val value: AttributeValue) : PublicOplogEntry() + + // ── Agent invocations ───────────────────────────────────────────── + data class AgentInvocationStarted(val timestamp: Timestamp, val invocation: AgentInvocation) : PublicOplogEntry() + data class AgentInvocationFinished( + val timestamp: Timestamp, + val result: AgentInvocationResult, + val methodName: String?, + val consumedFuel: Long, + val componentRevision: Long + ) : PublicOplogEntry() + data class PendingAgentInvocation(val timestamp: Timestamp, val invocation: AgentInvocation) : PublicOplogEntry() + data class CancelPendingInvocation(val timestamp: Timestamp, val idempotencyKey: String) : PublicOplogEntry() + + // ── Updates & snapshots ─────────────────────────────────────────── + data class PendingUpdate(val timestamp: Timestamp, val targetRevision: Long, val description: UpdateDescription) : PublicOplogEntry() + data class SuccessfulUpdate( + val timestamp: Timestamp, + val targetRevision: Long, + val newComponentSize: Long, + val newActivePlugins: List + ) : PublicOplogEntry() + data class FailedUpdate(val timestamp: Timestamp, val targetRevision: Long, val details: String?) : PublicOplogEntry() + data class Snapshot(val timestamp: Timestamp, val data: SnapshotData) : PublicOplogEntry() + + // ── Plugins, permission cards, remote transactions, memory, config ─ + data class ActivatePlugin(val timestamp: Timestamp, val plugin: PluginInstallationDescription) : PublicOplogEntry() + data class DeactivatePlugin(val timestamp: Timestamp, val plugin: PluginInstallationDescription) : PublicOplogEntry() + data class CardInstalled(val timestamp: Timestamp, val queuedEventIndex: Long?, val cardId: Uuid) : PublicOplogEntry() + // … BeginRemoteTransaction / CommittedRemoteTransaction / GrowMemory / + // ChangePersistenceLevel / OplogProcessorCheckpoint / CardEventQueued / + // CardInstallFailed / CardRevoked / CardExpired / … (see source for the full set) + + // ── The initial agent entry — the largest record (14 fields) ────── + data class Create( + val timestamp: Timestamp, + val agentId: AgentId, + val agentMode: AgentMode, + val componentRevision: Long, + val env: List>, + val createdBy: Uuid, + val environmentId: EnvironmentId, + val parent: AgentId?, + val componentSize: Long, + val initialTotalLinearMemorySize: Long, + val initialActivePlugins: List, + val localAgentConfig: List, + val originalPhantomId: Uuid?, + val instanceId: Uuid + ) : PublicOplogEntry() + + /** Defensive fallback for an unexpected/future variant tag; [tag] is the raw case index. */ + data class Unsupported(val tag: Int) : PublicOplogEntry() +} +``` + +The full list of 46 cases (as declared in the source) groups into: **lifecycle markers** +(`Suspend`, `NoOp`, `Interrupted`, `Exited`, `Restart`, `BeginAtomicRegion`, `EndAtomicRegion`); +**control flow** (`Jump`, `Revert`); **durable host calls** (`Start`, `End`, `Cancelled`); +**memory/storage** (`GrowMemory`, `FilesystemStorageUsageUpdate`, `ChangePersistenceLevel`); +**resources** (`CreateResource`, `DropResource`); **remote transactions** +(`BeginRemoteTransaction`, `PreCommitRemoteTransaction`, `PreRollbackRemoteTransaction`, +`CommittedRemoteTransaction`, `RolledBackRemoteTransaction`); **retry policy** (`SetRetryPolicy`, +`RemoveRetryPolicy`, `Error`); **logging** (`Log`); **spans** (`StartSpan`, `FinishSpan`, +`SetSpanAttribute`); **agent invocations** (`AgentInvocationStarted`, `AgentInvocationFinished`, +`PendingAgentInvocation`, `CancelPendingInvocation`); **updates/snapshots** (`PendingUpdate`, +`SuccessfulUpdate`, `FailedUpdate`, `Snapshot`); **plugins** (`ActivatePlugin`, +`DeactivatePlugin`, `OplogProcessorCheckpoint`); **permission cards** (`CardEventQueued`, +`CardInstalled`, `CardInstallFailed`, `CardRevoked`, `CardExpired`); the **`Create`** entry; and +the **`Unsupported`** fallback. + +### Supporting types + +Entry fields reference these value types (all in `cloud.golem.runtime.host`): + +```kotlin +/** WIT `timestamp`: seconds since the Unix epoch + sub-second nanos. */ +data class Timestamp(val seconds: Long, val nanoseconds: Int) + +/** WIT `oplog-region`: an inclusive [start, end] range of oplog indices. */ +data class OplogRegion(val start: Long, val end: Long) + +/** WIT `log-level`. */ +enum class LogLevel { STDOUT, STDERR, TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL } + +/** WIT `agent-mode`. */ +enum class AgentMode { DURABLE, EPHEMERAL } + +/** WIT `snapshot-data`: a worker-state snapshot blob + its MIME type. */ +data class SnapshotData(val data: List, val mimeType: String) + +/** WIT `plugin-installation-description`. */ +data class PluginInstallationDescription( + val grantId: Uuid, + val priority: Int, + val name: String, + val version: String, + val parameters: List> +) + +/** WIT `retry-policy-state`: persisted state of an active semantic retry policy (root = nodes[0]). */ +data class RetryPolicyState(val nodes: List) +``` + +Richer nested variants — `UpdateDescription` (`AutoUpdate` / `SnapshotBased`), `SpanData` +(`LocalSpan` / `ExternalSpan`), `AgentInvocation` (`AgentInitialization` / +`AgentMethodInvocation` / `SaveSnapshot` / `LoadSnapshot` / `ProcessOplogEntries` / +`ManualUpdate`), `AgentInvocationResult`, `StateNode`, `QueuedCardEvent`, and +`CardInstallFailure` — are declared alongside `PublicOplogEntry` in +`OplogApi.kt`; consult the source for their full shapes. + +## Examples + +### Auditing an agent's own oplog + +Because `GetOplog` / `SearchOplog` take an `AgentId`, an agent can read its **own** history +(`agentId` from `BaseAgent`) or, given the id, another agent's. + +```kotlin +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Endpoint +import cloud.golem.runtime.BaseAgent +import cloud.golem.runtime.host.GetOplog +import cloud.golem.runtime.host.LogLevel +import cloud.golem.runtime.host.PublicOplogEntry + +@Agent +class AuditAgent : BaseAgent() { + + /** Returns a human-readable summary of this agent's own recorded history. */ + @Endpoint + fun auditTrail(): List { + val summary = mutableListOf() + val oplog = GetOplog(agentId, start = 0L) + try { + while (true) { + val batch = oplog.getNext() ?: break // null => oplog exhausted + for (entry in batch) { + when (entry) { + is PublicOplogEntry.Create -> + summary += "created agent ${entry.agentId.agentId} (mode=${entry.agentMode})" + + is PublicOplogEntry.AgentInvocationFinished -> + summary += "invoked ${entry.methodName ?: ""}; fuel=${entry.consumedFuel}" + + is PublicOplogEntry.Log -> + if (entry.level == LogLevel.ERROR || entry.level == LogLevel.CRITICAL) + summary += "log[${entry.level}] ${entry.message}" + + is PublicOplogEntry.Error -> + summary += "FAILED: ${entry.error} (retry from ${entry.retryFrom})" + + is PublicOplogEntry.SuccessfulUpdate -> + summary += "updated to revision ${entry.targetRevision}" + + else -> { /* ignore the other entry kinds for this report */ } + } + } + } + } finally { + oplog.close() // handle is not GC-managed + } + return summary + } +} +``` + +### Searching an oplog + +```kotlin +import cloud.golem.runtime.host.PublicOplogEntry +import cloud.golem.runtime.host.SearchOplog + +fun findErrors(agentId: AgentId): List> { + val search = SearchOplog(agentId, text = "error") + val hits = mutableListOf>() + try { + while (true) { + val batch = search.getNext() ?: break + for ((index, entry) in batch) { + if (entry is PublicOplogEntry.Error) hits += index to entry.error + } + } + } finally { + search.close() + } + return hits +} +``` + +## Notes + +- **Read-only.** This binding covers the `golem:api/oplog@1.5.0` *read* surface only; there is + no API to write oplog entries — the runtime does that. +- **Always `close()`.** Both resources hold a host-side handle that is not tied to Kotlin/Wasm + GC. Wrap usage in `try`/`finally`. +- **`getNext()` paginates.** Treat it as an iterator that ends at `null`; a single call returns + only one batch. +- **`PublicOplogEntry.Unsupported`** is a defensive fallback. All 46 known cases are typed, so + seeing it means the host emitted a case newer than this SDK build knows about — match it + (or the `else` branch) so a `when` stays exhaustive against future hosts. +- **Payloads carry `TypedSchemaValue`.** `Start.request`, `End.response`, and agent-invocation + input/output are typed schema values (see [`types.md`](types.md)), not raw bytes. +- Mirrors the Scala SDK's `OplogApi.GetOplog` / `OplogApi.SearchOplog`. See the SDK overview in + [`../../README.md`](../../README.md). diff --git a/sdks/kotlin/docs/api/quota.md b/sdks/kotlin/docs/api/quota.md new file mode 100644 index 0000000000..b054145c85 --- /dev/null +++ b/sdks/kotlin/docs/api/quota.md @@ -0,0 +1,202 @@ +# Quota + +> Cooperative resource-quota capabilities for Golem agents — the `QuotaApi` binding over `golem:quota/types@1.5.0`. **Status:** Complete. + +## Overview + +The quota API lets an agent hold an **unforgeable capability** — a `QuotaToken` — that grants +the right to consume a named, rate-limited resource, and to **reserve** capacity from it before +doing work. It is the Kotlin binding for `golem:quota/types@1.5.0`. + +The model has three pieces: + +1. **`QuotaToken`** — an opaque, *affine* capability for a named resource. It carries no + readable content, only a handle to an owned host resource. A token can be `split` into a + child (keeping some capability locally and handing the rest away) and `merge`d back, and it + can be transferred to exactly one destination — after which it can no longer be used. +2. **`Reservation`** — a short-lived capability obtained via `QuotaToken.reserve(amount)`, + representing a pending consumption of `amount` units. You then `commit` the *actual* usage. +3. **`QuotaApi`** / `QuotaToken.withReservation` — a scoped helper that reserves, runs your + block, and commits the usage the block reports (committing zero and rethrowing on failure). + +The underlying `quota-token` capability is `golem:core/types@2.0.0`'s `quota-token` resource — +the same resource the SDK's [schema-value tree](types.md) already exposes as +`SchemaValue.QuotaTokenVal`. That resource has **no methods of its own**; this interface exposes +free functions that act on a token handle, plus the `reservation` resource. + +### Affine-handle discipline + +Both `QuotaToken` and `Reservation` wrap a *nullable* handle and null it out on the operation +that consumes it. This enforces linear/affine use at runtime: + +- `QuotaToken.merge(other)` and the SDK's schema-value machinery **take** a token's handle + (via the internal `take()`), leaving the source token consumed. Any later use traps with a + clear message telling you to `split` first if you needed to both keep and send the capability. +- `Reservation.commit(used)` nulls the reservation's handle; a second `commit` traps. There is + no manual `drop` for a reservation — dropping without committing is equivalent to committing + zero usage (matching the Scala reference). + +### The `reservation.commit` shape + +`reservation.commit` is declared in WIT as `static func(this: reservation, used: u64)` — a +**static** function that takes the resource by **value** (owned) as an explicit `this` +parameter, not a `[method]`. Calling it *consumes* the reservation: the host releases the +handle as part of the call, so no separate resource-drop is needed after a successful commit. +The SDK models this by taking the `Reservation`'s handle out before the host call. + +For the SDK overview see [`../../README.md`](../../README.md). Related: [Types](types.md) +(the `QuotaTokenVal` schema value that transfers a token across an agent boundary). + +## API reference + +### `QuotaToken` + +```kotlin +class QuotaToken { + fun reserve(amount: Long): Either + fun split(childExpectedUse: Long): QuotaToken + fun merge(other: QuotaToken) + fun withReservation(amount: Long, block: (Reservation) -> Pair): Either + + companion object { + fun create(resourceName: String, expectedUse: Long): QuotaToken + } +} +``` + +- **`create(resourceName, expectedUse)`** — request a quota capability for the named resource + (as declared in the manifest). `expectedUse` is the expected number of units per reservation; + it derives the credit rate and max-credit used for fair scheduling. +- **`reserve(amount)`** — reserve `amount` units from the local allocation. Blocks internally + until capacity is available or the resource's enforcement action fires. Returns + `Either.Right(reservation)` on success, or `Either.Left(FailedReservation)` when the + enforcement policy is `reject`. Traps (throws) if the token has already been transferred. +- **`split(childExpectedUse)`** — split off a child token carrying `childExpectedUse` units of + expected-use. The parent's expected-use is reduced by that amount and credits are divided + proportionally. Traps if `childExpectedUse` exceeds the parent's current expected-use, or if + the token was already transferred. +- **`merge(other)`** — merge `other` back into this token (combining expected-use and credits). + `other` is **consumed** and must not be used afterwards; this token stays usable. Traps if the + tokens refer to different resources, if either was already transferred, or if `other` is `this`. +- **`withReservation(amount, block)`** — convenience wrapper delegating to + [`QuotaApi.withReservation`](#quotaapi). + +### `Reservation` + +```kotlin +class Reservation { + fun commit(used: Long) +} +``` + +- **`commit(used)`** — commit the *actual* usage. `used` less than reserved returns the unused + capacity to the pool; `used` greater than reserved deducts the excess as "debt" from the + token's allocation. Consumes the reservation — a second `commit` traps. + +### `FailedReservation` + +```kotlin +data class FailedReservation(val estimatedWaitNanos: Long?) +``` + +Returned as `Either.Left` when a reservation is refused (enforcement policy = `reject`). When +present, `estimatedWaitNanos` is the host's estimate of how long until capacity would be +available. + +### `QuotaApi` + +```kotlin +object QuotaApi { + fun withReservation( + token: QuotaToken, + amount: Long, + block: (Reservation) -> Pair, + ): Either +} +``` + +Reserves `amount` units from `token`, runs `block`, then commits the actual usage `block` +returns as the first element of its `Pair` (the second element is the value to propagate). On +any exception thrown by `block`, it commits **zero** usage (returning all capacity to the pool) +and rethrows — so unused capacity is never leaked. + +## Examples + +All examples assume they run inside a Golem `@Agent` method. + +### Reserve, do work, commit actual usage (scoped) + +The recommended pattern — `withReservation` handles commit-on-success and commit-zero-on-failure +for you. `block` returns `Pair(actualUnitsUsed, result)`. + +```kotlin +import cloud.golem.runtime.Either +import cloud.golem.runtime.host.QuotaToken + +fun sendBatch(messages: List): Int { + val token = QuotaToken.create(resourceName = "outbound-emails", expectedUse = 100) + + val result = token.withReservation(amount = messages.size.toLong()) { _ -> + var sent = 0 + for (m in messages) { + if (deliver(m)) sent++ // stop early on failure -> commit only what we used + } + Pair(sent.toLong(), sent) // (units actually used, value to return) + } + + return when (result) { + is Either.Right -> result.value + is Either.Left -> { + val wait = result.value.estimatedWaitNanos + error("quota rejected; retry in about ${wait ?: 0} ns") + } + } +} +``` + +### Manual reserve / commit + +```kotlin +fun chargeOne(token: QuotaToken): Boolean = + when (val r = token.reserve(amount = 1)) { + is Either.Left -> false // rejected by enforcement policy + is Either.Right -> { + val reservation = r.value + doWork() + reservation.commit(used = 1) // consumes the reservation + true + } + } +``` + +### Split a token to delegate capability, then merge it back + +```kotlin +fun delegate(parent: QuotaToken) { + // Hand 30 units of expected-use to a child; parent keeps the rest and stays usable. + val child = parent.split(childExpectedUse = 30) + + // ... use `child` locally, or transfer it onward (e.g. as a QuotaTokenVal to another agent) ... + + // Reclaim the child's remaining capability. `child` is consumed by merge. + parent.merge(child) + // Using `child` after this point would trap. +} +``` + +## Notes + +- **Affine handles.** A `QuotaToken` can be transferred (via `merge`'s `other`, or the + schema-value machinery) exactly once; after transfer it traps on any use. If you need to both + keep and send a capability, `split` first. +- **`reserve` blocks.** It waits internally for capacity; it only returns `Either.Left` when the + resource's enforcement policy is `reject`. +- **`commit` semantics.** `used < reserved` returns capacity; `used > reserved` incurs debt. + Committing is mandatory to release a reservation cleanly, but dropping a reservation without + committing is treated as committing zero. +- **No manual drop.** Neither `Reservation` nor `QuotaToken` exposes a manual drop; consumption + happens through `commit` / `merge` / transfer, mirroring the Scala reference. +- **`quota-token` has no methods.** All behaviour lives in this interface's free functions; + the resource itself is the same one surfaced by [`SchemaValue.QuotaTokenVal`](types.md). + +See also: [Types](types.md) · [SDK README](../../README.md) diff --git a/sdks/kotlin/docs/api/rdbms.md b/sdks/kotlin/docs/api/rdbms.md new file mode 100644 index 0000000000..1b66d2cd7c --- /dev/null +++ b/sdks/kotlin/docs/api/rdbms.md @@ -0,0 +1,556 @@ +# RDBMS + +> Relational-database bindings for **Postgres**, **MySQL**, and **Apache Ignite 2.x** over `golem:rdbms/postgres@1.5.0`, `golem:rdbms/mysql@1.5.0`, and `golem:rdbms/ignite2@1.5.0`. **Status:** Complete. + +## Overview + +The RDBMS bindings let a Golem agent open a durable connection to a relational database, +run parameterised statements, iterate typed rows, and drive transactions. Three backends are +supported, each behind its own set of Kotlin types: + +| Backend | WIT package | Connection | Transaction | Value type | Error type | Result / Row / Column | +| --- | --- | --- | --- | --- | --- | --- | +| Postgres | `golem:rdbms/postgres@1.5.0` | `PostgresConnection` | `PostgresTransaction` | `PostgresDbValue` (45 cases) | `DbError` | `PostgresDbResult` / `PostgresDbRow` / `DbColumn` | +| MySQL | `golem:rdbms/mysql@1.5.0` | `MysqlConnection` | `MysqlTransaction` | `MysqlDbValue` (36 cases) | `MysqlDbError` | `MysqlDbResult` / `MysqlDbRow` / `MysqlDbColumn` | +| Ignite | `golem:rdbms/ignite2@1.5.0` | `IgniteConnection` | `IgniteTransaction` | `IgniteDbValue` (16 cases) | `IgniteDbError` | `IgniteDbResult` / `IgniteDbRow` / `IgniteDbColumn` | + +All three backends share the **same shape**: + +- `Connection.open(address)` opens a connection; `query`, `execute`, and + `beginTransaction` operate on it; `close()` releases it. +- `Transaction` has `query`, `execute`, `commit`, `rollback`, and `close`. +- Every fallible call returns [`Either`](transactions.md#either) — `Either.Right` + on success, `Either.Left(error)` on failure. No exceptions are thrown for database errors. +- `query` returns a fully-materialised result (columns + rows); `execute` returns the affected + row count (`Long`). +- Parameters are passed as `List<DbValue>` and default to `emptyList()`. + +Connections and transactions are backed by host resource handles and **must be `close()`d** +when done (both classes guard against use-after-close with a `check(!closed)`). + +> **Query streaming is out of scope.** The WIT interfaces expose a `query-stream` / +> `db-result-stream` resource for lazy, paginated results, but — matching the Scala reference +> SDK, which likewise does not implement it — the Kotlin SDK only exposes the eager `query` +> that materialises the whole `db-result`. Streaming a result incrementally is a separate, +> deliberately deferred capability. + +For the SDK overview see [`../../README.md`](../../README.md). Related host APIs: +[Transactions](transactions.md) (the `Either` type and higher-level transaction combinators) +and [Quota](quota.md). + +## Shared value model + +Several column value cases are shared verbatim across backends (they live in +`golem:rdbms/types@1.5.0` and are reused by the Postgres/MySQL value variants): + +```kotlin +/** golem:rdbms/types date record. */ +data class DbDate(val year: Int, val month: UByte, val day: UByte) + +/** golem:rdbms/types time record. */ +data class DbTime(val hour: UByte, val minute: UByte, val second: UByte, val nanosecond: UInt) + +/** golem:rdbms/types timestamp record. */ +data class DbTimestamp(val date: DbDate, val time: DbTime) + +/** golem:rdbms/types timestamptz record. */ +data class DbTimestampTz(val timestamp: DbTimestamp, val offset: Int) + +/** golem:rdbms/types timetz record. */ +data class DbTimeTz(val time: DbTime, val offset: Int) +``` + +> Note: Ignite does **not** use these records — it encodes temporal values as raw +> milliseconds/nanoseconds (see [Ignite](#ignite)). + +--- + +## Postgres + +`golem:rdbms/postgres@1.5.0`. The richest of the three backends: 45 `db-value` cases including +temporal, network, bit-string, range, and pgvector types, plus the recursive composite / domain +/ array / range cases. + +### `PostgresConnection` + +```kotlin +class PostgresConnection { + fun query(statement: String, params: List = emptyList()): Either + fun execute(statement: String, params: List = emptyList()): Either + fun beginTransaction(): Either + fun close() + + companion object { + fun open(address: String): Either + } +} +``` + +`execute` returns the number of affected rows (`u64`). The connection must be `close()`d. + +### `PostgresTransaction` + +```kotlin +class PostgresTransaction { + fun query(statement: String, params: List = emptyList()): Either + fun execute(statement: String, params: List = emptyList()): Either + fun commit(): Either + fun rollback(): Either + fun close() +} +``` + +Must be `close()`d whether or not `commit`/`rollback` was called. + +### Result, row, and column types + +```kotlin +data class PostgresDbResult(val columns: List, val rows: List) + +/** Mirrors Scala's DbColumn: carries only dbTypeName, not the structural column type. */ +data class DbColumn(val ordinal: Long, val name: String, val dbTypeName: String) + +data class PostgresDbRow(val values: List) { + fun getString(index: Int): String? // Null -> null; text/numeric cases return raw content + fun getInt(index: Int): Int? // Null -> null; Int4/Int2 fast path, else parses display string + fun getLong(index: Int): Long? // Null -> null; Int8/Int4 fast path, else parses display string +} +``` + +The row accessors return `null` for SQL `NULL`. `getString` returns the raw string content for +textual and numeric-string cases (`Text`/`Varchar`/`Bpchar`/`Numeric`/`Json`/`Jsonb`/`JsonPath`/`Xml`); +other cases fall back to a structural `toString()` dump. + +### Error type + +```kotlin +sealed class DbError { + data class ConnectionFailure(val message: String) : DbError() + data class QueryParameterFailure(val message: String) : DbError() + data class QueryExecutionFailure(val message: String) : DbError() + data class QueryResponseFailure(val message: String) : DbError() + data class Other(val message: String) : DbError() +} +``` + +### `PostgresDbValue` — column value coverage + +All 45 cases (used both as query parameters and as row values): + +```kotlin +sealed class PostgresDbValue { + // Numeric / boolean + data class Character(val value: Byte) : PostgresDbValue() // "char" + data class Int2(val value: Short) : PostgresDbValue() + data class Int4(val value: Int) : PostgresDbValue() + data class Int8(val value: Long) : PostgresDbValue() + data class Float4(val value: Float) : PostgresDbValue() + data class Float8(val value: Double) : PostgresDbValue() + data class Numeric(val value: String) : PostgresDbValue() // arbitrary-precision decimal as string + data class BooleanVal(val value: Boolean) : PostgresDbValue() + data class Money(val value: Long) : PostgresDbValue() + data class Oid(val value: UInt) : PostgresDbValue() + + // Text + data class Text(val value: String) : PostgresDbValue() + data class Varchar(val value: String) : PostgresDbValue() + data class Bpchar(val value: String) : PostgresDbValue() + data class Json(val value: String) : PostgresDbValue() + data class Jsonb(val value: String) : PostgresDbValue() + data class JsonPath(val value: String) : PostgresDbValue() + data class Xml(val value: String) : PostgresDbValue() + + // Temporal (shared records) + data class TimestampVal(val value: DbTimestamp) : PostgresDbValue() + data class TimestampTzVal(val value: DbTimestampTz) : PostgresDbValue() + data class DateVal(val value: DbDate) : PostgresDbValue() + data class TimeVal(val value: DbTime) : PostgresDbValue() + data class TimeTzVal(val value: DbTimeTz) : PostgresDbValue() + data class IntervalVal(val value: DbInterval) : PostgresDbValue() + + // Binary / identity + data class Bytea(val value: List) : PostgresDbValue() + data class Uuid(val highBits: Long, val lowBits: Long) : PostgresDbValue() + + // Network + data class InetVal(val value: IpAddress) : PostgresDbValue() + data class CidrVal(val value: IpAddress) : PostgresDbValue() + data class MacaddrVal(val value: MacAddress) : PostgresDbValue() + + // Bit strings + data class Bit(val value: List) : PostgresDbValue() + data class Varbit(val value: List) : PostgresDbValue() + + // Ranges + data class Int4RangeVal(val value: Int4Range) : PostgresDbValue() + data class Int8RangeVal(val value: Int8Range) : PostgresDbValue() + data class NumRangeVal(val value: NumRange) : PostgresDbValue() + data class TsRangeVal(val value: TsRange) : PostgresDbValue() + data class TsTzRangeVal(val value: TsTzRange) : PostgresDbValue() + data class DateRangeVal(val value: DateRange) : PostgresDbValue() + + // Composite / user-defined (recursive — see below) + data class EnumerationVal(val value: PostgresEnumeration) : PostgresDbValue() + data class CompositeVal(val value: PostgresComposite) : PostgresDbValue() + data class DomainVal(val value: PostgresDomain) : PostgresDbValue() + data class ArrayVal(val value: List) : PostgresDbValue() + data class RangeVal(val value: PostgresRange) : PostgresDbValue() + + // pgvector + data class VectorVal(val value: List) : PostgresDbValue() + data class HalfvecVal(val value: List) : PostgresDbValue() + data class SparsevecVal(val value: SparseVec) : PostgresDbValue() + + object Null : PostgresDbValue() +} +``` + +Supporting types for the composite / temporal / network / range cases: + +```kotlin +data class DbInterval(val months: Int, val days: Int, val microseconds: Long) + +sealed class IpAddress { + data class Ipv4(val a: UByte, val b: UByte, val c: UByte, val d: UByte) : IpAddress() + data class Ipv6( + val a: UShort, val b: UShort, val c: UShort, val d: UShort, + val e: UShort, val f: UShort, val g: UShort, val h: UShort, + ) : IpAddress() +} +data class MacAddress(val a: UByte, val b: UByte, val c: UByte, val d: UByte, val e: UByte, val f: UByte) + +data class SparseVec(val dim: Int, val indices: List, val values: List) + +data class PostgresEnumeration(val name: String, val value: String) +data class PostgresComposite(val name: String, val values: List) +data class PostgresDomain(val name: String, val value: PostgresDbValue) + +data class PostgresRange(val name: String, val value: ValuesRange) +data class ValuesRange(val start: ValueBound, val end: ValueBound) +sealed class ValueBound { + data class Included(val value: PostgresDbValue) : ValueBound() + data class Excluded(val value: PostgresDbValue) : ValueBound() + object Unbounded : ValueBound() +} + +// Typed range bounds (Int4Bound/Int8Bound/NumBound/TsBound/TsTzBound/DateBound each have +// Included(value) / Excluded(value) / Unbounded), plus the range pairs: +data class Int4Range(val start: Int4Bound, val end: Int4Bound) +data class Int8Range(val start: Int8Bound, val end: Int8Bound) +data class NumRange(val start: NumBound, val end: NumBound) +data class TsRange(val start: TsBound, val end: TsBound) +data class TsTzRange(val start: TsTzBound, val end: TsTzBound) +data class DateRange(val start: DateBound, val end: DateBound) +``` + +#### The `lazy-db-value` resource (recursive cases) + +Postgres's `enumeration` / `composite` / `domain` / `array` / `range` cases nest other +`db-value`s. At the WIT level these nested values go through a **`lazy-db-value` resource** +(`constructor(value: db-value); get() -> db-value`) — the recursion is indirect, through +resource handles in the host's table, which is why a single `db-value` has a bounded size on +the wire despite being conceptually recursive. + +The SDK hides this entirely: nested values are **fully materialised** into plain Kotlin data +(`PostgresComposite.values` is a `List`, `PostgresDomain.value` is a +`PostgresDbValue`, `ArrayVal` is a `List`, and so on). Callers never see a +handle — real composite/array/domain/range nesting is finite, so eager recursive resolution is +the right default. + +--- + +## MySQL + +`golem:rdbms/mysql@1.5.0`. Entirely **flat** — MySQL's `db-value` has no recursive cases, so +there is no `lazy-db-value` involvement. The Connection/Transaction shape mirrors Postgres +exactly. + +### Connection and transaction + +```kotlin +class MysqlConnection { + fun query(statement: String, params: List = emptyList()): Either + fun execute(statement: String, params: List = emptyList()): Either + fun beginTransaction(): Either + fun close() + companion object { fun open(address: String): Either } +} + +class MysqlTransaction { + fun query(statement: String, params: List = emptyList()): Either + fun execute(statement: String, params: List = emptyList()): Either + fun commit(): Either + fun rollback(): Either + fun close() +} +``` + +### Result, row, and column types + +```kotlin +data class MysqlDbResult(val columns: List, val rows: List) +data class MysqlDbColumn(val ordinal: Long, val name: String, val dbTypeName: String) + +data class MysqlDbRow(val values: List) { + fun getString(index: Int): String? // Null -> null + fun getInt(index: Int): Int? // Null -> null; IntVal/TinyInt/SmallInt/MediumInt fast path +} +``` + +> Unlike Postgres and Ignite, `MysqlDbRow` exposes only `getString` / `getInt` (no `getLong`), +> matching the Scala reference. + +### Error type + +```kotlin +sealed class MysqlDbError { + data class ConnectionFailure(val message: String) : MysqlDbError() + data class QueryParameterFailure(val message: String) : MysqlDbError() + data class QueryExecutionFailure(val message: String) : MysqlDbError() + data class QueryResponseFailure(val message: String) : MysqlDbError() + data class Other(val message: String) : MysqlDbError() +} +``` + +### `MysqlDbValue` — column value coverage + +All 36 cases: + +```kotlin +sealed class MysqlDbValue { + data class BooleanVal(val value: Boolean) : MysqlDbValue() + data class TinyInt(val value: Byte) : MysqlDbValue() + data class SmallInt(val value: Short) : MysqlDbValue() + data class MediumInt(val value: Int) : MysqlDbValue() + data class IntVal(val value: Int) : MysqlDbValue() + data class BigInt(val value: Long) : MysqlDbValue() + data class TinyIntUnsigned(val value: UByte) : MysqlDbValue() + data class SmallIntUnsigned(val value: UShort) : MysqlDbValue() + data class MediumIntUnsigned(val value: UInt) : MysqlDbValue() + data class IntUnsigned(val value: UInt) : MysqlDbValue() + data class BigIntUnsigned(val value: ULong) : MysqlDbValue() + data class FloatVal(val value: Float) : MysqlDbValue() + data class DoubleVal(val value: Double) : MysqlDbValue() + data class Decimal(val value: String) : MysqlDbValue() // arbitrary-precision as string + data class DateVal(val value: DbDate) : MysqlDbValue() + data class DateTimeVal(val value: DbTimestamp) : MysqlDbValue() + data class TimestampVal(val value: DbTimestamp) : MysqlDbValue() + data class TimeVal(val value: DbTime) : MysqlDbValue() + data class Year(val value: UShort) : MysqlDbValue() + data class FixChar(val value: String) : MysqlDbValue() + data class VarChar(val value: String) : MysqlDbValue() + data class TinyText(val value: String) : MysqlDbValue() + data class Text(val value: String) : MysqlDbValue() + data class MediumText(val value: String) : MysqlDbValue() + data class LongText(val value: String) : MysqlDbValue() + data class Binary(val value: List) : MysqlDbValue() + data class VarBinary(val value: List) : MysqlDbValue() + data class TinyBlob(val value: List) : MysqlDbValue() + data class Blob(val value: List) : MysqlDbValue() + data class MediumBlob(val value: List) : MysqlDbValue() + data class LongBlob(val value: List) : MysqlDbValue() + data class Enumeration(val value: String) : MysqlDbValue() + data class SetVal(val value: String) : MysqlDbValue() + data class Bit(val value: List) : MysqlDbValue() + data class Json(val value: String) : MysqlDbValue() + object Null : MysqlDbValue() +} +``` + +--- + +## Ignite + +`golem:rdbms/ignite2@1.5.0`. Also flat, and the smallest backend (16 `db-value` cases). Two +differences worth noting: `IgniteDbColumn` has **no `dbTypeName` field** (matching Scala's +`IgniteDbColumn`), and temporal values are encoded as raw millisecond/nanosecond longs rather +than the shared `DbDate`/`DbTime`/`DbTimestamp` records. `execute` returns `s64`. + +### Connection and transaction + +```kotlin +class IgniteConnection { + fun query(statement: String, params: List = emptyList()): Either + fun execute(statement: String, params: List = emptyList()): Either + fun beginTransaction(): Either + fun close() + companion object { fun open(address: String): Either } +} + +class IgniteTransaction { + fun query(statement: String, params: List = emptyList()): Either + fun execute(statement: String, params: List = emptyList()): Either + fun commit(): Either + fun rollback(): Either + fun close() +} +``` + +### Result, row, and column types + +```kotlin +data class IgniteDbResult(val columns: List, val rows: List) +data class IgniteDbColumn(val ordinal: Long, val name: String) // no type-name field + +data class IgniteDbRow(val values: List) { + fun getString(index: Int): String? // DbNull -> null + fun getInt(index: Int): Int? // DbNull -> null; DbInt/DbByte/DbShort fast path + fun getLong(index: Int): Long? // DbNull -> null; DbLong/DbInt fast path +} +``` + +### Error type + +```kotlin +sealed class IgniteDbError { + data class ConnectionFailure(val message: String) : IgniteDbError() + data class QueryParameterFailure(val message: String) : IgniteDbError() + data class QueryExecutionFailure(val message: String) : IgniteDbError() + data class QueryResponseFailure(val message: String) : IgniteDbError() + data class Other(val message: String) : IgniteDbError() +} +``` + +### `IgniteDbValue` — column value coverage + +All 16 cases: + +```kotlin +sealed class IgniteDbValue { + object DbNull : IgniteDbValue() + data class DbBoolean(val value: Boolean) : IgniteDbValue() + data class DbByte(val value: Byte) : IgniteDbValue() + data class DbShort(val value: Short) : IgniteDbValue() + data class DbInt(val value: Int) : IgniteDbValue() + data class DbLong(val value: Long) : IgniteDbValue() + data class DbFloat(val value: Float) : IgniteDbValue() + data class DbDouble(val value: Double) : IgniteDbValue() + data class DbChar(val value: Char) : IgniteDbValue() // 16-bit Unicode code unit + data class DbString(val value: String) : IgniteDbValue() + data class DbUuid(val highBits: Long, val lowBits: Long) : IgniteDbValue() + data class DbDate(val millis: Long) : IgniteDbValue() // ms since Unix epoch (UTC) + data class DbTimestamp(val millis: Long, val nanos: Int) : IgniteDbValue() // ms + sub-ms nanos + data class DbTime(val nanos: Long) : IgniteDbValue() // nanos since midnight + data class DbDecimal(val value: String) : IgniteDbValue() + data class DbByteArray(val value: List) : IgniteDbValue() +} +``` + +## Examples + +All examples assume they run inside a Golem `@Agent` method. `Either.Left` carries the +backend's error type; `Either.Right` carries the success value. + +### Open a connection and run a parameterised query (Postgres) + +```kotlin +import cloud.golem.runtime.Either +import cloud.golem.runtime.host.PostgresConnection +import cloud.golem.runtime.host.PostgresDbValue + +fun lookupUserEmail(userId: Int): String? { + val conn = when (val c = PostgresConnection.open("postgres://localhost/app")) { + is Either.Left -> error("connect failed: ${c.value}") + is Either.Right -> c.value + } + try { + return when (val r = conn.query( + "SELECT email FROM users WHERE id = $1", + listOf(PostgresDbValue.Int4(userId)), + )) { + is Either.Left -> error("query failed: ${r.value}") + is Either.Right -> r.value.rows.firstOrNull()?.getString(0) + } + } finally { + conn.close() + } +} +``` + +### Iterate rows and columns + +```kotlin +fun listActiveUsers(conn: PostgresConnection): List> = + when (val r = conn.query("SELECT id, name FROM users WHERE active = true")) { + is Either.Left -> emptyList() + is Either.Right -> r.value.rows.map { row -> + (row.getLong(0) ?: 0L) to (row.getString(1) ?: "") + } + } +``` + +### An INSERT via `execute` (returns affected row count) + +```kotlin +fun addUser(conn: PostgresConnection, name: String, email: String): Long = + when (val r = conn.execute( + "INSERT INTO users (name, email) VALUES ($1, $2)", + listOf(PostgresDbValue.Text(name), PostgresDbValue.Text(email)), + )) { + is Either.Left -> 0L + is Either.Right -> r.value // number of rows affected + } +``` + +### A transaction (Postgres) + +```kotlin +fun transfer(conn: PostgresConnection, from: Int, to: Int, amount: Long): Either { + val tx = when (val t = conn.beginTransaction()) { + is Either.Left -> return Either.Left(t.value) + is Either.Right -> t.value + } + try { + val debit = tx.execute( + "UPDATE accounts SET balance = balance - $1 WHERE id = $2", + listOf(PostgresDbValue.Int8(amount), PostgresDbValue.Int4(from)), + ) + if (debit is Either.Left) { tx.rollback(); return Either.Left(debit.value) } + + val credit = tx.execute( + "UPDATE accounts SET balance = balance + $1 WHERE id = $2", + listOf(PostgresDbValue.Int8(amount), PostgresDbValue.Int4(to)), + ) + if (credit is Either.Left) { tx.rollback(); return Either.Left(credit.value) } + + return tx.commit() + } finally { + tx.close() // always release the handle, committed or not + } +} +``` + +### MySQL and Ignite + +The same patterns apply, swapping the backend types: + +```kotlin +// MySQL — note '?' placeholders and MysqlDbValue +val conn = MysqlConnection.open("mysql://localhost/app") +// ... conn.query("SELECT name FROM users WHERE id = ?", listOf(MysqlDbValue.IntVal(42))) + +// Ignite — IgniteDbValue, temporal values as raw longs +val ic = IgniteConnection.open("ignite://localhost:10800") +// ... ic.query("SELECT ts FROM events WHERE id = ?", listOf(IgniteDbValue.DbLong(1L))) +``` + +## Notes + +- **Always `close()`** connections and transactions. Both guard against use-after-close and + will throw `IllegalStateException` (via `check`) if reused after closing. +- **Errors are values, not exceptions.** Every fallible call returns `Either`; only + programmer errors (use-after-close) throw. +- **`Null` is a value.** SQL `NULL` is represented by the backend's `Null` / `DbNull` case, and + the row accessors (`getString`/`getInt`/`getLong`) map it to `null`. +- **`execute` return type:** affected row count as `Long` (`u64` for Postgres/MySQL, `s64` for + Ignite). +- **`DbColumn` carries only `dbTypeName`** (Postgres/MySQL) or nothing but `ordinal`/`name` + (Ignite) — the structural `db-column-type` variant is intentionally not decoded, matching the + Scala reference SDK, since no accessor consumes it. +- **Decimal / numeric** values are carried as strings (`PostgresDbValue.Numeric`, + `MysqlDbValue.Decimal`, `IgniteDbValue.DbDecimal`) to preserve arbitrary precision. +- **Query streaming (`query-stream` / `db-result-stream`) is deliberately not implemented**, in + line with the Scala SDK. Use `query`, which materialises the full result. + +See also: [Transactions](transactions.md) · [Quota](quota.md) · [Types](types.md) · +[SDK README](../../README.md) diff --git a/sdks/kotlin/docs/api/retry.md b/sdks/kotlin/docs/api/retry.md new file mode 100644 index 0000000000..b391e398ed --- /dev/null +++ b/sdks/kotlin/docs/api/retry.md @@ -0,0 +1,325 @@ +# Retry + +> Semantic, host-level retry policies for Golem agents — the raw `RetryApi` binding over `golem:api/retry@1.5.0`, plus an idiomatic Kotlin **Retry DSL** for building policies and predicates. **Status:** Complete. + +## Overview + +Golem's `golem:api/retry` interface lets an agent register **named retry policies**. Each +policy pairs a **predicate** (when does this rule apply — matched against a retry context of +verb / noun-uri / status-code / error-type / …) with a **policy** (the strategy — exponential +backoff, fibonacci, count-box, jitter, and so on). The host consults these when deciding +whether and how to retry a failing operation. + +The SDK exposes two layers: + +1. **[`RetryApi`](#retryapi-host-functions)** — the low-level host binding. It marshals the + flattened `retry-policy` / `retry-predicate` node-list trees (a list of nodes with `s32` + index cross-references, root = `nodes[0]`, structurally like the schema-value tree) + field-for-field across the WIT boundary. Unlike the Scala SDK — which keeps these trees + opaque and passes JS objects straight through — the native path has no opaque-JS escape + hatch, so every layout is marshalled explicitly (layouts verified via `abi-dump`). + +2. **[The Retry DSL](#the-retry-dsl)** (`RetryDsl.kt`) — the ergonomic layer you should + normally use. It builds `Policy` / `Predicate` trees with `kotlin.time.Duration`, infix and + operator combinators (`and`, `or`, `!`, `andThen`), and `Props. eq ` leaves, then + flattens them into the node lists `RetryApi` expects (and can rebuild them on the way back). + +Start with the DSL; drop to `RetryApi`'s raw model only when you need the exact wire shapes. + +--- + +## The Retry DSL + +`RetryDsl.kt` is a native recasting of the Scala SDK's `Retry` object in Kotlin idiom. + +### Predicates + +Build predicate leaves from a [`Prop`](#props--prop) via its infix operators, then combine +them. + +```kotlin +sealed class Predicate { + infix fun and(that: Predicate): Predicate // And + infix fun or(that: Predicate): Predicate // Or + operator fun not(): Predicate // enables `!predicate` -> Not + + companion object { + val always: Predicate // Always (pred-true) + val never: Predicate // Never (pred-false) + } +} +``` + +Leaf cases (data classes on `Predicate`): `Eq`, `Neq`, `Gt`, `Gte`, `Lt`, `Lte`, `Exists`, +`OneOf`, `MatchesGlob`, `StartsWith`, `Contains`; combinators `And`, `Or`, `Not`; and the +objects `Always` / `Never`. + +### `Props` / `Prop` + +`Prop` names a retry-context property and produces predicate leaves via infix operators: + +```kotlin +class Prop(val name: String) { + infix fun eq(value: PredicateValue): Predicate // + String / Long / Int / Boolean overloads + infix fun neq(value: PredicateValue): Predicate // + String / Long / Int / Boolean overloads + infix fun gt(value: Long): Predicate // + Int overload + infix fun gte(value: Long): Predicate // + Int overload + infix fun lt(value: Long): Predicate // + Int overload + infix fun lte(value: Long): Predicate // + Int overload + infix fun matchesGlob(pattern: String): Predicate + infix fun startsWith(prefix: String): Predicate + infix fun contains(substring: String): Predicate + fun oneOf(vararg values: String): Predicate // + Long / Int overloads + val exists: Predicate +} +``` + +The `eq` / `neq` overloads pick the right [`PredicateValue`](#predicatevalue) case +automatically — `Props.statusCode eq 503` yields `PredicateValue.Integer`, +`Props.errorType eq "timeout"` yields `PredicateValue.Text`, `Props("k") eq true` yields +`PredicateValue.Bool`. + +`Props` holds the properties the host exposes, plus `custom` for anything else: + +```kotlin +object Props { + val verb; val nounUri; val uriScheme; val uriHost; val uriPort; val uriPath + val statusCode; val errorType; val function; val targetComponentId + val targetAgentType; val dbType; val trapType // each is a Prop + + fun custom(name: String): Prop + operator fun invoke(name: String): Prop // Props("my-header") +} +``` + +### Policies + +Start from a base and layer modifiers fluently, or combine whole policies. + +```kotlin +sealed class Policy { + // modifiers (each wraps `this`) + fun maxRetries(maxRetries: Long): Policy // count-box; must fit uint32 + fun within(limit: Duration): Policy // time-box + fun clamp(minDelay: Duration, maxDelay: Duration): Policy // requires min <= max + fun addDelay(delay: Duration): Policy + fun withJitter(factor: Double): Policy // 0.0.. e.g. 0.2 = ±20% + fun onlyWhen(predicate: Predicate): Policy // filtered-on + + // whole-policy combinators + infix fun andThen(that: Policy): Policy // fall back once exhausted + infix fun union(that: Policy): Policy // retry if EITHER would + infix fun intersect(that: Policy): Policy // retry only if BOTH would + + companion object { + val immediate: Policy // retry with no delay + val never: Policy // never retry + fun periodic(delay: Duration): Policy + fun exponential(baseDelay: Duration, factor: Double): Policy // factor finite & > 0 + fun fibonacci(first: Duration, second: Duration): Policy + } +} +``` + +Base/modifier cases are data classes/objects on `Policy` (`Periodic`, `Exponential`, +`Fibonacci`, `Immediate`, `Never`, `CountBox`, `TimeBox`, `Clamp`, `AddDelay`, `Jitter`, +`FilteredOn`, `AndThen`, `Union`, `Intersect`). All delays/limits are `kotlin.time.Duration`. + +Validation is **fail-fast** via `require` (Kotlin `IllegalArgumentException`) rather than +Scala's `Either[ValidationError, _]`: factors must be finite (and `> 0`, or `>= 0` for +jitter), `clamp` needs `min <= max`, durations must be non-negative, and `maxRetries` must fit +an unsigned 32-bit integer. + +### `NamedPolicy` + +```kotlin +data class NamedPolicy( + val name: String, + val policy: Policy, + val priority: Long = 0, + val predicate: Predicate = Predicate.always +) { + fun withPriority(value: Long): NamedPolicy + fun appliesWhen(value: Predicate): NamedPolicy +} +``` + +Defaults mirror the Scala SDK (`priority = 0`, `predicate = always`). Higher priority is +evaluated first. + +### DSL-typed `RetryApi` overloads + +These extensions accept/return DSL trees, flattening to (and rebuilding from) the raw node +lists for you: + +```kotlin +fun RetryApi.setRetryPolicy(policy: NamedPolicy): Unit +fun RetryApi.namedPolicies(): List +fun RetryApi.namedPolicy(name: String): NamedPolicy? +fun RetryApi.resolvePolicy( + verb: String, + nounUri: String, + properties: List> = emptyList() +): Policy? +``` + +### Flatten / unflatten (round-trip) + +The DSL trees convert to and from `RetryApi`'s flat model; the rebuild direction detects +cycles and out-of-range index references. + +```kotlin +fun Predicate.toRetryPredicate(): RetryPredicate +fun Policy.toRetryPolicy(): RetryPolicy +fun NamedPolicy.toNamedRetryPolicy(): NamedRetryPolicy + +fun RetryPredicate.toPredicate(): Predicate +fun RetryPolicy.toPolicy(): Policy +fun NamedRetryPolicy.toNamedPolicy(): NamedPolicy +``` + +### DSL examples + +Composing a predicate with `or` / `and` / `!` (from `RetryDslTest`): + +```kotlin +val predicate = (Props.statusCode eq 503) or + (Props.errorType eq "timeout") and + !(Props.function startsWith "internal.") +``` + +A layered policy with a filter and a fallback (from `RetryDslTest`): + +```kotlin +val policy = Policy.exponential(100.milliseconds, factor = 2.0) + .withJitter(0.2) + .clamp(50.milliseconds, 5.seconds) + .maxRetries(5) + .onlyWhen(Props.statusCode gte 500 and (Props.dbType neq "sqlite")) + .andThen(Policy.periodic(1.seconds).maxRetries(3)) +``` + +A named policy, adjusted with `withPriority` / `appliesWhen` / `oneOf` (from `RetryDslTest`): + +```kotlin +val named = NamedPolicy( + name = "flaky-http", + policy = Policy.fibonacci(100.milliseconds, 200.milliseconds).maxRetries(10), + priority = 42, + predicate = Props.uriScheme eq "https", +).withPriority(7).appliesWhen(Props.statusCode.oneOf(502, 503, 504)) +``` + +Registering and reading policies back inside an agent: + +```kotlin +import cloud.golem.runtime.host.* +import kotlin.time.Duration.Companion.milliseconds + +@Agent +class ResilientAgent { + @Endpoint + fun installRetryRules() { + val np = NamedPolicy( + name = "flaky-http", + policy = Policy.exponential(100.milliseconds, factor = 2.0) + .withJitter(0.2) + .maxRetries(5) + .onlyWhen(Props.statusCode eq 503 or (Props.errorType eq "timeout")), + priority = 10, + ) + RetryApi.setRetryPolicy(np) // DSL overload -> flattens + calls the host + } + + @Endpoint + fun listRetryRules(): List = + RetryApi.namedPolicies().map { it.name } // List, rebuilt from the host + + @Endpoint + fun resolveForRequest(): String? = + RetryApi.resolvePolicy( + verb = "GET", + nounUri = "https://api.example.com/things", + properties = listOf("status-code" to PredicateValue.Integer(503)), + )?.toString() // Policy? tree, or null if nothing matched +} +``` + +--- + +## `RetryApi` host functions + +Raw binding over `golem:api/retry@1.5.0`. Use these directly only when you need the exact +node-list model; otherwise prefer the [DSL overloads](#dsl-typed-retryapi-overloads). + +```kotlin +object RetryApi { + /** All retry policies active for this agent, in host-defined order. */ + fun getRetryPolicies(): List + + /** The named retry policy with [name], or null if none is registered. */ + fun getRetryPolicyByName(name: String): NamedRetryPolicy? + + /** Removes a named retry policy (persisted to the oplog). No-op if it doesn't exist. */ + fun removeRetryPolicy(name: String) + + /** Adds or overwrites a named retry policy (persisted to the oplog). */ + fun setRetryPolicy(policy: NamedRetryPolicy) + + /** Resolves the matching policy for an operation context, or null if no rule's predicate matches. */ + fun resolveRetryPolicy( + verb: String, + nounUri: String, + properties: List> + ): RetryPolicy? +} +``` + +### Raw model types + +The flat, wire-shaped model these functions operate on (root is always `nodes[0]`; children +are referenced by their `Int` index into the same list): + +```kotlin +sealed class PredicateValue { + data class Text(val value: String) : PredicateValue() + data class Integer(val value: Long) : PredicateValue() + data class Bool(val value: Boolean) : PredicateValue() +} + +data class RetryPredicate(val nodes: List) // PredicateNode: PropEq/PropNeq/…/PredAnd/PredOr/PredNot/PredTrue/PredFalse +data class RetryPolicy(val nodes: List) // PolicyNode: Periodic/Exponential/Fibonacci/Immediate/Never/CountBox/… + +data class NamedRetryPolicy( + val name: String, + val priority: UInt, + val predicate: RetryPredicate, + val policy: RetryPolicy +) +``` + +`PredicateNode` combinator cases (`PredAnd`, `PredOr`, `PredNot`) and `PolicyNode` structural +cases (`CountBox`, `TimeBox`, `AndThen`, `PolicyUnion`, …) carry `Int` indices into the node +list rather than nested nodes — that is the flattening the DSL's `toRetryPolicy` / +`toRetryPredicate` produce and `toPolicy` / `toPredicate` reverse. `PolicyNode` durations are +total nanoseconds (WIT `duration`); the DSL layer converts to/from `kotlin.time.Duration`. + +## Notes + +- Prefer the DSL: it is type-safe, validates eagerly, and round-trips losslessly (verified by + `RetryDslTest`). Reach for the raw `RetryApi` model only for exact wire inspection. +- `setRetryPolicy` and `removeRetryPolicy` are **persisted to the oplog**, so they replay + durably like any other Golem effect. +- `resolveRetryPolicy` / `resolvePolicy` return `null` when no registered rule's predicate + matches the given context — it does not fall back to a default. +- The node-list layouts (variant tags, payload offsets, field offsets) were verified via + `abi-dump` against `wit-native/deps/golem-1.x/golem-retry.wit`, not hand-derived. +- This is a *different* mechanism from [saga compensation](./transactions.md): retry policies + are declarative host-level rules; sagas are explicit compensating operations you write. + +## See also + +- [Transactions](./transactions.md) — saga/compensation transactions. +- [Guards & Checkpoint](./guards-checkpoint.md) — scoped runtime controls (the Scala retry + *guards* are intentionally omitted in favour of this DSL). +- [Kotlin SDK README](../../README.md) diff --git a/sdks/kotlin/docs/api/rpc.md b/sdks/kotlin/docs/api/rpc.md new file mode 100644 index 0000000000..79303f4398 --- /dev/null +++ b/sdks/kotlin/docs/api/rpc.md @@ -0,0 +1,319 @@ +# Agent-to-Agent RPC + +> Agent-to-agent RPC over `golem:agent/host@2.0.0`'s `wasm-rpc` resource — a low-level `WasmRpc` binding plus KSP-generated typed clients (`@RemoteAgent`). **Status:** ✅ Complete. + +## Overview + +Golem agents call each other over **wasm-rpc**: a durable, location-transparent invocation +mechanism where one agent invokes a method on another agent identified by its type name and +constructor arguments. This SDK exposes two layers: + +1. **Low-level host binding** — [`WasmRpc`](#wasmrpc), a direct wrapper over the + `golem:agent/host@2.0.0` `wasm-rpc` resource. You supply arguments as + [`SchemaValue`](types.md) trees and decode results yourself. Supports blocking, fire-and-forget, + async (via [`FutureInvokeResult`](#futureinvokeresult)), and scheduled/cancelable + (via [`CancellationToken`](#cancellationtoken)) invocation. +2. **Typed client** — annotate a Kotlin interface with [`@RemoteAgent`](#remoteagent), and KSP + generates a `Rpc` class that implements it. Each method encodes its arguments, invokes + the remote agent, and decodes the result back to the declared Kotlin return type — errors + surface as [`RpcException`](#rpcexception). This is what application code should normally use. + +Both layers ride `schema-value-tree`, so the full composite-type machinery +([`TypedSchemaValue`](types.md) / the WIT-type grammar) applies to arguments and results. +Values flow through `buildSchemaValueTree` / `liftSingleValue` under the hood. + +`WasmRpc`, `FutureInvokeResult`, and `CancellationToken` all hold a host-side handle that is +**not** tied to Kotlin/Wasm GC — call `close()` when done. `SchemaValue` and its variants are +documented in [`types.md`](types.md); see the SDK overview in [`../../README.md`](../../README.md). + +## API reference + +### `WasmRpc` + +The low-level client for invoking methods on another agent. Types live in +`cloud.golem.runtime`. + +```kotlin +class WasmRpc(agentTypeName: String, constructorArgs: SchemaValue) { + + /** + * Invokes [methodName] with [input] (typically a Record of the method's args), blocking for + * the result. [resultWitType] is the method's WIT return type used to decode the value + * ("()" for a unit return). + */ + fun invokeAndAwait(methodName: String, input: SchemaValue, resultWitType: String): RpcResult + + /** Fire-and-forget invoke: returns null on success, or the RpcError the host reported. */ + fun invoke(methodName: String, input: SchemaValue): RpcError? + + /** + * Invokes [methodName] with [input] asynchronously, returning a FutureInvokeResult to poll. + * [resultWitType] is the method's WIT return type ("()" for unit). + */ + fun asyncInvokeAndAwait(methodName: String, input: SchemaValue, resultWitType: String): FutureInvokeResult + + /** Schedules [methodName]([input]) to run at [scheduledSeconds].[scheduledNanoseconds] (Unix time). */ + fun scheduleInvocation(scheduledSeconds: Long, scheduledNanoseconds: Int, methodName: String, input: SchemaValue) + + /** Like scheduleInvocation, but returns a CancellationToken to cancel it before it fires. */ + fun scheduleCancelableInvocation(scheduledSeconds: Long, scheduledNanoseconds: Int, methodName: String, input: SchemaValue): CancellationToken + + /** Releases the wasm-rpc handle's guest-side handle-table entry. */ + fun close() +} +``` + +The constructor takes the target agent's registered **type name** plus its **constructor +arguments** as a `SchemaValue` — typically a `SchemaValue.Record` of the target constructor's +parameters. `resultWitType` is the method's WIT return type string used to decode the returned +value (e.g. `"s32"`, `"string"`, `"()"` for unit). + +### `RpcResult` + +The outcome of a blocking RPC call. + +```kotlin +sealed class RpcResult { + /** Success; [value] is null when the remote method returns unit / no value. */ + data class Ok(val value: SchemaValue?) : RpcResult() + data class Err(val error: RpcError) : RpcResult() +} +``` + +### `RpcError` + +The error arm of a wasm-rpc call's `result<_, rpc-error>`. + +```kotlin +sealed class RpcError { + data class ProtocolError(val message: String) : RpcError() + data class Denied(val message: String) : RpcError() + data class NotFound(val message: String) : RpcError() + data class RemoteInternalError(val message: String) : RpcError() + /** remote-agent-error(agent-error) — the nested agent-error payload is not yet decoded. */ + object RemoteAgentError : RpcError() +} +``` + +### `RpcException` + +```kotlin +/** Thrown by a KSP-generated typed RPC client when the remote call returns an RpcError. */ +class RpcException(val error: RpcError) : RuntimeException(error.toString()) +``` + +### `FutureInvokeResult` + +The pending result of an [`asyncInvokeAndAwait`](#wasmrpc) call. Poll [`get`](#futureinvokeresult) +until it returns non-null, or wait on the pollable returned by `subscribe`. + +```kotlin +class FutureInvokeResult { + /** A wasi:io/poll pollable handle that becomes ready when the invocation completes (caller owns it). */ + fun subscribe(): Int + + /** The result if the invocation has completed, or null if it is still pending. */ + fun get(): RpcResult? + + fun cancel() + + /** Releases the future-invoke-result handle's guest-side handle-table entry. */ + fun close() +} +``` + +### `CancellationToken` + +```kotlin +/** A handle to cancel a scheduleCancelableInvocation before it fires. */ +class CancellationToken { + fun cancel() + + /** Releases the cancellation-token handle's guest-side handle-table entry. */ + fun close() +} +``` + +### `@RemoteAgent` + +Marks a Kotlin interface as a typed client for a remote agent. `typeName` is the remote agent's +registered type name. + +```kotlin +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class RemoteAgent(val typeName: String) +``` + +KSP generates a `Rpc` class implementing the annotated interface. Its constructor takes +the remote agent's constructor arguments as a `SchemaValue`; each interface method becomes an +override that encodes its arguments, invokes the remote agent, and decodes the result. + +## How typed clients work + +Given a `@RemoteAgent`-annotated interface, [`RemoteAgentEmitter`] generates a class named +`Rpc`. For an interface like: + +```kotlin +import cloud.golem.annotations.RemoteAgent + +@RemoteAgent("counter") +interface CounterClient { + fun increment(amount: Int): Int + fun reset() +} +``` + +the generated code has this exact shape (constructor delegates to `WasmRpc`; each method builds a +`SchemaValue.Record` of its args, calls `invokeAndAwait`, and branches on `RpcResult`): + +```kotlin +// AUTO-GENERATED by golem-kotlin-ksp — do not edit +package + +import cloud.golem.runtime.SchemaValue +import cloud.golem.runtime.WasmRpc +import cloud.golem.runtime.RpcResult +import cloud.golem.runtime.RpcException + +/** Typed RPC client for the remote agent "counter" (implements your.package.CounterClient). */ +class CounterClientRpc(constructorArgs: SchemaValue) : your.package.CounterClient { + private val rpc = WasmRpc("counter", constructorArgs) + + override fun increment(amount: kotlin.Int): kotlin.Int { + val golemArgs = SchemaValue.Record(listOf(SchemaValue.S32(amount))) + val golemR = rpc.invokeAndAwait("increment", golemArgs, "s32") + return when (golemR) { + is RpcResult.Ok -> (golemR.value!! as SchemaValue.S32).v + is RpcResult.Err -> throw RpcException(golemR.error) + } + } + + override fun reset(): kotlin.Unit { + val golemArgs = SchemaValue.Record(listOf()) + val golemR = rpc.invokeAndAwait("reset", golemArgs, "()") + when (golemR) { is RpcResult.Ok -> Unit; is RpcResult.Err -> throw RpcException(golemR.error) } + } + + /** Releases the underlying wasm-rpc handle. */ + fun close() = rpc.close() +} +``` + +Argument encoding and result decoding are produced by `ConverterCodegen`, the same recursive +`SchemaValue` <-> Kotlin converter used by agent registration. It handles arbitrarily nested +composite types: primitives (`S32`/`Str`/…), records (data classes), `List`, `Option` (nullable), +enums, sealed hierarchies (variants), `Map`, and tuples — so remote methods can take and return +rich Kotlin types, not just primitives. A `Unit` return decodes to the `RpcResult.Ok -> Unit` +branch shown above. + +## Examples + +### Calling another agent with a typed client + +```kotlin +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Endpoint +import cloud.golem.annotations.RemoteAgent +import cloud.golem.runtime.BaseAgent +import cloud.golem.runtime.RpcException +import cloud.golem.runtime.SchemaValue + +// The remote agent's interface — KSP generates CounterClientRpc from this. +@RemoteAgent("counter") +interface CounterClient { + fun increment(amount: Int): Int + fun currentValue(): Int +} + +@Agent +class DashboardAgent : BaseAgent() { + + @Endpoint + fun bumpAndRead(by: Int): Int { + // constructorArgs = the target counter's constructor params (here: a name string). + val counter = CounterClientRpc( + SchemaValue.Record(listOf(SchemaValue.Str("global"))) + ) + try { + counter.increment(by) + return counter.currentValue() + } catch (e: RpcException) { + // e.error is an RpcError (Denied / NotFound / ProtocolError / …) + error("counter RPC failed: ${e.error}") + } finally { + counter.close() // handle is not GC-managed + } + } +} +``` + +### Low-level async invocation + +```kotlin +import cloud.golem.runtime.RpcResult +import cloud.golem.runtime.SchemaValue +import cloud.golem.runtime.WasmRpc + +fun incrementAsync() { + val rpc = WasmRpc("counter", SchemaValue.Record(listOf(SchemaValue.Str("global")))) + try { + val future = rpc.asyncInvokeAndAwait( + methodName = "increment", + input = SchemaValue.Record(listOf(SchemaValue.S32(5))), + resultWitType = "s32" + ) + try { + // Poll until complete (or block on future.subscribe()'s pollable). + var result: RpcResult? = future.get() + while (result == null) result = future.get() + when (result) { + is RpcResult.Ok -> println("new value = ${(result.value as SchemaValue.S32).v}") + is RpcResult.Err -> println("rpc error: ${result.error}") + } + } finally { + future.close() + } + } finally { + rpc.close() + } +} +``` + +### Scheduling a future invocation + +```kotlin +import cloud.golem.runtime.SchemaValue +import cloud.golem.runtime.WasmRpc + +fun scheduleReset(rpc: WasmRpc, atUnixSeconds: Long) { + // Cancelable — keep the token to call token.cancel() before it fires. + val token = rpc.scheduleCancelableInvocation( + scheduledSeconds = atUnixSeconds, + scheduledNanoseconds = 0, + methodName = "reset", + input = SchemaValue.Record(listOf()) + ) + // … later, if the reset is no longer wanted: + token.cancel() + token.close() +} +``` + +## Notes + +- **Prefer typed clients.** `@RemoteAgent` + the generated `Rpc` gives compile-time-checked + arguments and return types and turns errors into `RpcException`; drop to raw `WasmRpc` only when + you need async/scheduled invocation or dynamic method names. +- **Always `close()`.** `WasmRpc`, `FutureInvokeResult`, `CancellationToken`, and the generated + client's `close()` release host-side handles not managed by Kotlin/Wasm GC. Use `try`/`finally`. +- **`resultWitType` must match the remote method's WIT return type** for `invokeAndAwait` / + `asyncInvokeAndAwait` to decode correctly; use `"()"` for unit. Typed clients fill this in for + you from the interface's declared return type. +- **`RpcResult.Ok.value` is null on a unit return.** Typed clients handle this; raw callers must + guard before casting. +- **`RemoteAgentError`** currently carries no decoded payload — the nested `agent-error` is not + yet lifted. The other `RpcError` cases carry a `message` string. +- `golem:agent/host@2.0.0` is already imported for `parse-agent-id`, so RPC needs no new WIT + import. See [`types.md`](types.md) for the `SchemaValue` model and [`../../README.md`](../../README.md) + for the SDK overview. diff --git a/sdks/kotlin/docs/api/secrets.md b/sdks/kotlin/docs/api/secrets.md new file mode 100644 index 0000000000..f93d587e8c --- /dev/null +++ b/sdks/kotlin/docs/api/secrets.md @@ -0,0 +1,97 @@ +# Secrets + +> Revealing a `secret` resource handle back to its inner typed value, through the capability-gated +> `golem:secrets/reveal@0.1.0` interface. **Status:** Complete. + +## Overview + +A **secret** is an unforgeable handle to sensitive material held by the Golem runtime — opaque to +guest code. Secrets arrive as [`SchemaValue.SecretVal`](types.md) inside agent inputs and can be +passed around through schema values without ever exposing plaintext. + +`reveal` is the **capability-gated escape hatch** that converts a secret back to its inner value. +The capability *is the import*: a component that does not import `golem:secrets/reveal` cannot +reveal secrets at all. Every successful reveal is recorded in the calling agent's oplog as +`(calling-agent, secret-id, timestamp)` (the plaintext is never part of the audit record). + +> Prefer **host-mediated substitution** where a host capability accepts `borrow` directly +> (HTTP auth headers, signing, encryption) — the runtime substitutes plaintext at the syscall +> boundary, so it never crosses into guest memory. `reveal` is the loud-by-design fallback for +> genuinely custom protocols the host doesn't natively support. + +## API reference + +`cloud.golem.runtime.host.SecretApi`: + +```kotlin +object SecretApi { + /** Reveal a secret value to its inner [witType]-typed value. */ + fun reveal(secret: SchemaValue.SecretVal, witType: String): SchemaValue + + /** Reveal a raw `secret` resource handle. */ + fun reveal(secretHandle: Int, witType: String): SchemaValue +} +``` + +`witType` is the same rich WIT type-string grammar the [agent surface](types.md) uses — a primitive +(`"string"`, `"s64"`, …) or an arbitrarily nested composite (`"record"`, +`"list"`, …). The host validates it against the secret's pinned inner type and returns the +stored value, which `reveal` lifts into the matching [`SchemaValue`](types.md). + +The secret is **borrowed** — the caller keeps ownership of the handle and should release it with +`dropSecret(handle)` when done. + +On failure the host returns a `SecretError`, which `reveal` throws wrapped in a +`SecretRevealException`: + +```kotlin +sealed class SecretError { + data class Unavailable(val message: String) : SecretError() // resolution failed (store gone / partitioned) + data class VersionNotFound(val versionBytes: List) : SecretError() // pinned version destroyed + data class Internal(val message: String) : SecretError() // opaque runtime error +} + +class SecretRevealException(val error: SecretError) : RuntimeException(...) +``` + +## Examples + +### Reveal a string secret + +```kotlin +import cloud.golem.runtime.SchemaValue +import cloud.golem.runtime.dropSecret +import cloud.golem.runtime.host.SecretApi +import cloud.golem.runtime.host.SecretRevealException + +fun useApiKey(secret: SchemaValue.SecretVal): String { + try { + val revealed = SecretApi.reveal(secret, "string") as SchemaValue.Str + return revealed.v + } catch (e: SecretRevealException) { + error("could not reveal API key: ${e.error}") + } finally { + dropSecret(secret.handle) // borrowed handle: release it when done + } +} +``` + +### Reveal a composite secret + +```kotlin +// A secret whose inner type is record. +val creds = SecretApi.reveal(secret, "record") as SchemaValue.Record +val user = (creds.fields[0] as SchemaValue.Str).v +val token = (creds.fields[1] as SchemaValue.Str).v +``` + +## Notes + +- **Composite inner types are fully supported** — `reveal` builds the `expected` schema-graph from + `witType` (using the shared schema-graph builder) and lifts the returned `schema-value-tree` + against it, so records, variants, lists, maps, etc. all work. +- The import is the capability; nothing to configure beyond the SDK depending on the + `golem:secrets/reveal@0.1.0` interface (declared in the native world). +- `reveal` does **not** drop the secret handle (it borrows). Manage the handle's lifetime yourself. +- See [types.md](types.md) for the `SchemaValue` model and the WIT type-string grammar, and the + [SDK overview](../../README.md). diff --git a/sdks/kotlin/docs/api/tools.md b/sdks/kotlin/docs/api/tools.md new file mode 100644 index 0000000000..2eb436af22 --- /dev/null +++ b/sdks/kotlin/docs/api/tools.md @@ -0,0 +1,217 @@ +# Tools + +> Exposing an agent method as a `golem:tool@0.1.0` tool (`@Tool`), and discovering / invoking +> tools registered by other components (`golem:tool/host@0.1.0`). **Status:** 🟡 Partial — +> exposing tools, discovery (`getAllTools`/`getTool`), and all three invocation forms +> (fire-and-forget `invoke`, blocking `invokeAndAwait`, async `asyncInvokeAndAwait`) are done; +> only **streamed stdin** remains a follow-up. + +## Overview + +A Golem tool is a CLI-shaped capability a component exports alongside its +[agent](agent-model.md) surface. There are two directions: + +- **Exposing** a tool — annotate an `@Agent` method with [`@Tool`](#tool-annotation) + (`cloud.golem.annotations.Tool`). Its positional parameters are derived 1:1 from the method's + parameters, in declaration order. +- **Consuming** tools — use [`ToolHost`](#toolhost) (`cloud.golem.runtime.ToolHost`) to + discover the tools the agent has access to, and the [`ToolRpc`](#toolrpc) resource to invoke + one. + +For the SDK overview see [`../../README.md`](../../README.md). + +## `@Tool` annotation + +`cloud.golem.annotations.Tool` marks a method as a tool. Its identity is the root command name +(`name`); MVP scope is one tool = one root command (no subcommands), whose positionals map 1:1 +from the method's parameters in declaration order. `@Command` documents an individual +positional parameter. + +```kotlin +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Tool( + val name: String, + val description: String = "" +) + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class Command( + val description: String = "" +) +``` + +## Discovery — `ToolHost` + +`cloud.golem.runtime.ToolHost` reads the tools registered by other components in the +environment. A discovered tool is projected down to the fields the SDK can currently read +without decoding the full CLI-command-tree structure — its canonical identity (`name`), its +`version`, and the component that implements it: + +```kotlin +data class RegisteredTool(val name: String, val version: String, val implementedBy: ComponentId) + +object ToolHost { + /** Every tool the calling agent has access to in the current environment. */ + fun getAllTools(): List + + /** Looks up a single registered tool by name, iff the calling agent has access to it. */ + fun getTool(name: String): RegisteredTool? +} +``` + +`ComponentId` is the same `golem:core/types@2.0.0` id used across the [Host API](host-api.md). + +## Invocation — `ToolRpc` + +`cloud.golem.runtime.ToolRpc` is a handle for invoking a tool registered elsewhere. Construct +it with the tool's name, invoke it via one of the three forms below, and `close()` it when done +(the handle is not tied to Kotlin/Wasm GC). `stdin` is always `none` on every form (streamed +stdin is the remaining follow-up); `input` may be any composite `TypedSchemaValue`. + +```kotlin +class ToolRpc(toolName: String) { + /** Fire-and-forget. Returns null on success, or the ToolRpcError the host reported. */ + fun invoke(commandPath: List, input: TypedSchemaValue): ToolRpcError? + + /** Blocking. Returns the tool's result value + optional stdout stream, or an error. */ + fun invokeAndAwait(commandPath: List, input: TypedSchemaValue): ToolInvokeResult + + /** Async. Returns a future to poll (get) or wait on (subscribe). */ + fun asyncInvokeAndAwait(commandPath: List, input: TypedSchemaValue): ToolFutureInvokeResult + + /** Releases the tool-rpc handle. */ + fun close() +} + +/** A pending async invocation (golem:tool/host `future-invoke-result`). */ +class ToolFutureInvokeResult { + fun subscribe(): Int // wasi:io/poll pollable handle (caller-owned) + fun get(): ToolInvokeResult? // null while pending + fun cancel() + fun close() +} + +/** The awaited outcome: the tool's `invocation-result`, or an error. */ +sealed class ToolInvokeResult { + data class Ok(val value: ToolInvocationResult) : ToolInvokeResult() + data class Err(val error: ToolRpcError) : ToolInvokeResult() +} + +data class ToolInvocationResult( + val result: TypedSchemaValue?, // the tool's return value, fully decoded (self-describing) + val stdoutHandle: Int?, // opaque wasi:io output-stream handle for stdout, or null +) + +sealed class ToolRpcError { + data class ProtocolError(val message: String) : ToolRpcError() + data class Denied(val message: String) : ToolRpcError() + data class NotFound(val message: String) : ToolRpcError() + data class RemoteInternalError(val message: String) : ToolRpcError() + /** `remote-tool-error(tool-error)` — the nested tool-error payload is not yet decoded. */ + object RemoteToolError : ToolRpcError() +} +``` + +### Awaiting a tool's result + +```kotlin +val rpc = ToolRpc("formatter") +try { + when (val r = rpc.invokeAndAwait(listOf("format"), TypedSchemaValue("string", SchemaValue.Str(src)))) { + is ToolInvokeResult.Ok -> (r.value.result?.value as? SchemaValue.Str)?.v // the formatted output + is ToolInvokeResult.Err -> error("format failed: ${r.error}") + } +} finally { + rpc.close() +} +``` + +`input` is a [`TypedSchemaValue`](types.md) — a self-describing value pairing a WIT type string +with a matching `SchemaValue`. It supports the **full composite grammar** (primitives plus +records, lists, options, variants, enums, tuples, maps, results — arbitrarily nested): + +```kotlin +data class TypedSchemaValue(val witType: String, val value: SchemaValue) +``` + +## Examples + +Exposing a method as a tool: + +```kotlin +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Command +import cloud.golem.annotations.Tool + +@Agent +class GreeterAgent { + + @Tool(name = "greet", description = "Print a greeting for someone") + fun greet( + @Command(description = "the name to greet") name: String, + @Command(description = "how many times") times: Int + ): String = (1..times).joinToString("\n") { "hello, $name" } +} +``` + +Discovering the tools available to the current agent: + +```kotlin +import cloud.golem.runtime.ToolHost + +fun listTools(): List = + ToolHost.getAllTools().map { "${it.name}@${it.version}" } +``` + +Invoking a tool (fire-and-forget). The `input` here is a simple `string`, but any composite +`TypedSchemaValue` (e.g. `TypedSchemaValue("record", SchemaValue.Record(…))`) +works the same way: + +```kotlin +import cloud.golem.runtime.SchemaValue +import cloud.golem.runtime.ToolHost +import cloud.golem.runtime.ToolRpc +import cloud.golem.runtime.ToolRpcError +import cloud.golem.runtime.TypedSchemaValue + +fun runFormatter(path: String): String? { + val tool = ToolHost.getTool("formatter") ?: return "no such tool" + val rpc = ToolRpc(tool.name) + try { + val input = TypedSchemaValue("string", SchemaValue.Str(path)) + return when (val err = rpc.invoke(commandPath = listOf("format"), input = input)) { + null -> null // success + is ToolRpcError.NotFound -> "command not found: ${err.message}" + is ToolRpcError.Denied -> "denied: ${err.message}" + else -> "invocation failed: $err" + } + } finally { + rpc.close() + } +} +``` + +## Notes + +- **What works today:** exposing tools via `@Tool`, discovery via `ToolHost.getAllTools` / + `getTool`, and all three invocation forms — fire-and-forget `invoke` (`result<_, rpc-error>`), + blocking `invokeAndAwait` (`result`), and async + `asyncInvokeAndAwait` (`future-invoke-result`). Results decode fully: `invocation-result.result` + is a self-describing `typed-schema-value`, lifted with no prior type knowledge. `input` payloads + support the full composite grammar. +- **Follow-ups:** streamed **stdin** — every invoke form passes `stdin = none`; supplying a real + `wasi:io/streams` input-stream needs stream-construction plumbing the SDK doesn't have yet. + `ToolInvocationResult.stdoutHandle` is surfaced as an opaque `wasi:io` output-stream handle + (stream reads not yet wrapped). `ToolRpcError.RemoteToolError`'s nested `tool-error` is still + undecoded. +- **Discovery is lossy by design.** `RegisteredTool` projects the WIT `tool` record down to its + canonical identity field (`name` = `commands.nodes[0].name`), `version`, and `implementedBy`. + The full CLI-command-tree (options, flags, positionals, constraints) is not yet decoded. +- **`ToolRpcError.RemoteToolError`** currently carries no payload — the nested `tool-error` is + not yet decoded. +- Close each `ToolRpc` handle when done. + +See also: [Agent model](agent-model.md) · [Types](types.md) · [WASI](wasi.md) · +[Host API](host-api.md) · [SDK README](../../README.md). diff --git a/sdks/kotlin/docs/api/transactions.md b/sdks/kotlin/docs/api/transactions.md new file mode 100644 index 0000000000..5e3b574fe5 --- /dev/null +++ b/sdks/kotlin/docs/api/transactions.md @@ -0,0 +1,244 @@ +# Transactions (Saga / Compensation) + +> Compensating-transaction (saga) helpers for Golem agents, built entirely on existing host primitives (`getOplogIndex`/`setOplogIndex`, `markAtomicOperation`, `trap`) — no new WIT surface. **Status:** Complete. + +## Overview + +`Transactions` is pure application logic layered over the Golem host. It lets an agent +run a sequence of side-effecting **operations**, each paired with a **compensation** that +undoes it, so that a mid-sequence failure can roll the whole thing back cleanly. This is the +classic *saga* pattern. + +Two flavours are provided: + +- **Infallible** ([`infallibleTransaction`](#infallibletransaction)) — for work that *must* + eventually succeed. On any failure, every registered compensation runs in reverse order, + the oplog index is reset to the transaction's start, and the whole body re-executes. + It keeps retrying until all operations succeed. +- **Fallible** ([`fallibleTransaction`](#fallibletransaction)) — for work where you want to + *handle* the error yourself. On failure, compensations run best-effort (no retry) and the + failure is returned as a [`TransactionFailure`](#transactionfailure). + +### Execution model — synchronous, no async boundary + +This is a native port of the Scala SDK's `Transactions.scala`. The Scala version wraps every +step in a `Future`, but that is an artifact of its Scala.js single-threaded environment. The +native SDK's underlying host calls (`getOplogIndex`, `setOplogIndex`, `markBeginOperation`, +`markEndOperation`, `trap`) have **no async host boundary at all**, so the port is +**synchronous throughout** — no coroutines, no `suspend`. A synchronous translation is the +faithful one here, not a simplification. + +Rollback / retry works by moving the Golem oplog index: on failure the index is reset to the +value captured at the start of the transaction, which is how re-execution and durable replay +are achieved. See also [Guards & Checkpoint](./guards-checkpoint.md) for the lower-level +primitives this builds on. + +## API reference + +### `Either` + +A minimal local `Either` — this SDK has no Arrow/stdlib `Either` dependency; `Transactions` +introduces it (and [`Checkpoint`](./guards-checkpoint.md) reuses it). + +```kotlin +sealed class Either { + data class Left(val value: L) : Either() + data class Right(val value: R) : Either() +} +``` + +By convention `Left` carries an error and `Right` carries a success value. + +### `Operation` + +An atomic step with an execute half and a compensate half. + +```kotlin +class Operation( + private val run: (In) -> Either, + private val compensateFn: (In, Out) -> Either, +) { + fun execute(input: In): Either + fun compensate(input: In, output: Out): Either +} +``` + +Construct one with the [`Transactions.operation`](#transactionsoperation) factory rather than +the constructor directly. + +### `TransactionFailure` + +Describes how a fallible transaction failed. + +```kotlin +sealed class TransactionFailure { + data class FailedAndRolledBackCompletely(val error: Err) : TransactionFailure() + data class FailedAndRolledBackPartially(val error: Err, val compensationFailure: Err) : TransactionFailure() +} +``` + +- `FailedAndRolledBackCompletely` — the operation failed and *every* compensation succeeded. +- `FailedAndRolledBackPartially` — the operation failed and, while rolling back, a + compensation itself failed (`compensationFailure` carries that error; remaining + compensations are not attempted). + +### `Transactions.operation` + +```kotlin +fun operation( + run: (In) -> Either, + compensate: (In, Out) -> Either, +): Operation +``` + +Builds an [`Operation`](#operation) from an execute function and a compensate function. The +compensate function receives both the original `input` and the `output` the execute step +produced, so it has everything needed to undo the effect. + +### `Transactions.infallibleTransaction` + +```kotlin +fun infallibleTransaction(body: (InfallibleTransaction) -> A): A +``` + +Runs `body` inside an atomic region. Call +[`InfallibleTransaction.execute`](#infallibletransactionexecute) for each step. If any step +fails, all registered compensations run in reverse order, the oplog index resets to the +transaction start, and `body` re-runs from the top — repeating until it completes without +failure. An unexpected exception (anything other than the internal retry signal) traps. + +#### `InfallibleTransaction.execute` + +```kotlin +fun execute(operation: Operation, input: In): Out +``` + +Executes `operation` with `input`. On success it registers the compensation and returns the +raw `Out` value (no `Either` to unwrap — failures roll back and retry rather than being +returned). On failure it triggers rollback: all compensations registered so far run in +reverse order, then a retry is signalled. + +### `Transactions.fallibleTransaction` + +```kotlin +fun fallibleTransaction( + body: (FallibleTransaction) -> Either +): Either, A> +``` + +Runs `body` inside an atomic region and returns its result. If `body` returns `Either.Right`, +the transaction commits and the value is returned as `Right`. If it returns `Either.Left`, +registered compensations run in reverse order (best-effort) and a +[`TransactionFailure`](#transactionfailure) is returned as `Left`. Unlike the infallible +variant, there is no automatic retry. + +#### `FallibleTransaction.execute` + +```kotlin +fun execute(operation: Operation, input: In): Either +``` + +Executes `operation`. On success it registers the compensation and returns `Right(out)`. On +failure it returns the error as `Left` **without** rolling back — you decide how to proceed. +Typically you propagate the `Left` out of `body`, which triggers the rollback in +`fallibleTransaction`. + +#### `FallibleTransaction.onFailure` + +```kotlin +fun onFailure(error: Err): TransactionFailure +``` + +Runs all registered compensations in reverse order and classifies the outcome as +`FailedAndRolledBackCompletely` or `FailedAndRolledBackPartially`. `fallibleTransaction` +calls this for you when `body` returns `Left`; call it directly only if you manage the +`FallibleTransaction` yourself. + +## Examples + +### Infallible saga — book a trip, retry until it sticks + +```kotlin +import cloud.golem.runtime.Either +import cloud.golem.runtime.Transactions + +@Agent +class TripBookingAgent { + + private val reserveFlight = Transactions.operation( + run = { city -> Either.Right(flightApi.reserve(city)) }, // -> reservationId + compensate = { _, reservationId -> Either.Right(flightApi.cancel(reservationId)) }, + ) + + private val chargeCard = Transactions.operation( + run = { cents -> Either.Right(payments.charge(cents)) }, // -> chargeId + compensate = { _, chargeId -> Either.Right(payments.refund(chargeId)) }, + ) + + @Endpoint + fun bookTrip(city: String, priceCents: Int): String = + Transactions.infallibleTransaction { tx -> + // execute returns the raw success value; a failure rolls back + retries the whole body. + val reservationId = tx.execute(reserveFlight, city) + val chargeId = tx.execute(chargeCard, priceCents) + "booked $reservationId / paid $chargeId" + } +} +``` + +If `chargeCard` fails, the flight reservation's compensation (`flightApi.cancel`) runs, the +oplog rewinds, and the whole body re-executes — so a transient payment outage is retried +transparently. + +### Fallible saga — return a typed failure instead of retrying + +```kotlin +import cloud.golem.runtime.Either +import cloud.golem.runtime.TransactionFailure +import cloud.golem.runtime.Transactions + +@Endpoint +fun tryBookTrip(city: String, priceCents: Int): String { + val result = Transactions.fallibleTransaction { tx -> + val reservationId = when (val r = tx.execute(reserveFlight, city)) { + is Either.Right -> r.value + is Either.Left -> return@fallibleTransaction Either.Left(r.value) // rolls back + } + when (val c = tx.execute(chargeCard, priceCents)) { + is Either.Right -> Either.Right("booked $reservationId / paid ${c.value}") + is Either.Left -> Either.Left(c.value) // rolls back reservation + } + } + + return when (result) { + is Either.Right -> result.value + is Either.Left -> when (val f = result.value) { + is TransactionFailure.FailedAndRolledBackCompletely -> + "failed, fully rolled back: ${f.error}" + is TransactionFailure.FailedAndRolledBackPartially -> + "failed, PARTIAL rollback: ${f.error} (compensation also failed: ${f.compensationFailure})" + } + } +} +``` + +## Notes + +- Compensations are always run in **reverse registration order** (last operation undone first). +- **Infallible** `execute` returns `Out` directly; **fallible** `execute` returns + `Either`. That difference reflects retry-vs-return semantics. +- Returning `Either.Left` from a fallible `body` is what actually triggers rollback — a + successfully-`execute`d step is *not* undone unless the body ultimately signals failure. +- The whole transaction runs inside an atomic operation guard (see + [`Guards.markAtomicOperation`](./guards-checkpoint.md)); the guard is always dropped, even + on the failure/retry paths. +- Unexpected exceptions (not the internal retry signal) are surfaced via the host `trap` + function, which appears as an uncatchable wasm trap. + +## See also + +- [Guards & Checkpoint](./guards-checkpoint.md) — the atomic-region and oplog-rewind + primitives this builds on. +- [Retry](./retry.md) — declarative, host-level retry policies (a different mechanism from + saga compensation). +- [Kotlin SDK README](../../README.md) diff --git a/sdks/kotlin/docs/api/types.md b/sdks/kotlin/docs/api/types.md new file mode 100644 index 0000000000..bf248f2bd5 --- /dev/null +++ b/sdks/kotlin/docs/api/types.md @@ -0,0 +1,236 @@ +# Types + +> How Kotlin types map to Golem's WIT / schema value model for agent constructor parameters, method parameters, and return types — exactly what `TypeMapper.resolve` and `TypeDesc.toWit()` support. **Status:** Complete (per capability ledger). + +## Overview + +When KSP processes an [`@Agent`](agent-model.md) class, every constructor parameter, method +parameter, and method return type is resolved to a `TypeDesc` by +`cloud.golem.ksp.TypeMapper.resolve`. Each `TypeDesc` produces: + +- a **WIT type string** via `TypeDesc.toWit()` — consumed by the runtime schema-graph builder + and value lift, and +- recursive **`SchemaValue` ⟷ Kotlin converters** via `ConverterCodegen.encode` / `decode`, + used by the generated registration and RPC clients. + +The full composite set is modelled, and arbitrary nesting is supported: records inside +lists, options of maps, variants carrying records, tuples of enums, and so on. Field/case +**names** matter for the schema graph; at the value level everything is positional. + +See [Agent Model](agent-model.md) for where these types appear, and the +[SDK README](../../README.md) for the build/deploy flow. + +## Type mapping table + +| Kotlin type | WIT type (`toWit()`) | `TypeDesc` | Notes | +|-------------|----------------------|------------|-------| +| `Int` | `s32` | `Prim` | | +| `Long` | `s64` | `Prim` | | +| `Short` | `s16` | `Prim` | | +| `Byte` | `s8` | `Prim` | | +| `UInt` | `u32` | `Prim` | | +| `ULong` | `u64` | `Prim` | | +| `UShort` | `u16` | `Prim` | | +| `UByte` | `u8` | `Prim` | | +| `Float` | `f32` | `Prim` | | +| `Double` | `f64` | `Prim` | | +| `Boolean` | `bool` | `Prim` | | +| `String` | `string` | `Prim` | | +| `Unit` | `()` | `UnitT` | A method with no return value. | +| `T?` | `option` | `OptionT` | Wraps the non-null form; nesting recurses. | +| `List` | `list` | `ListT` | | +| `Map` | `map` | `MapT` | | +| `Pair` | `tuple` | `TupleT` | | +| `Triple` | `tuple` | `TupleT` | | +| `enum class` | `enum` | `EnumT` | Entry names in declaration order. | +| `sealed class` / `interface` | `variant` | `VariantT` | Object case = no payload (`_`); param case = record. | +| `data class` | `record` | `Record` | Fields = primary-constructor params, recursed. | +| `cloud.golem.Datetime` | `datetime` | `DatetimeT` | `data class Datetime(seconds: Long, nanoseconds: Int)`. | +| `cloud.golem.runtime.Either` | `result` | `ResultT` | `Right` = ok, `Left` = err; a `Unit` arm becomes `_`. | + +Anything else raises `Unsupported Kotlin type for WIT mapping: `. + +## `TypeDesc` and `toWit()` + +`TypeDesc` is the resolved agent-surface type. Each case's `toWit()` output: + +```kotlin +sealed class TypeDesc { + abstract fun toWit(): String + + // Prim("s32").toWit() == "s32" + data class Prim(val wit: String) : TypeDesc() + + // UnitT.toWit() == "()" + object UnitT : TypeDesc() + + // Record("com.acme.Point", [x:s32, y:s32]).toWit() == "record" + data class Record(val kotlinFqn: String, val fields: List) : TypeDesc() + + // ListT(Prim("string")).toWit() == "list" + data class ListT(val elem: TypeDesc) : TypeDesc() + + // OptionT(Prim("s32")).toWit() == "option" + data class OptionT(val inner: TypeDesc) : TypeDesc() + + // EnumT("com.acme.Color", ["Red","Green"]).toWit() == "enum" + data class EnumT(val kotlinFqn: String, val cases: List) : TypeDesc() + + // VariantT("com.acme.Shape", [Circle:record, Unknown:_]).toWit() + // == "variant,Unknown:_>" + data class VariantT(val kotlinFqn: String, val cases: List) : TypeDesc() + + // MapT(Prim("string"), Prim("s32")).toWit() == "map" + data class MapT(val key: TypeDesc, val value: TypeDesc) : TypeDesc() + + // TupleT("kotlin.Pair", [Prim("string"), Prim("s32")]).toWit() == "tuple" + data class TupleT(val kotlinFqn: String, val elems: List) : TypeDesc() +} + +data class Field(val name: String, val type: TypeDesc) +data class VariantCase(val name: String, val kotlinFqn: String, val payload: TypeDesc?) +``` + +The WIT-string grammar these produce (shared by the value lift and schema-graph builder): + +``` +primitives: bool, s8, s16, s32, s64, u8, u16, u32, u64, f32, f64, char, string +record (field names; body is positional at the value level) +variant (case names; `_` = no payload) +enum or enum (case names; the value carries only a case index) +list option tuple map result (`_` = unit ok/err) +``` + +## How resolution works (`TypeMapper.resolve`) + +- **Nullable first:** `T?` resolves to `OptionT(resolve(T))` before anything else. +- **Primitives / `Unit`:** looked up directly. +- **`List`:** single type argument recursed. +- **`Map`:** both type arguments recursed. +- **`Pair` / `Triple`:** every type argument recursed into a `TupleT`. +- **`enum class`:** cases are the enum entries in declaration order. +- **`sealed` class/interface:** each **direct** subclass is a variant case. An `object` + subclass (or one with no primary-constructor params) has **no payload** (`_`); a subclass + **with** primary-constructor params carries a **record** of those params. Case order is + fixed at resolution and reused for encode/decode. A sealed type with no subclasses errors. +- **`data class`:** a record whose fields are the primary-constructor parameters, recursed. +- **`cloud.golem.Datetime`:** maps to the `datetime` WIT type (a `DatetimeT`). +- **`cloud.golem.runtime.Either`:** maps to `result` — `Right` is the ok arm, `Left` + the err arm; a `Unit` arm becomes `_`. + +> **Other utility types.** The caller's [`Principal`](agent-model.md#baseagent) and the +> [`Uuid`](agent-model.md#baseagent) it may carry are runtime identity types (read via +> `BaseAgent.principal`), not agent-surface parameter types — see the [agent model](agent-model.md). + +## Value model (`SchemaValue` converters) + +`ConverterCodegen` generates the recursive mapping between the lifted `SchemaValue` tree and +your Kotlin values. The correspondence: + +| `TypeDesc` | `SchemaValue` variant(s) | Encode / decode | +|-----------|--------------------------|-----------------| +| `Prim(wit)` | `Bool`, `S8`..`S64`, `U8`..`U64`, `F32`/`F64`, `Chr`, `Str` | `.v` field | +| `UnitT` | `Unit_` | decode is not supported (a Unit return has no value to read) | +| `Record` | `Record(fields: List)` | positional by field index; rebuilt via the class constructor | +| `ListT` | `ListVal(items)` | `map` over items | +| `OptionT` | `OptionVal(inner: SchemaValue?)` | `?.let` over `inner` | +| `EnumT` | `EnumVal(caseIndex)` | `entries[caseIndex]` / `.ordinal` | +| `VariantT` | `VariantVal(caseIndex, payload)` | `when` over `caseIndex`; payload is a `Record` for payload cases | +| `MapT` | `MapVal(entries: List>)` | `associate` / `entries.map` | +| `TupleT` | `TupleVal(items)` | positional by element index; rebuilt via `Pair`/`Triple` ctor | + +## Examples + +### Records (data classes) as parameters and return types + +```kotlin +data class GeoPoint(val lat: Double, val lon: Double) // record +data class Place(val name: String, val at: GeoPoint) // record> + +@Endpoint(post = "/places") +fun addPlace(place: Place): String = place.name +``` + +### Lists, options, and maps + +```kotlin +// param: list> +// return: option>> +@Endpoint(post = "/nearest") +fun nearest(points: List, to: GeoPoint): Place? { /* ... */ } + +// return: map +@Endpoint(get = "/counts") +fun counts(): Map = mapOf("a" to 1, "b" to 2) +``` + +### Tuples + +```kotlin +// return: tuple +@Endpoint(get = "/top") +fun top(): Pair = "alice" to 42 + +// param: tuple +@Endpoint(post = "/vec") +fun addVec(v: Triple): Double = v.first + v.second + v.third +``` + +### Enums + +```kotlin +enum class Priority { Low, Medium, High } // enum + +@Endpoint(post = "/prioritize") +fun prioritize(p: Priority): Priority = if (p == Priority.Low) Priority.Medium else p +``` + +### Sealed classes → variants (object cases and record cases) + +```kotlin +sealed class Shape { + object Unknown : Shape() // Unknown:_ (no payload) + data class Circle(val radius: Double) : Shape() // Circle:record + data class Rect(val w: Double, val h: Double) : Shape() // Rect:record +} +// Shape.toWit() == variant,Rect:record> + +@Endpoint(post = "/area") +fun area(shape: Shape): Double = when (shape) { + is Shape.Circle -> Math.PI * shape.radius * shape.radius + is Shape.Rect -> shape.w * shape.h + Shape.Unknown -> 0.0 +} +``` + +### Arbitrary nesting + +```kotlin +data class Order( + val id: String, + val lines: List>, // list> + val coupon: String?, // option + val ship: Shape // nested variant +) +// record>,coupon:option,ship:variant<...>> +``` + +## Notes + +- **Integer widths.** The forward direction preserves each width the user writes + (`Int`→`s32`, `Long`→`s64`, `UInt`→`u32`, …). Per the `TypeMapper` source note, a WIT width + other than `s32` may not survive a full Kotlin → WIT → Kotlin round-trip through the + earlier Kotlin/JS binding, which collapsed every integer width to `Int`; `Int ⟷ s32` is + always exact. +- **`char`.** The grammar and `SchemaValue` (`Chr`) include `char`, but Kotlin `Char` is not + in `TypeMapper`'s primitive table — use `String`. +- **`result`.** The witType grammar recognizes `result`, but `TypeMapper.resolve` + does not produce it from a Kotlin type; model fallible returns with a sealed class + (→ variant) instead. +- **Object vs param variant cases.** A sealed subclass with no primary-constructor params + (including `object`) becomes a payloadless case (`_`); one with params becomes a record. +- **Case/field order is fixed** at resolution time and reused for both encode and decode, so + reordering enum entries or sealed subclasses changes the wire encoding. +- **Names are schema-only.** Field and case names live in the schema graph; the value tree is + positional, so lift matches by index. +- **Unsupported types error at compile time** with `Unsupported Kotlin type for WIT mapping`. diff --git a/sdks/kotlin/docs/api/wasi.md b/sdks/kotlin/docs/api/wasi.md new file mode 100644 index 0000000000..2ef57b7272 --- /dev/null +++ b/sdks/kotlin/docs/api/wasi.md @@ -0,0 +1,244 @@ +# WASI Capabilities + +> Native Kotlin/Wasm bindings to the WASI capabilities Golem hosts: +> `wasi:keyvalue@0.1.0`, `wasi:blobstore`, `wasi:config@0.2.0-draft`, `wasi:logging`, +> and `wasi:cli/environment@0.2.3`. **Status:** 🟢 Complete. + +## Overview + +These bindings live in `cloud.golem.runtime.wasi` and give an [`@Agent`](agent-model.md) +direct, JavaScript-free access to the WASI host capabilities Golem provides. Each is a thin +wrapper over raw `@WasmImport` bindings whose canonical-ABI signatures were verified against +the WIT under `wit-native/deps/`, and each mirrors the surface of its Scala-SDK counterpart. + +Fallible operations return the SDK's local [`Either`](transactions.md) type +(`cloud.golem.runtime.Either`) — `Either.Left(error)` or `Either.Right(value)`, each with a +`.value` property. Resource-backed capabilities (KeyValue's `Bucket`, Blobstore's +`Container`) hand back a handle wrapper that **must** be `close()`d when done, because the +underlying host handle is not tied to Kotlin/Wasm garbage collection. + +| Capability | WIT package | Entry point | +|------------|-------------|-------------| +| KeyValue | `wasi:keyvalue@0.1.0` (eventual + eventual-batch) | `Bucket.open(...)` | +| Blobstore | `wasi:blobstore` (unversioned) | `Blobstore` object | +| Config | `wasi:config@0.2.0-draft` (store) | `Config` object | +| Logging | `wasi:logging` (unversioned) | `Logging` object | +| Environment| `wasi:cli/environment@0.2.3` | `Environment` object | + +For the SDK overview and build/deploy flow see [`../../README.md`](../../README.md). + +## KeyValue + +`wasi:keyvalue@0.1.0`, the `types` / `eventual` / `eventual-batch` interfaces (the same subset +the Scala SDK wraps; the `atomic` / `cache` / `handle-watch` interfaces are not covered). A +`Bucket` is a named collection of key → `ByteArray` pairs. + +Errors are the resource-backed `wasi:keyvalue` error, resolved eagerly to its trace message at +the point of failure and surfaced as a `KvError`: + +```kotlin +class KvError internal constructor(handle: Int) { + val message: String +} + +class Bucket internal constructor(private val handle: Int) { + fun get(key: String): Either + fun set(key: String, value: ByteArray): Either + fun delete(key: String): Either + fun exists(key: String): Either + fun keys(): Either> + fun getMany(keys: List): Either> + fun deleteMany(keys: List): Either + fun close() + + companion object { + fun open(name: String): Either + } +} +``` + +`get` / `getMany` return `null` for a missing key (`getMany` returns one nullable entry per +requested key, in order). + +## Blobstore + +`wasi:blobstore` (unversioned package), the `blobstore` / `container` / `types` interfaces. A +`Container` is a named collection of binary objects. Here the error type is the package's plain +`type error = string`, so every result is `Either`. + +```kotlin +data class ContainerMetadata(val name: String, val createdAt: Long) +data class ObjectMetadata(val name: String, val container: String, val createdAt: Long, val size: Long) +data class ObjectId(val container: String, val name: String) + +object Blobstore { + fun createContainer(name: String): Either + fun getContainer(name: String): Either + fun deleteContainer(name: String): Either + fun containerExists(name: String): Either + fun copyObject(src: ObjectId, dest: ObjectId): Either + fun moveObject(src: ObjectId, dest: ObjectId): Either +} + +class Container internal constructor(private val handle: Int) { + fun name(): Either + fun info(): Either + fun getData(objectName: String, start: Long, end: Long): Either + fun writeData(objectName: String, data: ByteArray): Either + fun listObjects(): Either> + fun deleteObject(name: String): Either + fun deleteObjects(names: List): Either + fun hasObject(name: String): Either + fun objectInfo(name: String): Either + fun clear(): Either + fun close() +} +``` + +> **Note:** `getData` reads a byte range `[start, end)` of an object. `listObjects` reads a +> single batch of up to 1000 object names and does not paginate further — the same limitation +> as the Scala reference SDK; it is not a full listing for containers with more than 1000 +> objects. + +## Config + +`wasi:config@0.2.0-draft`, the `store` interface. Read-only access to string configuration +values. The `-draft` pre-release suffix is part of the package version. + +```kotlin +sealed class ConfigError { + data class Upstream(val message: String) : ConfigError() + data class Io(val message: String) : ConfigError() +} + +object Config { + /** `Right(null)` if the key is not found. */ + fun get(key: String): Either + fun getAll(): Either> +} +``` + +## Logging + +`wasi:logging` (unversioned package), the `logging` interface. Emits a structured log record +(level + context + message) to the host. + +```kotlin +enum class LogLevel { TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL } + +object Logging { + fun log(level: LogLevel, context: String, message: String) + + fun trace(message: String, context: String = "") + fun debug(message: String, context: String = "") + fun info(message: String, context: String = "") + fun warn(message: String, context: String = "") + fun error(message: String, context: String = "") + fun critical(message: String, context: String = "") +} +``` + +## Environment + +`wasi:cli/environment@0.2.3`. Read the process's environment variables, arguments, and initial +working directory. + +```kotlin +object Environment { + fun getEnvironment(): Map + fun getArguments(): List + fun initialCwd(): String? +} +``` + +## Examples + +Storing and reading a value in a KeyValue bucket from inside an agent: + +```kotlin +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Endpoint +import cloud.golem.runtime.Either +import cloud.golem.runtime.wasi.Bucket +import cloud.golem.runtime.wasi.Logging + +@Agent +class SessionAgent { + + @Endpoint(put = "/session/{key}") + fun remember(key: String, value: String) { + when (val bucket = Bucket.open("sessions")) { + is Either.Left -> Logging.error("open failed: ${bucket.value.message}") + is Either.Right -> { + val b = bucket.value + b.set(key, value.encodeToByteArray()) + b.close() + } + } + } + + @Endpoint(get = "/session/{key}") + fun recall(key: String): String? { + val bucket = (Bucket.open("sessions") as? Either.Right)?.value ?: return null + val result = bucket.get(key) + bucket.close() + return (result as? Either.Right)?.value?.decodeToString() + } +} +``` + +Putting and getting a blob: + +```kotlin +import cloud.golem.runtime.Either +import cloud.golem.runtime.wasi.Blobstore + +fun archive(report: ByteArray): Boolean { + val container = when (val c = Blobstore.getContainer("reports")) { + is Either.Right -> c.value + is Either.Left -> return false + } + try { + container.writeData("2026-07/summary.bin", report) + return container.hasObject("2026-07/summary.bin") is Either.Right + } finally { + container.close() + } +} +``` + +Reading config, logging, and inspecting the environment: + +```kotlin +import cloud.golem.runtime.Either +import cloud.golem.runtime.wasi.Config +import cloud.golem.runtime.wasi.Environment +import cloud.golem.runtime.wasi.LogLevel +import cloud.golem.runtime.wasi.Logging + +fun greeting(): String { + val name = when (val r = Config.get("greeting-name")) { + is Either.Right -> r.value ?: "world" + is Either.Left -> "world" + } + Logging.log(LogLevel.INFO, "startup", "cwd=${Environment.initialCwd()}") + Logging.info("resolved greeting name: $name") + return "hello, $name" +} +``` + +## Notes + +- **Close your handles.** `Bucket` and `Container` wrap host resources released only by + `close()`; a `try { ... } finally { handle.close() }` block is the idiomatic pattern. +- **Error shapes differ per capability.** KeyValue surfaces a `KvError` (with `.message`), + Blobstore uses a plain `String`, and Config uses the `ConfigError` sealed class — check each + section's signatures rather than assuming a uniform error type. +- **Config and Logging are `object`s**, so no handle bookkeeping is required; likewise + `Environment` and the `Blobstore` top-level object. +- These bindings match the scope of the Scala SDK's equivalents — where a capability omits an + interface (KeyValue's `atomic`/`cache`, Blobstore pagination beyond one batch), that mirrors + the Scala reference rather than being an oversight. + +See also: [Agent model](agent-model.md) · [Tools](tools.md) · [Middleware](middleware.md) · +[Host API](host-api.md) · [SDK README](../../README.md). From a2bcbd0610f1724b67daa42ba071b0dbcd6e146c Mon Sep 17 00:00:00 2001 From: JohnSColeman Date: Mon, 13 Jul 2026 00:58:49 +0700 Subject: [PATCH 07/11] test(kotlin): e2e and contract-test harness scripts --- .../contract-tests/ContractProbeAgent.kt | 184 ++++++++++++++++++ .../CounterAgentSnapshot.kt.fixture | 36 ++++ sdks/kotlin/scripts/contract-tests/README.md | 23 +++ sdks/kotlin/scripts/native-contract-tests.sh | 131 +++++++++++++ sdks/kotlin/scripts/native-e2e.sh | 71 +++++++ sdks/kotlin/scripts/native-snapshot-e2e.sh | 83 ++++++++ 6 files changed, 528 insertions(+) create mode 100644 sdks/kotlin/scripts/contract-tests/ContractProbeAgent.kt create mode 100644 sdks/kotlin/scripts/contract-tests/CounterAgentSnapshot.kt.fixture create mode 100644 sdks/kotlin/scripts/contract-tests/README.md create mode 100755 sdks/kotlin/scripts/native-contract-tests.sh create mode 100755 sdks/kotlin/scripts/native-e2e.sh create mode 100755 sdks/kotlin/scripts/native-snapshot-e2e.sh diff --git a/sdks/kotlin/scripts/contract-tests/ContractProbeAgent.kt b/sdks/kotlin/scripts/contract-tests/ContractProbeAgent.kt new file mode 100644 index 0000000000..9a137d32c9 --- /dev/null +++ b/sdks/kotlin/scripts/contract-tests/ContractProbeAgent.kt @@ -0,0 +1,184 @@ +package contractprobe + +import cloud.golem.BaseAgent +import cloud.golem.Datetime +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Endpoint +import cloud.golem.runtime.Checkpoint +import cloud.golem.runtime.Either +import cloud.golem.runtime.Guards +import cloud.golem.runtime.HostApi +import cloud.golem.runtime.Transactions +import cloud.golem.runtime.host.AttributeValue +import cloud.golem.runtime.host.ContextApi +import cloud.golem.runtime.host.DurabilityApi +import cloud.golem.runtime.host.DurableFunctionType +import cloud.golem.runtime.host.GetOplog +import cloud.golem.runtime.host.NamedPolicy +import cloud.golem.runtime.host.Policy +import cloud.golem.runtime.host.RetryApi +import cloud.golem.runtime.host.SecretApi +import cloud.golem.runtime.host.SecretRevealException +import cloud.golem.runtime.host.namedPolicy +import cloud.golem.runtime.host.setRetryPolicy +import kotlin.time.Duration.Companion.milliseconds + +/** + * Contract-test probe agent. Each @Endpoint exercises one SDK capability's host boundary and + * returns a String verdict: "OK " on a clean round-trip, "FAIL " on a handled + * mismatch. An unhandled host trap aborts the invoke (the driver classifies that separately). + * Scope is contract-only: prove the call crossed the boundary and returned the expected shape, + * NOT that the value is functionally correct. + */ +@Agent(mount = "/probe/{id}", description = "Contract-test probe agent") +class ContractProbeAgent(val id: String) : BaseAgent() { + + // --- Capability 1: Agent model / BaseAgent identity --- + @Endpoint(get = "/agent-model") + fun probeAgentModel(): String { + val aid = agentId + return if (aid.isNotEmpty()) "OK agentId=$aid type=$agentType name=$agentName" + else "FAIL empty agentId" + } + + // --- HTTP gateway coverage (one endpoint reachable over the HTTP API) --- + @Endpoint(post = "/http-echo") + fun httpEcho(): String = "OK http id=$id" + + // --- Capability 2: Type mapping (Kotlin <-> WIT / schema values) --- + + // Return-direction (lower): one value touching every mapped type family. + @Endpoint(get = "/return-all-types") + fun returnAllTypes(): AllTypes = AllTypes( + i8 = -8, i16 = -16, i32 = -32, i64 = -64L, + u8 = 8u, u16 = 16u, u32 = 32u, u64 = 64uL, + f32 = 1.5f, f64 = 2.5, flag = true, text = "hello", + opt = 7, nums = listOf(1, 2, 3), color = Color.GREEN, + shape = Shape.Circle(3.0), pair = Pair(9, "nine"), + dict = mapOf("k" to 1), res = Either.Right(42), ts = Datetime(1000L, 0), + ) + + // Lift-direction (host -> guest) for the types with unambiguous CLI literals. + @Endpoint(post = "/echo-record") + fun echoRecord(p: Pt): String = "OK ${p.x},${p.y}" + + @Endpoint(post = "/echo-list") + fun echoList(xs: List): String = "OK size=${xs.size}" + + @Endpoint(post = "/echo-opt") + fun echoOpt(o: Int?): String = "OK ${o ?: -1}" + + // --- Capability 3: Host API (read-only host imports) --- + @Endpoint(get = "/host-api") + fun probeHostApi(): String { + val meta = HostApi.getSelfMetadata() + val parsed = HostApi.parseAgentId(meta.agentId.agentId) + val types = HostApi.getAllAgentTypes() // may be empty -> still OK + val idem = HostApi.getIdempotenceMode() + return "OK metaId=${meta.agentId.agentId} parsed=${parsed::class.simpleName} " + + "types=${types.size} idem=$idem" + } + + // --- Capability 4: Oplog (get-oplog resource + entry decode) --- + @Endpoint(get = "/oplog") + fun probeOplog(): String { + val meta = HostApi.getSelfMetadata() + val oplog = GetOplog(meta.agentId, 0L) + var count = 0 + val kinds = mutableSetOf() + try { + while (true) { + val batch = oplog.getNext() ?: break + count += batch.size + batch.forEach { kinds.add(it::class.simpleName ?: "?") } + } + } finally { + oplog.close() + } + // A live agent always has at least a Create entry; if decode traps this never returns. + return if (count > 0) "OK entries=$count kinds=${kinds.size}" else "FAIL empty oplog" + } + + // --- Capability 5: Retry + Retry DSL (policy-tree marshalling) --- + @Endpoint(get = "/retry") + fun probeRetry(): String { + val policy = NamedPolicy("probe-policy", Policy.exponential(100.milliseconds, 2.0).maxRetries(5)) + RetryApi.setRetryPolicy(policy) // lower the tree to the host + val readBack = RetryApi.namedPolicy("probe-policy") // lift it back + return if (readBack != null && readBack.name == "probe-policy") { + "OK policy round-tripped: ${readBack.name}" + } else { + "FAIL policy not found after set" + } + } + + // --- Capability 6: Transactions (begin/commit machinery) --- + @Endpoint(get = "/transactions") + fun probeTransactions(): String { + // Minimal body that ignores the tx handle: exercises begin + commit without needing an Operation. + val result = Transactions.infallibleTransaction { 42 } + return if (result == 42) "OK transaction committed, result=$result" else "FAIL unexpected result=$result" + } + + // --- Capability 7: Guards & Checkpoint (scoped host-state guards) --- + @Endpoint(get = "/guards") + fun probeGuards(): String { + val a = Guards.withPersistenceLevel(HostApi.PersistenceLevel.PERSIST_NOTHING) { 1 } + val b = Guards.withIdempotenceMode(true) { 2 } + val c = Guards.atomically { 3 } + val cp = Checkpoint() // captures current oplog index + cp.assertOrRevert(true) // true -> no revert; false would trap-revert + return "OK guards ran: $a$b$c checkpoint-ok" + } + + // --- Capability 8: Secrets (reveal error-path; no provisioning available) --- + @Endpoint(get = "/secrets") + fun probeSecrets(): String = try { + // 0 is not a live secret handle. A wired boundary either lifts a SecretError (caught here) + // or the host rejects the handle as a trap (invoke exits non-zero -> driver classifies it). + SecretApi.reveal(0, "string") + "FAIL reveal(0) returned a value instead of erroring" + } catch (e: SecretRevealException) { + "OK SecretError lifted: ${e.error::class.simpleName}" + } + + // --- Capability 9: Context / tracing (span + invocation-context resources) --- + @Endpoint(get = "/context") + fun probeContext(): String { + val span = ContextApi.startSpan("probe-span") + span.setAttribute("probe.key", AttributeValue.StringValue("probe.value")) + val ctx = ContextApi.currentContext() + val trace = ctx.traceId() + ctx.close() + span.close() + return "OK span+context ran, traceId=${if (trace.isNotEmpty()) "present" else "empty"}" + } + + // --- Capability 10: Durability (durable-function marshalling; replay is out of scope) --- + @Endpoint(get = "/durability") + fun probeDurability(): String { + val begin = DurabilityApi.beginDurableFunction(DurableFunctionType.ReadRemote) + val state = DurabilityApi.currentDurableExecutionState() + DurabilityApi.endDurableFunction(DurableFunctionType.ReadRemote, begin, false) + return "OK durable region marshalled: begin=$begin live=${state.isLive}" + } +} + +enum class Color { RED, GREEN, BLUE } + +sealed class Shape { + data class Circle(val radius: Double) : Shape() + data class Rect(val w: Int, val h: Int) : Shape() + object Unknown : Shape() +} + +data class Pt(val x: Int, val y: Int) + +data class AllTypes( + val i8: Byte, val i16: Short, val i32: Int, val i64: Long, + val u8: UByte, val u16: UShort, val u32: UInt, val u64: ULong, + val f32: Float, val f64: Double, val flag: Boolean, val text: String, + val opt: Int?, val nums: List, val color: Color, val shape: Shape, + val pair: Pair, val dict: Map, + val res: Either, val ts: Datetime, +) diff --git a/sdks/kotlin/scripts/contract-tests/CounterAgentSnapshot.kt.fixture b/sdks/kotlin/scripts/contract-tests/CounterAgentSnapshot.kt.fixture new file mode 100644 index 0000000000..eae40f5fc0 --- /dev/null +++ b/sdks/kotlin/scripts/contract-tests/CounterAgentSnapshot.kt.fixture @@ -0,0 +1,36 @@ +package example_counter + +import cloud.golem.BaseAgent +import cloud.golem.Snapshotted +import cloud.golem.annotations.Agent +import cloud.golem.annotations.Description +import cloud.golem.annotations.Endpoint +import cloud.golem.annotations.Prompt + +data class CounterState(val value: Int) + +/** + * A durable counter agent with typed snapshot support. + * + * Mounted at /counters/{name}: each unique {name} is a separate agent instance with its own + * persistent state, managed durably by Golem. Compiled directly to Wasm (WasmGC) via + * Kotlin/Wasm -- no JavaScript, no QuickJS. The KSP processor reads these annotations at + * compile time and generates the actual `@WasmExport` guest functions + agent metadata. + */ +@Agent(mount = "/counters/{name}", description = "A durable counter agent") +class CounterAgent(val name: String) : BaseAgent(), Snapshotted { + override var state = CounterState(0) + + @Prompt("Increase the count by one") + @Description("Increments the counter and returns the new value") + @Endpoint(post = "/increment") + fun increment(): Int { + state = CounterState(state.value + 1) + return state.value + } + + @Prompt("Get the current counter value") + @Description("Returns the current value without modifying it") + @Endpoint(get = "/value") + fun getValue(): Int = state.value +} diff --git a/sdks/kotlin/scripts/contract-tests/README.md b/sdks/kotlin/scripts/contract-tests/README.md new file mode 100644 index 0000000000..f8e1b5f44c --- /dev/null +++ b/sdks/kotlin/scripts/contract-tests/README.md @@ -0,0 +1,23 @@ +# Contract-test harness + +`../native-contract-tests.sh ` scaffolds a Kotlin app, swaps in `ContractProbeAgent`, +builds/deploys it to a locally built golem server, and invokes one probe per capability to prove +the compiled-Kotlin ⇄ host ABI boundary works. + +**Scope: contract-only.** Each probe proves a host call crossed the boundary and returned the +expected *shape* without trapping — not that the value is functionally correct. Durability's +persist-then-replay behaviour is out of scope (the durability probe proves only that the +durable-function imports marshal). Agent snapshotting is covered by its own dedicated test in the +native-agent-snapshotting work, so it is intentionally not re-probed here. + +Each probe runs on its **own durable agent** (keyed by the method name) so a wasm trap in one probe +wedges only that worker and can't cascade false FAILs onto later probes. Kotlin agents use +`golem-cli`'s fallback TypeScript literal syntax for invoke args (records `{ field: value }`, lists +`[a,b,c]`, option-some as the bare value). + +Prerequisites (same as `native-e2e.sh`): SDK/KSP/gradle-plugin published to mavenLocal, and +`golem`/`golem-cli` built from this branch. On a machine whose default `java` is < 17, point +`JAVA_HOME` at a 17+ JDK for the gradle build. Exit 0 iff every probe passed. + +Capabilities probed (10): agent model, type mapping (lower + lift), host API, oplog, retry DSL, +transactions, guards & checkpoint, secrets, context/tracing, durability. diff --git a/sdks/kotlin/scripts/native-contract-tests.sh b/sdks/kotlin/scripts/native-contract-tests.sh new file mode 100755 index 0000000000..61247f79b8 --- /dev/null +++ b/sdks/kotlin/scripts/native-contract-tests.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Contract-test harness: scaffold -> build -> deploy -> probe, entirely through the real +# toolchain, swapping the scaffolded CounterAgent for ContractProbeAgent. Proves the +# compiled-Kotlin <-> host ABI boundary for each capability (contract-only: no trap + expected +# shape; NOT functional correctness). Mirrors native-e2e.sh's scaffold/build/server/deploy. +# +# Usage: native-contract-tests.sh +# Requires: SDK/KSP/gradle-plugin published to mavenLocal; golem/golem-cli built from this branch. +# GOLEM_BIN/GOLEM_CLI_BIN override the built binary paths. +set -uo pipefail # NOTE: not -e; a failing probe must not abort the run. + +WORKDIR="${1:?workdir}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOLEM="${GOLEM_BIN:-$(cd "$SCRIPT_DIR/../../.." && pwd)/target/debug/golem}" +GOLEM_CLI="${GOLEM_CLI_BIN:-$(cd "$SCRIPT_DIR/../../.." && pwd)/target/debug/golem-cli}" +FIXTURE="$SCRIPT_DIR/contract-tests/ContractProbeAgent.kt" +# Each probe runs on its OWN durable agent (keyed by the method name) so a wasm trap in one probe +# wedges only that worker -- it can't cascade false FAILs onto later probes via shared-agent +# recovery. (Learned the hard way: a shared "p1" made the oplog trap fail every subsequent probe.) +agent_for() { printf 'ContractProbeAgent("%s")' "$1"; } + +PASS=0; FAIL=0; RESULTS=() + +record() { # record