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.
Files changed (99) hide show
  1. package/README.md +195 -36
  2. package/dist/bin/supervisord.d.ts +2 -0
  3. package/dist/bin/supervisord.js +44 -0
  4. package/dist/bin/supervisord.js.map +1 -0
  5. package/dist/config.d.ts +49 -5
  6. package/dist/config.js +73 -9
  7. package/dist/config.js.map +1 -1
  8. package/dist/daemon/client.d.ts +20 -0
  9. package/dist/daemon/client.js +128 -0
  10. package/dist/daemon/client.js.map +1 -0
  11. package/dist/daemon/control_protocol.d.ts +100 -0
  12. package/dist/daemon/control_protocol.js +63 -0
  13. package/dist/daemon/control_protocol.js.map +1 -0
  14. package/dist/daemon/id.d.ts +18 -0
  15. package/dist/daemon/id.js +30 -0
  16. package/dist/daemon/id.js.map +1 -0
  17. package/dist/daemon/install.d.ts +132 -0
  18. package/dist/daemon/install.js +312 -0
  19. package/dist/daemon/install.js.map +1 -0
  20. package/dist/daemon/registry.d.ts +47 -0
  21. package/dist/daemon/registry.js +123 -0
  22. package/dist/daemon/registry.js.map +1 -0
  23. package/dist/daemon/rpc_child.d.ts +76 -0
  24. package/dist/daemon/rpc_child.js +130 -0
  25. package/dist/daemon/rpc_child.js.map +1 -0
  26. package/dist/daemon/supervisor.d.ts +38 -0
  27. package/dist/daemon/supervisor.js +301 -0
  28. package/dist/daemon/supervisor.js.map +1 -0
  29. package/dist/index.d.ts +62 -8
  30. package/dist/index.js +1232 -304
  31. package/dist/index.js.map +1 -1
  32. package/dist/mesh/canonical.d.ts +30 -0
  33. package/dist/mesh/canonical.js +61 -0
  34. package/dist/mesh/canonical.js.map +1 -0
  35. package/dist/mesh/client.d.ts +31 -0
  36. package/dist/mesh/client.js +56 -0
  37. package/dist/mesh/client.js.map +1 -0
  38. package/dist/mesh/encoding.d.ts +36 -0
  39. package/dist/mesh/encoding.js +53 -0
  40. package/dist/mesh/encoding.js.map +1 -0
  41. package/dist/mesh/self_revoke.d.ts +111 -0
  42. package/dist/mesh/self_revoke.js +182 -0
  43. package/dist/mesh/self_revoke.js.map +1 -0
  44. package/dist/mesh/siblings.d.ts +62 -0
  45. package/dist/mesh/siblings.js +95 -0
  46. package/dist/mesh/siblings.js.map +1 -0
  47. package/dist/mesh/types.d.ts +34 -0
  48. package/dist/mesh/types.js +11 -0
  49. package/dist/mesh/types.js.map +1 -0
  50. package/dist/mesh/verify.d.ts +17 -0
  51. package/dist/mesh/verify.js +77 -0
  52. package/dist/mesh/verify.js.map +1 -0
  53. package/dist/pairing/qr.d.ts +16 -5
  54. package/dist/pairing/qr.js +27 -8
  55. package/dist/pairing/qr.js.map +1 -1
  56. package/dist/pairing/storage.d.ts +41 -0
  57. package/dist/pairing/storage.js +158 -21
  58. package/dist/pairing/storage.js.map +1 -1
  59. package/dist/protocol/types.d.ts +23 -0
  60. package/dist/session/broker.d.ts +74 -0
  61. package/dist/session/broker.js +142 -4
  62. package/dist/session/broker.js.map +1 -1
  63. package/dist/session/broker_remote.d.ts +110 -0
  64. package/dist/session/broker_remote.js +397 -0
  65. package/dist/session/broker_remote.js.map +1 -0
  66. package/dist/session/cwd_lock.d.ts +28 -0
  67. package/dist/session/cwd_lock.js +89 -0
  68. package/dist/session/cwd_lock.js.map +1 -0
  69. package/dist/session/global_config.d.ts +9 -0
  70. package/dist/session/global_config.js +9 -0
  71. package/dist/session/global_config.js.map +1 -1
  72. package/dist/session/leader_election.d.ts +16 -0
  73. package/dist/session/leader_election.js +22 -0
  74. package/dist/session/leader_election.js.map +1 -1
  75. package/dist/session/local_config.d.ts +12 -5
  76. package/dist/session/local_config.js +24 -3
  77. package/dist/session/local_config.js.map +1 -1
  78. package/dist/session/peer.d.ts +28 -1
  79. package/dist/session/peer.js +69 -2
  80. package/dist/session/peer.js.map +1 -1
  81. package/dist/session/peer_inventory.d.ts +13 -0
  82. package/dist/session/peer_inventory.js +48 -0
  83. package/dist/session/peer_inventory.js.map +1 -0
  84. package/dist/session/setup_wizard.d.ts +32 -8
  85. package/dist/session/setup_wizard.js +45 -33
  86. package/dist/session/setup_wizard.js.map +1 -1
  87. package/dist/session/tools.d.ts +15 -7
  88. package/dist/session/tools.js +145 -31
  89. package/dist/session/tools.js.map +1 -1
  90. package/dist/transport/pi_forward_client.d.ts +29 -0
  91. package/dist/transport/pi_forward_client.js +62 -0
  92. package/dist/transport/pi_forward_client.js.map +1 -0
  93. package/dist/ui/footer.js +8 -6
  94. package/dist/ui/footer.js.map +1 -1
  95. package/docs/daemon.md +289 -0
  96. package/package.json +10 -3
  97. package/service-templates/launchd.plist.template +35 -0
  98. package/service-templates/systemd.service.template +19 -0
  99. package/skills/agent-network/SKILL.md +273 -294
@@ -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
- * Renders the QR + URI in the Pi TUI's output pane via stderr. `ctx.ui.notify`
61
- * collapses multi-line content into a single toast, so for ASCII art we need
62
- * the raw stderr capture that the Pi TUI exposes as scrollable log. Post
63
- * plano 14 the QR carries only `t/epk/n`, fits comfortably in the panel
64
- * without needing a separate Terminal window.
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
- qrTerminal.generate(uri, { small: true }, (qrcode) => {
68
- process.stderr.write(`\n📱 Scan to pair:\n\n${qrcode}\n${uri}\n`);
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
@@ -1 +1 @@
1
- {"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"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"}
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>;
@@ -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 keytar from "keytar";
4
+ import { AsyncEntry } from "@napi-rs/keyring";
5
5
  import { generateEd25519Keypair } from "./crypto.js";
6
- const KEYCHAIN_SERVICE = "dev.remotepi.mac";
7
- const PEERS_PATH = join(homedir(), ".pi", "remote", "peers.json");
8
- // ── Keychain ──────────────────────────────────────────────────────────────────
9
- async function loadKeypairFromKeychain(account) {
10
- const stored = await keytar.getPassword(KEYCHAIN_SERVICE, account);
11
- if (!stored)
12
- return null;
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
- async function saveKeypairToKeychain(account, kp) {
20
- const data = JSON.stringify({
21
- pk: Buffer.from(kp.publicKey).toString("base64"),
22
- sk: Buffer.from(kp.secretKey).toString("base64"),
23
- });
24
- await keytar.setPassword(KEYCHAIN_SERVICE, account, data);
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 existing = await loadKeypairFromKeychain("longterm-ed25519");
28
- if (existing)
29
- return existing;
30
- const kp = generateEd25519Keypair();
31
- await saveKeypairToKeychain("longterm-ed25519", kp);
32
- return kp;
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;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAuB,MAAM,aAAa,CAAC;AAE1E,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAC5C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;AAElE,iFAAiF;AAEjF,KAAK,UAAU,uBAAuB,CACpC,OAAe;IAEf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;IACnE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAA+B,CAAC;IAChE,OAAO;QACL,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC;QAC3C,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,qBAAqB,CAClC,OAAe,EACf,EAAoD;IAEpD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QAC1B,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAChD,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;KACjD,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,WAAW,CAAC,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB;IAC7C,MAAM,QAAQ,GAAG,MAAM,uBAAuB,CAAC,kBAAkB,CAAC,CAAC;IACnE,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,EAAE,GAAG,sBAAsB,EAAE,CAAC;IACpC,MAAM,qBAAqB,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;IACpD,OAAO,EAAE,CAAC;AACZ,CAAC;AAUD,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;QAC1D,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAkB;IAC9C,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAC;IAChC,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,MAAM,CAAC,UAAU,CAAC,CAAC;IACvE,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACb,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,qBAAqB;IAC5C,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;IACD,MAAM,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAiB;IAChD,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC;IACjE,IAAI,QAAQ,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACnD,MAAM,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1E,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/pairing/storage.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -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;