Skip to content

Contributing to Alcove ​

Thank you for your interest in contributing. Alcove handles personally identifiable information in high-stakes environments; correctness, security, and auditability matter more than velocity. Please read this document before opening a pull request.

Scope of this document ​

This is the pre-contribution quickstart: repository layout, how to build and test, the conventions CI and review enforce, and the pull-request and dependency-review process. It is not a reference. Keep deeper material out of it:

  • Dependency-internal assumptions and upgrade runbooks -> docs/spec/DEPENDENCY_PINS.md.
  • Test-infrastructure internals and the coverage rationale -> docs/TESTING.md.
  • Wire formats, constants, algorithm steps, and the "would only need revisiting if..." rationale behind them -> docs/spec/.

Repository Structure ​

Alcove is organized as an npm workspaces monorepo. The workspaces and the supporting directories, all of them:

PathDescription
packages/core/Shared library: PSI primitive, exchange orchestration, file-sync transport, config schemas
packages/peerjs-broker/The PeerJS-compatible WebRTC signaling broker: vendored server source plus a standalone entry point (npm start -w packages/peerjs-broker). Ships TypeScript source with no build step of its own, reading @alcove/core/untrusted-text -- the whole of its reach into core -- from that workspace's dist/; the web app's dev loop and tests run it beside the dev server
packages/websocket-relay/The WebSocket relay that pairs two parties of one exchange by a per-exchange proof and forwards their frames unread, for networks that stop TURN (npm start -w packages/websocket-relay). Ships TypeScript source with no build step of its own, reading @alcove/core/websocket-relay -- the whole of its reach into core -- from that workspace's dist/
packages/cli-contract/The CLI's machine interface (@alcove/cli-contract): the fd-3 event schema, warning sources, named exit codes and failure-cause field the CLI emits, specified in docs/spec/CLI_EVENTS.md. Built to dist/ by tsc, after core and before the CLI
packages/testkit/Test-only material shared by more than one workspace's test tree (@alcove/testkit), consumed as raw TypeScript with no build step; what qualifies and why: docs/TESTING.md and docs/notes/cross-workspace-test-material.md
apps/cli/Node.js CLI (alcove), built with Rollup, distributed as a Docker image
apps/web/React web app, built for hosting as a static site (dist/hosted/) and for the console; mounts no broker, and its browser parties dial the one above
docs/Documentation, three tiers: docs/ overview (conceptual/operational), docs/spec/ technical, docs/notes/ design records. docs/.vitepress/ configures the project site built from them
scripts/Repository checks CI runs (doc links, PR checklist, claim and drift guards) with their tests
support/Field guides for the environment around Alcove -- Windows, Docker, agency networks -- plus the FIPS measurement harness
lib/The vendored @openmined/psi.js tarball and its checksum
test_data/Two synthetic CSVs with partial overlap, for practicing a complete exchange
design/Interface design records: design/web-redesign/ is the linkage-bench mockup and rationale the web app implements. Not a build input
infra/OpenTofu for the project's hosted environment: the Cloudflare Pages projects of the web app and the project site, their custom domains and the Cloudflare zone settings, applied by the maintainer and run by nothing here -- CI deploys the sites themselves through pages_deploy.yaml and site_deploy.yaml. Beside it, the standing WebRTC relay's reference deployment and the standalone broker's host units, also run by nothing here
.github/The workflows CI runs, the pull-request template, and the dependabot configuration
.claude/The agent harness: role definitions, slash commands, tool hooks, the project-manager and orchestration rulesets, and the session scripts, governed by the conventions in CLAUDE.md
.devcontainer/The development container and its egress firewall

Prerequisites ​

  • Node.js 26 or later (npm 11 or later is included)
  • Docker for building the container image

The OpenMined PSI module is vendored at lib/openmined-psi.js-2.0.6-seclink.6.tgz (the WASM engine plus native N-API prebuilds). No Emscripten or native toolchain is required to work against it.

Development Setup ​

sh
git clone [email protected]:georgetown-mdi/alcove.git alcove
cd alcove
npm install
npm run build -w packages/core   # core must be built before the apps and the broker
npm run build -w packages/cli-contract   # then the CLI contract, before the CLI

For work that spans packages/core and the web app, run the dev loop from the repository root:

sh
npm run dev            # core's build watcher + the signaling broker + the web dev server
npm run dev:console    # core's build watcher + the console Node server, its client served through Vite middleware

The apps consume @alcove/core from its built dist/, so a core edit made while a bare npm run dev -w apps/web is running never reaches the page. The root script brings dist/ up to date before the server starts and rebuilds it on every later change; any one of its processes exiting stops the others. The web app mounts no signaling broker, so npm run dev also starts the standalone one (packages/peerjs-broker) on loopback and the dev server forwards /api/ to it; a bare npm run dev -w apps/web has no signaling.

No additional environment variables are required for local development or the tests. The CLI SFTP integration suite starts its own server; set ALCOVE_SFTP_BACKEND=native to run it against a native OpenSSH sshd instead of the in-process default (see docs/TESTING.md, which documents the variable).

Building ​

sh
npm run build -w packages/core   # must build before the apps and the broker; rebuild after any core change
npm run build -w packages/cli-contract   # after core, before the CLI; rebuild after any change to it
npm run build -w apps/cli        # -> apps/cli/dist/; Docker image built separately (docs/RELEASES.md)
VITE_SIGNALING_SERVER_URL=wss://signaling.example.org/api/ npm run build -w apps/web  # -> apps/web/dist/hosted/, a static site; names the broker (docs/DEPLOYMENT.md)
npm run site:build               # -> docs/.vitepress/dist/, the project site built from docs/ (docs/DEPLOYMENT.md)

Testing ​

sh
npm run test   # unit tests of core, cli-contract, the WebSocket relay, cli and web (not test:scripts); run before pushing a core change, since the apps mock core
npm test -w packages/core
npm test -w packages/cli-contract
npm test -w packages/websocket-relay
npm run test:unit -w apps/cli
npm run test:unit -w apps/web
npx vitest run path/to/file.test.ts   # single file

Building, running the dev server, or testing apps/web regenerates the checked-in src/routeTree.gen.ts (see apps/web/README.md), so it can churn in a working tree with no route change. Stage explicit paths rather than git add -A, which also sweeps stray review or scratch artifacts into a commit.

Integration and browser tests ​

Must pass before a PR merges to main or staging. Each suite is self-managing -- it starts and stops the server it needs -- so no manual setup beyond the builds above is required:

sh
npm run test:integration -w apps/cli         # SFTP adapter driven over a loopback server
npm run test:integration:webrtc -w apps/cli  # WebRTC transport over loopback werift peers
npm run test:integration:backend-agnostic -w apps/cli  # the integration files no SFTP backend differentiates
npm run test:integration -w apps/web
npm run test:browser     -w apps/web         # cross-impl vectors + live exchange, real Chromium
npm run test:interop     -w apps/web         # a real alcove process and a web party, one live exchange

The interop suite drives the built CLI, so run npm run build -w apps/cli first; it skips itself otherwise.

The native SFTP backends and hardened profiles, why the WebRTC, backend-agnostic, and interop suites are projects of their own, the console sentinel, the warm-server inner loop, and the browser-suite plumbing are in docs/TESTING.md.

Coverage ​

Coverage is an informational report (npm run coverage), not part of npm test and not a CI gate. There is no global percentage threshold by design -- do not add one. Rationale and what the report covers: docs/TESTING.md.

Code Conventions ​

  • TypeScript with strict mode throughout. Avoid any; if you must use it, add a comment explaining why.

  • Naming: camelCase in TypeScript; snake_case in user-facing JSON and YAML files. Semicolons required. Prefer a long, explicit, self-documenting name over a short one plus a clarifying comment.

  • Comments: write one only when the why is non-obvious - a hidden constraint, subtle invariant, or known limitation. Do not restate what the code does. Multi-line // blocks are permitted for complex runtime constraints that cannot fit on one line, under the six-line bound in Plain language below.

  • Encode runtime invariants as checks, not prose: a claim that something does not happen at runtime -- a line that never fires, an unreachable branch, a callback that never runs -- belongs in an executable check that fails when the claim breaks, not a comment or doc note that cannot, and a best-effort check says so, because a safety check is not a guarantee. The worked case, its assumption and its enforcing check -- a prose note there went stale while the check did not, the point of the rule: docs/spec/DEPENDENCY_PINS.md and the CLI integration console sentinel (apps/cli/test/consoleSentinel.ts).

  • No literal line numbers in an assertion: an assertion never names a literal line number of a real, tracked file -- an edit anywhere above it turns the check red for a reason unrelated to what the check guards. Key the assertion on the thing's identity instead: the endpoint or field literal, a table row's key, a declaration's name, a heading anchor, a message's fixed prefix. Where a failure report must state a line, derive it through the check's own parser -- lineOf in scripts/check-stun-default-claims.mjs, heading anchors in scripts/check-doc-links.mjs, sites keyed by file and enclosing function in apps/cli/test/unit/operatorConfigErrorSites.test.ts. An assertion against an inline synthetic fixture is outside this rule: the fixture and the line it names move together. A line number is still the right thing to write for human navigation in a report, an issue body, or a brief, marked as-of and re-derived when the work is picked up.

  • JSDoc: /** */ on all exports; /** @internal */ (with no description) for test-only exports.

  • Validation: define the TypeScript interface first, then derive the Zod schema with z.ZodType<Interface>. Apply camelizeKeys before Zod parsing so user-facing YAML/JSON remains snake_case while TypeScript sees camelCase. Write a string length ceiling with maxCodeUnits (packages/core/src/utils/maxCodeUnits.ts) rather than a bare .max(): Zod counts code points, the hand-written predicates sharing a ceiling count UTF-16 code units, and an ESLint rule refuses a bare bound across packages/core/src and the web app, its message naming the check and the one-line eslint-disable opt-out. How far that rule reaches, and the shapes it cannot see: eslint.boundaries.mjs.

  • Transport branching: connection.channel is the discriminant. Use allowlists (not blocklists) so a new channel is rejected unless explicitly added. The guards are in packages/core/src/config/connection.ts, packages/core/src/config/invitation.ts, packages/core/src/config/exchangeFile.ts, apps/cli/src/commands/exchange.ts, apps/cli/src/run/prepare.ts, and apps/cli/src/onlineBootstrap.ts.

  • New channels: add a discriminant value and config interface to packages/core/src/config/connection.ts, update the ConnectionConfig union, then update the guards. See existing sftp, webrtc, and filedrop entries for examples.

  • Security primitives: extract a shared cryptographic helper as soon as it is correct and tested rather than waiting for a second caller, and land a security-relevant fix at the single upstream chokepoint that closes the whole class, preferring a misuse made unrepresentable over one documented -- when that reaches past the issue's stated scope, say so and ask before landing it.

  • Sensitive-file parsing: parse any secret-bearing config or credential document only through the sensitive-parse chokepoint in @alcove/core (parseSensitiveYaml / parseSensitiveJson in packages/core/src/sensitiveFile.ts, re-exported by apps/cli/src/sensitiveFile.ts), never a raw YAML or JSON parser; an ESLint rule enforces it, and its message names the entry points and the one-line eslint-disable opt-out for a non-sensitive parse. Which documents qualify, how far the ban reaches, and the leak channels behind it: that module's header and docs/SECURITY_DESIGN.md.

  • Untrusted-JSON parsing: parse any untrusted JSON -- a partner wire frame, a transport-controlled file, an invitation token -- only through parseBoundedJson (packages/core/src/utils/boundedJson.ts), never a raw JSON.parse; an ESLint rule enforces it across @alcove/core, apps/web/src, and packages/peerjs-broker/src, naming the entry point and the one-line eslint-disable opt-out for a trusted parse. A fetched body is read the same way, through readBoundedJsonBody (packages/core/src/utils/boundedJsonBody.ts) under the cap that endpoint's answer needs, never a whole-body read (.json(), .text(), .arrayBuffer()), each of which the web app's own lint rules ban. The structural bounds it applies, and the uncatchable process-terminating abort they forestall: that module's header and docs/spec/PARSED_INPUT_BOUNDS.md.

  • Operator-facing escaping: escape untrusted text at ONE altitude, the display sink -- a fragment interpolated into an Error message or cause is composed RAW and sanitizeErrorForDisplay escapes the whole rendered chain once where it is shown, while a value that reaches a log.*, console.*, or UI sink without ever becoming an Error is escaped with sanitizeForDisplay at that call site; escaping at both altitudes double-escapes every literal backslash on its way to the operator. An ESLint rule bans rendering a raw error at a sink across packages/core/src, apps/cli/src, and packages/peerjs-broker/src, its message naming both entry points and the one-line eslint-disable opt-out. A second rule enforces the composition side across packages/ and apps/cli/, test sources included: an already-escaped Displayable may not be composed into an Error message, its cause, or a cause link (what it matches, and why it has no runtime half: docs/spec/DISPLAY_REDACTION.md). The shapes the sink ban matches (eslint.config.mjs), the console sentinel that is its runtime half, what bounds each of them, and the one route that still escapes twice (add no more): docs/spec/DISPLAY_REDACTION.md and docs/spec/DISPLAY_ESCAPE.md.

  • CLI durations: a duration-valued CLI flag parses its value through the shared parseDuration / human-readable <int><unit> format (apps/cli/src/util/duration.ts), read from args via durationFlagSeconds (apps/cli/src/util/flags.ts), never a bare integer of seconds, so the accepted value syntax stays consistent across flags.

  • CLI flag naming: a flag names the object of the action rather than the mechanism, stays self-scoping (a generic --yes overclaims), does not stutter with its subcommand, and a weighty or irreversible action gets no terse short form.

  • Windows paths: support wherever a user can supply a local path. Normalize backslashes on ingestion; use fileURLToPath for file:// URLs.

  • Plain language: use the plainest accurate word. Do not coin a metaphor where a domain word exists, and do not promote an internal name into text a user reads. A user-visible string states what happened and what to do, and nothing else -- no justification of the design, no reassurance the reader cannot act on, no refutation of an objection they did not raise. A comment inside code states a constraint, not an argument, and stays under roughly six lines -- the ceiling on the multi-line blocks the Comments bullet above permits; when the reasoning is longer than that, it is a design record and belongs in docs/notes/. A file header that CLAUDE.md or this document cites as the documentation of a hook, check script, or module is bounded by what it documents rather than by that length, and it still states constraints rather than arguing for them. The negative wordlist below is the list to check a rewrite against; it is a prompt to reread a sentence, not a substitution table, and no CI check enforces it -- a word-list check would fire on mintExchangeFile and honest-but-curious and teach contributors to route around it.

    Prefer the right-hand word. Judge each in context; none is a safe blind substitution. An entry reading "rewrite the sentence" offers no replacement: remove the word by rewriting the sentence around what it means, never by swapping in another word, since the swapped-in word becomes the next overused one.

    carries | carry | carried      -> has / contains / includes / states
        "sends" only where the subject is a party or a channel, never a message,
        list, or field. In a MUST, pick the verb that keeps what the sentence
        requires: a requirement on the contents of a list or field takes "contains".
    honest | honestly              -> correct / does not overstate
    load-bearing                   -> required / critical
        "required" where a rule or check demands it, "critical" where losing it
        breaks a behavior the sentence names.
    seam | seams                   -> boundary / call site / name the function
    surface | surfaces | surfaced  -> shows / reports / exposes / raise (VERB only)
        "raise" where the object goes to a person ("raise it with the owner").
    backstop                       -> safety check / fallback
        "safety check" where it catches what the main path missed, "fallback"
        where it runs in the main path's place.
    premise | premises             -> assumption
    footgun | foot-gun             -> hazard / mistake
        "hazard" for the shape that invites the error, "mistake" for what the
        person does with it.
    blast radius                   -> scope of impact / what it touches
    escape hatch                   -> override / opt-out / documented exception
    reads as                       -> is treated as / displays as
    settles                        -> determines / decides / fixes  (value sense only)
    deliberately                   -> delete, or "by design"
    worth naming/stating/carrying  -> delete the signpost
    true up | trued | true the     -> correct / fix
    first-class                    -> fully supported / primary
    genuinely | materially | crucially | notably -> delete
        Delete it where it only intensifies. Keep it where it is the one thing
        distinguishing two cases the sentence contrasts -- a "genuinely-null"
        received cell against an empty string (`docs/spec/EXCHANGE_RECORD.md`).
    bubble up | paper over | knob | legible -> raise / hide / setting / clear
    appliance | bench              -> console (prose and user-visible text only)
        The `/bench` route segments are compatibility redirects for links
        issued before the primary routes existed, and the `alcoveBenchStep`
        history-state marker is the same kind of key for entries the deployed
        build already wrote; both keep the word.
    battery                        -> the checks / the run
        The doctor code's own words (`apps/cli/src/doctor`): "the checks" for the
        set, "the run" for one execution of them ("stopped the battery" becomes
        "stopped the run").
    lock-in                        -> commitment / enforcement
        "commitment" for the recorded declaration, "enforcement" for the runtime
        check that aborts on a mismatch.
    warn-not-enforce               -> advisory (it warns and does not block)
    seat (a role, screen or app)   -> rewrite the sentence
    rides | rides on               -> rewrite the sentence
    stands (a rule or value)       -> rewrite the sentence
    earns | earns its place        -> rewrite the sentence
    owes | owed                    -> rewrite the sentence
    holds (a value or meaning)     -> rewrite the sentence
    rotted | rots                  -> rewrite the sentence
    accurate (as filler)           -> rewrite the sentence
    ceremony                       -> rewrite the sentence
    sealed                         -> rewrite the sentence
    

    These words are grounded in this repository and are NOT on the list: gate, mint, fan-out, round, ledger, canonical, attest, invariant, in flight, pin, drift, reconcile, sweep, harden, posture, hygiene, and the noun surface (attack surface, consent surface, ConsentSurfaceName). Neither are the terms of art -- honest-but-curious, semi-honest, honest party, the PSI round and round-trip, Promise settlement -- nor any identifier, filename, type name, fixture, or schema key.

  • Oxford comma: a list of three or more items puts a comma before its final "and" or "or": "keys, fields, and cleaning". User-visible strings and the docs under docs/ follow it; other text takes it as each file is touched.

  • Option names in running text: an option's own label -- on a radio button, checkbox, switch, or dropdown entry -- is in sentence case ("Broader matching"). Any other text that names the option, in the apps, the CLI, the docs, or the changelog, writes it in lowercase except at the start of a sentence: "choose broader matching".

  • Failure messages: a failure an operator sees states what happened in one plain sentence and what to do in a second, naming only the flag or control that applies to that run. A failure cause the CLI reports from core's failure-cause catalog takes its first sentence there and its second from the CLI's remedy map; which apps draw on the catalog is stated in the note below. Where the two live and how counts and error codes are written: docs/notes/failure-cause-catalog.md.

  • Markdown: an existing file keeps the wrap style it has, soft or hard, and is never reflowed; a new file uses semantic line breaks, starting each sentence or clause on its own line. Single space after periods, ASCII punctuation (- not em-dash, -> not arrow character).

  • Capital-letter emphasis: words set in capitals for emphasis come out of code comments and user docs as each file is touched. Agent-only rule text (CLAUDE.md and the files under .claude/) keeps them, where they mark the few lines an agent must not miss.

Linting, formatting, and the repository's guard checks are enforced by CI. Run them locally before pushing:

sh
npm run typecheck
npm run lint
npm run format
npm run check:all            # the repo-wide guards, driven from one list
npm run check:all -- --list  # what each covers, and what it does not run

Run these repo-wide commands once, after the last edit and after the tests for your change pass. While working, check only what you changed: npx eslint <paths>, npx tsc -p <workspace>/tsconfig.json --noEmit, and npx vitest run <file> for the test files you touched. Each repo-wide command reads the whole repository, and several agents running them at once on one machine slow every one of them.

check:all drives its checks from one list in scripts/run-checks.mjs, which also states which checks it does not run and why -- the production dependency audit needs the npm registry, and the rest need a token, a release tag, CI's own install, or minutes the merge path does not have. It runs past a failing check, so one red result does not hide the rest.

Every entry on that list and every hook .claude/settings.json registers states an expiry date, which npm run check:expiry-dates enforces: a proposal for a new check or hook states its date, and the review at each retro of the entries coming due -- renew each with a new date or delete it -- is in the header of scripts/check-expiry-dates.mjs.

Documentation ​

Alcove documentation is three-tier:

  • docs/ (overview) - conceptual and operational documents for program officers, security reviewers, compliance officers, IT staff, and contributors.
  • docs/spec/ - the technical specification tier: wire formats, byte encodings, normative constant values, protocol internals, and implementation-level design, for implementors and auditors. See docs/spec/README.md for the index and routing guide.
  • docs/notes/ - tracked, citeable design records and explorations: the model behind a mechanism, the options weighed, and the decisions taken. Non-normative - nothing here binds an implementation, and a note points at the spec for the normative rows rather than restating them. Indexed by docs/notes/README.md, which sets out the maturity ladder from scratch/ up to the formal tiers.

When behavior changes, update the matching tier:

  • User-configurable or operational behavior -> the relevant overview doc in docs/.
  • Wire format, protocol internals, or implementation-level spec -> the relevant docs/spec/ file.
  • A change touching both tiers updates both.

If you are writing a constant value, a byte/wire layout, an HKDF info string or other algorithm step, or the "would only need revisiting if..." rationale behind one of those, it belongs in docs/spec/ - regardless of which doc you currently have open. Overview docs (docs/) stay conceptual and operational, including operational rationale such as the coverage-gate decision.

All three documentation tiers (docs/, docs/spec/, docs/notes/) must stay scannable: no multi-hundred-word paragraphs -- use subheadings and lists. When an edit lands in a section that is already a wall of text, restructure the section rather than growing a sentence in place. One exception: a docs/spec/ paragraph holding a MUST, SHOULD, MUST NOT, or NEVER is not split by this rule, because the keyword binds the sentences of its own paragraph and splitting it changes what the spec requires -- that is a spec change, made on its own. The rule reads literally: only the uppercase keyword freezes a paragraph, not the lowercase word. Wherever spec prose is split or moved, a sentence stating an exception or limit, or holding "only" or "unless", stays with the claim it qualifies. Spec prose with no normative keyword stays inside the bound. A pull request changing docs/spec/ lists each sentence it adds, drops, or rewords in its own file, docs/spec/wording-changes/<branch>.txt (format), so a moved sentence is shown unchanged. The check runs on pull requests into staging only, since a release pull request to main spans several lists that were each checked on entering staging; format and check: scripts/check-spec-sentence-moves.mjs.

Write documentation as the target state, not a narration of what changed -- no "now", "previously", or "no longer"; the reader cannot see the diff, and change history belongs in the commit message. This applies in all three tiers and in source comments. check:all enforces the source-comment half over the comment lines a change adds or modifies, and names the override for the rare legitimate use; the documentation tiers are review's.

Use an obviously fake placeholder for any credential-shaped value in an example config or sample document -- GitHub push protection rejects a push whose commits add a real-looking secret anywhere, docs and example YAML included, and recovering means amending the offending commit rather than adding a follow-up on top of it.

The governance documents -- SECURITY.md, PRIVACY.md, docs/INCIDENT_RESPONSE.md, docs/SECURITY_DESIGN.md, docs/COMPLIANCE.md, and docs/SHARED_RESPONSIBILITY.md -- have review_owner and last_reviewed in their YAML front matter, so an agency reviewer can see who owns each one and when it was last confirmed correct without reading the git history. The owner is a role rather than a person, so it survives a change of maintainer. A substantive change to one of these -- anything that alters what the document asserts, as opposed to a typo, link, or formatting fix -- sets its last_reviewed to the date the change lands. The date means someone read the document and found it still correct, so leave it where it is for an edit that was not that.

Documentation-tier placement is in scope for code review: a reviewer flags spec-level detail written into a docs/ overview doc.

A figure in the docs (an SVG under docs/img/) is made or revised by the doc-figure skill's method, .claude/skills/doc-figure/SKILL.md.

Changelog ​

CHANGELOG.md is reader-facing release notes for whoever runs or vets Alcove from outside this repo, browsing to learn what it does and whether it is worth adopting -- not a second copy of the git history. It is a short list of the product's headline capabilities, not a record of the work done.

  • Pre-release, the default answer to "does this PR need a changelog entry?" is no. Add one only for a major feature -- a new capability a reader browsing the repo needs to know exists -- or a breaking change to something already listed. Everything else is skipped: individual flags and config fields, UI polish, operational and error-handling refinements, bug fixes, changed defaults, exit-code and format tweaks, internal refactors, test/CI/tooling changes, @alcove/core API reshapes, and doc-only edits. When in doubt, leave it out; a reviewer adds an entry back far more cheaply than the log recovers from bloat.
  • One or two lines per entry, stating the capability; push rationale and wire detail to docs/, docs/spec/, or the PR and link it with a trailing See docs/....
  • Group under Added / Changed / Deprecated / Removed / Fixed / Security; prefix a breaking change BREAKING:. Keep the Security section to the headline security posture, not each hardening change.
  • Once there are releases operators upgrade between, the bar drops to any change a reader acts on when deciding whether to upgrade -- a changed default, behavior, exit code, wire/on-disk format, or security fix -- and security entries are recorded exhaustively per release. Until then, hold the higher bar.

Commit Messages ​

  • Imperative mood, present tense: "Fix key rotation after failed exchange", not "Fixed ..." or "Fixes ...".
  • Subject line 50 characters or fewer. For a squash-merge message that budget includes the " (#NNNN)" pull-request suffix GitHub appends on merge, so draft the subject itself to roughly 42 characters; the PR title carries this budget, and npm run check:pr-checklist enforces it there as the field the check can measure -- for a multi-commit PR the title becomes the squash subject, while a single-commit PR's landing subject is its commit's own message, already governed by this same rule.
  • Include a body for non-trivial commits explaining motivation and context, not just what changed. Hard-wrap body lines at roughly 70 characters. A squash body is at most 100 words, an arbitrary working bound, and the formatter below refuses a longer one.
  • A pull request is squash-merged, so its squash message is the commit that lands. GitHub fills the merge box's subject from the PR title, so a pull request with more than one commit gets the message's BODY only, posted when the PR is opened for review, or before it is marked ready from a draft, as one comment on the PR holding a single fenced code block, which the maintainer pastes into the body field without change. No copy is kept anywhere else. A pull request with a single commit gets no squash comment: GitHub takes that commit's own message, so the writing goes into the commit itself.
  • The body wrap above is checked rather than remembered. node .claude/scripts/format-squash-message.mjs [<draft-path>] [--fenced] rewraps a draft body, drops the markdown and list markers a commit message does not take, and reports what it cannot fix without rewriting the body; --fenced prints the fenced comment, posted with gh pr comment <pr-number> --body-file <file>.

Pull Request Process ​

  1. For significant changes, open a draft issue on the Github project first to align on approach. Bug fixes and documentation improvements do not require a prior issue.
  2. Keep pull requests focused - one logical change per PR.
  3. Ensure typecheck, lint, format, and the relevant tests pass before marking the PR ready for review (CI enforces all four). Record what you ran and the coverage you added in the PR's Test plan, and resolve every line of the template Checklist.
  4. Changes within the security-review scope -- cryptographic code, the channel-security controls, credential or disclosure surfaces, or a security-relevant dependency -- require explicit security review before merging (see Dependency Policy for the full enumeration).
  5. Update documentation when behavior changes - see Documentation for which tier. Whether the change needs a CHANGELOG.md entry is set by Changelog; before the first release, most changes need none.
  6. A maintainer will review and squash-merge. A pull request green on every required check merges as-is, without being updated to the staging tip first. The static checks, the CLI gate, CodeQL and the native Alpine gate run again on every push to staging, and the web suite runs there through the deploy workflow when a push changes what it deploys, so a conflict between two pull requests that were each green on their own shows on the merged tip; dependency review compares one pull request's dependency changes, so it runs per pull request only. scripts/check-merge-gate-identities.mjs compares the required checks against the live rulesets: each names a job that exists, and every workflow declaring one is listed in GATING_WORKFLOWS and carries no path filter. Force-pushes to main are not permitted.

Pull Request Description ​

Opening a PR populates .github/PULL_REQUEST_TEMPLATE.md, whose inline comments explain each section and the writing conventions (ASCII, imperative mood, ## headings, board-reference verbs). Fill in what applies; delete any optional section with nothing non-obvious to say -- keep small PRs small.

Dependency Policy ​

A change requires explicit security review and maintainer approval before merging if it touches any of the following. This list is the trigger; docs/SECURITY_DESIGN.md is the model behind it, consulted only to decide a case the list does not settle.

  • Cryptographic code or its inputs: the PSI protocol, the P-256 ECDH key exchange / handshake / key schedule, token generation / rotation / derivation, or canonical encoding.
  • The application-layer AEAD: encryption, the nonce / sequence scheme, or the integrity / replay / reordering / gap checks.
  • The channel-hardening controls: the frame-size, directory-listing, liveness / timeout, connect-probe, and whole-exchange bounds, the web WebRTC data-channel inbound bound, plus the SFTP crash-safety and authenticated abort-marker controls.
  • Credential and secret handling: how the key file, signing identity, or result CSV is written or permissioned; how secrets are stored, transmitted, logged, or referenced (the configuration @path resolution).
  • Authentication and identity: the auth gate's fail-closed behavior, fingerprint / certificate pinning and verification, or token expiry and token_max_age_days enforcement.
  • What is disclosed: any change to what is sent on the wire, logged, displayed, or written to disk, or to what the result reveals (cardinality, linkage terms, consent-surfaced fields).
  • A security-relevant dependency (see Cryptographic dependencies, the SFTP stack, and the WebRTC stack below).

Modifying an existing control in these areas is in scope exactly as adding one is: a change that weakens or removes a guarantee triggers review no less than a new control does.

Alcove is licensed under Apache 2.0; add third-party dependencies conservatively. For every new dependency:

  1. Confirm the license permits Apache 2.0 distribution. Copyleft licenses (GPL, AGPL) are not compatible. The Dependency Review workflow enforces this automatically for the strong-copyleft GPL/AGPL family via its deny-licenses blocklist, failing any PR that introduces a dependency under one. That gate catches part of the rule, not all of it: it fails only on a declared denied SPDX id, so a passing check is not proof a dependency is clean -- one that ships no license metadata (or NOASSERTION) is reported but does not fail it. This review stays the authority for weak copyleft (LGPL, MPL), whose acceptability is linkage-dependent, and for any dependency whose license the action cannot resolve or that declares none (exempt a mis-flagged dependency with the workflow's allow-dependencies-licenses and clear it here). Both this rule and that gate scope to the npm tree. Each image's OS layer ships its base distribution's packages, some declaring GPL-3.0 or LGPL-3.0 terms, and is attributed instead in the lists beside NOTICE; see docs/spec/CONTAINER_IMAGES.md.
  2. Run npm audit --omit=dev -w packages/core -w apps/cli -w apps/web -- the scope the shipped image runs -- and resolve any known vulnerabilities before merging. Static Checks runs that exact command as a merge gate on every pull request and fails on any finding, so this is a faster local answer rather than the only one; resolve a red gate by a reviewed bump, never by npm audit fix. A finding outside that scope is a development-tree finding, reported by the scheduled Dependency Audit and triaged separately rather than at merge time; see docs/spec/DEPENDENCY_PINS.md.
  3. Prefer packages that are actively maintained and publish a security policy.
  4. If the package ships its own NOTICE file and is redistributed to end users, fold its attribution into the top-level NOTICE.

On a dependency-bump pull request, scope the review by the changed (+/-) manifest lines against this policy, and treat a green check on a PR opened days ago as stale until it is re-run against the current staging (@dependabot rebase).

Cryptographic dependencies - @openmined/psi.js and any AEAD, key-agreement, or key-derivation library - require explicit security review and maintainer approval before merging. These libraries underpin the privacy and integrity guarantees of every exchange. Dependency upgrades driven by security advisories take priority over feature work.

The SFTP stack (ssh2 / ssh2-sftp-client), the WebRTC stack (peerjs / peerjs-js-binarypack), and the CLI WebRTC peer (werift) are reached past their public APIs into internals, so each is exact-pinned (in apps/cli/package.json, apps/web/package.json, and apps/cli/package.json respectively). Every bump is a deliberate, security-reviewed edit -- never an npm audit fix that slips in unreviewed -- and must re-verify the internal assumptions first. Why they are pinned, the assumptions, and the per-stack upgrade checklist: docs/spec/DEPENDENCY_PINS.md.

Per-dependency licenses for the npm tree are recorded in the CycloneDX SBOM attached to each release - every direct and transitive dependency it resolves, with that dependency's license; see docs/RELEASES.md for the scope it is generated at and the residuals it leaves. Attributions for redistributed and vendored components are in the top-level NOTICE, which covers this repository's npm tree; each image's OS packages are listed beside it in NOTICE-os-packages-default.tsv and NOTICE-os-packages-fips.tsv, generated from the built image and compared against the committed list by Image Smoke.

Export Control ​

Alcove incorporates cryptographic software. Distribution may be subject to U.S. Export Administration Regulations (EAR). Most open-source cryptographic software qualifies for License Exception ENC under ECCN 5D002, but the exception requires a one-time notification to BIS and NSA. This notification is pending and will be completed before the 1.0 release. See docs/COMPLIANCE.md for the full regulatory framing.

Reporting Security Issues ​

Do not open a public issue for security vulnerabilities. See SECURITY.md for the private reporting process.

Reporting Other Issues ​

Open a GitHub issue. Include the version (Docker image tag or package.json version), the operating system, and a minimal reproducing case.