Augments LabsCrucible Code

Directories

The file tools reach the directory crucible was started in, and nothing else without you. A read that leads outside it, measured after symbolic links are resolved, is put to you the way a command is, and runs only on your yes. A write outside it is refused by the tool itself, in every mode. In ask mode the question still comes first, naming only "a path it could not resolve", and a yes is followed by that refusal. The tool refuses because the question a write outside would need has no honest wording: nothing out there was handed over. bash is the standing exception: a shell reaches whatever you can, which is why what bounds it is the question and the rules rather than a boundary on paths.

permissions.extraDirectories widens that reach:

{
  "permissions": {
    "extraDirectories": ["/home/you/src/shared-lib"]
  }
}

Entries are absolute paths, resolved once at startup the way the working directory already is. A relative entry is refused with an error naming the file and the position, because a path in a configuration file is not relative to anything the file knows. An absolute path also names one machine, so the entry belongs in ~/.crucible/config.json, the only file that may set it. A workspace file that does, committed or not, stops the start with cannot be set here, so a checkout can never widen the directories it reaches. /home/you/src/shared-lib means nothing to anyone else who clones.

The working directory stays the anchor: a relative path in a tool call still means what it means from there, and bash still runs there. An extra directory is reached by its own name.

What containment is measured against

A path is resolved once to say what a call is about, and resolved again inside the tool that acts on it, so a symbolic link planted while a question was on screen changes the answer rather than slipping past it. What that leaves is a path with no symbolic link anywhere in it, and on Unix the file is then reached by walking it rather than by naming it: crucible opens the directory the path was proved under, then each directory below in turn against the one before it, refusing any step that has become a link or has stopped being a directory, and asks for the file itself against the directory holding it. Every step is one system call inside a directory the step before it already reached, so there is no gap left between deciding a name is safe and using it. A new file is created at the end of the same walk, with the flag that makes the operating system refuse a symbolic link at the last component. edit reads and rewrites through a validated regular-file handle, then prepares the replacement privately under the proven parent instead of truncating that handle.

write does not truncate an existing file. It prepares a private file beside the destination, preserves the existing mode, flushes the complete contents, and renames the private file over the destination as one namespace operation. Unix then flushes the directory. A failure before the rename leaves the previous file whole; a failure of the final directory flush is reported after the replacement is already visible. Crucible also checks the destination's file identity immediately before commit and refuses a concurrent change. That check and rename are separate system calls, not a compare-and-swap primitive. edit uses the same commit path after reading at most 1,000,000 bytes through the opened file. Its result has the same ceiling, and cancellation is checked between fixed-size reads and again before the replacement is prepared. write, once cancelled, stops before each directory it makes and before its rename, leaving the file as it was; directories it already made may remain.

A link you meant is untouched by any of that. A checkout reached through one works, because the working directory is resolved when crucible starts and the link is never on the way down; so does a project that links to its own files, because resolving followed the link and settled containment about where it led. What the walk refuses is a link that was not there when the path was checked.

Windows opens a file by name, then validates the final path of the resulting handle before content is read. write prepares its file privately and commits by rename relative to a held, validated parent, so an ancestor changed to a directory reparse point cannot redirect the commit. Safe relative creation of a missing directory is unavailable through the Windows boundary used here, so write on Windows requires its parent directory to exist and fails closed instead of using a full-path fallback. Its handle-relative rename has no write-through form: the file is flushed before and after rename, but Windows does not make the same directory-durability promise as Unix.

What that bounds is crucible, on either platform. It is not a boundary on the machine, and two things get past it on Unix as well. A directory crucible is walking through can be moved out of the working directory and the file below it goes with it: the file opened is still the file that was checked, and whoever moved it could already read it. And a second hard name for a file elsewhere is not a link at all, so nothing distinguishes it from the original and no check made on names reaches it.

None of those takes a privilege beyond writing into that directory, which is the point: containment answers for what crucible resolves, and a working directory another local program is rearranging underneath it is outside what a check on paths can promise.

Reach is not permission

An extra directory moves the boundary, not the verdicts. A read there stops being asked about, because the directory was handed over at startup; a write there is still a write: under ask it prompts like any other, and a deny rule reaches it like any other path. Only an absolute pattern, such as deny edit(/home/you/src/shared-lib/**), can name one, because a file there has no spelling below the working directory, and src/** would honestly mean nothing there.