@cairnkeep/cli 2.24.0 → 2.26.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/README.md +6 -0
  3. package/docs/agents.md +18 -0
  4. package/docs/design/team-mode.md +17 -9
  5. package/docs/harness-compatibility.md +20 -0
  6. package/docs/learning/CURRICULUM-MAP.md +4 -2
  7. package/docs/learning/FEATURE-GUIDE.md +35 -3
  8. package/docs/learning/README.md +5 -3
  9. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  10. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  11. package/docs/learning/lessons/L02-installation.md +2 -2
  12. package/docs/learning/lessons/L03-first-project.md +1 -1
  13. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  14. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  15. package/docs/learning/lessons/L21-context-packs.md +1 -1
  16. package/docs/learning/lessons/L22-native-windows.md +2 -2
  17. package/docs/learning/lessons/L23-guided-setup.md +1 -1
  18. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  19. package/docs/learning/lessons/L25-playbooks.md +1 -1
  20. package/docs/learning/lessons/L26-context-intelligence.md +1 -1
  21. package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
  22. package/docs/learning/lessons/L28-security-assurance.md +6 -2
  23. package/docs/learning/lessons/L29-team-continuity.md +127 -0
  24. package/docs/learning/tracks/operator.md +4 -0
  25. package/docs/learning/video-scripts/V28-security-assurance.md +4 -0
  26. package/docs/learning/video-scripts/V29-team-continuity.md +84 -0
  27. package/docs/operating.md +23 -3
  28. package/docs/privacy-and-data-flow.md +45 -6
  29. package/docs/provider-data-flow-review.md +48 -0
  30. package/docs/security-assurance.md +39 -0
  31. package/docs/storage.md +32 -0
  32. package/docs/team-onboarding.md +226 -0
  33. package/docs/team-pilot.md +134 -0
  34. package/docs/team.md +245 -0
  35. package/examples/eval/bundled-fake.json +1 -1
  36. package/mcp-memory-server/dist/capability-config.js +12 -1
  37. package/mcp-memory-server/dist/team-cli.js +228 -0
  38. package/mcp-memory-server/dist/team-client.js +71 -0
  39. package/mcp-memory-server/dist/team-http.js +193 -0
  40. package/mcp-memory-server/dist/team-mcp.js +89 -0
  41. package/mcp-memory-server/dist/team-store.js +669 -0
  42. package/mcp-memory-server/package.json +2 -2
  43. package/package.json +1 -1
  44. package/scripts/cairn-cli.mjs +7 -0
  45. package/scripts/completion.sh +31 -3
  46. package/scripts/lib/team-retention.mjs +29 -0
  47. package/scripts/team-connect.mjs +366 -0
  48. package/scripts/test-ci-sharding.sh +4 -0
  49. package/scripts/test-codeql-cohort.mjs +35 -0
  50. package/scripts/test-completion.sh +11 -0
  51. package/scripts/test-package-install.sh +10 -0
  52. package/scripts/test-phase18-capability-lifecycle.sh +2 -2
  53. package/scripts/test-security-baseline.sh +1 -0
  54. package/scripts/test-team-docs.sh +61 -0
  55. package/scripts/test-team-installed.mjs +51 -0
  56. package/scripts/test-team-pilot.mjs +101 -0
  57. package/scripts/uninstall.sh +13 -0
  58. package/scripts/verify-security-baseline.mjs +20 -0
  59. package/scripts/verify-team-retention.mjs +3 -0
  60. package/scripts/windows-platform.mjs +6 -2
@@ -0,0 +1,134 @@
1
+ # An isolated team-continuity pilot
2
+
3
+ This guide is for the opt-in Cairnkeep 2.25.0 team preview. It is **not production admission**.
4
+ Obtain explicit pilot approval first. Keep automated rehearsal, human review,
5
+ native-harness observations and release authorization as separate evidence.
6
+ Use the exact matching source checkout for the source controls below, not an
7
+ unverified global command or a production overlay. The released CLI includes
8
+ the optional team commands, but installation does not authorize this pilot.
9
+
10
+ The pilot runs beside existing personal memory: a new private store, expiring
11
+ credentials, a separate loopback port and additional isolated client profiles.
12
+ It never imports a personal store or replaces an existing MCP registration.
13
+ Use synthetic project decisions, not employer, personal or customer data.
14
+ For a shorter role-by-role route and reader connection card, see
15
+ [Team onboarding](team-onboarding.md). It does not grant deployment consent.
16
+
17
+ ## 1. Prepare a private lab
18
+
19
+ Build the matching checkout. The following setup uses Bash on Linux/macOS; on
20
+ native Windows use the equivalent explicit CLI commands from [Team continuity](team.md).
21
+ Choose an absolute, **nonexistent** lab directory outside source checkouts,
22
+ personal memory, backup stores and other private stores. Do not reuse an old lab.
23
+
24
+ ```sh
25
+ TEAM_CAIRN=/absolute/path/to/feature-checkout/bin/cairn
26
+ TEAM_PILOT_DIR=/absolute/private/new-team-pilot
27
+ ```
28
+
29
+ Run this setup block in that shell. The operator chooses these two paths; the
30
+ block refuses an existing lab directory. Credential values are never printed.
31
+
32
+ <!-- team-pilot-setup:start -->
33
+ ```sh
34
+ set -e
35
+ : "${TEAM_CAIRN:?Set the absolute feature-checkout command}"
36
+ : "${TEAM_PILOT_DIR:?Set a new absolute private lab directory}"
37
+ case "$TEAM_CAIRN:$TEAM_PILOT_DIR" in /*:/*) ;; *) exit 1 ;; esac
38
+ mkdir -m 0700 "$TEAM_PILOT_DIR"
39
+ mkdir -m 0700 "$TEAM_PILOT_DIR/author-workspace" "$TEAM_PILOT_DIR/reader-workspace"
40
+ TEAM_PILOT_EXPIRY=$(node -p 'new Date(Date.now() + 2 * 3600000).toISOString()')
41
+ team_pilot_admin() {
42
+ (cd "$TEAM_PILOT_DIR" && env -i PATH="$PATH" CAIRN_TEAM=1 \
43
+ "$TEAM_CAIRN" team "$@" --data "$TEAM_PILOT_DIR/store")
44
+ }
45
+ team_pilot_admin init --organization trial-org
46
+ team_pilot_admin project create trial-project
47
+ team_pilot_admin project create unrelated-project
48
+ team_pilot_admin member set author --project trial-project --roles reader,contributor
49
+ team_pilot_admin member set reader --project trial-project --roles reader
50
+ team_pilot_admin member set reviewer --project trial-project --roles reader,reviewer,auditor
51
+ team_pilot_admin credential issue author --class workload --expires-at "$TEAM_PILOT_EXPIRY" --output "$TEAM_PILOT_DIR/author.token"
52
+ team_pilot_admin credential issue reader --class workload --expires-at "$TEAM_PILOT_EXPIRY" --output "$TEAM_PILOT_DIR/reader.token"
53
+ team_pilot_admin credential issue reviewer --class human --expires-at "$TEAM_PILOT_EXPIRY" --output "$TEAM_PILOT_DIR/reviewer.token"
54
+ team_pilot_admin doctor
55
+ ```
56
+ <!-- team-pilot-setup:end -->
57
+
58
+ If setup fails, stop and inspect only that generated lab; do not rerun against a
59
+ partially initialized directory. Keep the reviewer token outside both agent
60
+ workspaces. File-only credentials are private, but processes under the same OS
61
+ owner are not mutually isolated: use separate accounts or machines if that is
62
+ part of the pilot's threat model. Do not claim role tests prove OS isolation.
63
+
64
+ ## 2. Start only the disposable service
65
+
66
+ In a dedicated terminal, choose a separate unused port and run the exact feature
67
+ command from the lab directory. Do not kill another listener to claim its port.
68
+
69
+ ```sh
70
+ cd "$TEAM_PILOT_DIR"
71
+ env -i PATH="$PATH" CAIRN_TEAM=1 CAIRN_TEAM_HTTP=1 \
72
+ "$TEAM_CAIRN" team serve --data "$TEAM_PILOT_DIR/store" --port 17955
73
+ ```
74
+
75
+ Keep it in the foreground and use Ctrl+C for shutdown. It binds loopback only.
76
+ A second machine needs an authenticated SSH tunnel, not a public API listener.
77
+ Keep the production service, personal stores and existing MCP definitions intact.
78
+
79
+ ## 3. Use two additional client profiles
80
+
81
+ Configure only a new `cairn-team-memory` stdio server in disposable profiles for
82
+ the chosen harnesses. Do not edit their user-wide settings, trust lists or tokens.
83
+ Use the feature command with arguments `team mcp --project-root WORKSPACE` and
84
+ an explicit private workspace path for each client. Both clients receive:
85
+
86
+ - `CAIRN_TEAM=1`, `CAIRN_TEAM_ORGANIZATION=trial-org`;
87
+ - `CAIRN_TEAM_URL=http://127.0.0.1:17955` (or the explicit loopback tunnel);
88
+ - `CAIRN_TEAM_TOKEN_FILE` pointing to **only** their own workload credential.
89
+
90
+ Each tool call must explicitly name project `trial-project`; the working folder
91
+ and client name do not select or grant project access.
92
+
93
+ The author uses `author.token`; the other client uses `reader.token` and
94
+ `CAIRN_TEAM_MCP_TOOL_PROFILE=read-only`. Never give either model `reviewer.token`,
95
+ an administrative command, production credentials or an inherited cloud-provider
96
+ key for this lab. Use a provider explicitly authorized for the pilot. Starting
97
+ a server or downloading a model is not implicit subscription authorization.
98
+
99
+ ## 4. Exercise the actual review boundary
100
+
101
+ Have the author propose one exact synthetic project decision, with explicit
102
+ mutation consent. Record the immutable proposal ID, digest, selected bytes and
103
+ provenance privately. The reader must not see it before publication.
104
+
105
+ In the operator-only terminal, use `proposal-show` with the human reviewer
106
+ credential to inspect the **actual stored proposal**, not an agent's paraphrase.
107
+ Only after the reviewer explicitly approves those bytes, call `review` with
108
+ the exact proposal digest as described in [the reference](team.md#harness-connection).
109
+ Do not put a human token into an agent profile or script automatic human review.
110
+ Checking a hash does not establish human consent or verify a person's identity.
111
+
112
+ Restart the reader harness in a different empty working folder, retaining the
113
+ explicit authorized team project. Verify it reads identical approved text and
114
+ provenance. Capture actual tool-use evidence, not a model's claim of recall.
115
+ Exercise an unrelated-project denial, a read-only proposal denial, membership
116
+ removal on a connected client, and restore into a fresh store with old tokens
117
+ refused. Do not change production roles or activate the restored lab implicitly.
118
+
119
+ ## 5. Record, stop and decide
120
+
121
+ Keep a private, payload-free summary of source revision, client versions,
122
+ projects/roles, consent method and outcomes. Label unattended SDK controls as
123
+ automated rehearsal; do not turn their pass result into a human-pilot claim.
124
+ Record missing steps and failures, including model/tool-use failures.
125
+
126
+ Stop the foreground lab server. Verify its listener and clients have exited.
127
+ Revoke lab credentials if retaining the store; retain or remove only the exact
128
+ generated lab after preserving agreed evidence. Never purge a real memory base.
129
+ Confirm production configuration hashes are unchanged. No team setting is
130
+ enabled globally, so stopping the lab requires no production rollback.
131
+
132
+ Independent security review, platform checks, pilot approval/results and release
133
+ authorization remain distinct gates. Completing this guide does not itself
134
+ authorize a merge, release, production enrollment or fleet deployment.
package/docs/team.md ADDED
@@ -0,0 +1,245 @@
1
+ # Team continuity preview
2
+
3
+ Status: opt-in experimental preview included in **Cairnkeep 2.25.0**;
4
+ **not general production admission**. Existing local memory and trusted
5
+ shared-token HTTP are unchanged. Installing or upgrading does not enable team
6
+ sharing, enroll users or start a team service.
7
+
8
+ The governed internal security review and a separately approved isolated
9
+ native-harness pilot are complete. That pilot used selected synthetic context,
10
+ one actual human reviewer and two workload identities; it was not a study with
11
+ two independent human participants or an external professional assessment.
12
+ Independent production review and deployment-specific acceptance remain
13
+ necessary before using this preview with real organization data.
14
+
15
+ Team memory is a separate store of explicitly selected, reviewed project
16
+ context. It never scans or uploads personal stores, prompts, transcripts or
17
+ repositories. The same project IDs work across harnesses and working folders.
18
+ Sharing changes visibility: inspect and redact the selected text first.
19
+
20
+ For a trial alongside unchanged personal memory, use the
21
+ [isolated pilot guide](team-pilot.md). It keeps stores, credentials, ports and
22
+ client profiles separate and distinguishes actual human review from rehearsal.
23
+ Start with the [role-based handoff](team-onboarding.md) for operator, coding
24
+ client and human-reviewer tasks, a tested reader check and readiness checklist.
25
+
26
+ ## Trust boundary
27
+
28
+ The initial service has one organization, server-owned subjects, explicit
29
+ project memberships and expiring/revocable human or workload credentials.
30
+ Caller-supplied actor labels do not grant authority. Each operation rechecks
31
+ credentials and membership inside its transaction.
32
+
33
+ Roles are independent: reader retrieves approved memory; contributor submits
34
+ and inspects their proposals; reviewer inspects and approves/rejects proposals;
35
+ auditor reads scoped audit metadata. Maintainer does **not** imply review or
36
+ read authority. Membership administration currently trusts the local OS owner,
37
+ not a remote maintainer token. Workload credentials cannot review, even when
38
+ their subject has a reviewer role. Self-review is always refused.
39
+
40
+ Proposals retain exact selected bytes, source digest, base revision, expiry and
41
+ policy digest. Review confirms the proposal digest; stale policy/base produces a
42
+ conflict, never automatic merging. Publication, head update and audit append are
43
+ atomic. Audit stores identities, digests and outcomes—not memory text or tokens.
44
+ Hash links detect corruption; they do not prevent a trusted storage owner from
45
+ rewriting the database. Digests establish integrity, not publisher authenticity.
46
+
47
+ ## Enable explicitly
48
+
49
+ Use Node 22.13 or newer for the optional built-in SQLite store. Node 24/26 are
50
+ also supported. The core's Node engine floor remains 22; the optional team
51
+ store needs the later built-in SQLite API. Use current maintained patch releases.
52
+ No new runtime dependency or personal-store migration is needed.
53
+
54
+ ```sh
55
+ export CAIRN_TEAM=1
56
+ cairn team init --organization demo-org --data /absolute/private/team
57
+ cairn team project create alpha --data /absolute/private/team
58
+ cairn team member set alice --project alpha --roles reader,contributor --data /absolute/private/team
59
+ cairn team member set bob --project alpha --roles reader,reviewer --data /absolute/private/team
60
+ cairn team credential issue alice --class workload --expires-at 2026-12-01T00:00:00Z --output /absolute/private/alice.token --data /absolute/private/team
61
+ ```
62
+
63
+ Choose a future expiry appropriate to the deployment (maximum one year), not
64
+ the example date blindly. Credential output is exclusive and private; the raw
65
+ token is never printed. Keep tokens outside source/harness directories.
66
+ Organization state defaults to `~/.cairnkeep/team`, overridden by
67
+ `CAIRN_TEAM_BASE_DIR` or `--data`. This path convention is not required.
68
+
69
+ ```sh
70
+ export CAIRN_TEAM_HTTP=1
71
+ cairn team serve --data /absolute/private/team --port 7955
72
+ ```
73
+
74
+ The server binds loopback only. Access remotely through an encrypted,
75
+ authenticated tunnel; direct unauthenticated/public exposure is unsupported.
76
+ The API refuses browser origins, unexpected Host headers, oversized/invalid
77
+ requests and revoked credentials. It does not support CORS browser applications.
78
+ This is a token-first JSON API—not an OAuth-authorized remote MCP endpoint.
79
+ OIDC/SSO and standards-compliant remote MCP HTTP authorization are deferred.
80
+
81
+ ## Harness connection
82
+
83
+ Cairnkeep 2.26.0 (both client and team API) adds
84
+ the opt-in [guided connection and recovery workflow](team-onboarding.md#guided-connection-next-release--matching-source-checkout):
85
+ `cairn team connect`, `cairn team doctor --client` and `cairn team disconnect`.
86
+ It adds only a dedicated project-local entry, defaults to read-only, references
87
+ a private token file and pins the server project. No global MCP replacement or
88
+ personal upload occurs. The manual environment recipe below remains supported
89
+ on 2.25.0. The diagnostics API returns only authenticated project/credential
90
+ metadata and records a value-free audit event, not memory text.
91
+
92
+ Set `CAIRN_TEAM_URL`, `CAIRN_TEAM_ORGANIZATION` and `CAIRN_TEAM_TOKEN_FILE` in the
93
+ local harness environment, alongside `CAIRN_TEAM=1`. Tokens are file-only.
94
+ The URL may be loopback HTTP through a tunnel or HTTPS, without embedded
95
+ credentials, query, fragment or path prefix. Redirects are never followed.
96
+
97
+ Register **an additional** local stdio MCP named `cairn-team-memory`, command
98
+ `cairn`, arguments `team mcp --project-root /absolute/project`. Do not replace
99
+ the existing `cairn-memory` server or unrelated MCP entries. Every team call
100
+ names the explicit server project ID; cwd and display labels never select
101
+ authority. Existing personal memory remains independently available.
102
+
103
+ Tools: `team_memory_list`, `team_memory_read`, `team_memory_search`,
104
+ `team_memory_history` and `team_memory_propose`. No review/admin MCP exists.
105
+ Retrieval is deterministic substring matching, with no model/provider calls.
106
+ Personal-memory embeddings and reranking settings do not apply to team
107
+ retrieval; enabling them cannot send shared team text to a provider.
108
+ The remote boundary is marked open-world in MCP annotations; four retrieval
109
+ tools are observations, while proposal creation is an additive mutation and
110
+ requires the harness's explicit mutation consent. Approved memory is context,
111
+ not executable instruction or authority over maintained source.
112
+
113
+ The ordinary `CAIRN_MCP_TOOL_PROFILE`/project profile applies through equivalent
114
+ core names (`memory_list/read/search/history/write`). Optional
115
+ `CAIRN_TEAM_MCP_TOOL_PROFILE=full|read-only|custom` and
116
+ `CAIRN_TEAM_MCP_ALLOWED_TOOLS` further restrict the team catalog. Custom names
117
+ must be exact team tool names. `memory.write` and `memory.search` capability
118
+ settings restrict proposal and search when `CAIRN_CAPABILITY_CONTRACT` is on.
119
+ These intersections cannot grant server authority. Team results include a
120
+ separate effective profile digest; existing core/capability digests are unchanged.
121
+
122
+ Human CLI review uses a separately issued human credential in
123
+ `CAIRN_TEAM_TOKEN_FILE`:
124
+
125
+ ```sh
126
+ cairn team proposals --project alpha
127
+ cairn team proposal-show --project alpha --proposal PROPOSAL_ID
128
+ cairn team review --project alpha --proposal PROPOSAL_ID --decision approve --confirm PROPOSAL_DIGEST
129
+ cairn team search --project alpha --query gateway
130
+ cairn team read --project alpha --key decisions/gateway
131
+ ```
132
+
133
+ Propose with a private JSON input file containing request UUID, key, selected
134
+ value, source_scope (`selected-local` or `selected-project`), source_digest,
135
+ base_revision (null for a new key) and expires_at. Confirm the selected value's
136
+ UTF-8 SHA-256 with `cairn team propose --project alpha --input FILE --confirm DIGEST`.
137
+ Neither CLI nor MCP derives a candidate by reading personal memory for you.
138
+ Inspect the proposal's exact content and provenance before confirming review.
139
+
140
+ Failure is explicit; the bridge never falls back to personal/global storage or
141
+ a different identity. Local memory commands still work independently offline.
142
+
143
+ ## Recovery, retention and credential rotation
144
+
145
+ Local administration requires access as the trusted storage OS owner. It is
146
+ not exposed over HTTP or MCP. Keep the data directory private (`0700` on POSIX,
147
+ owner-only ACL on Windows); database, WAL, credential and snapshot files are
148
+ private too. Do not put a live store inside a source checkout or another store.
149
+
150
+ ```sh
151
+ cairn team doctor --data /absolute/private/team
152
+ cairn team backup --data /absolute/private/team --output /absolute/private/team-snapshot.json
153
+ cairn team restore --input /absolute/private/team-snapshot.json --confirm SNAPSHOT_DIGEST --data /absolute/private/restored-team
154
+ cairn team doctor --data /absolute/private/restored-team
155
+ ```
156
+
157
+ Backup is a consistent transaction including records and value-free audit.
158
+ The output must be a new file; it contains shared text, proposals, subject IDs,
159
+ memberships and token hashes. Encrypt and restrict retained copies separately:
160
+ the store and JSON snapshot are not encrypted by Cairnkeep. Restore checks
161
+ schema, object digests, references and audit links before publishing a new
162
+ directory. It never overwrites an existing store. Concurrent cooperating
163
+ restores are exclusive; a failure never activates partial restored state.
164
+ Every restored credential is revoked. Issue fresh credentials and explicitly
165
+ select the restored store only after doctor passes. No automatic service switch
166
+ or identity approval occurs. Snapshot digests prove integrity, not authenticity.
167
+
168
+ Doctor reports temporary-remnant counts without deleting or selecting them.
169
+ If an interrupted restore leaves a lock or temporary directory, stop competing
170
+ administration, verify process ownership and preserve a copy before manually
171
+ removing the exact abandoned path. Doctor never chooses which version to use.
172
+
173
+ Rotate a credential by issuing a fresh private token file, distributing it
174
+ through a separate secure channel, and revoking the old credential ID with
175
+ `cairn team credential revoke ID --data DIR`. Replacing the configured token
176
+ file makes the bridge use it on the next request; revocation and membership
177
+ changes are also enforced on already-connected clients. There are no cached
178
+ authorization grants. Protect human reviewer tokens from agent environments.
179
+
180
+ For a compromised workload token, revoke it first and remove project roles if
181
+ needed. Remove all of a subject's access to one project explicitly:
182
+
183
+ ```sh
184
+ cairn team member remove alice --project alpha --data /absolute/private/team
185
+ ```
186
+
187
+ This clears that project's roles immediately, including for connected clients.
188
+ Other project memberships and credentials remain unchanged; revoke credentials
189
+ separately for complete offboarding. Existing revisions and audit history are
190
+ retained. To re-enroll, use `member set` with an explicit nonempty `--roles` list.
191
+ Missing or empty role lists are rejected rather than silently removing access.
192
+
193
+ Inspect `cairn team audit --project ID` using an auditor credential;
194
+ accepted, denied and conflicted authenticated operations record bounded
195
+ metadata. Invalid credentials do not create unauthenticated identity records.
196
+ Audit integrity is not external tamper resistance against the OS owner.
197
+
198
+ Current retention is manual: approved revisions, pending/rejected proposals
199
+ and audit events do not expire out of storage automatically. Proposal expiry
200
+ prevents review, not retention. An operator can delete a selected shared key,
201
+ its revisions and associated proposals/reviews using its current head digest:
202
+
203
+ ```sh
204
+ cairn team memory-delete --project alpha --key decisions/gateway --confirm REVISION_DIGEST --data /absolute/private/team
205
+ ```
206
+
207
+ This is logical deletion, not certified erasure: audit metadata, SQLite free
208
+ pages/WAL and older snapshots may retain traces or bytes. Apply a separately
209
+ reviewed storage/backup disposal policy if erasure is required. Ordinary
210
+ uninstall retains team data. Purge operations overlapping the configured team
211
+ store are refused before any uninstall changes; a store selected only through
212
+ `--data` must remain outside purge targets or also be set as
213
+ `CAIRN_TEAM_BASE_DIR` for that retention check.
214
+
215
+ ## Bounded preview and admission
216
+
217
+ Selected values are at most 64 KiB UTF-8; requests 512 KiB; responses 2 MiB;
218
+ snapshots 64 MiB. Records are capped at 32,768 and 16 MiB of stored JSON;
219
+ audit at 100,000 events and 16 MiB, with a 1,000-event/256 KiB OS-operator reserve.
220
+ Limits fail closed rather than discarding records. Back up and review retention
221
+ before approaching them; the preview has no audit archival/compaction command.
222
+ Retrieval is bounded (100 list/history items, ten search results); the API
223
+ currently has no pagination. HTTP separately limits malformed authentication
224
+ per connection address and authenticated requests per subject to 120 per minute;
225
+ invalid token-shaped credentials receive ten attempts per token digest. Each
226
+ budget holds at most 512 identities, and invalid authentication cannot consume
227
+ an authenticated subject's quota. Headers are capped at 32 KiB, inactive
228
+ requests at ten seconds, and bridge fetches at fifteen seconds.
229
+
230
+ Digest confirmation proves selected bytes match; it cannot prove a human
231
+ clicked approval. MCP hosts must obtain mutation consent. Only separate human
232
+ CLI credentials can review, and the OS owner remains trusted to assign them.
233
+ No automatic setup, existing MCP replacement or fleet deployment is performed.
234
+
235
+ Release checks cover Node 22/24/26, native Windows, real Bash 3.2,
236
+ installed-package and container behavior. The internal governed review and
237
+ approved isolated native pilot covered exact human proposal approval,
238
+ cross-harness recall, offboarding, restore and project isolation. Automated
239
+ fixtures are regression evidence, not proof of human consent. These results do
240
+ not admit arbitrary production deployments: establish an independent security
241
+ review appropriate to the deployment, named operational ownership, a separately
242
+ approved limited real-data pilot with rollback, and explicit operator acceptance
243
+ before enabling a real organization. Keep reviewer credentials out of agents.
244
+ OIDC/SSO, multi-organization tenancy, team packs, remote maintainer administration,
245
+ provider retrieval and externally anchored audit are deferred.
@@ -1 +1 @@
1
- {"identifier":"cairn-offline-fake-v1","package_version":"2.24.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
1
+ {"identifier":"cairn-offline-fake-v1","package_version":"2.26.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
@@ -222,7 +222,18 @@ async function acquireLock(lockPath) {
222
222
  catch (error) {
223
223
  if (error.code !== "EEXIST")
224
224
  throw error;
225
- const info = await lstat(lockPath);
225
+ let info;
226
+ try {
227
+ info = await lstat(lockPath);
228
+ }
229
+ catch (inspectionError) {
230
+ // The current writer may release its lock after our mkdir
231
+ // reports contention but before inspection. Only disappearance
232
+ // admits another bounded acquisition attempt, never ownership.
233
+ if (inspectionError.code === 'ENOENT')
234
+ continue;
235
+ throw inspectionError;
236
+ }
226
237
  if (info.isSymbolicLink() || !info.isDirectory()) {
227
238
  throw new Error("Capability configuration lock is unsafe.");
228
239
  }
@@ -0,0 +1,228 @@
1
+ import { closeSync, existsSync, openSync, writeFileSync, fsyncSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { readStableJson } from './stable-file.js';
6
+ import { hardenPrivatePath } from './platform-security.js';
7
+ import { openTeamStore, restoreTeamSnapshot, verifyTeamSnapshot, prepareTeamPrivateDirectory, TEAM_SNAPSHOT_MAX_BYTES, TeamError } from './team-store.js';
8
+ import { createTeamHttpServer, teamRequest } from './team-http.js';
9
+ import { createTeamMcpServer, teamClientToken } from './team-mcp.js';
10
+ import { readTeamConnection, connectionToken } from './team-client.js';
11
+ const HELP = `cairn team — experimental token-first team continuity (default off)
12
+
13
+ CAIRN_TEAM=1 is required. Personal memory is never uploaded.
14
+
15
+ cairn team init --organization ID [--data DIR]
16
+ cairn team project create ID [--data DIR]
17
+ cairn team member set SUBJECT --project ID --roles LIST [--data DIR]
18
+ cairn team member remove SUBJECT --project ID [--data DIR]
19
+ cairn team credential issue SUBJECT --class human|workload --expires-at ISO --output FILE [--data DIR]
20
+ cairn team credential revoke ID [--data DIR]
21
+ cairn team serve [--data DIR] [--port PORT] # requires CAIRN_TEAM_HTTP=1; loopback only
22
+ cairn team connect [--project-root PATH] [--url URL --organization ID --project ID --token-file FILE --harness LIST] [--access read-only|full] [--apply --yes]
23
+ cairn team disconnect [--project-root PATH] [--apply --yes]
24
+ cairn team doctor --client [--project-root PATH]
25
+ cairn team mcp [--project-root PATH | --connection FILE] # local stdio MCP; no review/admin tools
26
+ cairn team list --project ID
27
+ cairn team proposals --project ID
28
+ cairn team audit --project ID # scoped, value-free metadata
29
+ cairn team read --project ID --key KEY [--revision DIGEST]
30
+ cairn team history --project ID --key KEY
31
+ cairn team search --project ID --query TEXT
32
+ cairn team proposal-show --project ID --proposal ID
33
+ cairn team propose --project ID --input FILE --confirm VALUE_DIGEST
34
+ cairn team review --project ID --proposal ID --decision approve|reject --confirm PROPOSAL_DIGEST
35
+ cairn team doctor [--data DIR]
36
+ cairn team backup --output FILE [--data DIR]
37
+ cairn team restore --input FILE --confirm SNAPSHOT_DIGEST --data NEW_DIR
38
+ cairn team memory-delete --project ID --key KEY --confirm REVISION_DIGEST [--data DIR]
39
+
40
+ Clients require CAIRN_TEAM_URL, CAIRN_TEAM_ORGANIZATION and CAIRN_TEAM_TOKEN_FILE.
41
+ Never put tokens in command arguments, source trees or harness config.
42
+ Team API is not OAuth MCP HTTP. Independent production security review and a
43
+ deployment-specific approved real-data pilot remain admission gates. See docs/team.md.
44
+ `;
45
+ function parse(args) {
46
+ const words = [];
47
+ const flags = {};
48
+ const known = new Set(['data', 'organization', 'project', 'roles', 'class', 'expires-at', 'output', 'port', 'project-root', 'key', 'revision', 'query', 'proposal', 'input', 'confirm', 'decision', 'connection']);
49
+ for (let i = 0; i < args.length; i++) {
50
+ const arg = args[i];
51
+ if (arg.startsWith('--')) {
52
+ const name = arg.slice(2);
53
+ if (!known.has(name) || flags[name] !== undefined || !args[i + 1] || args[i + 1].startsWith('--'))
54
+ throw new TeamError('invalid');
55
+ flags[name] = args[++i];
56
+ }
57
+ else
58
+ words.push(arg);
59
+ }
60
+ return { words, flags };
61
+ }
62
+ export function writePrivateExclusive(path, bytes) {
63
+ path = resolve(path);
64
+ const directory = dirname(path);
65
+ prepareTeamPrivateDirectory(directory, true);
66
+ const fd = openSync(path, 'wx', 0o600);
67
+ try {
68
+ hardenPrivatePath(path);
69
+ writeFileSync(fd, bytes, 'utf8');
70
+ fsyncSync(fd);
71
+ }
72
+ finally {
73
+ closeSync(fd);
74
+ }
75
+ }
76
+ async function main(args) {
77
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
78
+ console.log(HELP);
79
+ return;
80
+ }
81
+ if (process.env.CAIRN_TEAM !== '1')
82
+ throw new TeamError('disabled');
83
+ const { words, flags } = parse(args);
84
+ const [command, subcommand, subject] = words;
85
+ const allowed = {
86
+ init: ['data', 'organization'], project: ['data'], member: subcommand === 'remove' ? ['data', 'project'] : ['data', 'project', 'roles'],
87
+ credential: subcommand === 'issue' ? ['data', 'class', 'expires-at', 'output'] : ['data'],
88
+ serve: ['data', 'port'], mcp: ['project-root', 'connection'], doctor: ['data'], backup: ['data', 'output'],
89
+ restore: ['data', 'input', 'confirm'], 'memory-delete': ['data', 'project', 'key', 'confirm'],
90
+ list: ['project'], proposals: ['project'], audit: ['project'], read: ['project', 'key', 'revision'],
91
+ history: ['project', 'key'], search: ['project', 'query'], 'proposal-show': ['project', 'proposal'],
92
+ propose: ['project', 'input', 'confirm'], review: ['project', 'proposal', 'decision', 'confirm'],
93
+ };
94
+ if (!allowed[command] || Object.keys(flags).some(flag => !allowed[command].includes(flag)))
95
+ throw new TeamError('invalid');
96
+ const memberRoles = (flags.roles ?? '').split(',').filter(Boolean);
97
+ if (command === 'member' && (!['set', 'remove'].includes(subcommand) || !subject || words.length !== 3 || !flags.project || (subcommand === 'set' && memberRoles.length === 0)))
98
+ throw new TeamError('invalid');
99
+ const root = resolve(flags.data ?? process.env.CAIRN_TEAM_BASE_DIR ?? join(homedir(), '.cairnkeep', 'team'));
100
+ const print = (result) => console.log(JSON.stringify(result, null, 2));
101
+ let store;
102
+ try {
103
+ if (command === 'mcp') {
104
+ if (words.length !== 1)
105
+ throw new TeamError('invalid');
106
+ if (flags.connection && flags['project-root'])
107
+ throw new TeamError('invalid');
108
+ const connection = flags.connection ? readTeamConnection(resolve(flags.connection)) : undefined;
109
+ const token = connection ? () => connectionToken(connection) : () => teamClientToken();
110
+ token(); // validate private credential before startup
111
+ const server = await createTeamMcpServer(connection
112
+ ? { url: connection.url, organization: connection.organization, project: connection.project, readOnly: connection.access === 'read-only', token, projectRoot: connection.project_root }
113
+ : { url: process.env.CAIRN_TEAM_URL ?? '', organization: process.env.CAIRN_TEAM_ORGANIZATION ?? '', token, projectRoot: flags['project-root'] });
114
+ await server.connect(new StdioServerTransport());
115
+ return;
116
+ }
117
+ if (command === 'restore') {
118
+ if (words.length !== 1 || !flags.input || !flags.confirm || !flags.data)
119
+ throw new TeamError('invalid');
120
+ const snapshot = verifyTeamSnapshot(readStableJson(resolve(flags.input), { label: 'Team snapshot', maxBytes: TEAM_SNAPSHOT_MAX_BYTES, private: true }));
121
+ store = await restoreTeamSnapshot(root, snapshot, flags.confirm);
122
+ print({ restored: true, organization: store.organization, credentials_revoked: true });
123
+ return;
124
+ }
125
+ if (['init', 'project', 'member', 'credential', 'serve', 'doctor', 'backup', 'memory-delete'].includes(command)) {
126
+ if (command === 'init' && (words.length !== 1 || !flags.organization))
127
+ throw new TeamError('invalid');
128
+ store = await openTeamStore(root, command === 'init' ? { create: true, organization: flags.organization } : {});
129
+ if (command === 'doctor' && words.length === 1) {
130
+ print(await store.doctor());
131
+ return;
132
+ }
133
+ if (command === 'backup' && words.length === 1) {
134
+ if (!flags.output)
135
+ throw new TeamError('invalid');
136
+ const snapshot = await store.snapshot();
137
+ writePrivateExclusive(flags.output, `${JSON.stringify(snapshot)}\n`);
138
+ print({ snapshot_digest: snapshot.digest, written: true });
139
+ return;
140
+ }
141
+ if (command === 'memory-delete' && words.length === 1) {
142
+ await store.admin({ operation: 'memory-delete', project: flags.project, key: flags.key, confirm: flags.confirm });
143
+ print({ deleted: true, backup_copies_retained: true });
144
+ return;
145
+ }
146
+ if (command === 'init') {
147
+ print({ initialized: true, organization: store.organization, experimental: true });
148
+ return;
149
+ }
150
+ if (command === 'project' && subcommand === 'create' && subject && words.length === 3) {
151
+ await store.admin({ operation: 'project-create', project: subject });
152
+ print({ created: true });
153
+ return;
154
+ }
155
+ if (command === 'member') {
156
+ await store.admin({ operation: 'member-set', subject, project: flags.project, roles: subcommand === 'remove' ? [] : memberRoles });
157
+ print(subcommand === 'remove' ? { removed: true } : { updated: true });
158
+ return;
159
+ }
160
+ if (command === 'credential' && subcommand === 'issue' && subject && words.length === 3) {
161
+ if (!flags.output || existsSync(resolve(flags.output)))
162
+ throw new TeamError('invalid');
163
+ const credential = await store.issue({ subject, credential_class: flags.class, expires_at: flags['expires-at'] });
164
+ try {
165
+ writePrivateExclusive(flags.output, `${credential.token}\n`);
166
+ }
167
+ catch (error) {
168
+ await store.admin({ operation: 'credential-revoke', credential: credential.id });
169
+ throw error;
170
+ }
171
+ print({ credential_id: credential.id, token_file_created: true });
172
+ return;
173
+ }
174
+ if (command === 'credential' && subcommand === 'revoke' && subject && words.length === 3) {
175
+ await store.admin({ operation: 'credential-revoke', credential: subject });
176
+ print({ revoked: true });
177
+ return;
178
+ }
179
+ if (command === 'serve' && words.length === 1) {
180
+ const raw = flags.port ?? '7955';
181
+ if (!/^[1-9][0-9]{0,4}$/.test(raw) || Number(raw) > 65535)
182
+ throw new TeamError('invalid');
183
+ const server = createTeamHttpServer(store);
184
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(Number(raw), '127.0.0.1', resolve); });
185
+ process.stderr.write(`Team preview API listening on loopback port ${raw}.\n`);
186
+ await new Promise(resolve => {
187
+ const stop = () => { server.close(() => resolve()); server.closeAllConnections(); };
188
+ process.once('SIGINT', stop);
189
+ process.once('SIGTERM', stop);
190
+ });
191
+ return;
192
+ }
193
+ throw new TeamError('invalid');
194
+ }
195
+ if (words.length !== 1 || !flags.project)
196
+ throw new TeamError('invalid');
197
+ const common = { organization: process.env.CAIRN_TEAM_ORGANIZATION, project: flags.project, operation: command };
198
+ let input;
199
+ if (['list', 'proposals', 'audit'].includes(command))
200
+ input = common;
201
+ else if (['read', 'history'].includes(command))
202
+ input = { ...common, key: flags.key, ...(flags.revision ? { revision: flags.revision } : {}) };
203
+ else if (command === 'search')
204
+ input = { ...common, query: flags.query };
205
+ else if (command === 'proposal-show')
206
+ input = { ...common, proposal_id: flags.proposal };
207
+ else if (command === 'review')
208
+ input = { ...common, proposal_id: flags.proposal, decision: flags.decision, confirm: flags.confirm };
209
+ else if (command === 'propose') {
210
+ if (!flags.input || !flags.confirm)
211
+ throw new TeamError('invalid');
212
+ const selected = readStableJson(resolve(flags.input), { label: 'Team selected proposal', maxBytes: 512 * 1024, private: true });
213
+ if (!selected || typeof selected !== 'object' || Array.isArray(selected))
214
+ throw new TeamError('invalid');
215
+ input = { ...selected, ...common, confirm: flags.confirm };
216
+ }
217
+ else
218
+ throw new TeamError('invalid');
219
+ print(await teamRequest(process.env.CAIRN_TEAM_URL ?? '', teamClientToken(), input));
220
+ }
221
+ finally {
222
+ await store?.close();
223
+ }
224
+ }
225
+ void main(process.argv.slice(2)).catch(error => {
226
+ process.stderr.write(`${error instanceof TeamError ? error.message : 'Team operation failed; no credential or input content is logged.'}\n`);
227
+ process.exitCode = 1;
228
+ });