Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
retries = 1
fail-fast = false
failure-output = "immediate-final"
# Bound each stalled test attempt as on Windows; timeouts still fail the run.
slow-timeout = { period = "60s", terminate-after = 10 }

# Windows runs one curated subset of the suite instead of the whole workspace: tests
# that are not yet ported (Unix-only tools, transports and sandboxing) are left out. One
Expand Down
23 changes: 20 additions & 3 deletions crates/cyber-server/tests/file_observations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,30 @@ async fn exhausted_pinned_feedback_survives_alias_restart_and_requires_owned_use
}

#[cfg(unix)]
fn directory_alias(target: &std::path::Path, alias: &std::path::Path) {
std::os::unix::fs::symlink(target, alias).unwrap();
}
#[cfg(windows)]
fn directory_alias(target: &std::path::Path, alias: &std::path::Path) {
std::os::windows::fs::symlink_dir(target, alias).unwrap();
}
#[cfg(unix)]
fn remove_directory_alias(alias: &std::path::Path) {
std::fs::remove_file(alias).unwrap();
}
#[cfg(windows)]
fn remove_directory_alias(alias: &std::path::Path) {
std::fs::remove_dir(alias).unwrap();
}

#[cfg(any(unix, windows))]
#[tokio::test]
async fn unused_user_acknowledgement_cannot_follow_a_retargeted_alias() {
let h = Harness::new(Setup::default());
let alias = h.dir.path().join("alias");
let other = h.dir.path().join("other");
std::fs::create_dir(&other).unwrap();
std::os::unix::fs::symlink(&h.repo, &alias).unwrap();
directory_alias(&h.repo, &alias);
let session = h
.runtime
.create_session(cyber_server::runtime::CreateSession {
Expand All @@ -266,8 +283,8 @@ async fn unused_user_acknowledgement_cannot_follow_a_retargeted_alias() {
.await
.unwrap();
let original = admission_event(&h, &session.id, "user").await;
std::fs::remove_file(&alias).unwrap();
std::os::unix::fs::symlink(&other, &alias).unwrap();
remove_directory_alias(&alias);
directory_alias(&other, &alias);
let mut current = h
.runtime
.claim_pinned_file_watch(other.clone(), opened_directory(&other), BTreeMap::new())
Expand Down
23 changes: 20 additions & 3 deletions crates/cyber-tools/tests/file_observation_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1974,6 +1974,23 @@ async fn alias_joining_first_shares_the_canonical_owner_and_preserves_raw_sessio
}

#[cfg(unix)]
fn directory_alias(target: &std::path::Path, alias: &std::path::Path) {
std::os::unix::fs::symlink(target, alias).unwrap();
}
#[cfg(windows)]
fn directory_alias(target: &std::path::Path, alias: &std::path::Path) {
std::os::windows::fs::symlink_dir(target, alias).unwrap();
}
#[cfg(unix)]
fn remove_directory_alias(alias: &std::path::Path) {
std::fs::remove_file(alias).unwrap();
}
#[cfg(windows)]
fn remove_directory_alias(alias: &std::path::Path) {
std::fs::remove_dir(alias).unwrap();
}

#[cfg(any(unix, windows))]
#[tokio::test]
async fn symlink_retarget_loses_old_delivery_and_user_input_admits_the_new_physical_scope() {
let flow = support::flow::Flow::new(vec![], false);
Expand All @@ -1983,7 +2000,7 @@ async fn symlink_retarget_loses_old_delivery_and_user_input_admits_the_new_physi
initialize_project(&other, "https://example.test/alias-retarget.git");
let new_project = cyber_core::project::identify(&other).id;
let alias_path = flow.f.dir.path().join("location-alias");
std::os::unix::fs::symlink(&flow.f.repo, &alias_path).unwrap();
directory_alias(&flow.f.repo, &alias_path);
let recorder = Recorder::start().await;
automatic_hooks(&flow, &recorder);
let alias = flow
Expand All @@ -2010,8 +2027,8 @@ async fn symlink_retarget_loses_old_delivery_and_user_input_admits_the_new_physi
.any(|event| event["session_id"] == alias.id && event["file_path"] == "before-retarget")
})
.await;
std::fs::remove_file(&alias_path).unwrap();
std::os::unix::fs::symlink(&other, &alias_path).unwrap();
remove_directory_alias(&alias_path);
directory_alias(&other, &alias_path);
std::fs::write(
flow.f.repo.join("old-root-only"),
"other Session still observes this",
Expand Down
16 changes: 12 additions & 4 deletions docs/goal-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,18 @@ remaining required before activation. The original tested sources remain on
`evidence/plugin-native-host-pre-76-merge`. Follow-up #80 merged at `2745b4f0`
and #83 at `c3bd34c4` on 2026-10-31. These are foundations, diagnostics and the
observer resource correction; production native admission remains incomplete.
Continue the first workstream on one follow-up branch,
`feat/plugin-native-admission-followup`, retaining the same review and acceptance
gates. The raw datagram send-result correction strengthens measurement and does
not authorize Linux support changes or production activation.
PR #84 merged at `dd0c9279` after all 55 configured checks passed on
`62a3068e`, with one intentional historical diagnostic skip. Its corrected
native result witness and deterministic missing-guard regression passed Intel
and arm64; that accepts the verifier correction rather than production native
admission or the historical unknown race cause.

Continue the first workstream on the single follow-up branch
`test/file-observer-windows-alias-acceptance`: native Windows retarget acceptance,
retained source-bound native evidence, and the DirectoryAdded prerequisite design
for review. Keep native activation and Linux dependency changes behind their
existing review/acceptance gates. The Setup marker choices and DirectoryAdded
proposal also require their recorded review before implementation.

Native confinement foundations and regressions exist, but
production enforcement and the remaining plugin/hook scope are incomplete. The
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Additional directory admission and DirectoryAdded

Status: proposal for review; no directory admission or producer implementation.
This expands task 5.1's prerequisite. It does not complete the native plugin,
Setup, daily-coding or binary-editor work, or approve their separate decisions.

## Existing contract and inspected gap

The canonical `permissions-modes` requirement "Additional working directories"
requires repeatable `--add-dir`, `/add-dir` and
`POST /api/v1/sessions/:id/directories`. An addition must cover the Session's
ordinary external-directory checks, sandbox writable roots and instruction
Context Sources, persist `session.directory.added.1`, emit DirectoryAdded and
apply at the next Safe Boundary. Protected paths remain protected.

At source `43158f31`, these components do not share such an admission:

- `CreateSession`, `SessionInfo`, `TurnContext` and `Invocation` have a Location
and rules, but no committed additional-root snapshot or revision.
- `Ctx::check_external_paths` in `cyber-tools/src/host.rs` compares paths with the
Location. File browsing has a separate check in `host/files.rs`. Updating one
path would leave attachments, browsing or ordinary tools inconsistent.
- `sandboxing.rs` composes Location/configuration roots separately; adding a
permission rule alone cannot authorize the OS sandbox. Its isolated-solve
restriction deliberately clears additional configuration write roots.
- `ContextInputs` observes the Location's instructions. Directory instructions
need their own provenance-bearing Context Source integration.
- `runtime/drain.rs::boundary` owns Safe Boundary ordering. The existing
observational lifecycle hook captures Session scope under its lock and runs
handlers after releasing the lock.

The CLI attachment refusal already suggests `--add-dir`, but that suggestion is
not evidence that the flag, owner admission or effective directory exists. A
standalone hook emitter would therefore report a transition that did not happen.

## Proposed owner and durable model

Use one Session-owned directory service for all three public entry points. It
accepts user/owner intent, validates a bounded request ID and path, and refuses
archived Sessions and requests outside the caller's Session authority. A prompt,
model tool or hook decision cannot manufacture this owner operation. CLI creation
inputs must use that same service rather than changing global configuration.

Proposed bounds are 64 effective additional roots and 64 pending additions per
Session, 4,096 UTF-8 bytes per path and 128 bytes per request ID. These are choices
for review before schema implementation, not newly active limits.

Record pending intent separately from an effective addition. Exact request
retries inspect the original operation; reusing an ID with different input is
refused. A request for an already effective root is an idempotent no-op rather
than another grant or hook. Preserve the submitted spelling for display, while
the effective grant binds the resolved root and its native identity. This is
separate from Setup's proposed exact-stored-directory marker key.

Projectors remain deterministic and perform no filesystem IO. Prepare and pin
the directory outside writer/Session locks, then validate Session/configuration
revision and ownership at admission. Retargeting or replacing a root cannot
silently transfer an old grant to another physical directory. After restart,
rebuild native proof before releasing a new tool dispatch; a durable path string
alone is not that proof. Record unavailable scope instead of treating uncertainty
as an acknowledged effective addition.

## Safe Boundary publication

Prepare one immutable Session scope revision covering ordinary file decisions,
native sandbox roots and instruction-source inputs. Current Turns and tool
batches retain their captured revision. A pending addition cannot widen an
already running call or change another Session's scope.

At the next Safe Boundary, revalidate the prepared root and current Session,
commit the effective `session.directory.added.1` transition, and synchronously
publish the prepared revision before permitting another tool dispatch. Publication
must have no fallible preparation or asynchronous gap after the commit. If this
cannot fit the current owner API, revise the transaction design before coding;
do not publish independent mutable permission/context/sandbox lists.

Capture DirectoryAdded from the committed event identity and resulting Session
scope. Dispatch after releasing Session/writer locks, using existing durable
handler receipts and observational outcome rules. A failed or merely pending
request emits no successful addition. Duplicate delivery and unknown mutating
handler outcomes do not launch the same effect again after restart.

For an idle Session, an owner boundary may settle the addition without starting
a model Turn. If instruction preparation or native root validation fails, report
the pending/refused result explicitly; an API acknowledgement must distinguish
queued intent from an effective addition.

## Permission and context invariants

- The grant satisfies the ordinary external-directory scope check only. Read,
edit, Mode, managed denial, trust, taint, hidden-path and protected-owner checks
retain their authority. Plan Mode is not made writable by adding a root.
- Browsing, attachments, in-process editors and sandboxed shell/background tools
consume the same captured effective revision. Every target resolves against
actual authorized roots; a child symlink cannot borrow its parent's grant to
escape into an unrelated tree.
- Preserve the isolated-solve boundary and parent/child permission ceilings.
Specify and review any child/fork propagation explicitly; copying a path list
or another Session's event is not admission authority.
- Load additional directory instructions through bounded, permission-checked
Context Sources, with actual file provenance and existing InstructionsLoaded
semantics. DirectoryAdded does not replace instruction loading or move the
Session's Location/cwd/project identity.
- This operation does not widen plugin manifest capabilities, publish arbitrary
native observer roots or mutate Location/global configuration.

## Implementation and acceptance tasks

- [ ] D1 Review this prerequisite, bounds, identity/deduplication, idle-boundary
and child/fork semantics; add the permissions-modes/CLI/API deltas before code.
- [ ] D2 Add bounded pending/effective events and projections, request binding
and writer-atomic revision checks. Test duplicate/conflicting/concurrent
requests, archived Sessions, replay and transaction rollback with a real Store.
- [ ] D3 Integrate pinned native root preparation and one Safe Boundary snapshot
into all ordinary file, attachment, sandbox and context paths. Test grants
during held tool calls, root replacement/retarget, protected aliases, restart,
failed instruction loading and preservation of Mode/managed/child ceilings.
- [ ] D4 Add CLI, slash/TUI and owner HTTP inputs plus generated schema/SDK
artifacts. Exercise repeatable flags and actual owner requests, with no model
Turn or manufactured user presence for idle publication.
- [ ] D5 Emit committed DirectoryAdded with stable source identity; verify scope,
order, idempotency and unknown-handler no-replay through actual HTTP/command
handlers. A control omitting effective publication must fail a named behavior
assertion rather than succeeding because a hook was emitted.
- [ ] D6 Run native Linux/macOS/Windows checks and substantive security review;
reconcile public docs and task 5.1 only after complete acceptance.

The approved goal is unchanged. This proposal exposes a required prerequisite;
it neither substitutes a test-only hook for the feature nor closes a parent task.
Loading
Loading