remote-pi 0.1.2 → 0.2.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 +195 -36
- package/dist/bin/supervisord.d.ts +2 -0
- package/dist/bin/supervisord.js +44 -0
- package/dist/bin/supervisord.js.map +1 -0
- package/dist/config.d.ts +49 -5
- package/dist/config.js +73 -9
- package/dist/config.js.map +1 -1
- package/dist/daemon/client.d.ts +20 -0
- package/dist/daemon/client.js +128 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/control_protocol.d.ts +100 -0
- package/dist/daemon/control_protocol.js +63 -0
- package/dist/daemon/control_protocol.js.map +1 -0
- package/dist/daemon/id.d.ts +18 -0
- package/dist/daemon/id.js +30 -0
- package/dist/daemon/id.js.map +1 -0
- package/dist/daemon/install.d.ts +132 -0
- package/dist/daemon/install.js +312 -0
- package/dist/daemon/install.js.map +1 -0
- package/dist/daemon/registry.d.ts +47 -0
- package/dist/daemon/registry.js +123 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/rpc_child.d.ts +76 -0
- package/dist/daemon/rpc_child.js +130 -0
- package/dist/daemon/rpc_child.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +38 -0
- package/dist/daemon/supervisor.js +301 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/index.d.ts +62 -8
- package/dist/index.js +1232 -304
- package/dist/index.js.map +1 -1
- package/dist/mesh/canonical.d.ts +30 -0
- package/dist/mesh/canonical.js +61 -0
- package/dist/mesh/canonical.js.map +1 -0
- package/dist/mesh/client.d.ts +31 -0
- package/dist/mesh/client.js +56 -0
- package/dist/mesh/client.js.map +1 -0
- package/dist/mesh/encoding.d.ts +36 -0
- package/dist/mesh/encoding.js +53 -0
- package/dist/mesh/encoding.js.map +1 -0
- package/dist/mesh/self_revoke.d.ts +111 -0
- package/dist/mesh/self_revoke.js +182 -0
- package/dist/mesh/self_revoke.js.map +1 -0
- package/dist/mesh/siblings.d.ts +62 -0
- package/dist/mesh/siblings.js +95 -0
- package/dist/mesh/siblings.js.map +1 -0
- package/dist/mesh/types.d.ts +34 -0
- package/dist/mesh/types.js +11 -0
- package/dist/mesh/types.js.map +1 -0
- package/dist/mesh/verify.d.ts +17 -0
- package/dist/mesh/verify.js +77 -0
- package/dist/mesh/verify.js.map +1 -0
- package/dist/pairing/qr.d.ts +16 -5
- package/dist/pairing/qr.js +27 -8
- package/dist/pairing/qr.js.map +1 -1
- package/dist/pairing/storage.d.ts +41 -0
- package/dist/pairing/storage.js +158 -21
- package/dist/pairing/storage.js.map +1 -1
- package/dist/protocol/types.d.ts +23 -0
- package/dist/session/broker.d.ts +74 -0
- package/dist/session/broker.js +142 -4
- package/dist/session/broker.js.map +1 -1
- package/dist/session/broker_remote.d.ts +110 -0
- package/dist/session/broker_remote.js +397 -0
- package/dist/session/broker_remote.js.map +1 -0
- package/dist/session/cwd_lock.d.ts +28 -0
- package/dist/session/cwd_lock.js +89 -0
- package/dist/session/cwd_lock.js.map +1 -0
- package/dist/session/global_config.d.ts +9 -0
- package/dist/session/global_config.js +9 -0
- package/dist/session/global_config.js.map +1 -1
- package/dist/session/leader_election.d.ts +16 -0
- package/dist/session/leader_election.js +22 -0
- package/dist/session/leader_election.js.map +1 -1
- package/dist/session/local_config.d.ts +12 -5
- package/dist/session/local_config.js +24 -3
- package/dist/session/local_config.js.map +1 -1
- package/dist/session/peer.d.ts +28 -1
- package/dist/session/peer.js +69 -2
- package/dist/session/peer.js.map +1 -1
- package/dist/session/peer_inventory.d.ts +13 -0
- package/dist/session/peer_inventory.js +48 -0
- package/dist/session/peer_inventory.js.map +1 -0
- package/dist/session/setup_wizard.d.ts +32 -8
- package/dist/session/setup_wizard.js +45 -33
- package/dist/session/setup_wizard.js.map +1 -1
- package/dist/session/tools.d.ts +15 -7
- package/dist/session/tools.js +145 -31
- package/dist/session/tools.js.map +1 -1
- package/dist/transport/pi_forward_client.d.ts +29 -0
- package/dist/transport/pi_forward_client.js +62 -0
- package/dist/transport/pi_forward_client.js.map +1 -0
- package/dist/ui/footer.js +8 -6
- package/dist/ui/footer.js.map +1 -1
- package/docs/daemon.md +289 -0
- package/package.json +10 -3
- package/service-templates/launchd.plist.template +35 -0
- package/service-templates/systemd.service.template +19 -0
- package/skills/agent-network/SKILL.md +273 -294
package/dist/pairing/qr.js
CHANGED
|
@@ -46,6 +46,11 @@ sessionName,
|
|
|
46
46
|
roomId) {
|
|
47
47
|
// `r` (relay URL) removed in plano 14 — relay now comes from app config /
|
|
48
48
|
// pi-ext env|config|default chain. Keeps QR ~30-50 chars shorter.
|
|
49
|
+
// `n` (session name) is kept: the app uses it for the pre-pair_ok preview
|
|
50
|
+
// screen (showing the agent name immediately after scan, before the
|
|
51
|
+
// handshake completes). Dropping it briefly shrank the QR but the QR
|
|
52
|
+
// size no longer matters now that the copy-paste URI is rendered via
|
|
53
|
+
// `pi.sendMessage` into the chat panel (not the QR overflow area).
|
|
49
54
|
const epkB64 = Buffer.from(longtermEdPk).toString("base64url");
|
|
50
55
|
const params = new URLSearchParams({
|
|
51
56
|
t: token,
|
|
@@ -57,16 +62,30 @@ roomId) {
|
|
|
57
62
|
return `remotepi://pair?${params.toString()}`;
|
|
58
63
|
}
|
|
59
64
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
+
* Returns the QR ASCII as a string (pure Unicode block characters —
|
|
66
|
+
* `█ ▀ ▄` and space, NO ANSI escapes — qrcode-terminal v0.12 small mode
|
|
67
|
+
* is escape-free, see lib/main.js:48-53).
|
|
68
|
+
*
|
|
69
|
+
* The caller can either write the string to stderr (legacy path, breaks
|
|
70
|
+
* the Pi TUI layout) or inject it via `pi.sendMessage` (renders inside
|
|
71
|
+
* the chat panel as proper content).
|
|
72
|
+
*/
|
|
73
|
+
export function renderQRAscii(uri) {
|
|
74
|
+
let out = "";
|
|
75
|
+
qrTerminal.generate(uri, { small: true }, (qrcode) => { out = qrcode; });
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Legacy stderr writer — kept for the standalone CLI mode
|
|
80
|
+
* (`pi-extension/src/index.ts` bottom block, which runs outside a Pi TUI).
|
|
81
|
+
* Inside the Pi TUI extension flow, use `renderQRAscii` + `pi.sendMessage`
|
|
82
|
+
* instead — direct stderr writes from inside an extension break the TUI's
|
|
83
|
+
* scrollable output widget (the QR overflows the panel and other writes
|
|
84
|
+
* collide with the prompt area).
|
|
65
85
|
*/
|
|
66
86
|
export function displayQR(uri) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
87
|
+
const qrcode = renderQRAscii(uri);
|
|
88
|
+
process.stderr.write(`\n📱 Scan to pair:\n\n${qrcode}\n`);
|
|
70
89
|
}
|
|
71
90
|
/**
|
|
72
91
|
* Starts a rotating QR session: generates a new QR every 60s, printing it
|
package/dist/pairing/qr.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,UAAU,MAAM,iBAAiB,CAAC;AAEzC,MAAM,YAAY,GAAG,MAAM,CAAC;AAQ5B,4EAA4E;AAC5E,MAAM,OAAO,SAAS;IACZ,MAAM,GAAuB,IAAI,CAAC;IAE1C,mEAAmE;IACnE,aAAa;QACX,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/C,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,CAAC;QAC5C,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACpD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAC9B,CAAC;IAED,iDAAiD;IACjD,YAAY,CACV,KAAa;QAEb,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK;YAAE,OAAO,SAAS,CAAC;QAClE,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,OAAO,UAAU,CAAC;QAC5C,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;IACrB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;AAEzC,iFAAiF;AAEjF,MAAM,UAAU,UAAU,CACxB,KAAa,EACb,YAAwB,EAAE,4CAA4C;AACtE,WAAmB;AACnB;;;;;GAKG;AACH,MAAe;IAEf,0EAA0E;IAC1E,kEAAkE;IAClE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,CAAC,EAAE,KAAK;QACR,GAAG,EAAE,MAAM;QACX,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;KAC5B,CAAC,CAAC;IACH,IAAI,MAAM;QAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,mBAAmB,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AAChD,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import { type Ed25519Keypair } from "./crypto.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal backend interface for credential reads/writes. Swappable so
|
|
4
|
+
* tests can inject a controlled in-memory store without touching the OS
|
|
5
|
+
* keyring (which is shared with the developer's own credentials).
|
|
6
|
+
*
|
|
7
|
+
* Errors thrown by `read`/`write`/`delete` signal "backend unavailable on
|
|
8
|
+
* this platform" — callers fall back to the file store on first failure.
|
|
9
|
+
* Returning `undefined` from `read` means "no such entry" (a normal,
|
|
10
|
+
* non-error condition).
|
|
11
|
+
*/
|
|
12
|
+
export interface KeyStoreBackend {
|
|
13
|
+
read(service: string, account: string): Promise<string | undefined>;
|
|
14
|
+
write(service: string, account: string, value: string): Promise<void>;
|
|
15
|
+
delete(service: string, account: string): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
/** Test-only: swap (or clear with `null`) the keyring backend. */
|
|
18
|
+
export declare function _setKeyStoreBackendForTest(backend: KeyStoreBackend | null): void;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the Pi-secret Ed25519 keypair, generating + persisting one on
|
|
21
|
+
* first call. Resolution order:
|
|
22
|
+
* 1. New keyring service `dev.remotepi.pi`
|
|
23
|
+
* 2. Old keyring service `dev.remotepi.mac` (migrate → step 1, delete old)
|
|
24
|
+
* 3. File `~/.pi/remote/identity.json` (headless-Linux fallback)
|
|
25
|
+
* 4. Generate a fresh keypair + persist to the first available backend
|
|
26
|
+
*
|
|
27
|
+
* Idempotent: subsequent calls return the same identity. The migration
|
|
28
|
+
* runs at most once per machine (the old entry is deleted after copy).
|
|
29
|
+
*/
|
|
2
30
|
export declare function getOrCreateEd25519Keypair(): Promise<Ed25519Keypair>;
|
|
3
31
|
export interface PeerRecord {
|
|
4
32
|
name: string;
|
|
@@ -7,4 +35,17 @@ export interface PeerRecord {
|
|
|
7
35
|
}
|
|
8
36
|
export declare function listPeers(): Promise<PeerRecord[]>;
|
|
9
37
|
export declare function addPeer(record: PeerRecord): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Returns the set of distinct `remote_epk` values in peers.json.
|
|
40
|
+
*
|
|
41
|
+
* In the current pairing model (plan/23 + plan/24), each `remote_epk` is the
|
|
42
|
+
* Owner's Ed25519 pubkey — and we treat each as a distinct Owner the Pi has
|
|
43
|
+
* been paired with. Used by the mesh self-revoke poller (plan/24 Wave 3) to
|
|
44
|
+
* know which Owners' mesh blobs to fetch.
|
|
45
|
+
*/
|
|
46
|
+
export declare function listOwnerPubkeys(): Promise<string[]>;
|
|
10
47
|
export declare function removePeer(remoteEpk: string): Promise<boolean>;
|
|
48
|
+
/** Test-only: expose the identity-file path so tests can clean it. */
|
|
49
|
+
export declare const _IDENTITY_FILE_FOR_TEST: string;
|
|
50
|
+
/** Test-only: expose unlink for cleanup. */
|
|
51
|
+
export declare const _unlinkIdentityFileForTest: () => Promise<void>;
|
package/dist/pairing/storage.js
CHANGED
|
@@ -1,35 +1,147 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, writeFile, chmod, unlink } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import { AsyncEntry } from "@napi-rs/keyring";
|
|
5
5
|
import { generateEd25519Keypair } from "./crypto.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Pi-secret storage (plan/27 Wave E1).
|
|
8
|
+
*
|
|
9
|
+
* The Ed25519 long-term identity of this Pi lives in the platform keyring
|
|
10
|
+
* via `@napi-rs/keyring` (Keychain on macOS, libsecret on Linux desktop,
|
|
11
|
+
* Credential Manager on Windows — DPAPI-backed). When the keyring is
|
|
12
|
+
* unavailable (headless Linux without a D-Bus session, Docker containers,
|
|
13
|
+
* VPS without GNOME Keyring/KWallet running) we fall back to a
|
|
14
|
+
* file-backed store at `~/.pi/remote/identity.json` with `0o600`
|
|
15
|
+
* permissions and the parent dir at `0o700`.
|
|
16
|
+
*
|
|
17
|
+
* **Migration**: previous builds used `keytar` against service
|
|
18
|
+
* `dev.remotepi.mac`. This module reads from the old service if the new
|
|
19
|
+
* service is empty, copies the entry to the new service `dev.remotepi.pi`,
|
|
20
|
+
* and deletes the old one. Both keytar and `@napi-rs/keyring` address the
|
|
21
|
+
* same OS-level credential store on every supported platform, so the read
|
|
22
|
+
* succeeds without keeping the deprecated `keytar` dependency.
|
|
23
|
+
*/
|
|
24
|
+
const NEW_SERVICE = "dev.remotepi.pi"; // platform-neutral
|
|
25
|
+
const OLD_SERVICE = "dev.remotepi.mac"; // legacy keytar service (pre-2026-05-25)
|
|
26
|
+
const ACCOUNT = "longterm-ed25519";
|
|
27
|
+
const PI_DIR = join(homedir(), ".pi", "remote");
|
|
28
|
+
const IDENTITY_FILE = join(PI_DIR, "identity.json");
|
|
29
|
+
const PEERS_PATH = join(PI_DIR, "peers.json");
|
|
30
|
+
class NapiKeyringBackend {
|
|
31
|
+
async read(service, account) {
|
|
32
|
+
const entry = new AsyncEntry(service, account);
|
|
33
|
+
return entry.getPassword(); // returns undefined on no-entry
|
|
34
|
+
}
|
|
35
|
+
async write(service, account, value) {
|
|
36
|
+
const entry = new AsyncEntry(service, account);
|
|
37
|
+
await entry.setPassword(value);
|
|
38
|
+
}
|
|
39
|
+
async delete(service, account) {
|
|
40
|
+
const entry = new AsyncEntry(service, account);
|
|
41
|
+
try {
|
|
42
|
+
return await entry.deleteCredential();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let _backend = null;
|
|
50
|
+
function _getBackend() {
|
|
51
|
+
if (!_backend)
|
|
52
|
+
_backend = new NapiKeyringBackend();
|
|
53
|
+
return _backend;
|
|
54
|
+
}
|
|
55
|
+
/** Test-only: swap (or clear with `null`) the keyring backend. */
|
|
56
|
+
export function _setKeyStoreBackendForTest(backend) {
|
|
57
|
+
_backend = backend;
|
|
58
|
+
}
|
|
59
|
+
function _serialize(kp) {
|
|
60
|
+
const payload = {
|
|
61
|
+
pk: Buffer.from(kp.publicKey).toString("base64"),
|
|
62
|
+
sk: Buffer.from(kp.secretKey).toString("base64"),
|
|
63
|
+
};
|
|
64
|
+
return JSON.stringify(payload);
|
|
65
|
+
}
|
|
66
|
+
function _deserialize(stored) {
|
|
13
67
|
const parsed = JSON.parse(stored);
|
|
14
68
|
return {
|
|
15
69
|
publicKey: Buffer.from(parsed.pk, "base64"),
|
|
16
70
|
secretKey: Buffer.from(parsed.sk, "base64"),
|
|
17
71
|
};
|
|
18
72
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
73
|
+
// ── File fallback (headless Linux) ──────────────────────────────────────────
|
|
74
|
+
async function _readKeypairFromFile() {
|
|
75
|
+
try {
|
|
76
|
+
const raw = await readFile(IDENTITY_FILE, "utf8");
|
|
77
|
+
return _deserialize(raw);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function _writeKeypairToFile(kp) {
|
|
84
|
+
await mkdir(PI_DIR, { recursive: true, mode: 0o700 });
|
|
85
|
+
// Best-effort tighten of the dir in case it pre-existed with looser
|
|
86
|
+
// permissions (mkdir's mode is only applied to NEW dirs).
|
|
87
|
+
try {
|
|
88
|
+
await chmod(PI_DIR, 0o700);
|
|
89
|
+
}
|
|
90
|
+
catch { /* not fatal */ }
|
|
91
|
+
await writeFile(IDENTITY_FILE, _serialize(kp), { mode: 0o600 });
|
|
92
|
+
try {
|
|
93
|
+
await chmod(IDENTITY_FILE, 0o600);
|
|
94
|
+
}
|
|
95
|
+
catch { /* not fatal */ }
|
|
25
96
|
}
|
|
97
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Returns the Pi-secret Ed25519 keypair, generating + persisting one on
|
|
100
|
+
* first call. Resolution order:
|
|
101
|
+
* 1. New keyring service `dev.remotepi.pi`
|
|
102
|
+
* 2. Old keyring service `dev.remotepi.mac` (migrate → step 1, delete old)
|
|
103
|
+
* 3. File `~/.pi/remote/identity.json` (headless-Linux fallback)
|
|
104
|
+
* 4. Generate a fresh keypair + persist to the first available backend
|
|
105
|
+
*
|
|
106
|
+
* Idempotent: subsequent calls return the same identity. The migration
|
|
107
|
+
* runs at most once per machine (the old entry is deleted after copy).
|
|
108
|
+
*/
|
|
26
109
|
export async function getOrCreateEd25519Keypair() {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
110
|
+
const backend = _getBackend();
|
|
111
|
+
// ── Path A: keyring ────────────────────────────────────────────────────
|
|
112
|
+
try {
|
|
113
|
+
const existing = await backend.read(NEW_SERVICE, ACCOUNT);
|
|
114
|
+
if (existing)
|
|
115
|
+
return _deserialize(existing);
|
|
116
|
+
const legacy = await backend.read(OLD_SERVICE, ACCOUNT);
|
|
117
|
+
if (legacy) {
|
|
118
|
+
const kp = _deserialize(legacy);
|
|
119
|
+
await backend.write(NEW_SERVICE, ACCOUNT, legacy);
|
|
120
|
+
const deleted = await backend.delete(OLD_SERVICE, ACCOUNT);
|
|
121
|
+
console.info(`[remote-pi] Migrated Pi-secret from "${OLD_SERVICE}" to "${NEW_SERVICE}" ` +
|
|
122
|
+
`(old entry deleted: ${deleted})`);
|
|
123
|
+
return kp;
|
|
124
|
+
}
|
|
125
|
+
// Neither entry exists — generate and save to new service.
|
|
126
|
+
const fresh = generateEd25519Keypair();
|
|
127
|
+
await backend.write(NEW_SERVICE, ACCOUNT, _serialize(fresh));
|
|
128
|
+
console.info(`[remote-pi] Generated new Pi-secret in keyring "${NEW_SERVICE}"`);
|
|
129
|
+
return fresh;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// ── Path B: file fallback (typically headless Linux) ───────────────
|
|
133
|
+
console.warn("[remote-pi] WARNING: keyring unavailable, falling back to file-based " +
|
|
134
|
+
"storage at " + IDENTITY_FILE + " (mode 0600). Set up GNOME Keyring/" +
|
|
135
|
+
"KWallet for better security. " +
|
|
136
|
+
`Set PI_KEY_INSECURE_FALLBACK=true to suppress this warning. ` +
|
|
137
|
+
`Cause: ${String(err)}`);
|
|
138
|
+
const fromFile = await _readKeypairFromFile();
|
|
139
|
+
if (fromFile)
|
|
140
|
+
return fromFile;
|
|
141
|
+
const fresh = generateEd25519Keypair();
|
|
142
|
+
await _writeKeypairToFile(fresh);
|
|
143
|
+
return fresh;
|
|
144
|
+
}
|
|
33
145
|
}
|
|
34
146
|
export async function listPeers() {
|
|
35
147
|
try {
|
|
@@ -53,6 +165,21 @@ export async function addPeer(record) {
|
|
|
53
165
|
await mkdir(dirname(PEERS_PATH), { recursive: true });
|
|
54
166
|
await writeFile(PEERS_PATH, JSON.stringify({ peers }, null, 2));
|
|
55
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Returns the set of distinct `remote_epk` values in peers.json.
|
|
170
|
+
*
|
|
171
|
+
* In the current pairing model (plan/23 + plan/24), each `remote_epk` is the
|
|
172
|
+
* Owner's Ed25519 pubkey — and we treat each as a distinct Owner the Pi has
|
|
173
|
+
* been paired with. Used by the mesh self-revoke poller (plan/24 Wave 3) to
|
|
174
|
+
* know which Owners' mesh blobs to fetch.
|
|
175
|
+
*/
|
|
176
|
+
export async function listOwnerPubkeys() {
|
|
177
|
+
const peers = await listPeers();
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
for (const p of peers)
|
|
180
|
+
seen.add(p.remote_epk);
|
|
181
|
+
return [...seen];
|
|
182
|
+
}
|
|
56
183
|
export async function removePeer(remoteEpk) {
|
|
57
184
|
const peers = await listPeers();
|
|
58
185
|
const filtered = peers.filter((p) => p.remote_epk !== remoteEpk);
|
|
@@ -62,4 +189,14 @@ export async function removePeer(remoteEpk) {
|
|
|
62
189
|
await writeFile(PEERS_PATH, JSON.stringify({ peers: filtered }, null, 2));
|
|
63
190
|
return true;
|
|
64
191
|
}
|
|
192
|
+
// ── Test-only helpers ────────────────────────────────────────────────────────
|
|
193
|
+
/** Test-only: expose the identity-file path so tests can clean it. */
|
|
194
|
+
export const _IDENTITY_FILE_FOR_TEST = IDENTITY_FILE;
|
|
195
|
+
/** Test-only: expose unlink for cleanup. */
|
|
196
|
+
export const _unlinkIdentityFileForTest = async () => {
|
|
197
|
+
try {
|
|
198
|
+
await unlink(IDENTITY_FILE);
|
|
199
|
+
}
|
|
200
|
+
catch { /* fine if missing */ }
|
|
201
|
+
};
|
|
65
202
|
//# sourceMappingURL=storage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/pairing/storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/pairing/storage.ts"],"names":[],"mappings":"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"}
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -61,6 +61,25 @@ export type ServerMessage = {
|
|
|
61
61
|
session_name: string;
|
|
62
62
|
session_started_at: number;
|
|
63
63
|
room_id: string;
|
|
64
|
+
/**
|
|
65
|
+
* Plan/27 Wave A: identifies the host coding agent driving this
|
|
66
|
+
* pi-extension instance. `name` is hardcoded to "Pi coding agent"
|
|
67
|
+
* today; future Pi forks (Claude Code, OpenCode) populate their own
|
|
68
|
+
* here. `version` is the pi-extension `package.json` version.
|
|
69
|
+
* Optional in the wire schema so app-side parsing tolerates older
|
|
70
|
+
* Pi builds that predate this field — every new pairing emits both.
|
|
71
|
+
*/
|
|
72
|
+
harness?: {
|
|
73
|
+
name: string;
|
|
74
|
+
version: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Plan/27 Wave A: `os.hostname()` of the machine the Pi runs on.
|
|
78
|
+
* App displays it in the device list so the user can distinguish
|
|
79
|
+
* two paired PCs that happen to share a nickname or sit in the
|
|
80
|
+
* same project folder.
|
|
81
|
+
*/
|
|
82
|
+
hostname?: string;
|
|
64
83
|
} | {
|
|
65
84
|
type: "pair_error";
|
|
66
85
|
in_reply_to: string;
|
|
@@ -70,6 +89,10 @@ export type ServerMessage = {
|
|
|
70
89
|
type: "user_input";
|
|
71
90
|
id: string;
|
|
72
91
|
text: string;
|
|
92
|
+
} | {
|
|
93
|
+
type: "user_message";
|
|
94
|
+
id: string;
|
|
95
|
+
text: string;
|
|
73
96
|
} | {
|
|
74
97
|
type: "agent_chunk";
|
|
75
98
|
in_reply_to: string;
|
package/dist/session/broker.d.ts
CHANGED
|
@@ -7,6 +7,27 @@ import { type Envelope } from "./envelope.js";
|
|
|
7
7
|
*
|
|
8
8
|
* Auto-suffix on name collision: when a peer registers a name already taken,
|
|
9
9
|
* the broker assigns `<name>#N` and returns it in the register ack.
|
|
10
|
+
*
|
|
11
|
+
* ## ACK protocol (plan/25 Wave 0)
|
|
12
|
+
*
|
|
13
|
+
* For **unicast non-broker** envelopes the broker synchronously emits an ACK
|
|
14
|
+
* envelope back to the sender after deciding delivery:
|
|
15
|
+
*
|
|
16
|
+
* - target idle → mark target busy, deliver envelope, ACK `received`
|
|
17
|
+
* - target busy → drop envelope, ACK `busy`
|
|
18
|
+
*
|
|
19
|
+
* "Busy" tracking is driven by control envelopes `{type:"turn_state", busy}`
|
|
20
|
+
* the peer wrappers send on `turn_start`/`turn_end`. The broker also flips a
|
|
21
|
+
* peer to busy at the moment it delivers an envelope to it — this is the
|
|
22
|
+
* "received = commitment" rule: delivery itself is the promise the peer will
|
|
23
|
+
* handle the message in its upcoming turn. The wrapper's own turn_end clears
|
|
24
|
+
* it again. Atomicity between busy-check and busy-set is guaranteed by Node's
|
|
25
|
+
* single-threaded event loop — the block in `_route` does no `await` between
|
|
26
|
+
* the two.
|
|
27
|
+
*
|
|
28
|
+
* Broadcast/multicast/broker-addressed envelopes are not ACKed (no single
|
|
29
|
+
* authoritative recipient or no semantic match). The audit log carries the
|
|
30
|
+
* ACK status (`received | busy | dropped | none`) per envelope.
|
|
10
31
|
*/
|
|
11
32
|
export interface BrokerOptions {
|
|
12
33
|
server: Server;
|
|
@@ -14,12 +35,58 @@ export interface BrokerOptions {
|
|
|
14
35
|
/** Optional callback invoked after each successful route (testing/observability). */
|
|
15
36
|
onRouted?: (env: Envelope, deliveredTo: string[]) => void;
|
|
16
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Hook the broker calls before doing local routing, so cross-PC prefixes
|
|
40
|
+
* (`<pc_label>:<peer_name>`) can be handed off to a remote forwarder
|
|
41
|
+
* without baking transport knowledge into the broker. Wave C (plan/25)
|
|
42
|
+
* wires `broker_remote.ts` here.
|
|
43
|
+
*/
|
|
44
|
+
export interface RemoteRouter {
|
|
45
|
+
/**
|
|
46
|
+
* Try to claim responsibility for routing this envelope cross-PC.
|
|
47
|
+
* Returns true if claimed (broker MUST NOT also deliver locally). Returns
|
|
48
|
+
* false if the envelope should fall through to local routing — e.g., the
|
|
49
|
+
* prefix matches the local `pc_label`, the prefix is not a known remote
|
|
50
|
+
* label (backward-compat for local names containing `:`), or there's no
|
|
51
|
+
* prefix at all.
|
|
52
|
+
*/
|
|
53
|
+
tryRouteOutbound(env: Envelope): boolean;
|
|
54
|
+
/** Aggregated remote peer names (`<pc_label>:<peer_name>`) for the
|
|
55
|
+
* `list_peers` reply. Returns empty when nothing is known yet. */
|
|
56
|
+
listRemotePeers(): string[];
|
|
57
|
+
}
|
|
58
|
+
/** Local outcome of a cross-PC envelope injection. broker_remote uses this
|
|
59
|
+
* to construct the ACK envelope it sends back via the relay. */
|
|
60
|
+
export type RemoteInjectStatus = "received" | "busy" | "denied";
|
|
17
61
|
export declare class Broker {
|
|
18
62
|
private readonly peers;
|
|
63
|
+
/** Peers whose wrapper has signaled they are mid-turn, or to whom the
|
|
64
|
+
* broker has just delivered an envelope (received = commitment). */
|
|
65
|
+
private readonly busyPeers;
|
|
19
66
|
private readonly auditPath?;
|
|
20
67
|
private readonly onRouted?;
|
|
21
68
|
private readonly server;
|
|
69
|
+
/** Plan/25 Wave C: optional handoff for cross-PC routing. Null = local only. */
|
|
70
|
+
private remoteRouter;
|
|
22
71
|
constructor(opts: BrokerOptions);
|
|
72
|
+
/** Attach (or detach with null) a cross-PC router. Idempotent. */
|
|
73
|
+
setRemoteRouter(router: RemoteRouter | null): void;
|
|
74
|
+
/**
|
|
75
|
+
* Plan/25 Wave C entry point: deliver an envelope that arrived from a
|
|
76
|
+
* remote PC (via relay forward) into the local UDS mesh. Skips the
|
|
77
|
+
* `force from = conn.name` rule (that defense is anti-spoof for local
|
|
78
|
+
* peers; cross-PC has its own defense via the relay's verified `from_pc`).
|
|
79
|
+
*
|
|
80
|
+
* Returns the ACK status so the caller (broker_remote) can pack and
|
|
81
|
+
* forward an ACK envelope back across the relay:
|
|
82
|
+
* - `received` — target was idle (or `env.re != null`, see Wave 0
|
|
83
|
+
* bypass rule), envelope delivered, broker marked target busy if
|
|
84
|
+
* this is new work
|
|
85
|
+
* - `busy` — target mid-turn, envelope dropped
|
|
86
|
+
* - `denied` — no such local peer (or write failed) — caller maps to
|
|
87
|
+
* transport_error or denied ACK as it sees fit
|
|
88
|
+
*/
|
|
89
|
+
injectFromRemote(env: Envelope): RemoteInjectStatus;
|
|
23
90
|
/** Peers currently registered. Snapshot, safe to read. */
|
|
24
91
|
peerNames(): string[];
|
|
25
92
|
close(): Promise<void>;
|
|
@@ -31,6 +98,13 @@ export declare class Broker {
|
|
|
31
98
|
private _onClose;
|
|
32
99
|
private _route;
|
|
33
100
|
private _resolveTargets;
|
|
101
|
+
/**
|
|
102
|
+
* Writes an ACK envelope to the original sender's socket. Synchronous —
|
|
103
|
+
* the caller is inside `_route` and must keep busy-check/busy-set atomic.
|
|
104
|
+
* Broker → sender: `from="broker"`, `to=env.from`, `re=env.id`,
|
|
105
|
+
* `body={type:"ack", status, target}`.
|
|
106
|
+
*/
|
|
107
|
+
private _sendAckToSender;
|
|
34
108
|
private _handleBrokerMessage;
|
|
35
109
|
private _broadcastSystem;
|
|
36
110
|
private _appendAudit;
|