@cairnkeep/cli 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable user-facing changes are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [2.2.1] - 2026-07-20
7
+
8
+ ### Fixed
9
+
10
+ - Make default sandbox-volume names path-specific so same-named repositories
11
+ cannot share a workspace accidentally.
12
+ - Refuse to merge repository content into a non-empty unmarked workspace
13
+ volume.
14
+ - Document that sandbox copies include untracked files and must be treated as
15
+ sensitive persistent data.
16
+
6
17
  ## [2.2.0] - 2026-07-20
7
18
 
8
19
  ### Added
@@ -11,6 +11,10 @@ case "$mode" in
11
11
  exit 66
12
12
  fi
13
13
  if [ ! -e /workspace/.cairnkeep-sandbox-ready ]; then
14
+ if find /workspace -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then
15
+ echo "cairnkeep: refusing to initialize a non-empty unmarked workspace volume" >&2
16
+ exit 65
17
+ fi
14
18
  cp -a /source/. /workspace/
15
19
  : > /workspace/.cairnkeep-sandbox-ready
16
20
  fi
@@ -154,8 +154,14 @@ cairn-container workspace --repo /path/to/repository
154
154
  ```
155
155
 
156
156
  Rerunning the command with the same workspace volume resumes the copy. Use
157
- `--workspace-volume` when repositories have the same basename or when an
158
- explicit lifecycle is preferable.
157
+ `--workspace-volume` when an explicit lifecycle is preferable. Default volume
158
+ names include both the repository basename and a checksum of its absolute path,
159
+ so same-named repositories do not share a sandbox accidentally.
160
+
161
+ The initial copy includes tracked and untracked files. Treat the workspace
162
+ volume as sensitive local data and remove it when it is no longer needed.
163
+ Cairnkeep refuses to initialize a non-empty volume that lacks its sandbox
164
+ marker rather than merging unrelated content.
159
165
 
160
166
  `shared` mode is opt-in and bind-mounts the checkout read/write:
161
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -171,7 +171,8 @@ run_workspace() {
171
171
  repo=$(absolute_dir "$repo")
172
172
  case "$mode" in sandbox | shared) ;; *) fail "--mode must be sandbox or shared" ;; esac
173
173
  if [ -z "$workspace_volume" ]; then
174
- workspace_volume="cairnkeep-workspace-$(safe_name "$(basename "$repo")")"
174
+ repo_id=$(printf '%s' "$repo" | cksum | awk '{print $1}')
175
+ workspace_volume="cairnkeep-workspace-$(safe_name "$(basename "$repo")")-$repo_id"
175
176
  fi
176
177
 
177
178
  require_engine
@@ -80,4 +80,8 @@ has_arg CAIRN_WORKSPACE_MODE=shared
80
80
  lacks_arg "$repo:/source:ro,Z"
81
81
  has_arg bash
82
82
 
83
+ repo_id=$(printf '%s' "$repo" | cksum | awk '{print $1}')
84
+ run_launcher workspace --repo "$repo" --image example/workspace:1
85
+ has_arg "cairnkeep-workspace-repository-$repo_id:/workspace:Z,U"
86
+
83
87
  echo "PASS: container launcher security and workspace contracts"
@@ -21,12 +21,13 @@ server_image="localhost/cairnkeep-test-server:$suffix"
21
21
  workspace_image="localhost/cairnkeep-test-workspace:$suffix"
22
22
  volume="cairnkeep-test-data-$suffix"
23
23
  sandbox_volume="cairnkeep-test-workspace-$suffix"
24
+ conflict_volume="cairnkeep-test-conflict-$suffix"
24
25
  name="cairnkeep-test-$suffix"
25
26
  tmp=$(mktemp -d)
26
27
 
27
28
  cleanup() {
28
29
  "$ENGINE" rm -f "$name" >/dev/null 2>&1 || true
29
- "$ENGINE" volume rm -f "$volume" "$sandbox_volume" >/dev/null 2>&1 || true
30
+ "$ENGINE" volume rm -f "$volume" "$sandbox_volume" "$conflict_volume" >/dev/null 2>&1 || true
30
31
  "$ENGINE" image rm -f "$server_image" "$workspace_image" >/dev/null 2>&1 || true
31
32
  rm -rf "$tmp"
32
33
  }
@@ -180,4 +181,34 @@ else
180
181
  "$workspace_image" sh -c 'test "$(cat sandbox.txt)" = sandbox'
181
182
  fi
182
183
 
184
+ # A pre-populated, unmarked volume must not be merged with repository content.
185
+ "$ENGINE" volume create "$conflict_volume" >/dev/null
186
+ if [[ "$ENGINE" == podman ]]; then
187
+ "$ENGINE" run --rm --userns=keep-id:uid=10001,gid=10001 \
188
+ --volume "$conflict_volume:/workspace:Z,U" \
189
+ --env CAIRN_WORKSPACE_MODE=shared \
190
+ "$workspace_image" sh -c 'printf unrelated > unrelated.txt'
191
+ else
192
+ "$ENGINE" run --rm --user root --entrypoint sh \
193
+ --volume "$conflict_volume:/workspace" "$workspace_image" \
194
+ -c 'printf unrelated > /workspace/unrelated.txt; chown -R 10001:10001 /workspace'
195
+ fi
196
+ if [[ "$ENGINE" == podman ]]; then
197
+ conflict_mounts=(
198
+ --userns=keep-id:uid=10001,gid=10001
199
+ --volume "$tmp/repo:/source:ro,Z"
200
+ --volume "$conflict_volume:/workspace:Z,U"
201
+ )
202
+ else
203
+ conflict_mounts=(
204
+ --volume "$tmp/repo:/source:ro"
205
+ --volume "$conflict_volume:/workspace"
206
+ )
207
+ fi
208
+ if "$ENGINE" run --rm "${conflict_mounts[@]}" \
209
+ --env CAIRN_WORKSPACE_MODE=sandbox "$workspace_image" true >/dev/null 2>&1; then
210
+ echo "FAIL: sandbox merged into a non-empty unmarked volume" >&2
211
+ exit 1
212
+ fi
213
+
183
214
  echo "PASS: container stdio, guarded HTTP, persistence, shared, and sandbox modes"