I'm may be a crazy man but I'd like make an sci repl-eval loaded with a library as a MCP tool... for an online SAS service. Ie create todos, edit todos, etc. Question: is this insane? Or amazing? Or both? So basically instead of delineating all the tools for a service provide one todos_sci_repl tool with a narrow well defined composable clojure library with access to clojure.core as well. The biggest question is what is the basic baseline eval safety of SCI? How much would I have to do lock it down?
Well my calculus would be that for my use case that doesn't encourage the LLM to create unbounded sequences or loops that could not terminate that this would be a very rare occurrence So a 50% improvement on very rare/never? Is a pretty big improvement
But also my use case is in cljs land so these patches don't apply. I might have to patch in limits for unbounded seqs and loops, and I don't know if that's even possible yet… and any hit on performance is absolutely nonmaterial for this use case. But from what I've seen SCI is an absolutely killer lightweight scripting engine for SOTA LLMs. And CLJS products can just drop it in. WebMCP is coming and front end apps will be able to present a scripting engine as a tool rather than a set of MCP tools that don't compose.
you can override everything in core with your own thing by adding it in your config as {:namespaces {'clojure.core {'repeat <your-repeat-override-here>}}}
> But also my use case is in cljs land so these patches don't apply.
How would :interrupt-fn not apply here? It would work in CLJS?
My question still wasn't answered though. What would you do with the other 50% of unhandled cases (even though they might be rare). Kill the browser I guess?
@bhauman you might be interested in https://github.com/PEZ/epupp which is a browser plugin based on #scittle to tamper with any webpage from a REPL that your AI can connect to as well
Yeah the browser tab wouldn't probably get shut down with an out of control script message.
(epupp works on any page, even on pages that don't let you use JS eval since SCI does not use eval)
So I just looked at interrupt-fn pr yes that would meet my needs.
I agree that not having to kill your browser in 50% of cases is better
But this has never happened to me ever. But it did happen in Clojure repl back in the Claude 3.5-7 days. Especially on reading infinite seqs
in https://github.com/babashka/sci/commit/7145c3a8ef0a4329e23964f8b9eac34d0eebe93d I implemented handling interruptions for recursive function calls a while back. Don’t remember anymore if I found cases this didn’t handle back then.
It works for recursion but not in general (other cases mentioned above)
what’s an example? (repeat :x) seems to work
Realize it
ah right, that doesn’t work, thanks
makes sense. SCI should be safe by default, no access to file system, network, etc. the only unsafe bit is how long things can take to execute. but if you can control that in the host system, it should be fine.
@borkdude OK very good to know.
I've always been curious about this. Have there been any serious attempts of people trying to jailbreak sci? What about fuzzing? Is there a security model that says what behaviors would be considered security violations?
try to break it and report :)
of course I give no guarantees legal wise ;)
Is there a security model that defines what a security violation is?
good question, I guess there is not a formal definition, but I would consider these things: • mutate things in host, including Clojure vars in the host system • touch files on the file system, or write, delete etc
@borkdude is there a mutable object to shutdown the eval iteration
@bhauman it was there in the beginning, but this was very hard to give any guarantees about. this is why I removed it and said: solve it in the host.
e.g. you can implement some recursive function which takes a long time. or you can just do something like (apply + (repeat 1)) and wait forever. there's no interpreter in the middle of that anymore after it's running
there used to be Thread/stop but JVM removed it. there's ways to get it back though, CIDER has something like this using a java agent
what I could do is inject Thread/isInterrupted calls everywhere, but then I'd have to re-implement almost all of core where the above problem applies
Perhaps using some JVM bytecode magic you can still do this in the host or perhaps that's what the CIDER java agent is doing kind of, haven't really checked
ah here is some info on that: https://docs.cider.mx/cider/basics/up_and_running.html#enabling-nrepl-jvmti-agent
ah they wrote some C to bring back Thread/stop basically: https://github.com/nrepl/nrepl/blob/master/libnrepl/src/nrepl_agent.c
One interesting approach I've seen in the clojure IRC channel. They had a clojure evaluation bot, but it no longer worked since java security sandboxing was deprecated. What they did: use a new babashka process for every call. bb comes with SCI as well. So they evaluated their code in SCI in a babashka process and killed the process if it ran longer than n seconds.
I guess could build a similar dedicated binary if you want to have fast startup and less memory usage and do the "kill the process if it takes too long" thing or start a separate JVM/clojure process if startup time doesn't matter that much
> what I could do is inject Thread/isInterrupted calls everywhere, but then I’d have to re-implement almost all of core where the above problem applies
@borkdude are you sure that this is going to be that bad? why not add this to a place at a time, and when someone reports another case, also add it there? Are you concerned about the performance implications?
it's just a half baked solution since functions of clojure core, etc and functions that users expose to SCI won't handle this
and you consider it only worthwhile if it works in all cases?
yes
I see. Like a performance improvement, I’d consider it worthwhile if sci would handle interruptions better for some cases.
e.g. for the above use case or the clojure IRC use case putting some (Thread/interrupted) here and there isn't sufficient. Creating this expectation that it will sometimes work will often be misunderstood for "it supports this". It's one of the reasons I removed the "eval counter" solution way in the beginning.
how would it be a performance improvement to put (Thread/isInterrupted) calls in code?
I didn’t say it is an performance improvement, but that I see it similar to a performance improvement
sci being more responsive to an interruption I mean
so I’d consider it worthwhile, even it it only improves responsiveness in some areas
and I see it similar because it’s about how long you need to wait in case you hit ^C
but I wouldn’t want to give guarantees that it would work like this in all cases
I would not introduce a semi-working interrupt feature either, @borkdude
I did quite some tests and thinking around Sci as a sandbox, but to say this was exhaustive would be naive of course. I believe the implementation is solid. The main weakness is the possibility of recursion and occupying threads this way/Dos attacks. E.g. the fn and defn macros can be redefined to be safe, but then people can still call the underlying fn* . Not sure what can be done about this. The lazy infinite sequences such as repeat and range can be limited by reimplementing an old feature (:realize-max)
E.g. the fn and defn macros can be redefined to be safe, but then people can still call the underlying fn*
Not sure what you mean by thisSo you can rewrite the fn macros with an invocation counter and put a limit on it to limit the recursion possibility. But the underlying fn* you cannot rewrite, so as long as you can call that infinite recursion is still possible. I have some examples somewhere if that helps
I remember your examples from around 2020 or so. This is why I removed the feature ;)
I mean, I remember that we talked about it, not the concrete examples
Yeah exactly. I've been still thinking about it and I hope this can be solved somehow, some day as it would give opportunities for multitenancy. I will share the examples later. Not near a computer now
But for @bhauman purpose I think sci is safe enough. If you want to prevent access to disk and other dangerous things I believe sci is safe. As long as you hide dangerous things behind a controlled API I think clojure MCP would be ok. I don't infinite recursion would do much harm in that setting
multitenancy should be supported with SCI as in, if you fork a context for each customer, should be ok
I've actually thought of using sci and clojure MCP for that reason, but unfortunately just too many other things to do
I believe there are some potential problems with the forking. E.g redefinitions to vars will not propagate, but changes to values such as atoms will propagate. And I believe some other scenarios I found a long the way of experimentong with this
But this could be solved with more work I believe
re-definitions of vars do not propagate. I consider that a feature
Yeah I agree. Maybe better if I come up with some examples soon
So I meant the re-definition of vars do not propagate and that is what you want. But if we have a var that maps to an atom, forking will not stop changes to that atom to be propagated and that can be problem sometimes. I ran into this myself at least when I didn't expect it
If you give users access to that atom, yes they can change it
But again I think this is not relevant for clojure-mcp using sci as a sandbox
to clarify somewhat: • changes to vars are propagated if you expose users to the same vars • but if fork a context and let users create new vars, they won't be seen in the old context. that's just how clojure hashmaps work
and • you can create immutable vars for users so they can't modify them (but you can also map a function directly to a name without a var, even safer)
I am also happy to map out all pieces that need to be intercepted for full safety (e.g. under adversarial conditions), @borkdude already helped in pointing out all the edge cases. In general I think this is an important design space for Clojure right now.
Here's another issue about the same topic: https://github.com/babashka/sci/issues/1038 I'm still on the fence about it and leaning towards what @jackrusher was saying: > I would not introduce a semi-working interrupt feature either, @borkdude
I still would love to see bb responding to being interrupted 🙃
oh bb isn't going to have this, it's a disputable SCI feature which isn't going to be enabled in bb
mainly not for performance reasons
I couldn’t measure a performance impact when I implemented it…
ok if it isn't measurable then I may change my mind, but the other objection is that it's just "semi-working", I haven't changed my mind on that, but I remain open to be convinced otherwise.
but it could start like you said in sci, not in bb
java also has a semi working interrupt feature…
you can only interrupt threads if they’re doing work that can be interrupted
and that works in bb already :)
cc @whilo
but being an interpreter you have more control about what can be interrupted
and you can also do it for some cpu-bound things
there's more info in the issue about what the gaps are. https://github.com/babashka/sci/issues/1038
what are the downsides you can think of? • implementation complexity • only partial support like in the issue, which could result in user confusion and wrong expectations • performance penalty
anything else?
• implementation complexity: this isn't that complex and performance penalty is ok if you don't provide an :interrupt-fn. so that's ok.
• partial support: this is the main objection. I think you will end up re-creating almost all of clojure to basically be aware of your :interrupt-fn
• see point 1
I wonder if that can also be solved by documenting the limitations
like these More examples of long running programs that bypass interrupt-fn: aren’t solvable I think?
they are solvable if you provide overrides for the interop (something not fully supported but could be) which is made aware of your interrupt-fn ;)
jvm code that doesn’t check for interrupts doesn’t respond to them, isn’t that a known/expected limitation for this feature?
yeah well, I guess :interrupt-fn could be sold as: SCI-level interruption in loops/SCI-interpreted fns and the rest is up to you to override. But the question is: what are the useful use cases for this that are 100% valid without getting into the "rewrite all of core and interop" problem.
> My impression is that you basically have to rewrite almost all of clojure.core to make programs fully interruptible it would not be my understanding that adding some checks for interrupts in sensible places would make sci programs “fully interruptible”. Can you explain why you think this would have to be an all or nothing approach?
I guess one reason is you’ve been burnt by trying to add the safe eval counter stuff?
yeah that and that I haven't seen actual use cases where this is really helpful without getting into the "rewrite all of core" to make it work all the way
"semi-working" being the key term
why do you think it’s only useful without going all the way?
because something half-baked doesn't feel satisfying?
I think responding instantly to ctrl+c when I enter an infinite loop in bb in the repl would make it better
that already works, it exits the process?
oh, the REPL
that would be one use case, but it's still half-baked compared to how Thread/stop worked
I'll wait for what whilo has to say and why he needs his feature. there could be use cases that are more compelling
I’d be curious to learn if you’d like the feature more if you tried it in your local dev bb for a week
I think the main issue with this feature is a lifetime of bug reports from users who are confused about why “interrupt” didn't work. (And documenting the limitation won't prevent this.)
exactly
since interrupts also don’t always work on the host platform, I’m not sure it would be such a big deal. A drop in the bucket of bug reports in the borkverse perhaps? 🙃 Maybe not advertising this as a feature could also help.
but I can see how not advertising the feature doesn’t make sense for :interrupt-fn .
So, now that I'm about to put something like this in production I'm very interested in this feature. Even if it doesn't work all the time…
For my use case the LLM hasn't yet created an infinite loop but it would be nice to have a time out just in case….
And even if it worked 50% of the time that’s better than nothing.
https://github.com/mk/sci/commit/7145c3a8ef0a4329e23964f8b9eac34d0eebe93d was my patch for handling interrupts for function calls
follow up here: https://clojurians.slack.com/archives/C015LCR9MHD/p1781616545093549