You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
The MIT License text, in LICENSE. Cascade AI was already declared
MIT in its package metadata and README, but the licence itself was not in
the repository, so the README's licence link went nowhere. It now ships at
the root and in the npm package.
A daily allowance of browser sessions: 5 a day on Free, 50 on Pro. A
session is claimed the moment one is about to be opened, which is what the
provider bills for, and returned if none was, so a Browser-chip run that
never needs the browser costs nothing and concurrent runs cannot share the
last one. Each claim is returned on its own, so two opens at once never
leave a session that did not open counted. Once the day's sessions are gone the Browser chip switches
off and says when they reset (midnight UTC), and it stays off until
today's count has been read for this sign-in, so it cannot be turned on
for a run that will be refused. A run sent with it anyway is refused before anything is
spent, and a run that runs out partway through
is refused the next session with the same message shown in the chat. If
the allowance cannot be checked, the chat says so too, and a later refusal
for a different reason is shown as well. The
chip's tooltip shows how many are left today, and it follows a plan change
in either direction without a reload. A deployment without a browser
never asks for it. After a reconnect, the newest browser refusal is the
one shown. Only billed sessions are rationed:
a deployment driving its own browser over CDP opens none, so it has no
daily limit.
Changed
Cascade keeps a project's state outside the project. Its settings,
sessions, audit trail, world state, code index, run checkpoints and the
record of what local-only subtasks wrote used to sit in the project's own .cascade/, where every tool and command in the project had to be kept
from them and where they could be committed by accident. They now live in ~/.cascade-ai/projects/<folder name>-<hash>/, and the project holds only CASCADE.md and .cascadeignore. The folder, and those above it, are
readable by you alone. An existing .cascade/ is moved there the first
time anything in Cascade opens the project, and removed; if it was tracked
in git, that shows as its deletion. A .cascade that is a symlink is left
where it is, and what it points at untouched. A project renamed or moved
on its disk takes its state with it, found by the folder's identity on
disk; a copy, or a move to another disk, starts afresh, with a notice of
where the old state is. Another folder made at a project's path — the
old one deleted, a fresh clone — keeps its settings, local-only rules and
own keys, and has what the earlier one did and knew set aside. cascade doctor prints where a
project's config is. A hosted server names a state folder of its own
(useProjectStateDir), outside the workspace — which a folder inside it
is refused — and never writes the machine-global one. useProjectStateDir
returns a function a host calls when the run ends — one that fails as it
starts included: once the last run holding a workspace has, the process
forgets it, so what it keeps follows the runs going on, not every tenant
that ever ran. A state folder whose preparation fails is prepared again
the next time it is asked for. A host's folder is judged by where it will
be — through a symlinked parent, for one not made yet — and kept by that
path. A state folder others can read, which Cascade cannot tighten, or
one owned by another account however closed its mode, is refused rather
than used.
Provider keys are kept once, for the machine. They lived in both ~/.cascade-ai/credentials.json and each project's config; a project's
config now holds its providers without their keys, and every project uses
the machine-wide ones. A project whose key differed from the machine-wide
one when it moved keeps it, in the same file under that project, and goes
on using it ahead of the environment, as before. Processes saving at once
take turns under a lock, and each writes only the providers it changed,
so neither drops or brings back the other's change — nor does a save
taken back, when the config could not be written after it, undo what
another process saved in between. cascade init and the first-run
wizard add the providers chosen there to the machine's, removing none of
the others.
Browser screenshots are named @screenshots/<file>, a path image_analyze, the file tools and generate_document's images read
from the project's state folder, rather than an absolute path in the
project.
tools.dynamicToolSandbox offers wasm in place of worker.worker
ran agent-written tools in a bare thread that confined nothing. A config
that still names it gets the new WebAssembly sandbox, and Settings →
Advanced offers auto, wasm and isolate.
Commands no longer see provider keys in their environment.shell, run_code and git launch with the model and search providers' keys,
and any variable holding a configured secret, taken out. A project
script that needs one of them in a command it is asked to run no longer
gets it; tools.processJail: "off" restores the old behaviour.
A workspace's .cascadeignore takes effect. Nothing passed it to the
tools, so it protected nothing. The template cascade init writes lists node_modules/, dist/ and build/, so in such a workspace agents can
no longer read or change those through the file and search tools; delete
the lines to allow it. Agents may read .cascadeignore itself but not
change or remove it — the file tools refuse, and commands see it
read-only, whatever it links to or is linked from, and find it as it was
when they end — since a line taken out would unprotect a path in the next run.
A run with a budget plans within it, and always answers. A per-run
cap used to end the run the moment it was crossed, answer or not: half a
plan's work could be paid for and the reply was only "stopped to avoid
runaway cost". Now the planner is told how many sections and workers what
is left pays for, a plan over it keeps its first ones, and Cascade Auto
picks cheaper models as the money runs down. Work may spend 80% of the cap
(the cost cap and the per-run token cap alike). Past that no new work
starts, and the rest pays for writing the answer from what is finished.
Whenever the budget held planned work back, cut from the plan or not
started, a line under the answer says so. A worker whose draft was
finished keeps it, marked unchecked, instead of stopping to ask about it.
If nothing finished at all, the rest answers the question directly, the
way Fast answer does; if even that cannot be paid for, the finished
sections are returned as they are. The cap itself is never exceeded by
work, and nothing changes without one.
Cascade Cloud has a calmer look. Flat paper in light and "midnight" in
dark replace the frosted glass and background glows, and the azure → sky →
teal ramp is now the only accent, used where it means something: tiers and
live work. Answers are set in a serif for reading, and the header above
each reply is replaced by one receipt line underneath (T1 · model · $cost · saved $x) that opens /why, which now draws Cascade against all-T1 on one
scale. A live run shows a thin flowing spine beside the agent tree. Fonts
(Geist, Source Serif 4, Geist Mono) are bundled with the app rather than
loaded from a third party. Tier badges and the tier mix were still green /
amber / violet from an older palette and now match the tier colours used
everywhere else. The landing page and /docs follow the same design, with
the three-arc mark in place of the old three-bar one. The receipt names
the tier that answered even when a provider reports no token or cost
figures. The app now has the prototype's structure too: a sidebar with
Search, Files, Skills and an account menu (usage, settings, keys, import,
sign out); a menu on the chat's title to rename or delete it; the chat's
total saving at the top right, which no longer resets on reload and opens
what the whole chat spent against all-T1; and a composer whose attach, skill, tools and routing controls live in three
menus, with whatever is switched on shown as a removable chip. "Run detail"
replaces the Simple / Advanced view, and an orchestrated reply keeps its
agent tree, folded above the answer. The landing page follows the
prototype's layout too — the arcs card, a receipt, where Cascade runs, the
desktop downloads and the two plans — and signing in opens a small dialog
instead of sending you to the bottom of the page. See docs/design/calm-direction.md.
Fixed
pdf_create wrote relative to the process, not the workspace. In the
desktop app and the server those differ, so a PDF landed outside the
workspace, where no file check looked. It resolves its path in the
workspace like every other file tool.
File tools work in a workspace opened through a symlink. Each file's
real path was checked against the workspace's name rather than its real
path, so every file looked like an escape — which on macOS includes any
workspace under /tmp or /var.
Maths in an answer is typeset, whichever way the model wrote it. GPT
and Gemini write \(…\) and \[…\], which Markdown reads as escaped
brackets, so an equation reached the web chat as [ r = \operatorname{rank} E(\mathbb{Q}) ]. Those, and bare \begin{align*} blocks, now render as maths. The desktop chat rendered no
maths at all and now matches the web. Prices are no longer read as maths
either: "costs $5 and $10" used to show "5 and " as an equation. Code
blocks and code spans are left exactly as written.
/mcp lists your MCP servers instead of crashing. MCP tools are
registered as mcp__server__tool, but the command still split their names
on the old ::, so with any MCP tool connected it threw. It now reads each
tool's server from the tool itself.
/theme accepts the theme names Cascade uses now. The themes were
renamed (midnight, aurora, daybreak, bloom, tide, ember), and
config and --theme took the new names, but /theme still checked the old
list: /theme midnight answered "Unknown theme". It now takes either name
and lists the current ones. A name every object inherits, such as toString, is no longer taken for a theme by /theme, --theme or the
config, where it used to hand the renderer a function and break it.
A long answer no longer slows the chat down as it streams in. Each
token re-rendered the whole answer, so the work grew with the square of
its length. Both chats now re-render as often as the answer's size
allows, and show the finished answer in full once it is done.
A run that cannot do something now says so instead of inventing the
result. Asked to open a site with browser control and return its
chatbot's answers, a run that could not reach a browser "simulated" the
answers and presented them as results. Every prompt that can hand you an
answer now forbids that, including the correction and rewrite steps that
used to run without the worker's rules. The worker's self-check now sees
what each tool actually returned, so an output claiming a visit that the
browser refused fails the check. It is shown the parts of each result that
the answer draws on, from anywhere the worker read, not just the start of
each one, so a correct answer taken from further down a long page is no
longer mistaken for an invented one. That includes a short result read
early in a long run, and the work handed over by the steps a step depends
on, so a summary of what an earlier step did is no longer failed for
describing calls that step made. A revision made after that check has to
pass it too, and a section summary is told which of its workers did not
finish, so a half-done section no longer reads as complete.
Secret redaction catches more, and no longer leaks part of what it
matches. A rule without a capture group replaced only one substring of
its match, so an IP address could pass through with a single digit
removed. It now also catches sk-…, GitHub, Slack, Google and Stripe
tokens, bearer and Basic credentials, the value of any Authorization or Proxy-Authorization header however short, cookies in Cookie and Set-Cookie, private keys, values labelled by
names like ANTHROPIC_API_KEY, any value assigned to a password, secret,
token or key however short (DB_PASSWORD=hunter2, PGPASSWORD=…), the
keys a connection string names (AccountKey, SharedAccessKey, SECRET_KEY), a credential in an XML element (<password>…</password>)
or beside the name of one (a Kubernetes name: DB_PASSWORD / value: …
entry, a .NET <add key="ApiKey" value="…"/>), a YAML block scalar under
a secret name (password: | and the lines below it), a quoted value that
runs over lines or holds an escaped quote, a private key whose first line
was cut off, the signature
on a signed URL, and the password in a
connection URL (postgres://user:…@host). The
self-check and the reflection critic, which can run on a different
provider, get their evidence with credentials removed. The self-check
also recognises each tool's own way of reporting a failure — a failed run_code, a non-zero shell exit, an HTTP or GitHub API error — instead
of counting it as a result — an empty transcription and a media tool with
no model configured included — and only for the tool that uses that wording,
so a page or file that happens to start "Failed…" or "Error:" still
counts as what the tool returned. A failed MCP tool call now says so
instead of reading as its error text, and a call the worker answered with
a stand-in tool says which tool actually ran. The self-check sees what was written or
typed unless the field is a credential; #author is no longer mistaken
for one.
With the Browser chip on, a website errand is planned around the
browser. No tier above the worker was told the browser existed, so "go
to this site, ask it something, bring back the answer" was routed as a
multi-section plan whose workers wrote what the site might have said. The
complexity classifier, both planners and the worker now know the browser
is there, keep a website task on one worker that drives it end to end, and
never plan to invent a site's reply. A run whose only tool is the browser
also gets tool guidance again.
You are told when the browser is busy. When every browser session is
in use by another run, the refused run now shows "The browser is busy…"
in the chat instead of leaving the model as the only one who knew. The
notice clears if a retry gets the browser, and at the next run.
The live browser view appears on Complex runs. The top tier ran under
a different id from the one the run announced, so the panel, Stop and
take-over never showed while a real session was driven and billed.
The "section needs your decision" prompt says what you are deciding.
It shows what the section was asked to do, what it has so far, and what
each choice does to that work — on the web and in the desktop app.
Security
Hard-link aliases of protected or local-only dependency files stay protected,
including explicit rules through symlinked dependency folders, mixed-case
rules and **/ prefixes. Commands refuse cyclic symlinks reached by such
rules. Git calls run one at a time across processes, so overlapping policy
snapshots cannot restore weaker .cascadeignore rules.
Reading a provider's error no longer slows down on a crafted message.
Two patterns for "model … not found" were tried again from every
"model " in the message, so one made of them took time growing with its
length squared; they now read it in one pass.
Agents can no longer read .cascade/config.json. It holds your
provider keys in plain JSON, and nothing protected it: file_read
returned it, grep printed its lines, and the code index could embed it
and hand it back through code_search. It is now always protected, with
the dashboard secret, the session database, the audit trail and the
project's world state (with their keys) and the code index, and no .cascadeignore line can undo that. An index built before is pruned of
such files, and nothing is shown to its reranker that could not be shown;
where left-out files hold a search's best matches, it looks further for
ones it may show. Protection covers every tool that
takes a path — grep, glob and file_list among them, which used to
see everything, and could search outside the workspace — and follows
symlinks.
Agent-written tools always run confined. Without the optional isolated-vm addon — and the desktop app never ships it — they ran in a
worker thread whose code could reach process, and through it the
files, network and processes of the machine. They now run in a
WebAssembly sandbox (QuickJS) that ships with Cascade, on a thread of its
own with a memory cap and a hard kill at the deadline, reaching the
machine only through callTool and fetch. There is no unconfined
fallback left. The deadline ends what the code started too: an approval
it is waiting on is withdrawn, a tool call or request it made is told to
stop — shell and run_code kill their command — and nothing it asked
for starts afterwards.
privacy.paths is enforced when a file is read. It applied only to a
subtask whose assignment named a matching path, so a worker that came
across the file by search read it on a cloud model. Reading a local-only
file with file_read, file_edit, image_analyze or a grep of that
file now moves the subtask to a private model before the contents reach
any model, or refuses the read when there is none; so does grading an
acceptance criterion against one. A grep across folders leaves
local-only files out for a subtask that is not local-only, so that
whether they matched says nothing about them, and file_list and glob
leave out their names — and directories covered whole — since a name can
carry what a local-only subtask read. A file an assignment names, as an
artifact or in an acceptance criterion, is judged by what it is, so a
link to a local-only file counts; a protected one is not read to check
it. Nothing a local-only subtask knows leaves the machine
through Cascade's tools: it cannot message its peers, ask for more
workers or have a replacement tool written for it; web, browser, GitHub,
media, code_search and MCP tools are refused to it; and a tool the
agent wrote cannot fetch once it has read a local-only file. Local-only
files are kept out of the code index, never sent for transcription, and
not embedded by generate_document in a document that is not local-only.
What a local-only subtask makes goes to its private folder, outside the
project — @private/… to the file tools, $CASCADE_PRIVATE_DIR to its
commands, whose new files are moved there when they end — so a name it
chose, which could carry what it read, shows to no one else, and only a
local-only subtask reaches it; a new file in a folder a local-only pattern
covers whole stays there, with that folder's names hidden. What it changes
is local-only too — a file tool's destination before it writes, whatever
its commands changed once they end — and stays so in later runs, recorded
in the project's state folder even when a configured pattern covers it,
so narrowing the pattern later does not release it, whatever characters
its name holds (a backslash included); a write that leaves
the file as it was takes its record back off. What it deletes is marked
the same way — which files it chose to delete could say what it read —
so the rest sees the path as local-only, not as missing. When a command's
marks cannot be saved, they are tried again with the workspace still held,
and failing that, the files it changed are moved to its private folder
rather than left where another process could read them. Runs sharing a
workspace share the record: each merges its changes in under a lock, and
sees another's as they are made. A record that cannot be read — cut
short, edited by hand — stops a run starting rather than reading as none,
refuses every check mid-run until it can be read again (what was read
before may lack another run's new marks), and is never written over; nor is a credential store that cannot be read. The code index reads each file while no
local-only command can write, in any run or process, so it never embeds
what one is writing before it is marked. A planner reads nothing of a
local-only result from the project's world state, not even its length. Its commands write nowhere else (their /tmp is private, the rest of the machine and the git store read-only)
and run alone meanwhile — against every run in the workspace, in other
windows and processes too, and against a worker checking its own
artifacts. Nor does it write through a file with other
names — hard links, whose names outside the workspace could be neither
marked nor hidden: a file tool refuses, and to its commands such files,
and the package folders pnpm links them into, are read-only. Siblings waiting on it get a status line, not its
output, and no facts are taken from it into the knowledge graph. Each
fact now records the files it came from, and with any privacy.paths
rule, one with no record — every fact saved before this — or with a
source that has since become local-only is left out of T1's plan and of knowledge_graph_search for a caller that is not local-only. Nothing is
deleted: loosen the rules and they come back.
An approval it needs goes to you, not to the T2 and T1 models, which may
be cloud ones and would see the call's input — nor does an "always" one of
them gave earlier answer it. The request says it is a local-only
subtask's, and the hosted server, whose viewers are off the machine,
shows its tool but withholds its input. file_edit asks like a read does, and so does any change or deletion of a
local-only file by a subtask that is not local-only yet — file_write, file_delete, pdf_create, generate_document — which moves it to a
private model first, or is refused. Every file the assignment declares
counts at the start, one without an extension (Dockerfile) too. read_current_page, which takes no input, stays open to it.
Plugin tools are refused to it like
MCP ones, as is any tool registered from outside that does not declare localOnlySafe. Its output, tool calls and results, and streamed text are
kept out of the status and tool events the dashboard broadcasts and the
audit log records: those carry the same withheld line, a malformed call's
error included, and a tool name its model made up shows as a placeholder. A subtask that
turns local-only mid-run moves to the private
model, and that model's tool protocol, before its next call; only private
endpoints still usable count, not ones in backoff or out for the run.
shell, run_code and git run in a process jail. An approved
command could cat .cascade/config.json, read a local-only file, or read
Cascade's own environment from /proc — and approval can come from a
tier above rather than a person. On Linux they now run in bubblewrap:
Cascade's own files, the built-in secrets, ~/.cascade-ai, the usual
credential locations in the home folder (~/.ssh, ~/.aws, ~/.config/gcloud, ~/.kube, ~/.netrc, ~/.npmrc and the like, which
the git tool alone keeps, to push and pull with) and local-only paths
(for a worker that is not local-only) are mounted over — and the git
store too, while its history holds any of them, since a blob is as
readable as the file, or while git keeps anything no branch reaches — a
deleted branch, an amended commit, a file staged and unstaged — whose
contents no name tells; the git tool keeps working, runs only in the
workspace, compares only files file_read could read (git diff --no-index, or given a path outside the work tree, compares any file,
the home credentials it keeps included) and writes no diff to a file,
leaves them out of
its diffs and of what it stages or stashes (where they read as empty),
never runs a hook — git's configuration files are read-only to commands,
so none can name a program for it to run with the credentials it keeps —
and refuses a push that would send a commit holding one — asking the
destination what it has, not trusting tracking refs, through the same
jailed git the push uses, since reaching a remote can run a program. A
repository nested in the workspace, or a submodule, is checked as the
workspace's own is — every path its history holds, named as the
workspace names it — and its store hidden, and its config frozen, the
same way; every file a git config includes is frozen too, one not there
yet made empty first — and runs
no hook and none of the global or system git or ssh configuration there,
since a command could rewrite those and git would run what they name with
the history in view — and so is
a hard link to any of them, under whatever name. Each command gets its own process namespace and no
capabilities, and a local-only worker's commands get no network and no
Unix sockets, which would reach host daemons such as Docker's. A symlink
whose name is covered hides what it leads to, a whole directory included.
Tool calls wait while a command runs, and a command for the tool calls in
progress, and a tool call checks its path again once its turn comes, so no
command can swap a path a tool has checked for a symlink before the tool
opens it.
On macOS, sandbox-exec denies the same paths, and a local-only worker
cannot run commands: nothing there stops such a command from
hard-linking a file from outside the workspace in and writing to it, or
from leaving a child running that writes after it ends. Where neither
jailer works (Windows, or a Linux without working bubblewrap), shell
and run_code are refused, since taking the keys out of a command's
environment leaves the files on disk readable; tools.processJail: "off"
runs them anyway. There the git tool still runs, with the provider keys
taken out of its environment and no configuration that can start a
program, since one would run outside any jail: git reads no global or
system file (the words in them — line endings, your name — are passed on,
and a filter they define is marked required, so a file that needs it is
not added unfiltered), a repository whose own configuration names a
program (a diff or merge driver, a filter, a credential helper, an ssh
command) is refused with the setting named, and a commit your
configuration would sign is not made. Pushing over HTTPS then has no
credential helper, and ssh reads no ~/.ssh/config. Agents' file tools
can no longer change anything in .git, where git's configuration lives,
under any name. tools.processJail: auto (default), bwrap, sandbox-exec — that jailer or no commands — or off.
The code index's database is protected wherever it is. Only its
default place was, so with codeIndex.dbPath set the file tools and
commands could read the indexed text, deleted chunks included. The
configured file and its journals are now protected too — a relative dbPath is the workspace's, where it was opened relative to the process —
in git as well:
its blob hides the git store from commands, and a push that would send
it is refused — and a hard link
to any protected file is protected like the file — by the file tools,
commands and the code index alike. So are the run checkpoints in .cascade/resume/, which hold earlier prompts and outputs, and a write
through a symlink whose target, outside the workspace, does not exist yet
is refused like any other write outside it.
A private model is one whose endpoint is private.forceLocal chose
any model marked isLocal — which means it costs $0 — so an
OpenAI-compatible server on a public host configured with local: true,
or an Ollama pointed at someone else's machine, was sent local-only
subtasks. It is now judged by the endpoint's host: this machine, an IPv4
or IPv6 private range, or a .local name. A bare name like ollama does
not count by itself — a resolver's search domain or a hosts entry can send
it anywhere — so a provider reached that way (a compose service, a private
DNS zone) is declared with privateNetwork: true.
Commands see git's configuration without its credentials. A token in a
remote's URL, or an http.extraHeader, was readable to every command, and
what a command prints goes to the model. On Linux a command now sees a copy
with the user part of each URL and every secret-named key taken out; on
macOS, where there is no mount to put the copy in place, a configuration
holding one is not readable to commands at all. The git tool keeps the
real file to push and pull with.
The jail hides what it cannot look into. A folder in the workspace that
can be entered but not listed is hidden whole, a bare repository nested in
the workspace has its store checked like any other, and a store git cannot
locate (a malformed config) is hidden rather than left in view. A policy
file such as .cascadeignore that a command creates where there was none is
removed when the command ends.
T1 no longer falls back to the world-state log with privacy.paths
rules, whose entries say nothing of the files they came from; an
agent-written tool's fetch reads at most its limit of a response rather
than the whole of it; and a worker's approval request is withdrawn when its
run is cancelled.
More that reached a command or an agent-written tool is closed off.
An environment variable holding a configured provider key anywhere in its
value (Bearer <key>, a URL) is taken out of a command's environment, not
only one that is the key. The git tool refuses a push naming the program
to run at the other end (--receive-pack, --exec, however abbreviated),
which for a remote on this machine git runs here. A symlink on the way to
a git config file that a command replaces is put back when the command
ends, and the git tool no longer runs while a shell or run_code
command does, so it never reads one put there meanwhile. An agent-written
tool makes at most 100 calls to tools and fetch in a run, 4 at a time,
and is stopped as soon as its run is cancelled.
What fails is refused, not passed over. A .cascadeignore that is
there but cannot be read stops Cascade starting rather than reading as
none; a git configuration including more than 1,000 files refuses commands
rather than leave the rest writable (every include is followed, where 64
files were); and a Python artifact is checked by compiling it in memory, so
the check leaves no __pycache__ behind for a local-only file.
No tool runs past the approval it needs. An agent-written tool's callTool asks about every tool the registry's approval policy covers — tools.requireApprovalFor included — not only those marked dangerous. A
failed tool call's fallback no longer turns a tool that needs no approval
(a read) into one that does (a write), and the tool a fallback or a
synthesized replacement runs is validated and asked about as itself.
Checking an artifact never runs a compiler the workspace supplies: no npx tsc (which prefers a workspace's node_modules/.bin/tsc, or fetches
one) — Cascade's own TypeScript, when it has one — and Python only from a
PATH entry outside the workspace, run isolated. The workspace gate treats
markers it cannot list as an error and a lock it cannot read as held,
where both read as nobody there.
What can't be checked or put back now stops work rather than slipping
by. Commands are refused when the workspace folder itself can't be
listed; a .cascadeignore or git-config symlink a command changed that
can't be put back keeps every command and git call from running — in later
runs too — until it is; and an old .cascade/ entry that can't be moved
stops Cascade starting rather than being shadowed by fresh state. A name
that is .cascadeignore on a case-insensitive filesystem (.CASCADEIGNORE, .cascadeignore.) can't be written either. The workspace gate's lock is
removed at the end only while it is still this process's, a call waiting
for its turn gives up when its run is cancelled, a call naming a local-only
file makes the subtask local-only before its approval is asked (so the
request skips the tier models), and facts learned after a command or
another tool whose reads aren't seen record their sources as unknown.
Fewer ways around the jail and the privacy rules. Container daemons'
sockets (Docker, Podman, containerd, CRI-O, LXD, BuildKit) are hidden from
a cloud worker's commands, which could otherwise have a daemon mount the
host for them. The git tool refuses to push to a repository on this
machine that has receive hooks, which would run with the credentials it
keeps. Every tool is refused once the workspace folder has been moved or
replaced — by a symlink to /, say — since the run started. A protected
or local-only name that is a symlink covers the file it leads to under its
other names too, and a file with several names counts as protected when a
folder could not be searched for them. An agent-written tool's call
naming a local-only file makes the subtask local-only before approval is
asked. Artifact checks run Python only from a folder this user cannot
write to. Marks a local-only command made that cannot be saved keep other
Cascade processes out of the workspace until they are — and refuse them,
naming the files, should this process end first. Symlinks are no longer
carried out of an old .cascade/ into the state folder, where a relative
one would lead somewhere else.
Programs Cascade starts come from folders commands can't write to.
bubblewrap, ripgrep and the jail's own git calls, which run outside any
jail, and the git tool's git with everything it starts (ssh, gpg, a
credential helper), are taken only from PATH folders this user can't
write to — as root, only the system's program folders — so a command
can't leave its own git or bwrap in ~/.local/bin for the next call.
The git tool checks a repository's own configuration as it does without
a jail — naming no program — when that configuration was made or changed
since Cascade first saw it other than by the tool itself, as a repository
a command creates is; it refuses git push --signed; a push to a local
repository looks for receive hooks under a relative core.hooksPath in
its git directory, where they run. Commands no longer see a protected file
inside a symlinked folder outside the workspace (.env in shared/). A
local-only write's mark stays unless the file's contents are unchanged,
not just its times, which some filesystems keep to the second.
Commands reach less of the machine, and fail closed where they can't be
checked. Run as root, a command sees the system read-only apart from the
workspace, the temporary folders and the home folder, since root could
otherwise rewrite the programs Cascade itself runs; on macOS, Cascade run
as root runs no commands. Commands other than the git tool's no longer
reach the session's D-Bus, systemd's sockets (systemd-run --user would
start a program outside the jail) or the ssh and gpg agents. When a hidden
folder can't be read through, every other file in the workspace with
several names is hidden too, bar installed packages. An agent-written tool
can't call another one or itself, and each of its approval requests gets
its own id. A TypeScript artifact is checked on its own, without the files
it imports, so the errors the model sees quote nothing it wasn't allowed
to read. A project moved on its disk whose old state can't be looked for
or moved stops opening rather than start afresh and leave it behind.
Commands run one at a time in a workspace, across processes too: the
jail takes what it puts back when a command ends as it finds it when the
command starts, which a command already running could have swapped. A
hard link in the workspace to one of Cascade's own files (the keys, a
project's state) is protected as that file. Without a jailer, a git
call that changes .cascadeignore (a checkout of an older commit) has
the change undone and fails saying so. The filter that keeps local-only
commands off Unix sockets is kept in the project's state folder, out of
every command's reach, and written again whenever it isn't the filter. cascade index reads each file in the workspace gate across processes,
privacy rules or not. A rule naming a path inside node_modules hides it
from commands too.