@corenel/sidecar 0.11.0 → 0.13.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/README.md +110 -134
- package/dist/cli.js +4653 -1377
- package/dist/helm/corenel-sidecar/.helmignore +4 -0
- package/dist/helm/corenel-sidecar/Chart.yaml +11 -0
- package/dist/helm/corenel-sidecar/templates/NOTES.txt +14 -0
- package/dist/helm/corenel-sidecar/templates/_helpers.tpl +26 -0
- package/dist/helm/corenel-sidecar/templates/deployment.yaml +147 -0
- package/dist/helm/corenel-sidecar/templates/ingress.yaml +35 -0
- package/dist/helm/corenel-sidecar/templates/pvc.yaml +36 -0
- package/dist/helm/corenel-sidecar/templates/service.yaml +17 -0
- package/dist/helm/corenel-sidecar/values.yaml +81 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ A small local capability server for [Prompd](https://prompd.app). Run it next to
|
|
|
4
4
|
web app and the browser can reach your machine — most usefully, your **local LLMs**
|
|
5
5
|
(Ollama, LM Studio, vLLM) **without CORS**, plus your filesystem and (opt-in) shell.
|
|
6
6
|
|
|
7
|
-
It runs only while the command is open, binds to loopback by default, and pairs
|
|
8
|
-
a
|
|
7
|
+
It runs only while the command is open, binds to loopback by default, and pairs each
|
|
8
|
+
device once with a single-use code; after that the device proves its own key, and the
|
|
9
|
+
person using it proves who they are (see [Identity](#identity-and-who-may-connect)).
|
|
9
10
|
|
|
10
11
|
## Use
|
|
11
12
|
|
|
@@ -15,8 +16,10 @@ In Prompd, open **Connect a sidecar** and follow the steps, or run it directly:
|
|
|
15
16
|
npx @corenel/sidecar --allow-origin https://prompd.app
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
It prints a pairing
|
|
19
|
-
match the site you're connecting from (the app fills it in
|
|
19
|
+
It prints a pairing code and the sidecar's fingerprint; paste them into the app. The
|
|
20
|
+
`--allow-origin` flag must match the site you're connecting from (the app fills it in
|
|
21
|
+
for you). Sign in to the app first: only the account the sidecar is signed in as
|
|
22
|
+
(`corenel-sidecar login`) may connect, or with `--mode team`, members of its org.
|
|
20
23
|
|
|
21
24
|
## Options
|
|
22
25
|
|
|
@@ -26,8 +29,11 @@ match the site you're connecting from (the app fills it in for you).
|
|
|
26
29
|
| `--port <n>` | `4858` | Port to listen on. |
|
|
27
30
|
| `--root <dir>` | cwd | Root the file tools are confined to. |
|
|
28
31
|
| `--allow-shell` | off | Enable the `sidecar_shell_exec` tool (RCE on your machine — opt-in). |
|
|
29
|
-
| `--host <addr>` | `127.0.0.1` | Bind address. Non-loopback is reachable off-box;
|
|
32
|
+
| `--host <addr>` | `127.0.0.1` | Bind address. Non-loopback is reachable off-box and needs TLS (below, `--allow-lan`, a tunnel, or `--behind-tls-proxy`); the daemon refuses to serve plain `ws://` off the machine. |
|
|
33
|
+
| `--behind-tls-proxy` | off | A proxy in front terminates TLS (e.g. a container ingress): allows a non-loopback bind without a certificate of its own. |
|
|
34
|
+
| `--mode team` `--org <org id>` | solo | Team mode: members of the Clerk organization may connect (`corenel-sidecar config set org <org id>`; the ID starts with `org_`, from the Clerk dashboard). Solo: only the account the daemon is signed in as. |
|
|
30
35
|
| `--tls-cert <f>` `--tls-key <f>` | _(none)_ | Serve `wss://` (needed for a remote sidecar from an https page). |
|
|
36
|
+
| `--name <text>` | generated | The name the app shows for this sidecar (daemon.json `name`). Without it the name is generated, `<hostname> · <first root folder>`, plus ` :<port>` off the default port, so several sidecars on one machine are told apart. The banner prints it. |
|
|
31
37
|
| `--env <name>` | _(none)_ | Layer `daemon.<name>.json` over `daemon.json` (same state dir), under flags. See [Config environments](#config-environments). |
|
|
32
38
|
|
|
33
39
|
## Daemon (preview)
|
|
@@ -144,7 +150,7 @@ Manual smoke (manual/client-commanded trigger):
|
|
|
144
150
|
to use a state dir other than the default, or `--default-model <id>` to give
|
|
145
151
|
agents with no `settings.model` a fallback.
|
|
146
152
|
|
|
147
|
-
4. **Pair from the app** with the printed
|
|
153
|
+
4. **Pair from the app** with the printed code, then command a manual run of the
|
|
148
154
|
agent you created. Confirm the reply streams back in the app as it's generated,
|
|
149
155
|
and that a new session appears under `<state dir>/crew/<name>/sessions/<runId>/`.
|
|
150
156
|
|
|
@@ -255,8 +261,8 @@ list and what that gap means.
|
|
|
255
261
|
10s), a refusal — never fails the run; the ask reaches the same outcome
|
|
256
262
|
it would have reached without push, only later by that bounded wait.
|
|
257
263
|
- **Endpoints are restricted to known push services.** The daemon POSTs to
|
|
258
|
-
a subscription's endpoint, so an open-ended URL would let
|
|
259
|
-
|
|
264
|
+
a subscription's endpoint, so an open-ended URL would let any paired
|
|
265
|
+
device make it send requests to any host it can reach. A
|
|
260
266
|
subscription is refused unless its endpoint is `https://` on the default
|
|
261
267
|
port, carries no credentials, and its host is — or is a subdomain of —
|
|
262
268
|
`fcm.googleapis.com` (Chrome/Chromium), `push.services.mozilla.com`
|
|
@@ -294,137 +300,107 @@ list and what that gap means.
|
|
|
294
300
|
subscriptions. A browser re-sends its existing subscription on every
|
|
295
301
|
pairing, so the id it is filed under follows an account switch.
|
|
296
302
|
|
|
297
|
-
## Identity
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
`
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
- **Default issuer:** `https://api.corenel.ai` **[V]** `packages/sidecar/src/args.test.ts`
|
|
369
|
-
("identity verification is on by default against the API issuer"). Point at
|
|
370
|
-
a local corenel-api with `--identity-issuer <url>`, or the `daemon.json` key
|
|
371
|
-
`identityIssuer`.
|
|
372
|
-
- **https rule, with a loopback exception:** the issuer must be `https://`,
|
|
373
|
-
except a loopback host (`localhost`, `127.0.0.1`, `[::1]`), which may use
|
|
374
|
-
`http://` — for local development against a corenel-api with no TLS cert to
|
|
375
|
-
offer **[V]** `args.test.ts` ("rejects http:// on a non-loopback host",
|
|
376
|
-
"accepts %s (loopback issuer, for local development)" for
|
|
377
|
-
`http://localhost:4000`, `http://127.0.0.1:4000`, `http://[::1]:4000`); a
|
|
378
|
-
loopback-lookalike host (`http://localhost.evil.com`, etc.) is still
|
|
379
|
-
rejected **[V]** `args.test.ts` ("rejects the loopback lookalike %s").
|
|
380
|
-
- **`--no-identity`** turns verification off entirely (`daemon.json`:
|
|
381
|
-
`identity: false`); every connection then pairs unverified, exactly as
|
|
382
|
-
before this feature existed.
|
|
383
|
-
- **Keys over https:** the discovery document's `jwks_uri` must be `https://`
|
|
384
|
-
too — except under a loopback `http://` issuer, where an `http://`
|
|
385
|
-
`jwks_uri` on the issuer's own origin (same host and port) is accepted, so
|
|
386
|
-
a local corenel-api works end to end **[V]** `oidcVerifier.test.ts`
|
|
387
|
-
("loopback http issuer" describe block: same-origin http verifies; another
|
|
388
|
-
host, another loopback host or another port does not; an https issuer with
|
|
389
|
-
an http `jwks_uri` still fails).
|
|
390
|
-
- **Egress:** one OIDC discovery fetch (`<issuer>/.well-known/openid-configuration`)
|
|
391
|
-
plus one JWKS fetch per hour to the configured issuer, cached together; an
|
|
392
|
-
unknown `kid` triggers at most one extra refetch per 60 seconds.
|
|
393
|
-
- The Clerk token never reaches the daemon at all, and the assertion itself is
|
|
394
|
-
never logged and never stored — only the resulting user id is kept, in
|
|
395
|
-
memory, for the life of the connection.
|
|
396
|
-
- **[D]** The browser's worst case for the whole exchange is roughly 6 s (up to
|
|
397
|
-
3 s to fetch a fresh Clerk token, plus up to 3 s for the POST to
|
|
398
|
-
corenel-api) against the daemon's fixed 3 s bound from the challenge — a
|
|
399
|
-
slow answer pairs unverified, not refused.
|
|
303
|
+
## Identity and who may connect
|
|
304
|
+
|
|
305
|
+
Pairing proves two things on every connection: the DEVICE (a key the daemon
|
|
306
|
+
recorded when the device enrolled with a single-use pairing code; see the
|
|
307
|
+
device enrollment spec) and the PERSON (a verified identity). Both are
|
|
308
|
+
required; there is no switch to turn identity off. A connection that cannot
|
|
309
|
+
prove both gets the same `denied: invalid credentials` as any other bad
|
|
310
|
+
credential, and nothing is served before it is authenticated.
|
|
311
|
+
|
|
312
|
+
**Who may connect** follows the existing `--mode team` switch:
|
|
313
|
+
|
|
314
|
+
- **Solo** (the default): only the account this daemon is signed in as
|
|
315
|
+
(`corenel-sidecar login`, or `CORENEL_TOKEN`). The daemon learns that
|
|
316
|
+
account's Clerk user id at startup from `GET <endpoint>/auth/me` with its
|
|
317
|
+
own credential, re-reads it every 15 minutes and whenever its sign-in
|
|
318
|
+
changes, and FAILS CLOSED: until it knows, no one may connect, and the
|
|
319
|
+
banner's `who may connect:` line says why. An `org` set in solo mode is
|
|
320
|
+
ignored, with a log line saying so.
|
|
321
|
+
- **Team** (`--mode team`): only members of ONE Clerk organization, named by
|
|
322
|
+
its ID (`org_...`, from the Clerk dashboard; never a slug, which can be
|
|
323
|
+
renamed or taken by another org) with `corenel-sidecar config set org <org id>`
|
|
324
|
+
(daemon.json `org`, or `--org <org id>`). Team mode without a valid org ID
|
|
325
|
+
refuses to start, naming that command. Membership is checked by corenel-api
|
|
326
|
+
on every connect against Clerk's membership list for the verified user (not
|
|
327
|
+
the session's active org), cached at most 60 s; the daemon admits an
|
|
328
|
+
assertion whose `org_id` claim is its own org ID. Any role counts. A device
|
|
329
|
+
belongs to the first member who connected on it. Every open connection is re-challenged every 15 minutes
|
|
330
|
+
and closed when the same user cannot re-assert membership, so a removed
|
|
331
|
+
member is out within 15 minutes. Over the wire a member may revoke only
|
|
332
|
+
their own devices (those recorded under their verified user), and may read
|
|
333
|
+
a run's output (reports, check output, files) or watch it only for runs
|
|
334
|
+
they started;
|
|
335
|
+
the operator's local `corenel-sidecar devices revoke` keeps full power.
|
|
336
|
+
|
|
337
|
+
**The exchange.** Once the device key is proven (and, for an enrollment,
|
|
338
|
+
before the code is burned), the daemon sends `identity-challenge` naming its
|
|
339
|
+
own `sidecarId`, a fresh single-use nonce and, in team mode, its org. The
|
|
340
|
+
client obtains a one-minute RS256 assertion from corenel-api's
|
|
341
|
+
`POST /api/v1/pairing-assertions` (`{ aud, nonce, jkt, org_id? }`): a browser
|
|
342
|
+
with its Clerk session token, the `corenel` CLI with its `cnl_cli_` login
|
|
343
|
+
token, which must carry the `pair` scope (granted only when the person
|
|
344
|
+
approving `corenel login` ticks "Also let this machine connect to your
|
|
345
|
+
sidecars"; without it, log in again). That token goes to corenel-api and nowhere else; it never crosses the
|
|
346
|
+
daemon socket. The client answers with `identity-assert`, and the daemon
|
|
347
|
+
verifies the assertion locally against corenel-api's OIDC discovery and JWKS:
|
|
348
|
+
signature, `iss`, `exp`, `aud` (this daemon), `nonce` (this challenge),
|
|
349
|
+
`azp` (an allowlisted origin, or `corenel-cli` for a CLI assertion) and
|
|
350
|
+
`cnf.jkt`, which must be the thumbprint of the device key THIS connection
|
|
351
|
+
proved. That binding stops a live relay: a daemon that forwards another
|
|
352
|
+
daemon's challenge to a browser paired with it gets an assertion bound to
|
|
353
|
+
that browser's key for IT, which the other daemon refuses; and a client only
|
|
354
|
+
answers a challenge from the daemon that proved the key it pinned.
|
|
355
|
+
The whole identity leg is bounded at 10 s from the challenge; a missing,
|
|
356
|
+
late, oversized or invalid assertion is a refusal.
|
|
357
|
+
|
|
358
|
+
**What the client can say.** The refusal is uniform on the wire, but the
|
|
359
|
+
client knows when it had no identity to offer and says so: not signed in,
|
|
360
|
+
not a member of the org, or the sign-in service could not vouch in time. The
|
|
361
|
+
device is kept in those cases; only a device the daemon refused is
|
|
362
|
+
forgotten.
|
|
363
|
+
|
|
364
|
+
- **Default issuer:** `https://api.corenel.ai`. Point at a local corenel-api
|
|
365
|
+
with `--identity-issuer <url>` (daemon.json `identityIssuer`). It must be
|
|
366
|
+
`https://`, except a loopback host, which may use `http://`, and its
|
|
367
|
+
`jwks_uri` follows the same rule (same origin under a loopback issuer).
|
|
368
|
+
- **Egress:** one discovery fetch plus one JWKS fetch per hour to the issuer,
|
|
369
|
+
cached together; an unknown `kid` refetches at most once a minute. In solo
|
|
370
|
+
mode, one `GET <endpoint>/auth/me` at startup and every 15 minutes.
|
|
371
|
+
- The assertion is never logged or stored; only the verified user id is kept,
|
|
372
|
+
in memory, for the life of the connection (and recorded on the device).
|
|
400
373
|
- **[D]** corenel-api only stamps `azp` for request origins in its own CORS
|
|
401
|
-
allowlist
|
|
402
|
-
with a valid Clerk session; local https dev against a local corenel-api
|
|
403
|
-
needs that instance's `FRONTEND_URL` set to the dev origin.
|
|
374
|
+
allowlist, so a page served from an origin missing there is refused.
|
|
404
375
|
- **[D]** Both web apps call `https://api.corenel.ai/api/v1/pairing-assertions`
|
|
405
|
-
directly (`VITE_PAIRING_ASSERTION_URL`
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
376
|
+
directly (`VITE_PAIRING_ASSERTION_URL`), never through a front end's own
|
|
377
|
+
`/api` proxy, whose single egress address would share one rate-limit bucket.
|
|
378
|
+
- The runbook for the signing key (`corenel-pairing-signing-key`) lives in
|
|
379
|
+
corenel-api's `docs/pairing-signing-key.md`.
|
|
380
|
+
|
|
381
|
+
**Before a connection authenticates** the daemon keeps its cost bounded: a
|
|
382
|
+
daemon-wide limiter refuses an address after 10 failed hellos in a minute
|
|
383
|
+
(keyed by IPv4 address or IPv6 /64; off with `--behind-tls-proxy`, where
|
|
384
|
+
every peer is the proxy), lets one address hold at most 4 unauthenticated
|
|
385
|
+
connections, and holds at most 32 daemon-wide (HTTP 429 and 503 at the
|
|
386
|
+
upgrade); a plain HTTP request to the port gets
|
|
387
|
+
`426`; request headers must arrive within 10 s; a frame over 64 KiB before
|
|
388
|
+
the welcome closes the socket unparsed; and a hello that does not pair within
|
|
389
|
+
20 s is closed.
|
|
390
|
+
|
|
391
|
+
**`corenel doctor` Identity section.** Computed from the EFFECTIVE config: who
|
|
392
|
+
the daemon admits (solo, or the team org; team mode without an org is a
|
|
393
|
+
failure), the issuer URL and whether it passes the https/loopback rule, a
|
|
394
|
+
real discovery fetch (the same code the verifier uses), whether the
|
|
395
|
+
document's `issuer` matches, whether `jwks_uri` is accepted, how many usable
|
|
396
|
+
RS256 keys it lists (zero is a failure), the trusted origins, and a warning
|
|
397
|
+
when `--allow-lan` is on that a LAN-allowed page is refused unless its origin
|
|
398
|
+
is also in `allowOrigins`. Included in `--json`.
|
|
423
399
|
|
|
424
400
|
## Security
|
|
425
401
|
|
|
426
|
-
- Loopback by default; a browser connection requires
|
|
427
|
-
allowed Origin.
|
|
402
|
+
- Loopback by default; a browser connection requires a recorded device key, a verified
|
|
403
|
+
identity this daemon admits, **and** an allowed Origin.
|
|
428
404
|
- The HTTP proxy (for local LLMs) only reaches **loopback** hosts — it can't be used as
|
|
429
405
|
an SSRF pivot to internal services.
|
|
430
406
|
- `sidecar_shell_exec` is off unless you pass `--allow-shell`.
|