@bjorntech/key-ferry 0.0.0-stage → 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -2
  3. package/SECURITY.md +87 -0
  4. package/dist/contracts.d.ts +133 -0
  5. package/dist/contracts.d.ts.map +1 -0
  6. package/dist/contracts.js +29 -0
  7. package/dist/contracts.js.map +1 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +12 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/options.d.ts +44 -0
  13. package/dist/options.d.ts.map +1 -0
  14. package/dist/options.js +260 -0
  15. package/dist/options.js.map +1 -0
  16. package/dist/plugin.d.ts +24 -0
  17. package/dist/plugin.d.ts.map +1 -0
  18. package/dist/plugin.js +215 -0
  19. package/dist/plugin.js.map +1 -0
  20. package/dist/relay/index.d.ts +37 -0
  21. package/dist/relay/index.d.ts.map +1 -0
  22. package/dist/relay/index.js +480 -0
  23. package/dist/relay/index.js.map +1 -0
  24. package/dist/remote.d.ts +62 -0
  25. package/dist/remote.d.ts.map +1 -0
  26. package/dist/remote.js +227 -0
  27. package/dist/remote.js.map +1 -0
  28. package/dist/rpc.d.ts +112 -0
  29. package/dist/rpc.d.ts.map +1 -0
  30. package/dist/rpc.js +117 -0
  31. package/dist/rpc.js.map +1 -0
  32. package/dist/services/process.d.ts +56 -0
  33. package/dist/services/process.d.ts.map +1 -0
  34. package/dist/services/process.js +208 -0
  35. package/dist/services/process.js.map +1 -0
  36. package/dist/tui.d.ts +59 -0
  37. package/dist/tui.d.ts.map +1 -0
  38. package/dist/tui.js +93 -0
  39. package/dist/tui.js.map +1 -0
  40. package/dist/varlock/manager.d.ts +6 -0
  41. package/dist/varlock/manager.d.ts.map +1 -0
  42. package/dist/varlock/manager.js +425 -0
  43. package/dist/varlock/manager.js.map +1 -0
  44. package/dist/varlock/protocol.d.ts +67 -0
  45. package/dist/varlock/protocol.d.ts.map +1 -0
  46. package/dist/varlock/protocol.js +248 -0
  47. package/dist/varlock/protocol.js.map +1 -0
  48. package/docs/architecture.md +179 -0
  49. package/docs/quality.md +102 -0
  50. package/docs/releasing.md +149 -0
  51. package/docs/troubleshooting.md +214 -0
  52. package/examples/.env.schema +17 -0
  53. package/examples/opencode.jsonc +44 -0
  54. package/package.json +96 -4
@@ -0,0 +1,248 @@
1
+ /** Pure validation for the Varlock proxy CLI protocol.
2
+ *
3
+ * This module deliberately does not know about process.env, subprocesses, or
4
+ * Effect. Treat every value passed to it as hostile CLI output. In
5
+ * particular, a session record is not a proof of ownership until the caller
6
+ * supplies the PID, UUID, and project directory it created.
7
+ */
8
+ export const PROTOCOL_LIMITS = {
9
+ jsonBytes: 2 * 1024 * 1024,
10
+ sessions: 128,
11
+ mapEntries: 512,
12
+ stringBytes: 16 * 1024,
13
+ envKeyLength: 256,
14
+ };
15
+ const SAFE_ENV_KEYS = new Set([
16
+ "HTTP_PROXY",
17
+ "HTTPS_PROXY",
18
+ "ALL_PROXY",
19
+ "http_proxy",
20
+ "https_proxy",
21
+ "all_proxy",
22
+ "NO_PROXY",
23
+ "no_proxy",
24
+ "NODE_EXTRA_CA_CERTS",
25
+ "SSL_CERT_FILE",
26
+ "REQUESTS_CA_BUNDLE",
27
+ "CURL_CA_BUNDLE",
28
+ "GIT_SSL_CAINFO",
29
+ "CARGO_HTTP_CAINFO",
30
+ "DENO_CERT",
31
+ "NODE_USE_ENV_PROXY",
32
+ ]);
33
+ const PROXY_KEYS = [
34
+ "HTTP_PROXY",
35
+ "HTTPS_PROXY",
36
+ "ALL_PROXY",
37
+ "http_proxy",
38
+ "https_proxy",
39
+ "all_proxy",
40
+ ];
41
+ export class VarlockProtocolError extends Error {
42
+ code = "protocol_error";
43
+ constructor(message = "invalid Varlock protocol response") {
44
+ // Never interpolate CLI output into this message. Callers can safely
45
+ // expose it in diagnostics and model-visible errors.
46
+ super(message);
47
+ this.name = "VarlockProtocolError";
48
+ }
49
+ }
50
+ function fail(message) {
51
+ throw new VarlockProtocolError(message);
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+ function boundedString(value, field, max = PROTOCOL_LIMITS.stringBytes) {
57
+ if (typeof value !== "string" ||
58
+ value.length === 0 ||
59
+ value.length > max ||
60
+ value.includes("\0")) {
61
+ fail(`invalid Varlock ${field}`);
62
+ }
63
+ return value;
64
+ }
65
+ function boundedMap(value, field) {
66
+ if (!isRecord(value) || Object.keys(value).length > PROTOCOL_LIMITS.mapEntries)
67
+ fail(`invalid Varlock ${field}`);
68
+ const result = Object.create(null);
69
+ for (const key of Object.keys(value)) {
70
+ if (key === "__proto__" ||
71
+ key === "prototype" ||
72
+ key === "constructor" ||
73
+ key.length === 0 ||
74
+ key.length > PROTOCOL_LIMITS.envKeyLength ||
75
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
76
+ fail(`invalid Varlock ${field}`);
77
+ }
78
+ result[key] = boundedString(value[key], `${field} value`);
79
+ }
80
+ return result;
81
+ }
82
+ function parseJson(input) {
83
+ if (typeof input !== "string" ||
84
+ new TextEncoder().encode(input).byteLength > PROTOCOL_LIMITS.jsonBytes) {
85
+ fail("invalid Varlock JSON payload");
86
+ }
87
+ try {
88
+ return JSON.parse(input);
89
+ }
90
+ catch {
91
+ fail("invalid Varlock JSON payload");
92
+ }
93
+ }
94
+ function decodeSession(value) {
95
+ if (!isRecord(value))
96
+ fail("invalid Varlock session record");
97
+ const egressMode = value.egressMode;
98
+ if (egressMode !== "permissive" && egressMode !== "strict")
99
+ fail("invalid Varlock egress mode");
100
+ if (!Number.isSafeInteger(value.ownerPid) || value.ownerPid <= 0)
101
+ fail("invalid Varlock owner PID");
102
+ const session = {
103
+ id: boundedString(value.id, "session id", 128),
104
+ uuid: boundedString(value.uuid, "session UUID", 128),
105
+ ownerPid: value.ownerPid,
106
+ cwd: boundedString(value.cwd, "session cwd"),
107
+ startedAt: boundedString(value.startedAt, "session start time", 128),
108
+ updatedAt: boundedString(value.updatedAt, "session update time", 128),
109
+ ...(value.endedAt !== undefined
110
+ ? { endedAt: boundedString(value.endedAt, "session end time", 128) }
111
+ : {}),
112
+ egressMode,
113
+ env: boundedMap(value.env, "session environment"),
114
+ };
115
+ if (value.placeholderOverrides !== undefined) {
116
+ session.placeholderOverrides =
117
+ boundedMap(value.placeholderOverrides, "placeholder overrides");
118
+ }
119
+ return session;
120
+ }
121
+ /** Decode only the JSON array emitted by `varlock proxy status --format json`. */
122
+ export function decodeSessionList(input) {
123
+ const value = parseJson(input);
124
+ if (!Array.isArray(value) || value.length > PROTOCOL_LIMITS.sessions)
125
+ fail("invalid Varlock session list");
126
+ return value.map(decodeSession);
127
+ }
128
+ /** Exact ownership match. No cwd-prefix or PID-only matching is accepted. */
129
+ export function findOwnedSession(sessions, identity) {
130
+ const directory = boundedString(identity.projectDirectory, "project directory");
131
+ if (!Number.isSafeInteger(identity.ownerPid) || identity.ownerPid <= 0)
132
+ fail("invalid owner PID");
133
+ const uuid = boundedString(identity.sessionUuid, "session UUID", 128);
134
+ return sessions.find((session) => session.ownerPid === identity.ownerPid &&
135
+ session.uuid === uuid &&
136
+ session.cwd === directory &&
137
+ session.endedAt === undefined);
138
+ }
139
+ function validateProxyUrl(value) {
140
+ let url;
141
+ try {
142
+ url = new URL(value);
143
+ }
144
+ catch {
145
+ fail("invalid Varlock proxy URL");
146
+ }
147
+ if (url.protocol !== "http:" ||
148
+ (url.hostname !== "127.0.0.1" &&
149
+ url.hostname !== "localhost" &&
150
+ url.hostname !== "[::1]" &&
151
+ url.hostname !== "::1")) {
152
+ fail("Varlock proxy must use a loopback HTTP URL");
153
+ }
154
+ if (url.username ||
155
+ url.password ||
156
+ url.search ||
157
+ url.hash ||
158
+ (url.pathname !== "/" && url.pathname !== "")) {
159
+ fail("invalid Varlock proxy URL");
160
+ }
161
+ const port = Number(url.port);
162
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
163
+ fail("invalid Varlock proxy port");
164
+ return url;
165
+ }
166
+ /** Validate wiring without returning credentials or writing ambient env state. */
167
+ export function validateProxyWiring(env) {
168
+ const keys = Object.keys(env);
169
+ if (keys.length > PROTOCOL_LIMITS.mapEntries ||
170
+ keys.some((key) => key === "__proto__" ||
171
+ key === "prototype" ||
172
+ key === "constructor" ||
173
+ key.length > PROTOCOL_LIMITS.envKeyLength ||
174
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)))
175
+ fail("invalid Varlock proxy environment");
176
+ const https = env.HTTPS_PROXY ?? env.https_proxy;
177
+ const http = env.HTTP_PROXY ?? env.http_proxy;
178
+ if (!https || !http)
179
+ fail("Varlock proxy environment is incomplete");
180
+ const proxyUrl = validateProxyUrl(https);
181
+ if (validateProxyUrl(http).origin !== proxyUrl.origin)
182
+ fail("Varlock proxy URLs do not match");
183
+ for (const key of PROXY_KEYS) {
184
+ if (env[key] !== undefined && validateProxyUrl(env[key]).origin !== proxyUrl.origin)
185
+ fail("Varlock proxy URLs do not match");
186
+ }
187
+ const caPath = env.NODE_EXTRA_CA_CERTS;
188
+ if (!caPath ||
189
+ !caPath.startsWith("/") ||
190
+ caPath.length > PROTOCOL_LIMITS.stringBytes ||
191
+ caPath.endsWith("/")) {
192
+ fail("invalid Varlock CA certificate path");
193
+ }
194
+ if (caPath.split("/").includes("..") || caPath.split("/").includes("."))
195
+ fail("invalid Varlock CA certificate path");
196
+ if (!caPath.endsWith("/ca-cert.pem"))
197
+ fail("invalid Varlock CA certificate path");
198
+ // Session env contains the child view as well as wiring. Return only the
199
+ // documented wiring allowlist, never arbitrary child keys or credentials.
200
+ const wiringEnv = Object.create(null);
201
+ for (const key of keys)
202
+ if (SAFE_ENV_KEYS.has(key))
203
+ wiringEnv[key] = env[key];
204
+ return { proxyUrl: proxyUrl.origin, caPath, env: wiringEnv };
205
+ }
206
+ /**
207
+ * Select explicitly requested child keys. A key is safe only when the session
208
+ * record has an exact placeholder assignment and the full env contains that
209
+ * exact placeholder. Prefixes are intentionally not trusted. Keys absent from
210
+ * placeholderOverrides (including @proxy=passthrough values) are rejected.
211
+ */
212
+ export function selectPlaceholderEnvironment(fullEnv, session, requestedKeys) {
213
+ if (!Array.isArray(requestedKeys) || requestedKeys.length > PROTOCOL_LIMITS.mapEntries)
214
+ fail("invalid requested Varlock keys");
215
+ const overrides = session.placeholderOverrides;
216
+ if (!overrides)
217
+ fail("Varlock session has no placeholder assignments");
218
+ const selected = Object.create(null);
219
+ const seen = new Set();
220
+ for (const key of requestedKeys) {
221
+ if (seen.has(key))
222
+ fail("duplicate requested Varlock key");
223
+ seen.add(key);
224
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key.length > PROTOCOL_LIMITS.envKeyLength)
225
+ fail("invalid requested Varlock key");
226
+ const placeholder = overrides[key];
227
+ if (placeholder === undefined || fullEnv[key] !== placeholder)
228
+ fail("requested key is not proven to be placeholder-backed");
229
+ selected[key] = placeholder;
230
+ }
231
+ return selected;
232
+ }
233
+ /** Safe status allowlist; deliberately drops tokens, env maps, and placeholders. */
234
+ export function sanitizeSessionStatus(session, running) {
235
+ const wiring = validateProxyWiring(session.env);
236
+ return {
237
+ id: session.id,
238
+ uuid: session.uuid,
239
+ ownerPid: session.ownerPid,
240
+ cwd: session.cwd,
241
+ startedAt: session.startedAt,
242
+ updatedAt: session.updatedAt,
243
+ egressMode: session.egressMode,
244
+ proxyUrl: wiring.proxyUrl,
245
+ running,
246
+ };
247
+ }
248
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/varlock/protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,179 @@
1
+ # OpenCode V2 and Varlock architecture
2
+
3
+ ## Runtime design
4
+
5
+ The runtime is Effect-native. The installed OpenCode V2 plugin package
6
+ exposes a separate native entrypoint, `@opencode/plugin/effect`, and its
7
+ dependencies pin `effect` to **4.0.1**. The Promise API is not the foundation
8
+ for this project and will not be wrapped superficially.
9
+
10
+ Verified package facts from `@opencode/plugin@2.0.30`:
11
+
12
+ ```ts
13
+ import { Plugin } from "@opencode/plugin/effect"
14
+
15
+ export default Plugin.define({
16
+ id: "key-ferry",
17
+ effect: (ctx) => Effect.gen(function* () {
18
+ // Native Effect plugin setup.
19
+ }),
20
+ })
21
+ ```
22
+
23
+ The native plugin type is `effect(context): Effect.Effect<void, never, R>`.
24
+ Native registrations and transforms return Effects, and registration disposal
25
+ is also an Effect. `ctx.mcp.transform(...)` and `ctx.mcp.reload()` are native
26
+ Effect APIs. See the V2 [plugin](https://opencode.ai/v2/docs/build/plugins),
27
+ [RPC](https://opencode.ai/v2/docs/build/plugins/rpc), and
28
+ [MCP](https://opencode.ai/v2/docs/mcp-servers) documentation.
29
+
30
+ ## Manager boundary
31
+
32
+ `src/contracts.ts` defines the boundary for the sidecar manager and OpenCode
33
+ adapter:
34
+
35
+ - `ProjectKeyFerryManager` is an Effect `Context.Service` tag.
36
+ - `ProjectKeyFerryManagerFactory.layer()` returns a typed `Layer`.
37
+ - Sidecar acquisition belongs in a scoped layer using
38
+ `Effect.acquireRelease`.
39
+ - The process must be owned by exact PID plus generated ownership/session ID.
40
+ - No ambiguous daemon adoption and no global `stop --all` operation.
41
+ - `ensureStarted`, `refresh`, and teardown must cooperate with interruption.
42
+ - Lifecycle and refresh operations must be serialized internally, represented
43
+ by `withExclusive`.
44
+ - `dispose` is idempotent and only stops resources owned by this manager.
45
+ - Varlock's public CA material is handled as trust material with the required
46
+ public-readable `0644` permissions; it is not treated as a credential.
47
+
48
+ The process service is injectable through the `VarlockProcess` Effect service,
49
+ so sidecar behavior can be tested without spawning a real process.
50
+
51
+ ## Varlock constraints encoded by the contract
52
+
53
+ - Never call the Varlock loader from the service if it mutates global
54
+ `process.env`.
55
+ - The foreground proxy must be owned and started with project cwd, explicit
56
+ session, and reload support.
57
+ - `--allow-reload` is mandatory for the owned proxy process.
58
+ - Raw status data may contain endpoint tokens, data-plane tokens, environment
59
+ values, and placeholder overrides. It must remain inside the protocol
60
+ adapter.
61
+ - Public `SafeStatus` contains only phase, project path, generation, optional
62
+ PID/endpoint metadata, timestamps, sanitized tagged errors, and safe
63
+ remote-configuration diagnostics.
64
+ - `ResolvedEnvironment` is an internal capability only. It must not cross RPC,
65
+ CLI, logging, or model-context boundaries.
66
+ - Never forward Varlock's complete `--full` environment map. It may contain
67
+ placeholders and raw `@proxy=passthrough` secrets. The protocol adapter must
68
+ enforce the later-approved placeholder-only allowlist.
69
+ - Refresh must have a bounded timeout, account for the CLI's fifteen-minute
70
+ maximum, and fail closed when refresh fails.
71
+ - Strict egress is required by default. The manager validates the schema's
72
+ exact `# @proxyConfig={egress="strict"}` header and does not invent an
73
+ unsupported `--allow-domain` flag.
74
+ - Local MCP integration uses the supported environment proxy/CA path. It does
75
+ not pretend that a sandbox provides this isolation.
76
+
77
+ ## MCP integration
78
+
79
+ The adapter has two explicit protection modes under `options.servers`.
80
+
81
+ ```ts
82
+ type ServerProtection =
83
+ | { readonly env: readonly string[] | Readonly<Record<string, string>> }
84
+ | {
85
+ readonly remote: {
86
+ readonly headers: Readonly<Record<string, string | {
87
+ readonly env: string
88
+ readonly prefix?: string
89
+ readonly suffix?: string
90
+ }>>
91
+ }
92
+ }
93
+ ```
94
+
95
+ Local mappings are injected into the selected local MCP process environment.
96
+ Remote mappings are only accepted for a configured `type: "remote"` server
97
+ with explicit `oauth: false`. A selected remote server with OAuth omitted or
98
+ configured is disabled with a safe diagnostic. An unselected remote server is
99
+ left to native OpenCode transport and OAuth handling.
100
+
101
+ OpenCode V2 exposes `ctx.mcp.transform`, `ctx.mcp.reload`, and the mutable
102
+ `Mcp.ServerConfig`, but no public per-server fetch, dispatcher, transport, or
103
+ registry extension. Remote protection therefore uses a plugin-owned relay:
104
+
105
+ ```text
106
+ OpenCode Streamable HTTP/SSE client
107
+ -> authenticated 127.0.0.1 relay
108
+ -> Varlock HTTP CONNECT proxy
109
+ -> fixed HTTPS MCP origin/path
110
+ ```
111
+
112
+ The transformed MCP registry contains only the relay URL, the scoped relay
113
+ capability, and safe non-credential headers. Mapped credential headers are
114
+ constructed inside the relay from placeholder values. The persisted OpenCode
115
+ configuration is never rewritten.
116
+
117
+ The relay binds to loopback and uses a per-instance token. It forwards
118
+ Streamable HTTP POST/GET/DELETE semantics, streams SSE, preserves MCP session
119
+ headers, aborts upstream work on downstream cancellation, and rejects
120
+ downstream paths that were not registered from the fixed upstream origin. Its
121
+ legacy SSE endpoint rewrite is implemented. Registered session routes are
122
+ capped at 256; duplicate routes do
123
+ not consume capacity and new routes are rejected after the cap. The remote
124
+ Varlock integration is covered by an opt-in synthetic HTTPS fixture.
125
+
126
+ Relay TLS uses CONNECT followed by one verified `tls.connect` call with the
127
+ manager CA, target `servername`, and `rejectUnauthorized: true`. The custom
128
+ HTTP agent returns that verified TLS socket for HTTP framing. No global TLS
129
+ flags are changed and no certificate verification is weakened. Compressed or
130
+ oversized response framing is bounded and fails closed.
131
+
132
+ Remote target validation requires HTTPS, rejects URL credentials and fragments,
133
+ and rejects literal local/private IP targets by default. An explicit
134
+ `allowPrivateNetwork` policy can opt into such targets. Corporate DNS names
135
+ are not rejected merely because they may resolve to internal addresses. The
136
+ relay never accepts an arbitrary downstream URL.
137
+
138
+ Manager generation is part of relay ownership. On refresh, sidecar loss, or
139
+ relay health failure, old relays are stopped, old local capabilities become
140
+ invalid, MCP sessions are reset through `ctx.mcp.reload()`, and protected
141
+ servers remain disabled until replacement succeeds.
142
+
143
+ Doctor runs the normal Varlock readiness/environment reconciliation and then
144
+ reconciles or recreates unhealthy selected `oauth:false` remote relays. It does
145
+ not provide a credential prompt, replace native OAuth, or silently restore a
146
+ direct remote connection.
147
+
148
+ Verification uses native V2 types, mocks, package imports, and synthetic HTTPS
149
+ remote transport. The normal suite and gated Varlock fixtures are separate
150
+ verification layers; neither is a production certification or a fully live
151
+ OpenCode host/TUI run.
152
+
153
+ ## Module ownership
154
+
155
+ 1. `src/contracts.ts`: Effect service tags, layers, scoped lifecycle,
156
+ sanitized errors, process seams, and safe/public versus internal types.
157
+ 2. `src/varlock/protocol.ts`: pure protocol parsing and placeholder allowlist
158
+ validation. Owned by the protocol track.
159
+ 3. `src/varlock/manager.ts`: project-scoped service implementation, serialized
160
+ state transitions, fail-closed readiness, refresh, and acquire/release.
161
+ 4. `src/plugin.ts`: native V2 plugin layer, local/remote MCP transforms, RPC,
162
+ relay lifecycle, and cleanup.
163
+ 5. `src/rpc.ts`: secret-free status and refresh RPC definitions.
164
+ 6. `src/options.ts`: validation of explicit project-scoped configuration.
165
+ 7. `src/tui.ts`: optional status/refresh UX over the RPC shape.
166
+ 8. `src/remote.ts`: adapter-side remote relay contract, target/header
167
+ validation, placeholder wiring, generation coordination, and safe
168
+ diagnostics.
169
+ 9. `src/relay/**`: standalone authenticated HTTP relay implementation.
170
+ 10. Process execution remains behind the injectable process service.
171
+
172
+ The manager must be acquired before MCP/provider integrations are enabled and
173
+ must be released after their registrations are disposed. A failed start or
174
+ refresh leaves protected connections disabled rather than falling back to
175
+ ambient environment variables.
176
+
177
+ Cleanup is scope-owned and project-owned: it stops only the sidecar and relay
178
+ resources acquired by this plugin instance. It does not perform a global
179
+ Varlock shutdown and does not claim full sandbox isolation.
@@ -0,0 +1,102 @@
1
+ # Quality checks
2
+
3
+ This document describes the checks maintained in this repository. It is a
4
+ maintainer reference, not a claim of production certification.
5
+
6
+ ## Typecheck, tests, and build
7
+
8
+ Run the ordinary checks from the repository root:
9
+
10
+ ```sh
11
+ bun typecheck
12
+ bun test
13
+ bun run build
14
+ bun pm pack --dry-run
15
+ ```
16
+
17
+ The normal test suite uses mocks and synthetic fixtures. The optional Varlock
18
+ live fixtures are a separate integration layer: use the globally installed CLI
19
+ (for example, Homebrew's `varlock`) or override it with an absolute
20
+ `VARLOCK_LIVE_BINARY` path. They use synthetic strict-HTTPS credentials and do
21
+ not require provider authentication, production network access, or committed
22
+ secrets. See the live-fixture commands in the README.
23
+
24
+ ## Runtime-source coverage
25
+
26
+ `bun run test:coverage` runs Bun's coverage output and then the repository's
27
+ LCOV checker. The checker keeps only runtime sources under `src/`, including
28
+ contracts and plugin code, and excludes generated code and test files. It
29
+ computes global weighted totals from executable LCOV line records and function
30
+ records; Bun's per-file thresholds are not used.
31
+
32
+ The gate is:
33
+
34
+ - lines: at least 90%
35
+ - functions: at least 75%
36
+
37
+ The check is a runtime-source gate, not a branch-coverage claim. Bun 1.4.2
38
+ does not emit branch coverage in the coverage output used here. CI captures
39
+ the LCOV and text report and uploads both on Ubuntu and macOS. The thresholds
40
+ and source-selection rules are implemented in
41
+ `scripts/check-coverage.ts`, so changes to the gate should update this
42
+ document and that checker together.
43
+
44
+ ## Formatting
45
+
46
+ Use:
47
+
48
+ ```sh
49
+ bun run format
50
+ bun run format:check
51
+ ```
52
+
53
+ Both commands use Oxfmt 0.72.0 and the repository settings aligned with the
54
+ Alchemy formatter configuration referenced by `oxfmt.config.ts`. The settings
55
+ use double quotes, semicolons, 100-column lines, LF endings, trailing commas,
56
+ and import sorting. Markdown and MDX are intentionally ignored by Oxfmt, as
57
+ are generated and coverage directories. This document is therefore reviewed
58
+ as Markdown rather than treated as Oxfmt-formatted source.
59
+
60
+ ## CRAP policy
61
+
62
+ CRAP is a per-function quality gate alongside the aggregate runtime-source
63
+ coverage gate; it is not a security guarantee. The standard per-function score
64
+ is:
65
+
66
+ ```text
67
+ CRAP = CC² × (1 − statement coverage)³ + CC
68
+ ```
69
+
70
+ where `CC` is cyclomatic complexity and own statement coverage is bounded to
71
+ 0–1. Coverage is the ratio of covered executable statements owned by that
72
+ function, not Istanbul's function-hit percentage. A function with `CC > 6`
73
+ cannot pass the threshold of `6`, even at full coverage.
74
+
75
+ Run the gate with:
76
+
77
+ ```sh
78
+ bun run test:crap
79
+ ```
80
+
81
+ The runner instruments a temporary mirror of the TypeScript runtime sources
82
+ with Istanbul, executes that mirror with Bun, and writes the JSON report to
83
+ `coverage/crap.json` in CI (`CRAP_JSON=1 bun run test:crap 2>&1 | tee
84
+ coverage/crap.json`). Instrumentation is temporary and is not production
85
+ instrumentation. Bun does not provide aggregate branch coverage here; this
86
+ policy uses own-statement coverage only.
87
+
88
+ `quality/crap-baseline.json` is a committed historical baseline generated from
89
+ committed `HEAD`, separately from the working-tree metrics. Do not blindly
90
+ regenerate it when the gate fails. A semantic AST fingerprint and qualified
91
+ scope function identity are used, so formatting changes and source-line shifts
92
+ do not reset identity.
93
+
94
+ Unchanged legacy violations warn. New or modified functions with a score above
95
+ 6 fail. An unchanged function also fails if its score worsens from the
96
+ baseline, even when its resulting score remains below 6. A quality run can
97
+ therefore fail while changed functions are being refactored; existing legacy
98
+ warnings do not mean that the whole repository has CRAP ≤ 6.
99
+
100
+ The metric detects complexity and test-coverage regressions; it is not a
101
+ security guarantee. Live fixtures and production integration remain separate
102
+ verification work and are not established by this gate.