@matuto-ai/pi-broker-connector 0.1.0 → 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 +20 -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
|
@@ -28,29 +28,37 @@ Or, after the npm-scope correction PR is merged into `main`:
|
|
|
28
28
|
pi install git:github.com/matuto-ai/pi-broker-connector
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
This uses the default branch; wait for the scope correction to merge first. To test before merge, append `@` and the exact reviewed commit from the scope-correction PR. Existing tags are unchanged and may still contain the old npm package identity; do not use an old tag as evidence of the new scope.
|
|
31
|
+
This uses the default branch; wait for the scope correction to merge first. To test before merge, append `@` and the exact reviewed commit from the scope-correction PR. Existing tags are unchanged and may still contain the old npm package identity; do not use an old tag as evidence of the new scope. Publish a new package version only after its source changes have been reviewed and the owner is ready to release it.
|
|
32
32
|
|
|
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,6 +119,8 @@ Tests cover private bundle validation, exact session isolation, duplicate owners
|
|
|
111
119
|
|
|
112
120
|
## Publishing
|
|
113
121
|
|
|
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.
|
|
123
|
+
|
|
114
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.
|
|
115
125
|
|
|
116
126
|
For OIDC trusted publishing, configure the npm package's trusted publisher for GitHub owner `matuto-ai`, repo `pi-broker-connector`, workflow **`npm-publish.yml`** (filename only), allowing direct `npm publish`. No `NPM_TOKEN` is embedded or created. Use npm CLI >=11.5.1 with the supported Node version. If the npm package does not yet exist, the owner must complete the first package publication/account setup using npm's supported process before configuring its package settings. That setup is not performed by this repository. If the first manual publication uses `0.1.0`, bump the package version and matching release tag before the next automated publish; npm does not allow republishing the same version.
|
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 });
|