Skip to content

v0.83.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 23:12
6c4e9f0

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.