@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.
- package/LICENSE +21 -0
- package/README.md +209 -2
- package/SECURITY.md +87 -0
- package/dist/contracts.d.ts +133 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +29 -0
- package/dist/contracts.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +44 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +260 -0
- package/dist/options.js.map +1 -0
- package/dist/plugin.d.ts +24 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +215 -0
- package/dist/plugin.js.map +1 -0
- package/dist/relay/index.d.ts +37 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/index.js +480 -0
- package/dist/relay/index.js.map +1 -0
- package/dist/remote.d.ts +62 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +227 -0
- package/dist/remote.js.map +1 -0
- package/dist/rpc.d.ts +112 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +117 -0
- package/dist/rpc.js.map +1 -0
- package/dist/services/process.d.ts +56 -0
- package/dist/services/process.d.ts.map +1 -0
- package/dist/services/process.js +208 -0
- package/dist/services/process.js.map +1 -0
- package/dist/tui.d.ts +59 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +93 -0
- package/dist/tui.js.map +1 -0
- package/dist/varlock/manager.d.ts +6 -0
- package/dist/varlock/manager.d.ts.map +1 -0
- package/dist/varlock/manager.js +425 -0
- package/dist/varlock/manager.js.map +1 -0
- package/dist/varlock/protocol.d.ts +67 -0
- package/dist/varlock/protocol.d.ts.map +1 -0
- package/dist/varlock/protocol.js +248 -0
- package/dist/varlock/protocol.js.map +1 -0
- package/docs/architecture.md +179 -0
- package/docs/quality.md +102 -0
- package/docs/releasing.md +149 -0
- package/docs/troubleshooting.md +214 -0
- package/examples/.env.schema +17 -0
- package/examples/opencode.jsonc +44 -0
- 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.
|
package/docs/quality.md
ADDED
|
@@ -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.
|