@nopeek/agent-bridge 0.5.6 → 0.6.1
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 +9 -7
- package/dist/bot.d.ts +3 -2
- package/dist/bot.js +9 -7
- package/dist/bridge.d.ts +47 -29
- package/dist/bridge.js +314 -198
- package/dist/capabilities.d.ts +7 -5
- package/dist/capabilities.js +9 -9
- package/dist/cli.js +7 -6
- package/dist/config.d.ts +24 -5
- package/dist/config.js +77 -6
- package/dist/control.d.ts +5 -3
- package/dist/control.js +18 -15
- package/dist/localapi.js +48 -23
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@ Run your own AI agents as **end-to-end-encrypted NoPeek bots** — from your Mac
|
|
|
4
4
|
|
|
5
5
|
The bridge is a small always-on process that:
|
|
6
6
|
|
|
7
|
-
1. **Pairs** with your NoPeek account — one tap in the NoPeek app ("Connect this computer"), or a one-time pairing code (`npr_…`) if you prefer the terminal.
|
|
8
|
-
2. **Runs every bot
|
|
7
|
+
1. **Pairs** with your NoPeek account — one tap in the NoPeek app ("Connect this computer"), or a one-time pairing code (`npr_…`) if you prefer the terminal. **Multi-account**: one installed bridge holds any number of pairings — each NoPeek account on this computer taps "Add this account" and its bots run alongside everyone else's.
|
|
8
|
+
2. **Runs every bot each paired account owns** — each bot connects as a real NoPeek user with its own server device, publishes MLS key packages, and decrypts messages locally like any other client. The server never sees plaintext.
|
|
9
9
|
3. **Pipes each incoming message to your "brain"** — any shell command (message on stdin, reply on stdout) or any HTTP webhook — and sends the reply back into the encrypted channel.
|
|
10
10
|
|
|
11
11
|
When you create a new bot in the NoPeek app, the bridge adopts it live over its control connection. No restart, no redeploy.
|
|
@@ -45,14 +45,14 @@ The bridge serves a loopback-only HTTP API on `127.0.0.1:8790` — this is what
|
|
|
45
45
|
|
|
46
46
|
| Endpoint | Auth | Purpose |
|
|
47
47
|
| --- | --- | --- |
|
|
48
|
-
| `GET /` | none | Minimal status: `{ok, service, version, paired, machine, uptime}` |
|
|
49
|
-
| `POST /pair` | the `npr_…` secret itself
|
|
50
|
-
| `GET /status` | `x-nopeek-runtime: rt_…` | Full status: bots, brains
|
|
48
|
+
| `GET /` | none | Minimal status: `{ok, service, version, paired, pairings, machine, uptime}` (`pairings` = account count only — never ids) |
|
|
49
|
+
| `POST /pair` | the `npr_…` secret itself (validated against the API) | `{pairingSecret, appId, apiUrl?, label?, runtimeId?}` — ADDS a pairing; only an exact duplicate (same secret) is rejected (`409 ALREADY_PAIRED`) |
|
|
50
|
+
| `GET /status` | `x-nopeek-runtime: rt_…` | Full status: `pairings: [{runtimeId, appId, label, connected, bots}]` per account, plus brains and a flattened `bots` list |
|
|
51
51
|
| `GET /detect` | `x-nopeek-runtime` | Agent runtimes found on PATH (hermes, claude, llm, ollama) with suggested commands |
|
|
52
52
|
| `PUT /brains` | `x-nopeek-runtime` | `{brainCmd?, brainUrl?, map?: {"<handle>": {cmd|url|echo}|null}}` — applies live, persists |
|
|
53
|
-
| `DELETE /pair
|
|
53
|
+
| `DELETE /pair?runtimeId=rt_…` | `x-nopeek-runtime` | Remove ONE account's pairing (its bots stop; others keep running). Without the query: remove ALL pairings (the pre-0.6 behavior) |
|
|
54
54
|
|
|
55
|
-
The `x-nopeek-runtime` header is
|
|
55
|
+
The `x-nopeek-runtime` header is a runtime id — a capability only an owner's logged-in app can fetch from the NoPeek server, so a random webpage poking `127.0.0.1` can't read your bot list or change brain commands. ANY paired account's runtime id is accepted. Everything the app configures persists to `~/.nopeek-bridge/settings.json` (mode 600); pre-0.6 single-pairing settings are migrated to the multi-account format automatically on first start.
|
|
56
56
|
|
|
57
57
|
## How the brain works
|
|
58
58
|
|
|
@@ -171,6 +171,8 @@ Example config file:
|
|
|
171
171
|
}
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
(The single `NOPEEK_PAIRING_CODE`/`NOPEEK_APP_ID` pair is still read everywhere for convenience; the bridge's own `settings.json` stores pairings as a `NOPEEK_PAIRINGS` array — one entry per paired account — and migrates the legacy keys automatically.)
|
|
175
|
+
|
|
174
176
|
## Health endpoint
|
|
175
177
|
|
|
176
178
|
`GET http://localhost:8790/` →
|
package/dist/bot.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BridgeConfig, BrainBackend } from "./config.js";
|
|
1
|
+
import type { BridgeConfig, Pairing, BrainBackend } from "./config.js";
|
|
2
2
|
export interface BotInfo {
|
|
3
3
|
userId: string;
|
|
4
4
|
handle: string;
|
|
@@ -16,6 +16,7 @@ export declare class BotRunner {
|
|
|
16
16
|
handled: number;
|
|
17
17
|
brainKind: string;
|
|
18
18
|
private cfg;
|
|
19
|
+
private pairing;
|
|
19
20
|
private np;
|
|
20
21
|
private stopped;
|
|
21
22
|
private refreshTimer;
|
|
@@ -28,7 +29,7 @@ export declare class BotRunner {
|
|
|
28
29
|
private cantPost;
|
|
29
30
|
private log;
|
|
30
31
|
private logErr;
|
|
31
|
-
constructor(info: BotInfo, cfg: BridgeConfig);
|
|
32
|
+
constructor(info: BotInfo, cfg: BridgeConfig, pairing: Pairing);
|
|
32
33
|
/** Re-read the effective brain (e.g. after a live server backend change) so
|
|
33
34
|
* status reflects it immediately. The next message re-resolves regardless. */
|
|
34
35
|
refreshBrainKind(): void;
|
package/dist/bot.js
CHANGED
|
@@ -16,6 +16,7 @@ export class BotRunner {
|
|
|
16
16
|
handled = 0;
|
|
17
17
|
brainKind;
|
|
18
18
|
cfg;
|
|
19
|
+
pairing;
|
|
19
20
|
np = null;
|
|
20
21
|
stopped = false;
|
|
21
22
|
refreshTimer = null;
|
|
@@ -37,9 +38,10 @@ export class BotRunner {
|
|
|
37
38
|
cantPost = new Set();
|
|
38
39
|
log;
|
|
39
40
|
logErr;
|
|
40
|
-
constructor(info, cfg) {
|
|
41
|
+
constructor(info, cfg, pairing) {
|
|
41
42
|
this.info = info;
|
|
42
|
-
this.cfg = cfg;
|
|
43
|
+
this.cfg = cfg; // global brain/data config (shared, mutated live by setBrains)
|
|
44
|
+
this.pairing = pairing; // this bot's account: apiUrl + appId + runtime secret
|
|
43
45
|
this.brainKind = resolveBrain(cfg, info.handle).kind;
|
|
44
46
|
const tag = `[bot:@${info.handle}]`;
|
|
45
47
|
this.log = (m) => console.log(`${tag} ${m}`);
|
|
@@ -71,10 +73,10 @@ export class BotRunner {
|
|
|
71
73
|
this.connected = false;
|
|
72
74
|
}
|
|
73
75
|
async mintSession() {
|
|
74
|
-
const res = await fetch(`${this.
|
|
76
|
+
const res = await fetch(`${this.pairing.apiUrl}/v1/apps/${this.pairing.appId}/bots/${this.info.userId}/runtime-session`, {
|
|
75
77
|
method: "POST",
|
|
76
78
|
headers: {
|
|
77
|
-
authorization: `Bearer ${this.
|
|
79
|
+
authorization: `Bearer ${this.pairing.pairingCode}`,
|
|
78
80
|
"content-type": "application/json",
|
|
79
81
|
},
|
|
80
82
|
});
|
|
@@ -90,7 +92,7 @@ export class BotRunner {
|
|
|
90
92
|
* rather than open. */
|
|
91
93
|
async refreshAccess() {
|
|
92
94
|
try {
|
|
93
|
-
const res = await fetch(`${this.
|
|
95
|
+
const res = await fetch(`${this.pairing.apiUrl}/v1/apps/${this.pairing.appId}/bots/${this.info.userId}/access`, { headers: { authorization: `Bearer ${this.pairing.pairingCode}` } });
|
|
94
96
|
if (!res.ok)
|
|
95
97
|
throw new Error(`access HTTP ${res.status}`);
|
|
96
98
|
const j = (await res.json());
|
|
@@ -133,9 +135,9 @@ export class BotRunner {
|
|
|
133
135
|
// what a bot wants — peers claim those packages to send it welcomes. The
|
|
134
136
|
// FileStore makes restarts reuse the same device instead of minting new ones.
|
|
135
137
|
const np = await NoPeek.connect({
|
|
136
|
-
apiUrl: this.
|
|
138
|
+
apiUrl: this.pairing.apiUrl,
|
|
137
139
|
sessionToken: session.sessionToken,
|
|
138
|
-
appId: this.
|
|
140
|
+
appId: this.pairing.appId,
|
|
139
141
|
userId: this.info.userId,
|
|
140
142
|
platform: "server",
|
|
141
143
|
storage: store,
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type { BridgeConfig, BrainSpec } from "./config.js";
|
|
2
|
-
export declare const VERSION = "0.
|
|
1
|
+
import type { BridgeConfig, Pairing, BrainSpec, BrainBackend } from "./config.js";
|
|
2
|
+
export declare const VERSION = "0.6.0";
|
|
3
3
|
export interface PairRequest {
|
|
4
4
|
pairingSecret: string;
|
|
5
5
|
appId: string;
|
|
6
6
|
apiUrl?: string;
|
|
7
|
+
/** Human label for this pairing (shown in status; e.g. the account name). */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** The runtime id the app just minted alongside the secret. Stored so the
|
|
10
|
+
* local API can authorize this pairing's calls before the control socket
|
|
11
|
+
* has authenticated (auth.ok remains authoritative and overwrites it). */
|
|
12
|
+
runtimeId?: string;
|
|
7
13
|
}
|
|
8
14
|
export interface BrainsPatch {
|
|
9
15
|
/** Global command brain; null clears it. */
|
|
@@ -20,29 +26,50 @@ export declare class PairError extends Error {
|
|
|
20
26
|
readonly code: "ALREADY_PAIRED" | "PAIR_REJECTED" | "PAIR_UNREACHABLE" | "BAD_REQUEST";
|
|
21
27
|
constructor(code: "ALREADY_PAIRED" | "PAIR_REJECTED" | "PAIR_UNREACHABLE" | "BAD_REQUEST", message: string);
|
|
22
28
|
}
|
|
29
|
+
/** One paired account's status as reported over the local API. */
|
|
30
|
+
export interface PairingStatus {
|
|
31
|
+
runtimeId: string | null;
|
|
32
|
+
appId: string;
|
|
33
|
+
label: string | null;
|
|
34
|
+
connected: boolean;
|
|
35
|
+
bots: Array<Record<string, unknown>>;
|
|
36
|
+
}
|
|
23
37
|
export declare class BridgeApp {
|
|
24
38
|
readonly cfg: BridgeConfig;
|
|
25
39
|
readonly startedAt: number;
|
|
26
|
-
private
|
|
27
|
-
private control;
|
|
40
|
+
private runtimes;
|
|
28
41
|
private capabilitiesTimer;
|
|
29
42
|
private stopped;
|
|
30
43
|
constructor(cfg: BridgeConfig);
|
|
31
44
|
get paired(): boolean;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
/** Every runtime id this bridge answers for (persisted or live-auth'd).
|
|
46
|
+
* The local API accepts ANY of these as the x-nopeek-runtime capability. */
|
|
47
|
+
runtimeIds(): string[];
|
|
35
48
|
start(): void;
|
|
36
49
|
stop(): void;
|
|
50
|
+
private startRuntime;
|
|
51
|
+
/** Re-probe + report brain availability every ~5 min (once per pairing) so a
|
|
52
|
+
* login/logout on this computer surfaces in each account's picker without a
|
|
53
|
+
* reconnect. onAuthed covers initial + reconnect reports; this covers drift.
|
|
54
|
+
* unref so a running interval never keeps the process alive on its own. */
|
|
55
|
+
private ensureCapabilitiesTimer;
|
|
56
|
+
/**
|
|
57
|
+
* Pair ANOTHER NoPeek account onto this bridge (or the first one — same
|
|
58
|
+
* flow). Every request is validated against the API before anything is
|
|
59
|
+
* persisted: the npr_ secret is server-minted and unguessable, so possession
|
|
60
|
+
* of a VALID one is the proof. Only an exact duplicate (same secret, i.e.
|
|
61
|
+
* literally the same pairing) is rejected.
|
|
62
|
+
*/
|
|
63
|
+
pair(req: PairRequest): Promise<Pairing>;
|
|
37
64
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
65
|
+
* Undo pairing (device keys stay either way).
|
|
66
|
+
* unpair() — legacy no-arg: remove EVERY pairing (pre-0.6 shape).
|
|
67
|
+
* unpair(runtimeId) — remove just that account's pairing; others keep running.
|
|
68
|
+
* Returns false when a runtimeId was given but matches no pairing.
|
|
41
69
|
*/
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/** Apply a brain change live (next message uses it) and persist it. */
|
|
70
|
+
unpair(runtimeId?: string): boolean;
|
|
71
|
+
/** Apply a brain change live (next message uses it) and persist it.
|
|
72
|
+
* Brains are GLOBAL per machine — handles are globally unique. */
|
|
46
73
|
setBrains(patch: BrainsPatch): void;
|
|
47
74
|
/**
|
|
48
75
|
* Record a SERVER-provided backend for a handle and make it take effect.
|
|
@@ -51,21 +78,12 @@ export declare class BridgeApp {
|
|
|
51
78
|
* either way but is inert while a local override exists. When it IS the
|
|
52
79
|
* effective brain (no local override), provision the soul/profile up front so
|
|
53
80
|
* the very first message doesn't wait on it. Persisted for reconnects.
|
|
81
|
+
* (Internal — called by each PairingRuntime; the map is machine-global.)
|
|
54
82
|
*/
|
|
55
|
-
|
|
83
|
+
applyServerBackend(rawHandle: string, backend: BrainBackend): void;
|
|
56
84
|
statusMinimal(): Record<string, unknown>;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* Auto-provision a brain for a newly adopted bot: run BRAIN_PROVISION_CMD
|
|
62
|
-
* (e.g. "create a Hermes profile with its own soul + memory for this handle")
|
|
63
|
-
* and store its stdout as the bot's brain command. Best-effort — on any
|
|
64
|
-
* failure the bot simply keeps the default brain.
|
|
65
|
-
*/
|
|
66
|
-
private provisionBrain;
|
|
67
|
-
/** Fetch the authoritative bot list and start anything we're missing. */
|
|
68
|
-
private syncBots;
|
|
69
|
-
private startCore;
|
|
70
|
-
private stopCore;
|
|
85
|
+
/** Full status (authenticated). `callerRuntimeId` — when known — keeps the
|
|
86
|
+
* legacy top-level `runtime`/`appId` fields pointing at the CALLER's own
|
|
87
|
+
* pairing so pre-0.6 clients keep working unchanged. */
|
|
88
|
+
statusFull(callerRuntimeId?: string): Record<string, unknown>;
|
|
71
89
|
}
|