logicsrc/docs/opencontext/integration.md
Anthony Ettinger 3ab8a4b38b
Add the LogicSRC OpenContext specification (#132)
* Add the LogicSRC OpenContext specification

OpenContext is an open specification for durable, portable, permissioned,
provenance-aware context shared between humans and AI agents. It defines how
organizational knowledge is described, authorized, versioned, resolved,
audited, and handed between replaceable workers without losing institutional
state.

Follows the OpenPRD/OpenOntology pattern already in the repo: self-contained
JSON Schemas in @logicsrc/schemas, a reference implementation package, CLI
subcommands, docs, examples, and an OpenPRD record.

Schemas (8, all self-contained so a third party can fetch one file and
validate against it with no further resolution):
  manifest, object, bundle, role, provenance, decision, diagnostic,
  audit-event — registered in @logicsrc/validators and schemas:validate.

Reference implementation (@logicsrc/opencontext):
  loader with upward manifest discovery, the full resolution pipeline,
  authority/supersession, permissions, redaction, lifecycle, provenance,
  deterministic digests, doctor, search, graph, history/diff, guarded writes,
  audit events, and file/http/git/sqlite adapters.

CLI: all 15 specified commands, as a standalone `opencontext` binary and as
`logicsrc context`, sharing one implementation so the two cannot drift.

Design decisions worth noting:

- Supersession is declared, never inferred from version numbers. Inferring it
  would hide the governance failure it represents and make
  multiple-active-versions and duplicate-canonical impossible to detect.

- The bundle digest identifies the resolved context, not the moment it was
  computed, so generated_at/bundle_id/digest/as_of are excluded while objects,
  lifecycle states, exclusions and warnings are covered. That is what lets a
  decision record cite exactly the context that produced it.

- A role's own max_classification beats an inherited one, so a ceiling on a
  shared base role cannot silently cap a role deliberately granted more;
  requesting several roles at once still takes the lowest, so combining roles
  never escalates.

- Scope wildcards match whole dotted segments only. A trailing .* covers a
  subtree; an interior * matches exactly one segment. Substring matching here
  would be an access-control bug.

- --include narrows an existing scope and is applied after it, never merged
  into it, so a request can never widen what a role holds.

Verified: 226 tests across core primitives, permissions/redaction, the
resolution pipeline, security, the published conformance fixtures (13 valid,
35 invalid, 8 resolution scenarios), project behaviour, and the five shipped
examples — which are held to --strict and a 100% health score. Benchmarks meet
every published budget (resolve 1,000 objects in ~33ms against a 2s target).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Point install docs at @logicsrc/opencontext; record the npm name collision

The unscoped `opencontext` name is already published on npm by an unrelated
third party (federicodeponte/opencontext, 2.0.0), so `npx opencontext` would
install a stranger's package. Docs now use `npx @logicsrc/opencontext`; the bin
stays named `opencontext` so the command reads as the PRD specifies once
installed.

Recorded in PRD 0003 as a blocker to resolve before any publication, along with
the fact that no @logicsrc spec package has ever been published, so there is no
existing release path to slot into.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Advance the logicsrc-mcp next-PRD-id assertion to 0004

standards.test.ts asserts prd_next_id against the live prd/ directory, so
adding PRD 0003 makes the next free id 0004. The test's own comment
anticipates this: "advances with every PRD added".

Caught by CI, not locally — the earlier verification ran per-package tests for
the packages this branch touches, and logicsrc-mcp is coupled to the PRD
directory without importing from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 11:46:11 -07:00

6.6 KiB

Integration patterns

OpenContext never requires a specific LLM provider, framework, or database. These are the shapes people actually deploy.

System prompts

The most direct use: resolve, render, prepend.

import { OpenContext, renderBundle } from "@logicsrc/opencontext";

const oc = await OpenContext.load("./opencontext.yaml");
const { bundle } = oc.resolve({ agent: "support-agent", task: userMessage });

const messages = [
  { role: "system", content: renderBundle(bundle, "markdown") },
  { role: "user", content: userMessage }
];

The Markdown renderer groups by layer, opens with a statement that everything below is context rather than instruction, and fences untrusted spans. Do not flatten it into raw text — the envelope is load-bearing. See security.

Record the digest alongside whatever the agent produces, and the decision stays reconstructable after the model is replaced.

RAG pipelines

OpenContext is the control plane above retrieval, not a replacement for it.

// 1. Your retriever proposes candidates.
const candidates = await vectorStore.search(query, { k: 50 });

// 2. OpenContext decides what this consumer may actually see.
const scope = oc.scope({ agent: "support-agent" });
const allowed = candidates.filter((hit) => {
  const object = oc.get(hit.id);
  return object ? authorize(object, scope).allowed : false;
});

Two rules worth stating plainly:

  • Never rank before authorizing. Embedding similarity has no idea what a role may read.
  • Retrieval rank is not authority. The top hit is not thereby the truth; a reference note that scores well does not outrank a canonical policy.

A reasonable division of labour: vectors find candidates, OpenContext decides eligibility and authority, and the bundle is what reaches the model.

MCP servers

MCP is complementary. A server exposes operations over the same resolution rules:

context.get       one object, subject to authorization
context.search    lexical search, authorized before results are returned
context.resolve   a bundle for a consumer and task
context.list      what exists in scope
context.explain   why an object was included or excluded

MCP access must use the same permission and resolution rules as the CLI and SDK. An MCP server that resolves with a wider scope than the agent holds is a privilege escalation wearing a protocol.

Carry the caller's identity into resolve({ agent }) rather than resolving unrestricted and filtering afterwards.

Agent frameworks

Bundles are framework-neutral: resolve, render, hand over.

const bundle = oc.bundle({ agent: agentId, task });

// Any framework — the bundle is just text plus metadata.
agent.setSystemPrompt(renderBundle(bundle, "markdown"));
agent.setCapabilities(bundle.permissions ?? []);

bundle.permissions carries the capability strings the role holds, for your runtime to enforce. OpenContext transports and scopes them; it does not perform your application's actions.

For multi-agent systems, give each agent a role in the manifest rather than a bespoke prompt. A hand-written prompt is context that exists only inside that agent — exactly the state this specification exists to end.

CLI agents

opencontext resolve --role support --task "$TASK" --format markdown > /tmp/context.md
my-agent --system /tmp/context.md "$TASK"

Discovery searches upward, so this works from any directory in the project.

Human onboarding

The same bundle that briefs an agent briefs a person:

opencontext resolve --role support --format markdown > onboarding.md

If it is not good enough for a new hire, it is not good enough for an agent — and the reverse is the useful test for whether your context is actually written down.

CI/CD

name: OpenContext

on: [pull_request, push]

jobs:
  context:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx @logicsrc/opencontext validate --strict
      - run: npx @logicsrc/opencontext doctor --strict
      - run: npx @logicsrc/opencontext bundle --role support --output bundle.json
      - uses: actions/upload-artifact@v4
        with:
          name: context-bundle
          path: bundle.json

Useful gates:

opencontext conflicts --strict              # reject duplicate canonical policies
opencontext stale --strict                  # reject expired required context
opencontext doctor --strict --min-score 95  # enforce a health floor

Publishing the bundle as a build artifact means every release records exactly what its agents knew.

GitOps

Context lives in the repository and changes through pull requests, reviewed like code.

  • A policy change is a diff a human approves.
  • opencontext diff shows the governance-relevant fields, not every byte.
  • git://<rev>/<path> reads context out of any past commit, offline.
  • Branch protection on context/ gives you approval workflow without a hosted service.

API servers

app.post("/context/resolve", async (req, res) => {
  const identity = await authenticate(req);          // your IdP, not OpenContext

  const { bundle } = oc.resolve({
    agent: identity.agentId,                          // never from the request body
    task: req.body.task
  });

  res.json(bundle);
});

Take the consumer identity from your authenticated session, never from the payload. OpenContext enforces what a named consumer may read; it does not authenticate who is asking.

Recording decisions

const { bundle } = oc.resolve({ agent, task });
const answer = await model.complete(renderBundle(bundle, "markdown"), task);

oc.add({
  id: `decisions.${today}-${slug}`,
  type: "decision",
  title,
  decision: answer.decision,
  rationale: answer.rationale,
  decided_by: { type: "agent", id: agent },
  bundle: { bundle_id: bundle.bundle_id, digest: bundle.digest }
}, { allowPromotion: false });

Note allowPromotion: false. An agent records a decision at ordinary authority; a human promotes it to approved. Observed context does not become truth automatically.

Anti-patterns

Copying context into a prompt template. It drifts within a week, and nothing tells you.

Resolving unrestricted and filtering later. Unauthorized context has already been ranked, logged, and possibly cached.

Treating a bundle as a cache. It is a snapshot for one task at one instant. Re-resolve; it costs milliseconds.

Writing back inferred context as canonical. That is how a model's guess becomes company policy without anyone deciding.

Stripping the untrusted envelope to save tokens. The label is what stops a ticket from reading as an instruction.