Context Platform
Documentation

Version Latest

Lenses and sharing

Create and share lenses, saved retrieval views over chosen sources, with share levels, expiry, approval above a classification line, built-in system lenses and the share target lookup.

A lens is a saved selection of sources and pinned chunks that retrieval runs through. People create lenses for a task, share them with colleagues, freeze them as immutable snapshots, and fork them.

Sharing a lens grants no access to content. Retrieval through a lens still authorises every space for the person reading and applies DLP and classification unchanged: a lens can never return a chunk that the reader's own retrieval would not.

Routes

MethodPathWhat
GET/api/v1/context/lensesLenses the caller sees: owned, shared with them, and the system lenses that apply. ?sharedWithMe=true, ?system=true|false
POST/api/v1/context/lensesCreate a lens (context:lens:create)
GET/api/v1/context/lenses/{id}One lens; accessLevel is owner, view, use or edit
PATCH/api/v1/context/lenses/{id}Update or archive; versioned (409 on a stale version); never when frozen
POST/api/v1/context/lenses/{id}/shareShare with several targets
GET/api/v1/context/lenses/{id}/sharesThe targets, with effective and violations (owner only)
PUT, DELETE/api/v1/context/lenses/{id}/shares/{kind}/{targetId}Add or change one share; remove one share
POST/api/v1/context/lenses/{id}/freezeFreeze with a snapshot (never a system lens)
POST/api/v1/context/lenses/{id}/applyApply: the lens's sources as the caller may use them
POST/api/v1/context/lenses/{id}/forkFork into a lens the caller owns

A person may own at most 500 live lenses and an organisation 20,000 (KEYSTONE_CONTEXT_LENSES_PER_OWNER, KEYSTONE_CONTEXT_LENSES_PER_ORG; system lenses do not count).

Who sees a lens

  1. The creator.
  2. A share target: the user, a member of the group (groups nested up to 5 levels included), or a member of the team (Directory team memberships, direct or through groups).
  3. System lenses, by rule (below).

A lens a person cannot see is a 404, never a 403. Nothing is cached: a removed share or a person leaving a team applies to the next request on every path, including snapshots made from the lens.

Share a lens

POST /api/v1/context/lenses/{id}/share
Content-Type: application/json

{"targets": [
  {"kind": "team", "id": "<team id>", "level": "use", "expiresAt": "2026-12-31T00:00:00Z"},
  {"kind": "user", "id": "<user id>", "level": "view"}
]}
LevelThe person may
viewSee the lens and retrieve through it
useAlso apply and fork it
editAlso change its sources and pinned documents (not its name, description or archive state)
  • A share without a level is use (or the first level the policy allows); a share without an expiry gets the policy's default expiry.
  • Targets must belong to the organisation (a user must be a member); otherwise 422.
  • Sharing is checked against the sharer's collaboration policy: lens sharing switched on, every space of the lens at or below the classification ceiling, the audience within the share scope, the number of targets, the allowed levels and the maximum expiry. A refusal is 403 context.collab_policy.<reason> and nothing is stored.
  • PUT .../shares/{kind}/{targetId} is idempotent; a changed level or later expiry changes the share in place. DELETE works on frozen and archived lenses and on targets that no longer exist, and needs only ownership.
  • Sharing, approving and other decisions need a signed-in person: an API key gets 403 context.person_required.

Shares that stop holding

A share confers access only while it has not expired and still complies with its sharer's policy as it is now, judged at read time. A share that breaks a tightened policy is ineffective at once, listed with effective: false and violations (capability_disabled, classification_ceiling, approval_required, scope_exceeded, too_many_targets, level_not_allowed, expiry_exceeds_max), and never deleted: when the policy allows it again, it holds again.

Approval above a classification line

When the policy sets approval_above, a share of content classified above that line is not refused. It is stored as a pending share and an approval request goes to the holders of the policy's share_approvers roles whose scope covers the content:

  1. The share request must carry a reason (at most 2000 characters; 422 context.share.reason_required without it).
  2. The answer is 202 with pendingShares; the lens is unchanged and the share confers nothing yet.
  3. Approvers decide in the approvals inbox (GET /api/v1/approvals/inbox, requests of agent keystone-context, action context.collab.share). The requester can never approve their own request.
  4. An approval is checked again against the policy in force at that moment. If a rule now fails, or the content became more sensitive than the approvers were told, the share ends as refused with a stable refusalReason.
  5. A pending share expires after KEYSTONE_CONTEXT_PENDING_SHARE_TTL (default 14 days). The requester can cancel it with POST /api/v1/context/collaboration/pending/{id}/cancel.

GET /api/v1/context/collaboration/pending lists the caller's requests and the ones they may decide. When nobody but the requester holds an approver role for the content, the share is refused with approval_required.

Approver roles come from role bindings in Keystone Access: a person holds org_admin, department_admin or team_lead when a binding on the organisation, a business unit or a team gives them a role that grants context:space:admin (for example Owner and Admin). In a new organisation the Owner and Admins approve everything; to let a team lead approve, bind a role with context:space:admin on the team.

System lenses

Every organisation has three built-in lenses, kept up to date by the space reconciliation job:

LensSourcesWho sees it
onboardingThe organisation space's docs and codebase sourcesEvery member
incidentThe organisation space's issues and docs sourcesEvery member
team-{teamId}The team space's codebase and docs sourcesMembers of the team and administrators

A system lens carries system, systemKind and autoApplyWhen (first_session, team_member, incident_declared) as data; the server never pushes it, the client decides when to apply it. Only administrators may change its description and sources or archive it; nobody may rename, share or freeze one. A fork is an ordinary lens owned by the forker.

Finding share targets

People do not know ids. GET /api/v1/context/collaboration/targets and .../targets/resolve are the one lookup behind the sharing picker of the first-party apps and the text handles of the CLI (user:<email>, team:<slug>, group:<slug>).

GET /api/v1/context/collaboration/targets?q=plat&kinds=user,group,team&limit=10
GET /api/v1/context/collaboration/targets/resolve?handle=team:payments
  • The search needs 2 to 64 characters and matches them as literal text (no wildcards) against display names, slugs and, for callers who may read the Directory (directory.members:read), emails. With no query it lists the caller's own teams, their groups and teammates.
  • Only targets the caller's policy would let a share reach are returned. A hidden, suspended, removed or other-organisation person and an unknown name all give the same empty answer.
  • resolve answers one target, 404 context.targets.not_found, or 409 context.targets.ambiguous with up to 10 visible candidates; it never guesses. User handles need directory.members:read.
  • Pages of 10 (at most 25), with the next cursor in X-Next-Cursor. A person may make KEYSTONE_CONTEXT_TARGETS_PER_MINUTE lookups a minute (default 30); over it, 429 with Retry-After.
  • Each lookup is audited with the query's length and the number of results, never the query.

From the CLI:

ni keystone context lens create --name="refunds" --source=<source id> --share=team:<team id>

Last reviewed 17 October 2026View as Markdown

Lenses and sharing · Docs · Neural Inverse