@matuto-ai/pi-broker-connector 0.1.1 → 0.1.2
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 +18 -10
- package/package.json +1 -1
- package/src/config-path.mjs +43 -0
- package/src/config.mjs +4 -3
- package/src/extension.mjs +29 -8
package/README.md
CHANGED
|
@@ -33,24 +33,32 @@ This uses the default branch; wait for the scope correction to merge first. To t
|
|
|
33
33
|
### From npm (only after publication)
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
pi install npm:@matuto-ai/pi-broker-connector@0.1.
|
|
36
|
+
pi install npm:@matuto-ai/pi-broker-connector@0.1.2
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
The proposed scope must be controlled by the publisher before publication. A repository name or workflow does not create an npm account/package. Until published, use the checkout route above; do not treat the npm command as currently available.
|
|
40
40
|
|
|
41
|
-
### Connect
|
|
41
|
+
### Connect — no restart or environment variable needed
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
With this version loaded, run one command **in the current Pi session**:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
/psst-connect /absolute/path/to/connection.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Use the existing bundle path; keep its `token` next to `connection.json`. Do not copy the JSON alone. The command connects the current native session and saves **only the absolute path**, as one line in `~/.config/psst/pi-config-path`. It does not copy credentials or alter the process environment. `/reload` reads that TXT again. You can also create that TXT yourself (mode `0600`, directory `0700`) and use `/reload`.
|
|
50
|
+
|
|
51
|
+
For an existing installation, after the owner publishes this version, update the package **inside the same sandbox**, then use `/reload` in the running Pi and the command above:
|
|
44
52
|
|
|
45
53
|
```sh
|
|
46
|
-
|
|
47
|
-
export PSST_NAME='Pi coding'
|
|
48
|
-
# Optional: a durable path INSIDE this sandbox, owned by its user, mode 0700
|
|
49
|
-
export PSST_STATE_DIR=/absolute/private/psst/pi/state
|
|
50
|
-
pi
|
|
54
|
+
pi install npm:@matuto-ai/pi-broker-connector@0.1.2
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
This installer command is not a second agent session. No sandbox recreation or Pi process restart is required. Version 0.1.1 does not have `/psst-connect`; updating the package and reloading are required first.
|
|
58
|
+
|
|
59
|
+
Keep `connection.json` and `token` private to the sandbox user (`0600`), directory `0700`. Never paste tokens into prompts, command arguments, Git, or shared config. Paths must refer to real files rather than symlinks. A `/tmp` bundle works for the current container but may disappear on container removal; remembering its path does not make the bundle persistent.
|
|
60
|
+
|
|
61
|
+
Existing `PSST_CONFIG_FILE` and `PSST_CONFIG` configurations remain supported: on session start/reload, precedence is `PSST_CONFIG_FILE`, then `PSST_CONFIG`, then the saved TXT. `/psst-connect` explicitly selects the path for the current instance; it warns if inherited environment settings will take precedence on the next reload. `PSST_NAME` defaults to `Pi`; `PSST_STATE_DIR` defaults to `~/.local/state/psst/pi`. `/psst-status` displays status and the session's exact client ID. Repeated connect closes the previous runtime before reopening the same session inbox; invalid input leaves the current connection untouched. Connection failures retain the selected path for `/reload` retry.
|
|
54
62
|
|
|
55
63
|
Expected non-secret bundle structure, generated by the broker enrollment utility:
|
|
56
64
|
|
|
@@ -111,7 +119,7 @@ Tests cover private bundle validation, exact session isolation, duplicate owners
|
|
|
111
119
|
|
|
112
120
|
## Publishing
|
|
113
121
|
|
|
114
|
-
The `0.1.
|
|
122
|
+
The `0.1.2` patch adds current-session connection through `/psst-connect` and a saved TXT path. After this PR is merged, the owner can create a GitHub release with tag **`v0.1.2`**. Preparing this PR does not publish that release or npm version.
|
|
115
123
|
|
|
116
124
|
The release workflow follows the existing Pi plugin convention: a published GitHub release triggers checks and npm publishing. It requires a `v<package.version>` tag and runs the native host gate before publishing. Nothing is published by CI on pull requests or ordinary pushes.
|
|
117
125
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { constants, openSync, closeSync, fstatSync, readFileSync, realpathSync, mkdirSync, lstatSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { LocalError } from '../vendor/broker/reference/errors.mjs';
|
|
6
|
+
|
|
7
|
+
export function validateConfigPath(value) {
|
|
8
|
+
if (typeof value !== 'string' || !isAbsolute(value) || value.length > 4096 || /[\x00-\x1f\x7f]/.test(value)) throw new LocalError('invalid_configuration');
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// This TXT contains a path only, never the bundle or token.
|
|
13
|
+
export function createConfigPathStore(home = homedir()) {
|
|
14
|
+
const directory = join(home, '.config', 'psst');
|
|
15
|
+
const file = join(directory, 'pi-config-path');
|
|
16
|
+
return {
|
|
17
|
+
read() {
|
|
18
|
+
let fd;
|
|
19
|
+
try {
|
|
20
|
+
try { lstatSync(file); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
21
|
+
if (realpathSync(file) !== file) throw new Error();
|
|
22
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
23
|
+
const stat = fstatSync(fd);
|
|
24
|
+
if (!stat.isFile() || stat.size > 4097 || (stat.mode & 0o077) || (process.getuid && stat.uid !== process.getuid())) throw new Error();
|
|
25
|
+
return validateConfigPath(readFileSync(fd, 'utf8').replace(/\n$/, ''));
|
|
26
|
+
} catch { throw new LocalError('invalid_configuration'); }
|
|
27
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
28
|
+
},
|
|
29
|
+
write(path) {
|
|
30
|
+
validateConfigPath(path);
|
|
31
|
+
let temporary;
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
34
|
+
const stat = lstatSync(directory);
|
|
35
|
+
if (realpathSync(directory) !== directory || !stat.isDirectory() || (stat.mode & 0o077) || (process.getuid && stat.uid !== process.getuid())) throw new Error();
|
|
36
|
+
temporary = join(directory, `.pi-config-path-${randomUUID()}`);
|
|
37
|
+
writeFileSync(temporary, `${path}\n`, { mode: 0o600, flag: 'wx' });
|
|
38
|
+
renameSync(temporary, file);
|
|
39
|
+
} catch { throw new LocalError('invalid_configuration'); }
|
|
40
|
+
finally { if (temporary) { try { unlinkSync(temporary); } catch {} } }
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -16,9 +16,10 @@ export function readPrivateJson(path) {
|
|
|
16
16
|
finally { if (fd !== undefined) closeSync(fd); }
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function readConfig(env = process.env) {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export function readConfig(env = process.env, explicitPath) {
|
|
20
|
+
const selected = explicitPath || env.PSST_CONFIG_FILE || env.PSST_CONFIG;
|
|
21
|
+
if (!selected) return null;
|
|
22
|
+
const path = resolve(selected);
|
|
22
23
|
const raw = readPrivateJson(path);
|
|
23
24
|
if (raw.format !== 'broker-client-bundle-v1' || !Array.isArray(raw.allowed_client_types) || !raw.allowed_client_types.includes('pi')
|
|
24
25
|
|| raw.protocol_version !== '1' || raw.file_paths_relative_to !== 'connection_file_directory'
|
package/src/extension.mjs
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
+
import { createConfigPathStore, validateConfigPath } from './config-path.mjs';
|
|
1
2
|
import { readConfig } from './config.mjs';
|
|
2
3
|
import { PiBrokerSession } from './session.mjs';
|
|
3
4
|
import { createCooperativeInbox } from './cooperative-inbox.mjs';
|
|
4
5
|
import { createTools, invokeTool } from '../vendor/broker/reference/tools.mjs';
|
|
5
6
|
import { LocalError, safeError } from '../vendor/broker/reference/errors.mjs';
|
|
6
7
|
|
|
7
|
-
export function installExtension(pi, Type, { configReader = readConfig, createSession = options => new PiBrokerSession(options) } = {}) {
|
|
8
|
+
export function installExtension(pi, Type, { configReader = readConfig, pathStore = createConfigPathStore(), env = process.env, createSession = options => new PiBrokerSession(options) } = {}) {
|
|
8
9
|
let owner = null, facade = null, ready = false, status = 'not_configured';
|
|
9
|
-
let generation = 0;
|
|
10
|
+
let generation = 0, operations = Promise.resolve();
|
|
11
|
+
const schedule = operation => {
|
|
12
|
+
const epoch = ++generation;
|
|
13
|
+
ready = false;
|
|
14
|
+
const next = operations.then(() => operation(epoch));
|
|
15
|
+
operations = next.catch(() => {});
|
|
16
|
+
return next;
|
|
17
|
+
};
|
|
10
18
|
const notify = (ctx, text, kind = 'info') => { if (ctx?.hasUI) ctx.ui.notify(text, kind); };
|
|
11
19
|
const setStatus = ctx => { if (ctx?.hasUI) ctx.ui.setStatus('psst', `Psst: ${status}`); };
|
|
12
20
|
const stop = async () => {
|
|
13
|
-
generation++;
|
|
14
21
|
const prior = owner; owner = null; facade = null; ready = false;
|
|
15
22
|
if (prior) await prior.close();
|
|
16
23
|
};
|
|
17
|
-
|
|
24
|
+
const connect = async (ctx, epoch, selectedConfig) => {
|
|
18
25
|
await stop();
|
|
19
|
-
|
|
26
|
+
if (epoch !== generation) return;
|
|
20
27
|
try {
|
|
21
|
-
const config = configReader();
|
|
28
|
+
const config = selectedConfig || configReader(env, env.PSST_CONFIG_FILE || env.PSST_CONFIG || pathStore.read());
|
|
22
29
|
if (!config) { status = 'not_configured'; setStatus(ctx); return; }
|
|
23
30
|
const hostSessionId = ctx.sessionManager.getSessionId();
|
|
24
31
|
const current = createSession({ config, hostSessionId,
|
|
@@ -39,12 +46,26 @@ export function installExtension(pi, Type, { configReader = readConfig, createSe
|
|
|
39
46
|
if (prior) await prior.close();
|
|
40
47
|
setStatus(ctx); notify(ctx, `Psst could not connect (${status}). Check the private bundle and use /reload to retry.`, 'warning');
|
|
41
48
|
}
|
|
49
|
+
};
|
|
50
|
+
pi.on('session_start', async (_event, ctx) => schedule(epoch => connect(ctx, epoch)));
|
|
51
|
+
pi.on('session_shutdown', async () => schedule(async () => { await stop(); status = 'stopped'; }));
|
|
52
|
+
pi.registerCommand('psst-connect', {
|
|
53
|
+
description: 'Connect this Pi session using an absolute connection.json path; remember only the path',
|
|
54
|
+
handler: async (args, ctx) => {
|
|
55
|
+
try {
|
|
56
|
+
const path = validateConfigPath(args.trim());
|
|
57
|
+
const config = configReader(env, path);
|
|
58
|
+
pathStore.write(path);
|
|
59
|
+
await schedule(epoch => connect(ctx, epoch, config));
|
|
60
|
+
if (env.PSST_CONFIG_FILE || env.PSST_CONFIG) notify(ctx, 'An existing PSST_CONFIG_FILE/PSST_CONFIG environment setting still takes precedence after /reload.', 'warning');
|
|
61
|
+
notify(ctx, ready ? 'Psst connected. Configuration path saved; no restart needed.' : 'Psst path saved. Check /psst-status; /reload retries the connection.');
|
|
62
|
+
} catch (error) { notify(ctx, `Psst could not save configuration (${safeError(error).error?.code || 'invalid_configuration'}). Use /psst-connect /absolute/path/connection.json.`, 'warning'); }
|
|
63
|
+
},
|
|
42
64
|
});
|
|
43
|
-
pi.on('session_shutdown', async () => { await stop(); status = 'stopped'; });
|
|
44
65
|
pi.registerCommand('psst-status', {
|
|
45
66
|
description: 'Show this Pi session’s Psst broker status and client ID',
|
|
46
67
|
handler: async (_args, ctx) => {
|
|
47
|
-
notify(ctx, ready && owner ? `Psst ${status}; client ID ${owner.inbox.identity.client_id}; inbox ${owner.inbox.stats().pending_count} pending. Cooperative mode: ask the agent to check broker_receive.` : `Psst ${status}.
|
|
68
|
+
notify(ctx, ready && owner ? `Psst ${status}; client ID ${owner.inbox.identity.client_id}; inbox ${owner.inbox.stats().pending_count} pending. Cooperative mode: ask the agent to check broker_receive.` : `Psst ${status}. Use /psst-connect /absolute/path/connection.json. No restart required.`);
|
|
48
69
|
},
|
|
49
70
|
});
|
|
50
71
|
const empty = Type.Object({}, { additionalProperties: false });
|