pi-onlyne 1.2.2 → 2.0.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/src/relay.mjs DELETED
@@ -1,299 +0,0 @@
1
- // The relay guard's policy: `<plugin package dir>/relay.toml`.
2
- //
3
- // The guard exists because a session narrated work in progress and then
4
- // reported `done` with its todos untouched, leaving the downstream writer
5
- // waiting on a handoff that never happened. The policy below is the minimum a
6
- // session owes downstream before `onlyne_complete` may end it, expressed in
7
- // delivery facts only: which roles this session handed something to, never
8
- // what the text said (that is the critic layer's business, not the adapter's).
9
- //
10
- // The file sits next to `package.json`, so the policy travels with the plugin
11
- // copy a generated workspace carries: `onlyne server generate` copies the
12
- // package to `<ws>/.onlyne/agent/<pkg-name>/` and `.pi/settings.json` loads
13
- // that copy (`crates/onlyne-server/src/generate.rs`). It is deliberately not
14
- // `<ws>/.onlyne/config.toml`: the client parses that file as `ClientConfig`,
15
- // which is `#[serde(deny_unknown_fields)]` and `additionalProperties: false`
16
- // (`crates/onlyne-config/src/client.rs`, `schema/config-client.schema.json`),
17
- // so a plugin-owned key there would make the client refuse to start. A `[local]`
18
- // fragment merged into it has the same problem.
19
- //
20
- // The accepted body is a closed subset of TOML — flat `key = value` lines, the
21
- // two keys below, one-line arrays of double-quoted strings — because this
22
- // package parses its own files by hand and the runtime has no npm dependencies.
23
- // Anything outside the subset is reported on stderr and ignored, the same
24
- // degrade-don't-disable way `.pi/onlyne.json` behaves. A missing file is the
25
- // default, which is "no guard": absent policy means the plugin behaves exactly
26
- // as it did before this module existed.
27
- //
28
- // relay_required = ["writer"] # these roles must have received a handoff
29
- // relay_required_count = 2 # ... or this many distinct downstream roles
30
- //
31
- // `relay_required` wins when both are present.
32
- //
33
- // The file is the manual installation's escape hatch. A generated workspace
34
- // carries the same policy in its spec, and the client injects it into every
35
- // session process it spawns, so the environment comes first:
36
- //
37
- // ONLYNE_RELAY_REQUIRED=writer,auditor # the spec's `relay_required`
38
- // ONLYNE_RELAY_COUNT=2 # the spec's `relay_count`
39
- //
40
- // A variable that is set and unparsable is reported on stderr and ignored, and
41
- // with nothing usable in the environment the file is read as before.
42
-
43
- import { readFileSync } from "node:fs";
44
- import { dirname, join } from "node:path";
45
- import { fileURLToPath } from "node:url";
46
-
47
- /** File name, resolved next to the plugin's `package.json`. */
48
- export const RELAY_FILE = "relay.toml";
49
-
50
- /** Fixed marker a waived completion's ledger head starts with. */
51
- export const FORCED_PREFIX = "relay-guard-forced: ";
52
-
53
- /** No policy: an empty list and no count, both frozen together. */
54
- export const DEFAULT_RELAY = Object.freeze({ required: Object.freeze([]), count: null });
55
-
56
- /** The client's injected policy variables, filled from the spec's entry. */
57
- export const RELAY_ENV_REQUIRED = "ONLYNE_RELAY_REQUIRED";
58
- export const RELAY_ENV_COUNT = "ONLYNE_RELAY_COUNT";
59
-
60
- /**
61
- * The policy file this module reads by default: beside `package.json`, the way
62
- * `protocol.mjs` reads the plugin version.
63
- */
64
- export function relayPath() {
65
- return join(dirname(fileURLToPath(import.meta.url)), "..", RELAY_FILE);
66
- }
67
-
68
- /**
69
- * Whether a loaded policy guards anything: a non-empty list, or a positive
70
- * count. A malformed key leaves its own dimension off, so one bad line cannot
71
- * silently arm the guard with the wrong rule.
72
- *
73
- * @param {{ required?: string[], count?: number | null } | null | undefined} config
74
- */
75
- export function relayEnabled(config) {
76
- if (!config) return false;
77
- if (Array.isArray(config.required) && config.required.length > 0) return true;
78
- return Number.isInteger(config.count) && config.count > 0;
79
- }
80
-
81
- /**
82
- * Parse one `relay.toml` body.
83
- *
84
- * @param {string} text
85
- * @param {string} [name] file name for diagnostics
86
- * @returns {{ required: string[], count: number | null, warning: string | null }}
87
- */
88
- export function parseRelay(text, name = RELAY_FILE) {
89
- const config = { required: [], count: null };
90
- const warnings = [];
91
- const warn = (line, detail) => warnings.push(`${name}:${line}: ${detail}`);
92
-
93
- String(text)
94
- .split(/\r?\n/)
95
- .forEach((raw, index) => {
96
- const line = index + 1;
97
- const body = stripComment(raw).trim();
98
- if (!body) return;
99
- const assignment = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\S.*)$/.exec(body);
100
- if (!assignment) {
101
- warn(line, `not a \`key = value\` line (${JSON.stringify(raw.trim())}); ignored`);
102
- return;
103
- }
104
- const [, key, value] = assignment;
105
- if (key === "relay_required") {
106
- const list = parseStringArray(value);
107
- if (list === null) {
108
- warn(line, 'relay_required must be one line of double-quoted names, e.g. ["writer"]; ignored');
109
- return;
110
- }
111
- config.required = list;
112
- return;
113
- }
114
- if (key === "relay_required_count") {
115
- const count = /^[0-9]+$/.test(value) ? Number(value) : 0;
116
- if (count < 1) {
117
- warn(line, "relay_required_count must be a positive integer; ignored");
118
- return;
119
- }
120
- config.count = count;
121
- return;
122
- }
123
- warn(line, `unknown key ${JSON.stringify(key)}; ignored`);
124
- });
125
-
126
- return { ...config, warning: warnings.length > 0 ? warnings.join("; ") : null };
127
- }
128
-
129
- /**
130
- * The policy the client injected from the spec, when it injected one.
131
- *
132
- * The list is one comma-joined variable, in the order the spec wrote it; blank
133
- * entries are dropped, so a stray comma is not a role name. A variable that is
134
- * set but unparsable is reported and ignored rather than adopted, which keeps a
135
- * typo from arming the guard with a rule nobody wrote — and `specified` then
136
- * says the environment supplied nothing, so the file still gets its turn.
137
- *
138
- * @param {Record<string, string | undefined>} [env]
139
- * @returns {{ required: string[], count: number | null, specified: boolean, warning: string | null }}
140
- */
141
- export function envRelay(env = process.env) {
142
- const warnings = [];
143
- let required = null;
144
- let count = null;
145
-
146
- const rawRequired = env[RELAY_ENV_REQUIRED];
147
- if (rawRequired !== undefined) {
148
- const names = String(rawRequired)
149
- .split(",")
150
- .map((name) => name.trim())
151
- .filter(Boolean);
152
- if (names.length > 0) required = names;
153
- else warnings.push(`${RELAY_ENV_REQUIRED}: no role names in ${JSON.stringify(rawRequired)}; ignored`);
154
- }
155
-
156
- const rawCount = env[RELAY_ENV_COUNT];
157
- if (rawCount !== undefined) {
158
- const text = String(rawCount).trim();
159
- const parsed = /^[0-9]+$/.test(text) ? Number(text) : 0;
160
- if (parsed > 0) count = parsed;
161
- else warnings.push(`${RELAY_ENV_COUNT} must be a positive integer, got ${JSON.stringify(rawCount)}; ignored`);
162
- }
163
-
164
- return {
165
- required: required ?? [],
166
- count,
167
- specified: required !== null || count !== null,
168
- warning: warnings.length > 0 ? warnings.join("; ") : null,
169
- };
170
- }
171
-
172
- /**
173
- * Read the policy: what the client injected from the spec first, then the file
174
- * beside `package.json`.
175
- *
176
- * `source` names the winner, and `present` answers the narrower question the
177
- * file itself raises: the environment winning means the file was never read, so
178
- * a stale `relay.toml` cannot outlive the spec entry that replaced it.
179
- *
180
- * @param {{ readFile?: (path: string) => string, path?: string, env?: Record<string, string | undefined> }} [options]
181
- * @returns {{ required: string[], count: number | null, path: string, present: boolean, source: "env" | "file" | "none", warning: string | null }}
182
- */
183
- export function loadRelay(options = {}) {
184
- const readFile = options.readFile ?? ((path) => readFileSync(path, "utf8"));
185
- const path = options.path ?? relayPath();
186
- const injected = envRelay(options.env ?? process.env);
187
- if (injected.specified) {
188
- return {
189
- required: injected.required,
190
- count: injected.count,
191
- path,
192
- present: false,
193
- source: "env",
194
- warning: injected.warning,
195
- };
196
- }
197
- let raw;
198
- try {
199
- raw = readFile(path);
200
- } catch {
201
- return {
202
- ...DEFAULT_RELAY,
203
- path,
204
- present: false,
205
- source: "none",
206
- warning: injected.warning,
207
- };
208
- }
209
- const parsed = parseRelay(raw, path);
210
- return {
211
- required: parsed.required,
212
- count: parsed.count,
213
- path,
214
- present: true,
215
- source: "file",
216
- warning: [injected.warning, parsed.warning].filter(Boolean).join("; ") || null,
217
- };
218
- }
219
-
220
- /**
221
- * The verdict for one `onlyne_complete`, as a refusal message or `null`.
222
- *
223
- * `delivered` is the set of roles this session's own successful `onlyne_send`
224
- * calls reached. List mode is literal: every named role must be in it. Count
225
- * mode counts distinct downstream roles, so a send to this role itself and a
226
- * send back to the role that assigned the task (the upstream) do not count —
227
- * neither of them hands work further down the cluster.
228
- *
229
- * @param {{ required?: string[], count?: number | null } | null} config
230
- * @param {Iterable<string>} delivered
231
- * @param {{ role?: string | null, upstream?: string | null }} [context]
232
- * @returns {string | null}
233
- */
234
- export function relayRefusal(config, delivered, context = {}) {
235
- if (!relayEnabled(config)) return null;
236
- const sent = new Set([...delivered].map((name) => String(name)));
237
-
238
- if (Array.isArray(config.required) && config.required.length > 0) {
239
- const missing = config.required.filter((name) => !sent.has(name));
240
- if (missing.length === 0) return null;
241
- return refusalText(
242
- `missing handoff to: ${missing.join(", ")}`,
243
- `this session delivered to: ${listOf([...sent])}`,
244
- );
245
- }
246
-
247
- const { role = null, upstream = null } = context;
248
- const downstream = [...sent].filter((name) => name !== role && name !== upstream);
249
- if (downstream.length >= config.count) return null;
250
- return refusalText(
251
- `missing handoff: ${config.count - downstream.length} of ${config.count} required distinct downstream roles`,
252
- `delivered downstream: ${listOf(downstream)}`,
253
- );
254
- }
255
-
256
- /** The one refusal sentence: what is missing, what exists, and the way out. */
257
- function refusalText(shortfall, evidence) {
258
- return (
259
- `relay guard: ${shortfall} (${evidence}); ` +
260
- "send the missing edge with onlyne_send, then call onlyne_complete again — or call it with " +
261
- 'force:true and a non-empty reason to waive the guard and stamp the ledger head with ' +
262
- `"${FORCED_PREFIX}<reason>"`
263
- );
264
- }
265
-
266
- /** A comma-joined list, or `none` when there is nothing to name. */
267
- function listOf(names) {
268
- return names.length > 0 ? names.join(", ") : "none";
269
- }
270
-
271
- /** Everything before an unquoted `#`; the subset has no multi-line strings. */
272
- function stripComment(line) {
273
- let quoted = false;
274
- for (let index = 0; index < line.length; index += 1) {
275
- const char = line[index];
276
- if (char === '"' && line[index - 1] !== "\\") quoted = !quoted;
277
- else if (char === "#" && !quoted) return line.slice(0, index);
278
- }
279
- return line;
280
- }
281
-
282
- /** One line of double-quoted strings, or `null` for anything else. */
283
- function parseStringArray(body) {
284
- const trimmed = body.trim();
285
- if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
286
- const items = [];
287
- let rest = trimmed.slice(1, -1).trim();
288
- if (rest === "") return items;
289
- for (;;) {
290
- const item = /^"((?:[^"\\]|\\.)*)"\s*/.exec(rest);
291
- if (!item) return null;
292
- items.push(item[1].replace(/\\(.)/g, "$1"));
293
- rest = rest.slice(item[0].length).trimStart();
294
- if (rest === "") return items;
295
- if (!rest.startsWith(",")) return null;
296
- rest = rest.slice(1).trimStart();
297
- if (rest === "") return null; // a trailing comma is not TOML
298
- }
299
- }
@@ -1,210 +0,0 @@
1
- // The relay guard's policy reader and its verdict.
2
- //
3
- // The reader is the half that decides whether a workspace guards anything at
4
- // all: a missing file, a malformed line or an unknown key must leave the guard
5
- // off (or leave only the sound half of it on) instead of refusing completions
6
- // on a policy nobody wrote. The client's injected policy outranks the file, and
7
- // the file is the fallback a manual installation still has.
8
-
9
- import assert from "node:assert/strict";
10
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
- import { tmpdir } from "node:os";
12
- import { dirname, join } from "node:path";
13
- import { afterEach, test } from "node:test";
14
-
15
- import {
16
- DEFAULT_RELAY,
17
- RELAY_ENV_COUNT,
18
- RELAY_ENV_REQUIRED,
19
- RELAY_FILE,
20
- envRelay,
21
- loadRelay,
22
- parseRelay,
23
- relayEnabled,
24
- relayPath,
25
- relayRefusal,
26
- } from "./relay.mjs";
27
-
28
- const cleanups = [];
29
- afterEach(() => {
30
- while (cleanups.length > 0) cleanups.pop()();
31
- });
32
-
33
- /** One temp directory holding a `relay.toml` body, when given one. */
34
- function workspace(body) {
35
- const dir = mkdtempSync(join(tmpdir(), "pi-onlyne-relay-"));
36
- cleanups.push(() => rmSync(dir, { recursive: true, force: true }));
37
- if (body !== undefined) {
38
- writeFileSync(join(dir, RELAY_FILE), body);
39
- }
40
- return dir;
41
- }
42
-
43
- test("a missing policy file leaves the guard off", () => {
44
- const dir = workspace();
45
- const relay = loadRelay({ path: join(dir, RELAY_FILE), env: {} });
46
- assert.deepEqual(
47
- { required: relay.required, count: relay.count },
48
- { required: DEFAULT_RELAY.required, count: DEFAULT_RELAY.count },
49
- );
50
- assert.equal(relay.present, false);
51
- assert.equal(relay.source, "none");
52
- assert.equal(relay.warning, null);
53
- assert.equal(relayEnabled(relay), false);
54
- assert.equal(relay.path, join(dir, RELAY_FILE));
55
- });
56
-
57
- test("the default policy path is the one beside the plugin's package.json", () => {
58
- const path = relayPath();
59
- assert.ok(path.endsWith(`/${RELAY_FILE}`), path);
60
- // A generated workspace loads the vendored copy of this package, so the
61
- // policy has to travel inside it (`crates/onlyne-server/src/generate.rs`).
62
- assert.ok(existsSync(join(dirname(path), "package.json")), dirname(path));
63
- });
64
-
65
- test("both policy keys are honoured, comments and blank lines included", () => {
66
- const list = parseRelay('# which handoffs this session owes\nrelay_required = ["writer", "auditor"]\n');
67
- assert.deepEqual(list.required, ["writer", "auditor"]);
68
- assert.equal(list.count, null);
69
- assert.equal(list.warning, null);
70
-
71
- const counted = parseRelay("relay_required_count = 2 # distinct downstream roles\n");
72
- assert.deepEqual(counted.required, []);
73
- assert.equal(counted.count, 2);
74
- assert.equal(counted.warning, null);
75
-
76
- const both = parseRelay('relay_required = []\nrelay_required_count = 3\n');
77
- assert.deepEqual(both.required, []);
78
- assert.equal(both.count, 3);
79
- });
80
-
81
- test("a body outside the closed subset warns and keeps the default", () => {
82
- const unsupported = parseRelay(
83
- ["[relay]", 'relay_required = [', ' "writer",', "]", "relay_required_count = 0", "write = true"].join("\n"),
84
- "relay.toml",
85
- );
86
- assert.deepEqual(unsupported.required, []);
87
- assert.equal(unsupported.count, null);
88
- assert.match(unsupported.warning, /relay\.toml:1: not a `key = value` line/);
89
- assert.match(unsupported.warning, /relay\.toml:2: relay_required must be one line/);
90
- assert.match(unsupported.warning, /relay\.toml:4: not a `key = value` line/);
91
- assert.match(unsupported.warning, /relay\.toml:5: relay_required_count must be a positive integer/);
92
- assert.match(unsupported.warning, /relay\.toml:6: unknown key "write"/);
93
-
94
- // One bad line does not take the sound one with it.
95
- const partial = parseRelay('relay_required = ["writer"]\nrelay_required_count = two\n');
96
- assert.deepEqual(partial.required, ["writer"]);
97
- assert.equal(partial.count, null);
98
- assert.match(partial.warning, /relay_required_count must be a positive integer/);
99
- });
100
-
101
- test("a policy file on disk reaches the caller with its warnings", () => {
102
- const dir = workspace('relay_required = ["writer"]\nnonsense\n');
103
- const relay = loadRelay({ path: join(dir, RELAY_FILE), env: {} });
104
- assert.equal(relay.present, true);
105
- assert.equal(relay.source, "file");
106
- assert.deepEqual(relay.required, ["writer"]);
107
- assert.equal(relayEnabled(relay), true);
108
- assert.match(relay.warning, /relay\.toml:2:/);
109
- });
110
-
111
- test("an injected policy is the one in force, and the file is not consulted", () => {
112
- const dir = workspace('relay_required = ["legacy"]\n');
113
- const relay = loadRelay({
114
- path: join(dir, RELAY_FILE),
115
- env: { [RELAY_ENV_REQUIRED]: "writer, auditor", [RELAY_ENV_COUNT]: "2" },
116
- });
117
- assert.deepEqual(relay.required, ["writer", "auditor"]);
118
- assert.equal(relay.count, 2);
119
- assert.equal(relay.source, "env");
120
- assert.equal(relay.present, false, "the file is not where this policy came from");
121
- assert.equal(relay.path, join(dir, RELAY_FILE));
122
- assert.equal(relay.warning, null);
123
- assert.equal(relayEnabled(relay), true);
124
- // Both forms travel when the spec names both, and the list still decides.
125
- assert.match(relayRefusal(relay, ["builder", "auditor"]), /missing handoff to: writer/);
126
- });
127
-
128
- test("the environment spells the spec's two keys", () => {
129
- const listed = envRelay({ [RELAY_ENV_REQUIRED]: "writer,, auditor ," });
130
- assert.deepEqual(listed.required, ["writer", "auditor"]);
131
- assert.equal(listed.count, null);
132
- assert.equal(listed.specified, true);
133
- assert.equal(listed.warning, null);
134
-
135
- const counted = envRelay({ [RELAY_ENV_COUNT]: "2" });
136
- assert.deepEqual(counted.required, []);
137
- assert.equal(counted.count, 2);
138
- assert.equal(counted.specified, true);
139
-
140
- const nothing = envRelay({});
141
- assert.deepEqual(nothing.required, []);
142
- assert.equal(nothing.count, null);
143
- assert.equal(nothing.specified, false, "an absent pair is not a policy");
144
- assert.equal(nothing.warning, null);
145
- });
146
-
147
- test("an unparsable variable is reported and the file keeps its turn", () => {
148
- const dir = workspace('relay_required = ["legacy"]\n');
149
- const relay = loadRelay({
150
- path: join(dir, RELAY_FILE),
151
- env: { [RELAY_ENV_COUNT]: "two" },
152
- });
153
- assert.equal(relay.source, "file");
154
- assert.equal(relay.present, true);
155
- assert.deepEqual(relay.required, ["legacy"]);
156
- assert.equal(relay.count, null);
157
- assert.equal(relayEnabled(relay), true);
158
- assert.match(relay.warning, /ONLYNE_RELAY_COUNT must be a positive integer, got "two"/);
159
-
160
- // A variable that names no role is not a policy either; with no file behind
161
- // it the guard stays off, and the reason it is off is reported.
162
- const off = loadRelay({ path: join(workspace(), RELAY_FILE), env: { [RELAY_ENV_REQUIRED]: ", ," } });
163
- assert.deepEqual(off.required, []);
164
- assert.equal(off.count, null);
165
- assert.equal(off.source, "none");
166
- assert.equal(off.present, false);
167
- assert.equal(relayEnabled(off), false);
168
- assert.match(off.warning, /ONLYNE_RELAY_REQUIRED: no role names in ", ,"/);
169
- });
170
-
171
- test("the verdict names every missing edge, and the way out", () => {
172
- const refusal = relayRefusal({ required: ["writer", "auditor"] }, ["auditor"]);
173
- assert.match(refusal, /^relay guard: missing handoff to: writer \(/);
174
- assert.match(refusal, /force:true and a non-empty reason/);
175
- assert.match(refusal, /"relay-guard-forced: <reason>"/);
176
- assert.equal(relayRefusal({ required: ["writer"] }, ["writer"]), null);
177
- // The list wins when both keys are present, however many roles were reached.
178
- assert.match(
179
- relayRefusal({ required: ["writer"], count: 2 }, ["builder", "auditor"]),
180
- /missing handoff to: writer/,
181
- );
182
- });
183
-
184
- test("count mode wants distinct downstream roles, not echoes", () => {
185
- const policy = { required: [], count: 2 };
186
- const self = "planner";
187
- const upstream = "supervisor";
188
- // A send back to the role that assigned this task, and a note to itself, are
189
- // not handoffs further down the cluster.
190
- const echoed = relayRefusal(policy, [upstream, self], { role: self, upstream });
191
- assert.match(echoed, /missing handoff: 2 of 2 required distinct downstream roles/);
192
- assert.match(echoed, /delivered downstream: none/);
193
-
194
- const one = relayRefusal(policy, [upstream, self, "builder"], { role: self, upstream });
195
- assert.match(one, /missing handoff: 1 of 2 required distinct downstream roles/);
196
- assert.match(one, /delivered downstream: builder/);
197
-
198
- const enough = relayRefusal(policy, [upstream, self, "builder", "writer"], { role: self, upstream });
199
- assert.equal(enough, null);
200
- // Two handoffs to the same role are one edge, not two.
201
- const repeated = relayRefusal(policy, ["builder", "builder"], { role: self, upstream });
202
- assert.match(repeated, /missing handoff: 1 of 2/);
203
- });
204
-
205
- test("no policy, or a policy that guards nothing, never refuses", () => {
206
- assert.equal(relayRefusal(DEFAULT_RELAY, []), null);
207
- assert.equal(relayRefusal({ required: [], count: 0 }, []), null);
208
- assert.equal(relayRefusal(undefined, []), null);
209
- assert.equal(relayEnabled({ required: [], count: null }), false);
210
- });