Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ jobs:
# The C ABI mount-root guard is Windows-conditional, so the Linux
# workspace test run compiles it out. Nothing else runs bashkit-capi's
# tests on Windows, which left them never executing anywhere.
- name: Test C ABI mount-root validation
run: cargo test -p bashkit-capi --lib
- name: Test C ABI mount-root validation and runtime callbacks
run: cargo test -p bashkit-capi --lib --test runtime_functions

# The adoption shape this covers — bashkit as an agent's shell, with commands
# bridged to host processes — exists to work on Windows without a real bash.
Expand Down
18 changes: 9 additions & 9 deletions contracts/capability-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,32 +237,32 @@
},
"c_abi": {
"label": "C ABI",
"supported": ["execution_limits", "deadlines", "output_caps", "cwd", "env", "virtual_filesystem"],
"supported": ["execution_limits", "deadlines", "output_caps", "cwd", "env", "virtual_filesystem", "runtime_python", "runtime_typescript", "host_functions_python", "host_functions_typescript", "cancellation", "mounts"],
"evidence": {
"execution_limits": ["crates/bashkit-capi/tests/abi.rs#configures_environment_files_limits_and_final_environment"],
"deadlines": ["crates/bashkit-capi/tests/abi.rs#configured_deadline_aborts_execution"],
"output_caps": ["crates/bashkit-capi/tests/abi.rs#reports_output_truncation_flags"],
"cwd": ["crates/bashkit-capi/tests/abi.rs#configures_environment_files_limits_and_final_environment"],
"env": ["crates/bashkit-capi/tests/abi.rs#configures_environment_files_limits_and_final_environment"],
"virtual_filesystem": ["crates/bashkit-capi/tests/abi.rs#reads_and_writes_binary_vfs_content"]
"virtual_filesystem": ["crates/bashkit-capi/tests/abi.rs#reads_and_writes_binary_vfs_content"],
"runtime_python": ["crates/bashkit-capi/tests/runtime_functions.rs#c_runtime_callbacks_run_on_caller_thread_and_reject_reentry"],
"runtime_typescript": ["crates/bashkit-capi/tests/runtime_functions.rs#c_runtime_callbacks_run_on_caller_thread_and_reject_reentry"],
"host_functions_python": ["crates/bashkit-capi/tests/runtime_functions.rs#c_runtime_callbacks_accept_replies_from_other_threads"],
"host_functions_typescript": ["crates/bashkit-capi/tests/runtime_functions.rs#c_runtime_callbacks_accept_replies_from_other_threads"],
"cancellation": ["crates/bashkit-capi/tests/abi.rs#cancellation_aborts_running_execution_and_stays_sticky_until_cleared"],
"mounts": ["crates/bashkit-capi/tests/abi.rs#runtime_mount_and_unmount_round_trip"]
},
"unsupported": {
"runtime_env": "The C ABI exposes no post-construction configuration surface.",
"cancellation": "ABI v1 exposes synchronous execute with no cancellation handle.",
"stdin": "ABI v1 execute accepts script bytes only.",
"custom_builtins": "ABI v1 has no callback registration table.",
"host_call_suspension": "ABI v1 has no asynchronous event and resume API.",
"tool_callbacks": "ABI v1 has no tool-callback registration table.",
"network_policy": "ABI v1 deliberately ships only git, jq, and VFS capabilities.",
"transport_hooks": "ABI v1 has no host HTTP transport vtable.",
"mounts": "ABI v1 supports seeded and mutable VFS files, not filesystem mounts.",
"snapshots": "ABI v1 has no snapshot functions.",
"runtime_python": "ABI v1 omits embedded language runtimes.",
"runtime_typescript": "ABI v1 omits embedded language runtimes.",
"runtime_sqlite": "ABI v1 omits embedded language runtimes.",
"interactive_terminal": "Not exposed in the C ABI.",
"host_functions_python": "ABI v1 currently omits embedded language runtimes and runtime callback registration.",
"host_functions_typescript": "ABI v1 currently omits embedded language runtimes and runtime callback registration."
"interactive_terminal": "Not exposed in the C ABI."
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit-capi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ crate-type = ["cdylib", "rlib"]
# `tzdata` is listed explicitly: the C ABI is a general-purpose embedding
# surface, so `date` must keep honouring named `TZ=` zones the way it did
# before that became a feature.
bashkit = { path = "../bashkit", default-features = false, features = ["git", "jq", "tzdata", "realfs"] }
bashkit = { path = "../bashkit", default-features = false, features = ["git", "jq", "tzdata", "realfs", "python", "typescript"] }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
40 changes: 40 additions & 0 deletions crates/bashkit-capi/examples/runtime_functions.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "bashkit.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>

static BashkitBytes bytes(const char *text) {
BashkitBytes value = {(const uint8_t *)text, strlen(text)};
return value;
}

static void lookup(void *data, BashkitRuntimeCall *call) {
unsigned *calls = (unsigned *)data;
BashkitBytes request = bashkit_runtime_call_request(call);
assert(request.len > 0);
assert(!bashkit_runtime_call_cancelled(call));
++*calls;
/* A real dispatcher parses request JSON and calls an application service.
It may instead transfer call to a worker, respond there, then free it. */
assert(bashkit_runtime_call_respond_json(call, bytes("42")) == BASHKIT_OK);
bashkit_runtime_call_free(call);
}

int main(void) {
const char *config = "{\"schema_version\":1,\"python\":true,\"typescript\":true,"
"\"external_functions\":[\"lookup\"],\"typescript_external_functions\":[\"lookup\"]}";
unsigned calls = 0;
Bashkit *bash = NULL;
BashkitError *error = NULL;
assert(bashkit_create_with_runtime_handler_v1(bytes(config), lookup, &calls, &bash, &error) == BASHKIT_OK);
BashkitResult *result = NULL;
assert(bashkit_execute(bash, bytes("python -c 'print(lookup())'; ts -c 'await lookup()'"), &result, &error) == BASHKIT_OK);
assert(bashkit_result_exit_code(result) == 0);
BashkitBytes output = bashkit_result_stdout(result);
assert(output.len == 6 && memcmp(output.ptr, "42\n42\n", 6) == 0);
assert(calls == 2);
fwrite(output.ptr, 1, output.len, stdout);
bashkit_result_free(result);
bashkit_free(bash);
return 0;
}
39 changes: 39 additions & 0 deletions crates/bashkit-capi/include/bashkit.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ typedef struct bashkit Bashkit;
typedef struct bashkit_result BashkitResult;
typedef struct bashkit_error BashkitError;
typedef struct bashkit_buffer BashkitBuffer;
typedef struct bashkit_runtime_call BashkitRuntimeCall;

typedef struct {
const uint8_t *ptr;
Expand All @@ -40,6 +41,8 @@ typedef uint32_t BashkitStatus;
#define BASHKIT_IO_ERROR 5u
#define BASHKIT_UNSUPPORTED 6u
#define BASHKIT_CANCELLED 7u
#define BASHKIT_BUSY 8u
#define BASHKIT_MAX_RUNTIME_VALUE_BYTES 10000000u
#define BASHKIT_INTERNAL_ERROR 255u

/* Static views returned by these functions remain valid for process lifetime. */
Expand All @@ -62,6 +65,42 @@ BASHKIT_API BashkitStatus bashkit_execute(
BashkitResult **out_result,
BashkitError **out_error);

/* Embedded runtime callbacks (capability runtime-host-functions).
Config schema v1 adds python/typescript booleans, external_functions and
typescript_external_functions arrays. Names require an enabled runtime and
this constructor's non-NULL handler. One dispatcher receives both languages.

handler runs on bashkit_execute's caller thread and must return promptly.
It receives OWNERSHIP of call: respond/fail and free it once, or transfer it
to another thread and return. Request JSON contains language, name, args,
and kwargs for Python. JSON values are bounded to 64 nesting levels, finite
numbers and signed 64-bit integers. No arbitrary language objects.

respond_json copies input; invalid input leaves the call unanswered. Exactly
one response is accepted. Late replies return CANCELLED. Poll cancelled to
stop host I/O after timeout/cancel; free an unanswered call to fail promptly.
Every call must be freed, including cancelled calls. View validity ends at
call_free. Respond/query may run concurrently, but never race call_free.

Blocking handle APIs called from a handler return BUSY, including on another
handle (nested current-thread runtimes are forbidden). Callback-enabled
handles return BUSY for concurrent blocking operations. cancel/clear_cancel
and runtime_call_* stay available. Queue other work after handler returns.

handler/user_data must remain valid through handle lifetime and executions.
Never unwind/longjmp through the callback or free a Bashkit during use. The
shared library must stay loaded until all handles/results/calls are freed. */
typedef void (*BashkitRuntimeHandlerV1)(void *user_data, BashkitRuntimeCall *call);
BASHKIT_API BashkitStatus bashkit_create_with_runtime_handler_v1(
BashkitBytes config_json, BashkitRuntimeHandlerV1 handler, void *user_data,
Bashkit **out_bash, BashkitError **out_error);
BASHKIT_API BashkitBytes bashkit_runtime_call_request(const BashkitRuntimeCall *call);
BASHKIT_API uint32_t bashkit_runtime_call_cancelled(const BashkitRuntimeCall *call);
BASHKIT_API BashkitStatus bashkit_runtime_call_respond_json(
BashkitRuntimeCall *call, BashkitBytes value);
BASHKIT_API BashkitStatus bashkit_runtime_call_fail(BashkitRuntimeCall *call);
BASHKIT_API void bashkit_runtime_call_free(BashkitRuntimeCall *call);

/* Cancellation requires the `cancellation` capability. Both are lock-free and
safe to call from any thread while bashkit_execute is blocked on the same
handle; execution aborts at the next command boundary with
Expand Down
Loading
Loading