Team sharing has two boundaries: what content is approved to leave a personal device, and which principals may perform which operations on a shared instance. ChatCrystal’s cloud mode already has token authentication and normalized remote import. The pinned commit does not create separate member accounts or project-level read/write roles. Distributing one shared token does not create permission isolation.
Current cloud capabilities
server/src/runtime/cloud.ts enables cloud mode through CHATCRYSTAL_CLOUD_MODE=true or a nonempty CHATCRYSTAL_API_TOKEN. server/src/routes/setup.ts installs Bearer-token checks for protected API routes. Health, setup status/completion, and auth verify are public exceptions; setup, verification, and token rotation have their own checks. The rotate route verifies currentToken in its request body, so not every route is guarded only by Bearer authentication. server/src/services/auth.ts supports an environment token or a scrypt-stored single token and failed-attempt controls. The API rotates only a persisted token; environment tokens must be changed in the deployment environment. This is not multiple accounts, individual revocation, or RBAC.
server/src/routes/import.ts disables server-side local scans in cloud mode and provides ingest. server/src/cli/commands/import.ts parses on a device holding source histories and uploads a normalized payload; noninteractive use requires explicit –yes. server/src/runtime/watcherPolicy.ts prevents cloud-mode watching. Neither automatic scanning of everyone’s desktop from a shared host nor a complete absence of remote import matches current behavior. Multiple clients can submit; concurrency pressure, isolation, and conflict recovery are not thereby tested.
The seven MCP tools use the same API connection through a client; see MCP protocol boundaries. Tool names do not identify the team’s dataset or establish member permissions for a gateway.
Define the trust boundary before choosing a layout
| Layout | Boundary retained | Additional work |
|---|---|---|
| Personal libraries plus reviewed readable exports | Private histories remain on their owner’s device first | Export versions, redaction, provenance, updates |
| Shared instance within one trusted group | Current single-token access | Holder capabilities, rotation impact, concurrent recovery |
| Shared instance behind an identity gateway | External member/role policy can be defined | Complete method/path tests, direct-access prevention, gateway maintenance and audit |
| Separate instances for different trust groups | Data and credentials stay separate | Multi-instance updates and retrieval strategy |
These are deployment designs, not built-in team UI features or measured member-count and memory recommendations. tag, project_key, and scope describe retrieval or knowledge semantics, not token permission partitions. Manual global memory versus automatic project memory cannot promise that a member cannot read another project.
Review content first and define reads by operation
Source histories may include paths, code, credentials, or unreviewed conclusions. Obtain the owner’s approval, define shareable scope, redact, and record provenance before uploading. Automatic summaries do not guarantee redaction. Server ingest is a data entry point; command confirmation is not organizational approval.
GET-only access blocks read-like operations such as POST /api/memory/recall, while potentially exposing sensitive configuration GETs. Define roles against exact methods and paths, then test actual client workflows. Prevent direct access to the shared-token backend at the network layer. The following checks a proposed gateway policy, not ChatCrystal authentication. Roles are parameters; there is no identity authentication, HTTP proxy, real redactor, import, or backend.
Save this as team.mjs under Node 24 and run node team.mjs.
import assert from 'node:assert/strict';
// Proposed gateway policy; this is not ChatCrystal's token hook.
const rules = new Map([
['reader', new Set(['POST /api/memory/recall', 'GET /api/notes'])],
['importer', new Set(['POST /api/import/ingest'])],
]);
function authorize(role, method, rawPath, target, approvedTarget) {
assert.equal(target, approvedTarget);
const url = new URL(rawPath, 'https://gateway.invalid');
if (url.pathname.includes('%') || url.pathname.includes('..')) throw new Error('ambiguous path');
if (!rules.get(role)?.has(`${method} ${url.pathname}`)) throw new Error('denied');
}
const target = 'reviewed-instance-a';
let stored = [];
function ingest(role, items) {
authorize(role, 'POST', '/api/import/ingest', target, target);
for (const item of items) {
if (item.approval !== 'approved' || item.classification !== 'team-shareable')
throw new Error('unapproved input');
if (/secret|password|token/i.test(item.content)) throw new Error('redaction required');
}
stored.push(...items.map(({ id, content }) => ({ id, content })));
}
authorize('reader', 'POST', '/api/memory/recall', target, target);
assert.throws(() => authorize('reader', 'POST', '/api/import/ingest', target, target), /denied/);
assert.throws(() => authorize('reader', 'GET', '/api/notes', 'other-instance', target));
assert.throws(() => authorize('reader', 'DELETE', '/api/notes/1', target, target), /denied/);
assert.throws(() => authorize('reader', 'POST', '/api/queue/cancel', target, target), /denied/);
assert.throws(() => authorize('reader', 'GET', '/api/config', target, target), /denied/);
assert.throws(() => authorize('unknown', 'GET', '/api/notes', target, target), /denied/);
const approved = {
id: 'reviewed-1',
content: 'use a scoped transaction',
approval: 'approved',
classification: 'team-shareable',
};
ingest('importer', [approved]);
assert.equal(stored.length, 1);
assert.throws(
() => ingest('importer', [approved, { ...approved, id: 'private-2', approval: 'pending' }]),
/unapproved/
);
assert.equal(stored.length, 1);
assert.throws(
() => ingest('importer', [{ ...approved, content: 'password=example' }]),
/redaction/
);
assert.equal(stored.length, 1);
console.log('proposed gateway roles and whole-batch review gate pass on fixtures');
Permission and approval checks need rejection fixtures
Remove rule matching: the reader-ingest rejection assertion must fail. Move stored.push before reviewing all inputs: the pending-content batch pollutes synthetic storage and the count assertion must fail. Restore and run again. Keyword checks only reject the example; they are not dependable redaction or sensitive-data detection. URL/method rules are not a complete secure proxy implementation.
Before deployment, separately test missing/wrong tokens, old tokens after rotation, reader recall and denied writes, unapproved payloads, the wrong instance, gateway bypass, and retrieval after restore. Receipts in server/src/services/memory/writeback.ts and quality feedback in server/src/services/experience/reviews.ts serve business purposes; neither is a complete per-member operation audit. A shared token cannot reliably identify the person acting. If external systems own identity, audit, and content approval, record their owners and retention policy in the deployment agreement.
Synchronization and backup cannot copy an active database blindly
Do not point multiple desktops at one active chatcrystal.db or overwrite a running library through file synchronization. sql.js operates in memory and exports data; vectra-index is another dataset. See WASM persistence and vector-index boundaries. Stop all writers or take a consistent snapshot, copy the agreed data directory, and verify recovery in isolation. Active source-tool SQLite also needs main-database/WAL consistency; copying one state.vscdb alone does not establish a complete backup.
Sharing does not require centralizing every history. Test approved imports, provenance, retrieval, rejection, and recovery for the same task before choosing what enters a shared library. See the CLI knowledge workflow for automation steps.