Skip to content

feat(project): an untrusted project is held to the shape a bundle has - #871

Closed
raymondk wants to merge 24 commits into
rk/bundle-symlinksfrom
rk/trusted-shape
Closed

raymondk wants to merge 24 commits into
rk/bundle-symlinksfrom
rk/trusted-shape

Conversation

@raymondk

@raymondk raymondk commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #868, which is stacked on #866. The rule for a project someone else handed over, and a flag on the commands a deploy service runs.


A project the CLI loads is trusted the way the user's own checkout is: a build step may run any command, a sync step any script, and a manifest may name any path on the machine. A project from elsewhere — a bundle an agent produced, a checkout nobody has read — may not, and nothing in the crate said what it may do instead (#863).

icp build, icp deploy and icp sync get --untrusted, which holds the project two ways:

What it reads goes through files::Confined at the project root (#866), so every path must be under the root and reached through no symlink. That covers:

  • manifests, argument files and environment files, which the loader reads — an init_args path outside the project would otherwise be deployed to a canister;
  • local recipe templates: RecipeFetcher now reads a file:// template through the project's filesystem;
  • the module a build step names, which the builder reads;
  • what the CLI keeps under .icp: the id store, the artifact store and a network's directory resolve their paths through the same Confined, so a symlink planted there is refused rather than written through.

What it runs is on a list, stated as what is allowed so that a kind of step added later is refused until someone decides otherwise:

  • a build step is a pre-built module;
  • a sync step is a plugin;
  • the file each names is a regular file the confined filesystem will give. A plugin is opened by its runtime rather than read through the filesystem, and a pipe nobody writes to would hang the read, so untrusted::vet asks before anything opens the file.

A recipe is rendered as the project loads, and the steps it produces are held like any other. A module or plugin named by URL is not on the list. untrusted::Vetted wraps any ProjectLoad so every load is vetted; each command refuses before reaching any network. No other command takes the flag, and nothing changes without it.

$ icp build --untrusted
Error: the project asks for more than an untrusted project may

Caused by:
    canister 'backend' names '/srv/bundle/linked.wasm', which is not a regular file in the project: an untrusted project runs only a file under its root, reached through no symlink

Decisions worth a look:

  • Reading is confined. An earlier revision left reads unrestricted and vetted only what runs; review pointed out that an argument file read from anywhere is then deployed, so the rule changed.
  • A symlink is refused wherever it leads, including to a file inside the project. The rule is the shape of the tree, not where any one link goes.
  • Recipes are allowed: a local template is confined, a URL or registry recipe is still fetched, and the steps either renders to are vetted.
  • No checksum is required on a module: the same untrusted hand wrote the module and the checksum.
  • A relative --project-root-override / ICP_PROJECT_ROOT is joined to the working directory, for every command: Confined takes an absolute root.

Known gaps:

  • A step file that is refused gets a generic message; the precise reason (outside the root, or which component is a link) is only reported for files the loader reads.
  • A planted link under .icp fails the build with "failed to save wasm artifact"; the build renderer shows only the top line of the error, so the reason is not printed.
  • The wasm fetcher's local checksum read is still made on the host directly; vet has already checked the file by then.

Tests: the vet on the project model with each kind of step on both sides of the list; an on-disk test through Confined (outside the root, a link out, a link within, a linked directory, a directory, a named pipe); unit tests for the two stores with a planted link; and eleven CLI tests covering each refusal, a file and a recipe template read from outside, symlinks, link/../file, a planted .icp link, a relative root, and that deploy and sync refuse as build does while no other command takes the flag.

Ref: #863, #865

🤖 Generated with Claude Code

raymondk and others added 2 commits November 5, 2026 07:46
…ct from elsewhere

`icp project bundle` holds every path a manifest names to the project it
is bundling, and refuses one that resolves outside. Loading a project
holds nothing: the loader joins each path to its base and reads it
through the host filesystem, which is right for the user's own tree and
wrong for a project someone else handed over, a bundle an agent passes
to a deploy service, say. A manifest in such a bundle could name
`../../etc/hostname` as an argument file and have it read into the
canister's arguments at load, before any caller could check.

`files::Confined` is the host filesystem held to one directory. Every
path is resolved against the root before the disk sees it: a relative
path is joined, `.` dropped, `..` applied, and a path that comes to rest
outside the root, by climbing, by being absolute elsewhere, or through a
symlink inside the root that points out, is refused with `OutsideRoot`.
The predicates answer false for such a path, scratch directories are made
under the root, and `resolve` hands out the real path for a caller that
needs one for something that does not read through the trait.

The loader resolves every path a manifest names through the filesystem
it is given, so confining that one seam holds all of them, canister
directories and dependency projects included, without the loader
enumerating its fields; the tests load one manifest through both
filesystems to show the difference. Nothing changes for the CLI, which
keeps loading through `HostFileSystem`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The symlink walk was the wrong layer. A symlink is the business of
whoever puts a tree on disk: bundling writes only regular files and
directories, and a consumer unpacking an archive it does not trust
refuses any entry that is not, so the tree Confined reads has no links
to follow. The user's own tree may link wherever it likes, and the host
follows it. Confined now resolves every path lexically against the root
and does nothing else.

Canonicalizing the root in `new` is also what failed CI on macOS and
Windows: the root was stored as `/private/var/...` or `runneradmin`,
while every absolute path the loader passed was spelled as the caller
got it (`/var/...`, `RUNNER~1`), and so refused. The root is now kept
as spelled, and a unix test reproduces the failure through a symlinked
alias of the project directory.

The doc claim that one seam holds every loader read is narrowed: a
local recipe template is read by the `Resolve` implementation, which a
consumer holding an untrusted tree must confine or refuse itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@raymondk
raymondk added this pull request to stack #869 November 5, 2026 21:54
Copilot AI balanced review requested due to automatic review settings November 5, 2026 22:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical untrusted-mode bypasses in artifact and lock writes, local Wasm loading, managed-image mounts, and shell completion still permit reads or writes outside the project.

5 open issues
What changed in this PR

Adds an opt-in --untrusted mode intended to restrict externally supplied projects to bundle-like behavior, building on the existing filesystem confinement and bundle safeguards.

Changes:

  • Adds reusable step vetting, recipe refusal, and a vetted project loader.
  • Extends confined filesystem access to reject symlinks below the project root.
  • Wires the policy into CLI initialization, with tests and documentation.
File Description
docs/​reference/​cli.md Documents the global --untrusted flag.
crates/​icp-project/​src/​untrusted.rs Defines allowed steps, recipe refusal, and loader vetting.
crates/​icp-project/​src/​lib.rs Exposes the policy module and load error.
crates/​icp-project/​src/​files.rs Adds symlink-refusing confinement and tests.
crates/​icp-cli/​tests/​untrusted_tests.rs Tests accepted projects, refusals, and flag handling.
crates/​icp-cli/​src/​main.rs Parses and forwards the new flag.
crates/​icp-cli/​src/​complete.rs Supplies the trust argument for completion initialization.
crates/​icp-app/​src/​context/​init.rs Composes confinement, recipe refusal, and loader vetting.
CHANGELOG.md Announces untrusted mode and its intended restrictions.

Review effort: Balanced

Comment thread crates/icp-app/src/context/init.rs Resolved
Comment thread crates/icp-app/src/context/init.rs Resolved
Comment thread crates/icp-cli/src/complete.rs Resolved
Comment thread crates/icp-project/src/untrusted.rs Resolved
Comment thread crates/icp-project/src/untrusted.rs Resolved
Comment thread crates/icp-project/src/untrusted.rs Resolved
Comment thread crates/icp-project/src/untrusted.rs Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Path vetting accepts directories and FIFOs as modules or plugins, allowing a pre-built step with a FIFO and sha256 to block indefinitely during checksum reading.

1 open issue
5 resolved since last review

Review effort: Balanced

Comment thread crates/icp-project/src/untrusted.rs Resolved
Copilot AI balanced review requested due to automatic review settings November 9, 2026 12:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unchecked environment canisters can bypass vetting and execute unvetted deployment steps, while symlink-spelled project roots can cause valid modules or plugins to be rejected.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

High severity Vet canisters in every environment

crates/​icp-project/​src/​untrusted.rs:127

Vetted accepts any ProjectLoad, but checks only Project.canisters; a custom loader can put a script or outside-project plugin in an independent environment canister and still pass vetting. Deployment executes those environment copies (operations/deploy.rs:530–544), so the unvetted step can run. Apply the same step and path checks to every environment's canisters before returning the project.

1 resolved since last review

Review effort: Balanced

Comment thread crates/icp-project/src/untrusted.rs Resolved
@raymondk
raymondk marked this pull request as ready for review November 9, 2026 13:55
@raymondk
raymondk requested a review from a team as a code owner November 9, 2026 13:55
raymondk added a commit to dfinity/imcp2 that referenced this pull request Nov 9, 2026
dfinity/icp-cli#871 now asks, after resolving where a module or plugin
really is, whether a regular file is there, since a directory cannot be
read as a module and a pipe is read from whoever writes to it. The vet
asks the filesystem it is given, and a path that does not resolve is now
refused as resolving "to anything" rather than "to a file".

The host follows, and resolves the extraction root once before the
filesystem is held to it. The vet hands the filesystem a resolved path,
and `Confined` holds every path to the root by spelling alone, so a root
named through a link — macOS keeps its temporary directories under
`/var`, a link to `/private/var` — would put every resolved path outside
it and refuse every bundle. `dunce` resolves it, as icp-project's own
host filesystem does, keeping a Windows path free of the `\\?\` prefix.

Pins the four icp-cli crates to the head of #871. Two tests: a directory
of the bundle named as a module fails the load naming what was found,
and a root named through a link is the directory it leads to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 10, 2026 00:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the opt-in untrusted-project validation change.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 10, 2026 01:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are recorded for the opt-in untrusted-project execution restrictions.

No open issues 🎉

Review effort: Balanced

Comment thread crates/icp-project/src/untrusted.rs Resolved
Copilot AI balanced review requested due to automatic review settings November 10, 2026 12:35
raymondk and others added 17 commits November 10, 2026 14:40
…y name

Three holes in the archive walk, from review:

- The output path is checked against the copied directories by its
  spelling, which a symlink inside one of them gets around: with
  `assets/shared -> ../shared` and `--output shared/bundle.tar.gz`, the
  walk met the archive it was writing and copied a partial copy of it
  into itself. The canonical output now travels with the walk, and an
  entry that resolves to it is refused, named by the path it was
  reached by.
- `crate::fs::read_dir` skips a name that is not UTF-8, which for a
  bundle meant a file silently left out where tar's walk had carried
  it. The walk lists directories itself and refuses such a name, since
  the archive cannot carry it either way.
- A link was called dangling whenever canonicalizing it failed and
  `read_link` succeeded, which also covers a link that loops or leads
  somewhere unreadable. Only a `NotFound` is dangling now; the rest
  keep their own cause.

Entries met under a link are listed where the directory really is but
named by the path they were reached by, so every message says what the
user wrote rather than where it led.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…stale doc comment

The new `## Experimental` heading sat above the existing Intel macOS
chore entry, which made that general change read as experimental. The
heading and the bundle entry now follow it, as in v1.8.0.

`validate_output_path` kept its old doc comment above the new one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ames

The walk resolved a directory as an entry and again on entering it; the
resolved path is now handed down. list_dir returns names rather than paths
the caller took the names back out of. Doc comments and the changelog entry
say the same in fewer words.

Co-Authored-By: Claude <noreply@anthropic.com>
The canonical output was worked out before the build, when an output path
that is a dangling symlink resolves to its own spelling. Creating the file
follows the link, so the walk was told to refuse a path the bundle was not
written to, and one linked into a copied directory was read into itself
without end. The output is now resolved after it is created.

Co-Authored-By: Claude <noreply@anthropic.com>
…from where a link led

A files: entry that is itself a link to the output was read into the
bundle half-written, and one that is a named pipe was read forever; both
are now refused as they are on the walk. The walk resolved each entry by
the path it was reached by, following every link crossed so far again, so
a chain of directory links longer than the host follows in one path
failed; each entry is now resolved from the directory's real path.

Co-Authored-By: Claude <noreply@anthropic.com>
A project the CLI loads is trusted the way the user's own checkout is:
a build step may run any command, a recipe may come from any file or
URL, a module may be downloaded from anywhere, and a manifest may name
any path on the machine, through any symlink. A project from elsewhere
(a bundle an agent produced, a checkout nobody has read) should get none
of that, and until now nothing in the crate said what it should get
instead; a consumer such as the imcp2 deploy server had to assemble the
rule itself from the seams.

`icp_project::untrusted` now states the rule, which is the shape
`icp project bundle` writes, and supplies the pieces that hold a project
to it:

- `files::Confined::refusing_links` is the confined filesystem of #866
  with one more refusal: a path that passes through a symlink anywhere
  under the root, wherever the link leads. The lexical `Confined::new`
  stays as it is, for a tree whose links were refused on the way in.
  Each component below the root is looked at as it is on disk, so no
  canonicalizing, and the root is still matched as the caller spells it.
- `untrusted::NoRecipes` is the `Resolve` that fetches nothing, so a
  project naming a recipe fails to load rather than loading and then
  being refused: the recipe is build instructions read from wherever the
  manifest points, and the loader reads it before anyone can look.
- `untrusted::vet` refuses a loaded project with a script build or sync
  step, or a module or sync plugin fetched from a URL, and
  `untrusted::Vetted` wraps a `ProjectLoad` so every load is vetted.

The CLI gets a global `--untrusted` flag that composes the three: the
project root is located up front, the filesystem is confined to it and
refuses links, no recipe is fetched, and the loader is vetted. Nothing
changes without the flag. A checksum on a module is not asked for: the
same untrusted hand wrote the module and the checksum, so it says
nothing about what the project can reach.

Ref: #863, #865

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… not

`untrusted::vet` listed the steps it refused: a script, a module from a
URL, a plugin from a URL. A kind of step added later would have passed
unexamined. It now holds every step to the two an untrusted project may
have, `build_step_allowed` (a pre-built module read from a file in the
project) and `sync_step_allowed` (a plugin read from a file in the
project), and names the first step that is neither, with the step
itself on one line so the reader sees what it was.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d anchor a relative root

Two gaps a review turned up in what `--untrusted` holds a project to.

The module a pre-built step or a plugin step names was fetched by the
CLI's shared fetcher, which reads a local source straight off the host:
its checksum was taken of a file outside the root or through a symlink,
and for a plugin the unchecked path went on to Wasmtime, which opens it
for itself. `untrusted::LocalModules` is the `wasm::Fetch` of an
untrusted project: every module is read through the confined filesystem
first, so the refusal happens before any checksum and before the path
goes anywhere, and its message is the filesystem's own; a module named
by URL is not fetched.

A relative `--project-root-override` or `ICP_PROJECT_ROOT`, such as
`bundle`, came back from the locator as spelled, and the confined
filesystem then held `bundle/icp.yaml` to `bundle/bundle/icp.yaml`, so a
valid project failed to load. The override is now joined to the working
directory before the locator is built, spelled as the user spelled it,
with nothing canonicalized.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A deploy service handed a project by an agent is what asks for the flag,
and those are the commands it runs; nothing else acts on what a project
says. The flag is read off the command before the context is composed,
since it is the context that applies it. On every other command it is
now an unexpected argument.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…may not

The vet has been a list of what is allowed since ae3367e; the flag's
help, the changelog, the context's doc comment and the symlink message
still read as a list of refusals. Each now states the list — a pre-built
module from a file in the project, a plugin from a file in the project,
no recipe, every path a regular file or directory under the root — and
that anything else is refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The line is between reading and running. Reading is not restricted: the
loader reads argument files, manifests and recipe templates from wherever
the manifest points, through the ordinary host filesystem and fetchers,
and a recipe's rendered steps are held like any other. What runs is held
to a list: a build step is a pre-built module, a sync step is a plugin,
and the file each names is within the project — judged by where it
really is, root and file both canonicalized, so a link is seen for where
it leads and a link within the project is let be.

That makes the vet the whole rule, applied to the loaded project, and
retires the link-refusing filesystem, the recipe refusal, the module
fetch wrapper, and the up-front root locate and relative-root anchoring
they needed. `Refused` is boxed on the load error, which it would
otherwise push past clippy's size threshold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… lead where they lead

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Canonicalization says where a path lands, not what is there, so a
directory or a named pipe within the project passed the vet. A pipe is
then read from whoever writes to it, and one nobody writes to is waited
on forever, at the checksum or at the build. The vet now asks the
filesystem whether the resolved path is a regular file and refuses
otherwise, naming what it found, with the mock, on-disk and CLI tests
to show it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The vet judged where a module leads on its resolved path, and then asked
the filesystem what was there by that same resolved path. A filesystem
that knows paths by how its caller spells them — `Confined`, with the
project reached through a link, as a macOS temp dir is through `/var` —
resolves the module to a place not under that spelling and so has no
answer, and a module within the project was refused as not a file. The
CLI composes the vet over the host filesystem, where the two spellings
answer alike, so nothing shipped was affected; an embedder composing it
over a confined one would have been.

Where a path leads is still judged resolved; what is there is now asked
of the path as spelled, which a link is followed from either way. The
test double follows a path to where it resolves before answering, and an
on-disk test holds a root spelled through a link to accepting its own
module and still refusing a directory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
vet asked build_step_allowed and then matched the step again with an
unreachable arm; local_module and local_plugin now answer both at once,
and match every kind so a new one must be decided on. init.rs keeps to
the change it needs. The CLI tests drop the cases the unit tests already
hold on disk (a directory, a pipe, a link within).

Co-Authored-By: Claude <noreply@anthropic.com>
…ned to its root

--untrusted held a project to what it may run and let it read anywhere.
It now also loads and builds through files::Confined at the project root,
so a path a manifest names must be under the root and reached through no
symlink: argument and environment files, local recipe templates, which
the recipe fetcher now reads through the project's filesystem, and the
module or plugin a step names, which vet asks the same filesystem for.
A relative root override is anchored at the working directory, since
Confined takes an absolute root.

Co-Authored-By: Claude <noreply@anthropic.com>
…ilesystem

The id store, the artifact store and a network's directory were written
and read on the host directly, so a symlink planted under .icp in a
handed-over tree sent a built module, an id mapping or a truncating lock
file wherever it pointed. Each now takes the Confined an untrusted project
is held to and resolves its paths through it first.

Co-Authored-By: Claude <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Untrusted deployments can overwrite files outside the project through unchecked domain-publication paths, and relative template paths resolve inconsistently between trusted and untrusted modes.

2 open issues
2 resolved since last review

Review effort: Balanced

Comment on lines +181 to +182
confined.resolve(&paths.lock_file())?;
confined.resolve(&paths.network_descriptor_path())?;
let (tmpl, should_cache) = match &tmpl_source {
TemplateSource::LocalPath(path) => {
let bytes = read(path).context(ReadFileSnafu)?;
let bytes = self.files.read(path).await.context(ReadFileSnafu)?;
@raymondk
raymondk removed this pull request from stack #869 November 10, 2026 22:57
raymondk added a commit that referenced this pull request Nov 10, 2026
…irectory handle

`--untrusted` on build, deploy and sync limits the steps a project may run
to a pre-built module and a plugin, and reads everything through
`files::Confined` at the project root.

`Confined` opens the root once as a `cap_std::fs::Dir` and makes every read
and write beneath that handle, so the operating system refuses a path that
leaves the root in the same step that opens it (`openat2` on Linux). On top
of that it refuses any path through a symlink, wherever the link leads.

This is #866 and #871 on main, without #868, and with `Confined` built on
cap-std in place of a path walk followed by a plain open.

Co-Authored-By: Claude <noreply@anthropic.com>
@raymondk

Copy link
Copy Markdown
Collaborator Author

closed in favor of 873

@raymondk raymondk closed this Nov 11, 2026
raymondk added a commit that referenced this pull request Nov 11, 2026
…irectory handle (#873)

* feat(project): --untrusted, with the project read beneath a cap-std directory handle

`--untrusted` on build, deploy and sync limits the steps a project may run
to a pre-built module and a plugin, and reads everything through
`files::Confined` at the project root.

`Confined` opens the root once as a `cap_std::fs::Dir` and makes every read
and write beneath that handle, so the operating system refuses a path that
leaves the root in the same step that opens it (`openat2` on Linux). On top
of that it refuses any path through a symlink, wherever the link leads.

This is #866 and #871 on main, without #868, and with `Confined` built on
cap-std in place of a path walk followed by a plain open.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(untrusted): refuse a pipe before it is read, vet what a plugin is handed, take a recipe path from the working directory

- Confined refuses to read what is not a regular file, before the open: a
  pipe nobody writes to would otherwise hold an args, env or recipe read
  forever.
- vet holds each dirs: and files: entry of a plugin step to a directory, or
  a regular file or directory, in the project. The plugin runtime opens
  those itself.
- A relative file:// recipe path is joined to the working directory before
  it is read, as it resolves for a trusted project, rather than to the
  confined root.
- Say that .icp is the tool's own state and is not vetted, and that the
  plugin runtime opens by path after the look.
- Wrap the host feature list, which had passed taplo's column width.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(untrusted): leave .icp to the tool, unguarded

What is under .icp is the tool's own state, and whoever takes a project
from elsewhere discards any that came with it. So the id store, the
artifact store and a network's directory go back to what they are on main,
with no confined filesystem handed to them.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(untrusted): refuse a recipe, rather than read it and vet what it renders to

A recipe is build instructions read from a file or fetched from a URL
while the project loads. Under --untrusted the loader is now given
untrusted::NoRecipes, a Resolve that fetches nothing, so a project naming
a recipe fails to load, wherever the recipe is from.

RecipeFetcher goes back to what it is on main: it no longer reads a local
template through the project's filesystem, since it is never asked for one
for an untrusted project.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(untrusted): the project is where the command stands, and no root is searched for

Finding a workspace root means reading the manifests above the working
directory and following the dependencies they name, with the host's own
filesystem, before there is a root to confine those reads to. It also let
a project above that declares this one be adopted as the root, which
widened what the untrusted project could read to the whole of that
project.

Under --untrusted the root is now the working directory, or the one
--project-root-override names, and the locator is pinned to it. A
directory with no icp.yaml is refused, saying so.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(untrusted): say that --untrusted is run from the directory that holds icp.yaml

It takes that directory as the project root and does not travel up to a
project above. Said in the flag's help, the CLI reference, the project
model and project dependencies pages, the changelog and the module doc.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(untrusted): say in the flag's help that .icp is not checked and is to be deleted

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(untrusted): say that vet checks no containment and takes it from the filesystem it is given

vet asks the filesystem whether a path is a file or a directory and takes
its word, so Vetted::new with HostFileSystem accepts a path outside the
root or behind a symlink without an error. Say so on both.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants