Hacker Newsnew | past | comments | ask | show | jobs | submit | e1g's commentslogin

Tactically, this problem is commonly known as “colored functions”[1], and the only option in JS is to have some other runtime coordinate your function execution; in JS, that solution is Effect[2]

[1] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...

[2] https://effect.website/


There are a lot of libraries which can help you deal with this, but ultimately the parent is right.

I usually arrange my programs to have a call tree of all my async code, and separate call trees of sync processing work. You want to know ahead of time which is which. If you have a sync function which needs data that’s only available via a network request, take that data in as a function parameter or something. And make the caller responsible for making that data available before the function is called.

It’s a simple model. It’s fast and quite easy to understand once you’re used to it. But you do need to plan ahead, and structure your programs with a plan.


A hallmark of good architecture is adaptability to unexpected changes in requirements. Planning ahead helps with 'known unknowns', but it's impractical when building across N years in a dynamic environment - "knowing ahead of time" is just not possible for anything non-trivial. You need strong architectural primitives that don't scale based on developers' omniscience.

For example, say you have a workflow where, when someone signs up, you generate a user label, e.g., `$firstName $lastName`. You decide to move that to a function that might consider their personal title, preferred name, etc. Currently, it's a pure sync operation. Then, you discover people don't fill out that form at all, but some log in with Google, and you can use the name there as a fallback. Under flexible, this change is local: you can put that into your `createUserLabel(userInfo)`, and it can decide to fire off an API call to fill in any missing data, etc. In Promises land, this would taint the entire tree of everything everywhere that called that method, and all of those things must evolve or be refactored. In an Effectful system, this (previously unplanned) change remains isolated to that one function. Multiply that by every decision over multiple years for software looking for PMF, and requiring developers to "know ahead" severely slows down your ability to evolve.


Languages with effect systems typically let callers inherit the effects of their callees (e.g., calling an async function means the caller also is async), or force them to handle the effect (e.g., spawn the async call as a task and waiting synchronously for it to finish).

Effects are just a generalization, where async/await is one particular effect.

But: The fact that an operation now does some kind of I/O, or waits for user input, or whatever else you might express using async, has an _enormous_ impact on the architecture of your program. The “virality” of async is completely a feature, because it forces you to actually deal with that change, resulting in much more robust software.

It’s “inconvenient” because the architecture of your program changed. That’s what the job is, though. Languages that don’t help you here (by hiding that you made a change with huge ramifications) make it actively harder to deliver working software, in my opinion. You get there faster, but it won’t keep working.


The problem with effects systems is that effects aren't generalisable. Every effect is unique. Sometimes they can be applied automatically and sometimes not.

You can have the compiler automatically recompile map with async to make map<async>, likewise map<pure> and map<nofail> and map<noblock> but they will not be optimal; map<async> could be parallel but isn't. And you probably want to control the amount of parallelism at each call site, which just makes it a completely different function. It's likely that you wrote map in a way that uses a loop counter and it's possible the compiler can't prove it's pure. map<abortable> is likely correct, but the compiler has absolutely no way to prove that, and other functions won't be correct if you naively make them possible to abort from outside.


It's a fundamental architecture change only if we assume async == slow (or potentially slow) which isn't always true. Otherwise you might want to run something inline that the language designer made async - such as writing a file in /tmp.

Obviously, "slow" has very different meanings depending on context. Allocating a task object or interacting with the runtime in any way may be quite slow when you're in a tight inner loop, but it may be perfectly fast in the context of handling an HTTP request.

Async is almost never free of charge. The only language that can get close to zero cost is Rust, but even it needs heap allocations for async recursion.


That’s a good example. I would refactor that code to have a different signature and change all callers to fetch the relevant data.

I’d probably insist on doing that even in a blocking language where it’s not necessary. Interspersing database or network requests all through a codebase is horrible. Before you know it, someone is calling that function in a loop and you’re doing N serialised database queries. And you can’t even tell that that’s happening from the function signature. Your program just gets slow as your database grows. To say nothing of the correctness problems from issuing these queries outside of a transaction.

I worked on a project that was written like this in Python. The code was packed full of “convenient” sql queries. Some http requests took seconds to render. Turns out those request handlers were issuing thousands of individual sql queries, loading hundreds of megabytes from our database. A lot of the queries were redundant. The backend was just overfetching the same data over and over in tiny helper functions. Because of how the code was written, fixing performance required huge refactors all over the codebase.

File, network and database queries should not be spread all over “for convenience”. Fetching user data and processing it are different tasks. They generally shouldn’t be combined into a single function.


The solution to this is assume an async spine to your program, and branch off to as much sync code as possible. It's the same lesson you learn wrangling the IO monad in Haskell, or dependencies (like databases) in OO-land.

To go with this, don't be afraid of changing your code. If you get an unexpected change that means a whole hierarchy has to become async, bite the bullet and change the hierarchy. Such things happen.

This works for all applications, but not libraries where you don't control your callers. In that case it may make sense to make something async pre-emptively if you think requirements might change in a way that requires it but you can never predict every change successfully and you might need to make a V2 library.


LLMs are good at refactoring. So function color mismatch is no longer a problem while explicit io helps to read and understand the code.

I'm curious about your mental model. Would it be accurate to say that the async tree is the "IO program" and the sync functions operate on pure data, or is it more complicated than that?

Yeah, more or less. I think Haskell programmers are right on this.

On a Mac, you can do this via OS-native Seatbelt sandboxing - you define a static text file and it locks down the process so it can't read/write anything it's not supposed to. The syntax is a bit archaic, so I built a Bash wrapper around it to dynamically detect which agent you use and from what dir, and then generate the policy to lock down its process - https://github.com/eugene1g/agent-safehouse


Hey, thank you. Looks like a tool easy enough to use and start with the agentic world in a sandboxed and much safer manner.

Just clarifying a bit (haven't gone through details yet):

1. So, a harness gets installed within the bounds of this script via agent-safehouse? (I doubt this is the case)

2. Or they are installed as a normal app, but when I run them in the terminal within the bounds of "safehouse <harness>" (saw this) they are bound by the rules I set, right?

I feel it's the latter and I think it's good enough for the time being.

.....

Mac offers some controls at least for personal files and "Full Disk Access" etc. But it's so badly implemented and poorly shown that it's confusing.

Like I can see in "Privacy & Security > Full Disk Access" OpenCode is listed and and "Full Disk Access" toggled off. (I anyway remove OpenCode from here completely by the "-" button after selecting it, just to make sure).

Then I go back a step and in "Privacy & Security > Files & Folders > OpenCode" has "Full Disk Access" is listed there. Strange!


“safehouse <harness>” hides and protects your filesystem from the agent CLI - it cannot read/write/delete anything outside of the current folder, no matter what Bash/code it tries (short of finding some 0-day within MacOS sandboxing). We added exceptions for dev tooling (so eg NPM works), but generally keep same defaults (eg no Docker access by default as having that socket bypasses all protection)


It took me 3 months to go from “I will never use it” to “I will never not use it”. Plus it’s always fun to watch the creators of Effect get confused about which brackets to where. The syntax is archaic, luckily we don’t write syntax by hand any more.


Safehouse author here - glad you found it helpful. It didn't even occur to me to add --offline mode because my mental model is anchored in agents which often require network access. Until we add this, the easiest option is to create a custom policy and then reference it with Safehouse (I usually setup shell aliases/funciton for this)

  ;; in ~/.config/agent-safehouse/no-network.sb
  (deny network*)

  safehouse --append-profile="$HOME/.config/agent-safehouse/no-network.sb" -- <command>


Hello! I was definitely not looking to block all network access. I would want to maintain a whitelist of hosts and IPs. I don't know if that's possible?


Unfortunately, no, MacOS sandboxing does not allow that type of blocking. Only broad categories like "local" vs "remote", but no selective targeting based on domain names or IPs.


In JS land, this problem (streaming, resuming, recovering, multi-client, etc) has been fully solved by https://durablestreams.com - and it can be self-hosted, or managed via Cloudflare DO.


Cloudflare Sessions API and Anthropic Routines have a really similar model. Where they are hosting the 'session store' for you, and giving you access to it over long-polling (or sometimes websockets).

It's a bit harder to do agent presence ('is the agent still there') with this model without heartbeats, but possible.

It's good to see the industry starting to address the "durable sessions" problem, because it sucks.


We actually had to revert our rollout of CF Workers because enough of our users were in Spain and couldn’t access endpoints at seemingly arbitrary times (due to the matches)


Your customers should be proper Spaniards and be watching the match, hence not noticing the downtime! /s


Your answer is better than mine


They are only seemingly arbitrary to people that are not actually paying attention. Now that people are, the blocks are known in advance to those that look at a the schedule. Sure, it sucks to have to build this into your own schedule, but that's better than it happening "unexpectedly". You could do something crazy like import these times into your own calendar with reminders.


Absolutely ridiculous to make people do that. What you're proposing is not a real solution. The real solution is to not block wide IP ranges at the random desire of some private football league.


I'm not sure what you're saying. Obviously the schedule of matches is public. But what are you suggesting the business does during this time...? Their site is offline.


Service Unavailable For Scheduled Maintenance

However, my post was a tounge-in-cheek response.


For jailing local agents on a Mac, I made Agent Safehouse - it works for any agent and has many sane default for developers https://agent-safehouse.dev


Claude Code and Codex quickly figure out they are inside sandbox-exec environment. Maybe because they know it internally. Other agents often realize they are being blocked, and I haven't seen them go haywire yet.

Big love for Pi - it was the first integration I added to Safehouse. I wanted something that offers strong guarantees across all agents (I test and write them nonstop), has no dependencies (e.g., the Node runtime), and is easy to customize, so I didn't use the Anthropic sandbox-runtime.


Interesting, that's not been my experience! Maybe you've got the list of things to allow/block just right. While testing different policies I've frequently seen Opus 4.6 go absolutely nuts trying to get past a block, unless I made it more clear what was happening.

Yeah I think for general use the transparency of what your thing does is really great compared to a pile of TypeScript and whatnot.


Let’s make something so popular and useful that they can’t drop it.


IYKYK, it’s the new Bootstrap!

The alternative would be “no site”, which is still somehow worse.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: