sibyl-system 1.3.0 → 1.3.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 +26 -13
- package/dist/cli.js +37 -12
- package/dist/index.js +91 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
A standalone [opencode](https://opencode.ai) plugin:
|
|
6
6
|
|
|
7
7
|
- **`sibyl_consult`** — a three-voter review council (MELCHIOR / BALTHASAR / CASPER)
|
|
8
|
-
that audits an artifact against a goal and returns a **fail-closed
|
|
8
|
+
that audits an artifact against a goal and returns a **fail-closed verdict**
|
|
9
|
+
(`APPROVE` / `REJECT` / `CANNOT_ANSWER`).
|
|
9
10
|
- **`sibyl_swarm`** — a lightweight workflow swarm: an ARCHITECT persona plans a
|
|
10
11
|
dependency-ordered task graph, deterministic workers execute it in waves, and
|
|
11
12
|
the result is aggregated into a verdict.
|
|
@@ -24,8 +25,10 @@ Council voters, swarm workers and the swarm judge run as **child sessions of the
|
|
|
24
25
|
calling session** (`parentID` from the tool context): the TUI session picker
|
|
25
26
|
lists root sessions only, so no sibyl ballot ever clutters your top-level
|
|
26
27
|
session list. Transcripts stay in the DB, reachable from the parent session and
|
|
27
|
-
mirrored into the run's sealed reply files.
|
|
28
|
-
to top-level creation
|
|
28
|
+
mirrored into the run's sealed reply files. A seatless (CLI/headless) caller
|
|
29
|
+
falls back to top-level creation, and its voter rows are **garbage-collected
|
|
30
|
+
on success** (the reply files are the durable record; failed votes keep their
|
|
31
|
+
DB rows for salvage). `sibyl_review` roles run fully isolated under /tmp with
|
|
29
32
|
their own HOME/DB (`src/lane/isolated.ts`), never touching yours.
|
|
30
33
|
|
|
31
34
|
## v1.1 — the review chamber (民主集中制)
|
|
@@ -44,6 +47,7 @@ Laws: docs/isolation-laws.md. Mechanism: docs/democratic-centralism.md.
|
|
|
44
47
|
node dist/cli.js run --target doc.md --goal "Is this sound?" --model your-provider/your-model
|
|
45
48
|
node dist/cli.js status --tail 5
|
|
46
49
|
node dist/cli.js spotcheck <runId>
|
|
50
|
+
node dist/cli.js run --target - --profile exam --scenario scenarios/<id>.json ... # behavioral exam venue (bell preflight mandatory)
|
|
47
51
|
```
|
|
48
52
|
|
|
49
53
|
## Install
|
|
@@ -94,7 +98,7 @@ stderr, registers nothing, and returns empty hooks.
|
|
|
94
98
|
| Tool | Arguments | Behavior |
|
|
95
99
|
|------|-----------|----------|
|
|
96
100
|
| `sibyl_consult` | `{ artifact, goal }` | `artifact` is a file path or inline multi-line text (≤ 256 KiB). The three councilors audit it in parallel; each reply is parsed into a verdict (one in-session JSON-only repair shot per voter). Returns the tally, merged reasons/must-fix, run id, and per-voter reply file paths. |
|
|
97
|
-
| `sibyl_swarm` | `{ artifact, goal, judge? }` | ARCHITECT decomposes goal + artifact into a strict-JSON workflow schema; workers are minted deterministically and dispatched in dependency waves; drafts land in the run's space dir. Verdict: `APPROVE` / `REJECT` / `EXHAUSTED
|
|
101
|
+
| `sibyl_swarm` | `{ artifact, goal, judge? }` | ARCHITECT decomposes goal + artifact into a strict-JSON workflow schema; workers are minted deterministically and dispatched in dependency waves; drafts land in the run's space dir. Verdict: `APPROVE` / `REJECT` / `EXHAUSTED`, forced to `CANNOT_ANSWER` when a declared worker left no terminal row (W2) or zero drafts were produced. With `judge: true`, one extra judge pass may replace the derived verdict — an unrecognized or failed judge reply keeps the derived one. |
|
|
98
102
|
| `sibyl_status` | `{ runId? }` | Read-only. Lists all recorded runs (newest last) + the last chamber-ledger rows, or shows one run's full record and space dir. |
|
|
99
103
|
| `sibyl_review` | `{ target, goal, seed?, maxRounds? }` | v1.1 general democratic-centralism chamber: launches the isolated evidence→clash→judge pipeline detached and returns a receipt (explicitly NOT a verdict); the single voice lands in the run record at terminal state. CLI twin: `sibyl-chamber`. |
|
|
100
104
|
| `sibyl_anchor_check` | `{ sessionId, index?, body?, expectSha256?, label? }` | Read-only verifier for "human anchor" claims (an order/approval message the owner says they sent): re-checks the claim against the engine's own session store instead of trusting a transcribed string. Closed 4-state verdict `MATCH` / `MISMATCH` / `ABSENT` / `ERROR`; the receipt carries a hash of the canonical view read, so later store mutation shows up as a view-hash difference. No `expectSha256` claim → non-verdict. |
|
|
@@ -119,7 +123,7 @@ defaults.
|
|
|
119
123
|
| `timeoutMs` | `240000` | Per-child-session timeout (create + prompt share the budget). Integer ≥ 1. |
|
|
120
124
|
| `concurrencyK` | `4` | Cap on parallel workers per wave (also capped by the schema's own `concurrency`). Integer 1–8. |
|
|
121
125
|
| `staggerMs` | `2000` | Delay between worker launches within a wave, to avoid rate-limit bursts. Integer ≥ 0. |
|
|
122
|
-
| `lane` | see below | v1.1 isolated-lane mechanics for `sibyl_review` / `sibyl-chamber` (`src/lane/isolated.ts`). Sub-keys: `runRoot` (string, default `"/tmp"`) — parent dir for run dirs; `opencodeBin` (non-empty string; discovery order: `$OPENCODE_BIN` → `~/.local/bin/opencode` → `/usr/local/bin/opencode` → `/usr/bin/opencode` → bare PATH name `"opencode"`; never empty, a wrong guess fails loud at spawn naming the bin) — the binary launched for each isolated role; `configSource` (default `~/.config/opencode/opencode.jsonc`) — **read-copied** into each role home, never written back; `roleTimeoutMs` (integer ≥ 5000, default `600000`) — per-role wall-clock budget. |
|
|
126
|
+
| `lane` | see below | v1.1 isolated-lane mechanics for `sibyl_review` / `sibyl-chamber` (`src/lane/isolated.ts`). Sub-keys: `runRoot` (string, default `"/tmp"`) — parent dir for run dirs; `opencodeBin` (non-empty string; discovery order: `$OPENCODE_BIN` → `~/.local/bin/opencode` → `/usr/local/bin/opencode` → `/usr/bin/opencode` → bare PATH name `"opencode"`; never empty, a wrong guess fails loud at spawn naming the bin) — the binary launched for each isolated role; `configSource` (default `~/.config/opencode/opencode.jsonc`) — **read-copied** into each role home, never written back; `roleTimeoutMs` (integer ≥ 5000, default `600000`) — per-role wall-clock budget; `examMaskDirs` (array of absolute paths, default `[]`) — dirs tmpfs-masked from exam candidates (grader files, other runs); empty = an unsandboxed venue that says so plainly on the voice face. |
|
|
123
127
|
| `modelPolicy` | `{ allowedPrefixes: ["local-"] }` | v1.1 E4/F1 seating allow-list: only pool models whose `providerID/modelID` starts with one of these prefixes may take a seat; everything else is denied at launch. Array of non-empty strings, **at least 1 entry** (an empty list is rejected as a deny-everything typo). Set this to your own machine's providers — the shipped default is a prefix pattern, not a specific model. |
|
|
124
128
|
| `chamber` | see below | v1.1 review-chamber seats and rounds. Sub-keys: `maxRounds` (integer 1–8, default `3`) — clash/cross-critique round cap; `roles` (`{ evidence, pro, con, judge }` → pool slot names, each default `"default"`); `judgePool` (non-empty array of slot names, default `["default"]`) — the slots the independent judge may be drawn from (E2 pool+seed committed before launch). |
|
|
125
129
|
|
|
@@ -140,6 +144,8 @@ fails the release if any vendor model id reappears on a shipped surface.
|
|
|
140
144
|
|
|
141
145
|
## State layout
|
|
142
146
|
|
|
147
|
+
- **Chamber ledger**: `~/.sibyl/chamber-ledger.jsonl` — append-only, one JSON
|
|
148
|
+
record per line, serial-stamped at append (A5); `sibyl_status` reads its tail.
|
|
143
149
|
- **Runs file**: `~/.sibyl/runs.json` (Windows: `%USERPROFILE%\.sibyl\runs.json`) —
|
|
144
150
|
one record per run (id, kind, status, verdict tally, operator notes), co-located
|
|
145
151
|
with `spaces/` under the one `~/.sibyl` state root, so npm upgrades (which ship a
|
|
@@ -158,13 +164,18 @@ fails the release if any vendor model id reappears on a shipped surface.
|
|
|
158
164
|
Plainly stated:
|
|
159
165
|
|
|
160
166
|
- Approval needs **≥ 2 of 3 approvals and zero error/missing votes**.
|
|
161
|
-
|
|
167
|
+
- When the council genuinely spoke (all three ballots substantive, or ≥ 2
|
|
168
|
+
substantive rejects) and approval was not reached, the verdict is **REJECT**.
|
|
169
|
+
- When error/missing seats leave no decision (2A + 1 error, 2A + 1 missing,
|
|
170
|
+
a tie with an error seat), the verdict is **CANNOT_ANSWER** — a wounded
|
|
171
|
+
instrument, NOT a substantive reject, and it can never approve. Repair the
|
|
172
|
+
infra and re-run (since v1.2; pinned by `test/council.test.ts`).
|
|
162
173
|
- Error votes, missing votes, and malformed verdicts all count against approval.
|
|
163
174
|
- A reply that still doesn't parse after its single repair shot becomes a
|
|
164
175
|
0-confidence REJECT ballot with reason `verdict-unparseable: …`.
|
|
165
176
|
- There is no lenient mode, by design.
|
|
166
177
|
|
|
167
|
-
## Ballot-integrity laws (W1–
|
|
178
|
+
## Ballot-integrity laws (W1–W4, drafted 2026-10-04/05 after the campaign postmortems; shipped in v1.3.0)
|
|
168
179
|
|
|
169
180
|
- **Convener recusal (W1).** Every consult/swarm/review run resolves the
|
|
170
181
|
convening execution chain through the engine's own session store
|
|
@@ -189,7 +200,9 @@ Plainly stated:
|
|
|
189
200
|
## Security notes
|
|
190
201
|
|
|
191
202
|
- Consult/swarm send the artifact text to your configured model pool providers.
|
|
192
|
-
- The plugin writes
|
|
203
|
+
- The plugin writes under its state paths (the runs file and the per-run space) and,
|
|
204
|
+
for chamber/exam lanes, sealed run directories under `lane.runRoot`
|
|
205
|
+
(default `/tmp`) — role homes, transcripts, pidfiles, CHECKSUMS.
|
|
193
206
|
- Voter/worker turns run with `bash`, `edit`, and `write` disabled per prompt.
|
|
194
207
|
|
|
195
208
|
## Architecture (`src/`)
|
|
@@ -206,7 +219,9 @@ index.ts plugin entry: parse options → share one RunStore + client
|
|
|
206
219
|
│ validation → exactly one repair → fail-closed REJECT
|
|
207
220
|
├── council/ councilor personas + tallyVotes (majority2of3 default,
|
|
208
221
|
│ unanimous exported); pure aggregation, zero IO
|
|
209
|
-
├── state/ RunStore: atomic RMW runs.json, per-run space dirs
|
|
222
|
+
├── state/ RunStore: atomic RMW runs.json, per-run space dirs;
|
|
223
|
+
│ v1.1 chamber records: EOF-append ledger (A5), face-last
|
|
224
|
+
│ regeneration, CHECKSUMS + spotcheck (A4)
|
|
210
225
|
├── swarm/ planner (ARCHITECT schema) → minter (deterministic worker
|
|
211
226
|
│ roster) → dispatcher (dependency waves, stagger,
|
|
212
227
|
│ suspend-on-rate-limit, resume) → aggregate (report +
|
|
@@ -218,8 +233,6 @@ index.ts plugin entry: parse options → share one RunStore + client
|
|
|
218
233
|
│ fail-closed, dissent sealed by path+hash)
|
|
219
234
|
├── exam/ v1.1 E3: scenario-as-data behavioral exams, mechanical
|
|
220
235
|
│ transcript/disk signal grading, canary veto
|
|
221
|
-
├── state/ RunStore + v1.1 chamber records: EOF-append ledger (A5),
|
|
222
|
-
│ face-last regeneration, CHECKSUMS + spotcheck (A4)
|
|
223
236
|
├── personas.ts registry: 3 councilors + ARCHITECT + judge/repair texts,
|
|
224
237
|
│ model slots — the single source the W3 face hashes
|
|
225
238
|
├── terminal.ts W2 terminal-row grammar (ok|error|timeout), parse + the
|
|
@@ -240,7 +253,7 @@ index.ts plugin entry: parse options → share one RunStore + client
|
|
|
240
253
|
|
|
241
254
|
This is **not** the oh-my-openagent swarm. It has no `team_*` tools, no
|
|
242
255
|
cross-agent message bus, and no fleet-orchestration dependency of any kind —
|
|
243
|
-
it is a clean-room implementation (~
|
|
256
|
+
it is a clean-room implementation (~5K lines of product code, comments excluded) that happens to
|
|
244
257
|
share the problem space. `sibyl_swarm` is a single tool call driving a
|
|
245
258
|
PLAN→MINT→DISPATCH→AGGREGATE pipeline over ordinary child sessions.
|
|
246
259
|
|
|
@@ -248,7 +261,7 @@ PLAN→MINT→DISPATCH→AGGREGATE pipeline over ordinary child sessions.
|
|
|
248
261
|
|
|
249
262
|
```bash
|
|
250
263
|
npm run typecheck # tsc --noEmit, strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
|
|
251
|
-
npm test #
|
|
264
|
+
npm test # full unit suite, fully offline (no network, no LLM; ~420 at v1.3.0 — the run prints the exact count)
|
|
252
265
|
npm run build # esbuild bundle → dist/index.js + dist/cli.js (ESM)
|
|
253
266
|
node smoke/run-smoke.mjs # offline smoke of the shipped surface (see smoke/README.md)
|
|
254
267
|
./ship.sh # the full release gate: all of the above + portability scan of the packed tarball
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ var __export = (target, all) => {
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { spawn as spawn2 } from "node:child_process";
|
|
11
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
11
12
|
import { copyFile as copyFile2, mkdir as mkdir6, readFile as readFile6 } from "node:fs/promises";
|
|
12
13
|
import { basename, dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "node:path";
|
|
13
14
|
|
|
@@ -847,6 +848,17 @@ import { createWriteStream, existsSync } from "node:fs";
|
|
|
847
848
|
import { copyFile, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
848
849
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
849
850
|
var DEFAULT_PINNED_PATH = "/usr/bin:/bin";
|
|
851
|
+
function composeSandboxArgs(opts) {
|
|
852
|
+
const argv = [opts.bwrapBin, "--ro-bind", "/", "/", "--dev", "/dev", "--die-with-parent"];
|
|
853
|
+
for (const m of opts.maskDirs) argv.push("--tmpfs", m);
|
|
854
|
+
for (const k of opts.keepDirs) argv.push("--dir", posixDirname(k), "--bind", k, k);
|
|
855
|
+
argv.push("--", opts.bin, ...opts.args);
|
|
856
|
+
return argv;
|
|
857
|
+
}
|
|
858
|
+
function posixDirname(p) {
|
|
859
|
+
const i = p.lastIndexOf("/");
|
|
860
|
+
return i <= 0 ? "/" : p.slice(0, i);
|
|
861
|
+
}
|
|
850
862
|
function rolePaths(cfg, role) {
|
|
851
863
|
const roleDir = join3(cfg.runDir, role);
|
|
852
864
|
const home = join3(roleDir, "home");
|
|
@@ -911,8 +923,12 @@ async function launchRole(cfg, role, spec) {
|
|
|
911
923
|
await writeFile3(transcriptPath, "", "utf8");
|
|
912
924
|
await writeFile3(stderrPath, "", "utf8");
|
|
913
925
|
const args = composeRunArgs(spec);
|
|
926
|
+
const bwrap = cfg.bwrapBin ?? "/usr/bin/bwrap";
|
|
927
|
+
const mask = cfg.sandboxMaskDirs ?? [];
|
|
928
|
+
const sandboxed = mask.length > 0 && existsSync(bwrap);
|
|
929
|
+
const spawnArgv = sandboxed ? composeSandboxArgs({ bwrapBin: bwrap, maskDirs: mask, keepDirs: [cfg.runDir], bin: cfg.opencodeBin, args }) : [cfg.opencodeBin, ...args];
|
|
914
930
|
return new Promise((resolvePromise) => {
|
|
915
|
-
const child = spawn(
|
|
931
|
+
const child = spawn(spawnArgv[0], spawnArgv.slice(1), {
|
|
916
932
|
cwd: paths.workspace,
|
|
917
933
|
env,
|
|
918
934
|
detached: true,
|
|
@@ -934,7 +950,8 @@ async function launchRole(cfg, role, spec) {
|
|
|
934
950
|
transcriptPath,
|
|
935
951
|
sessionId: extractSessionId(transcript),
|
|
936
952
|
startedAt,
|
|
937
|
-
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
953
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
954
|
+
sandboxed
|
|
938
955
|
};
|
|
939
956
|
if (spawnError2 !== void 0) result.error = spawnError2;
|
|
940
957
|
resolvePromise(result);
|
|
@@ -20222,7 +20239,10 @@ var pluginOptionsSchema = external_exports.object({
|
|
|
20222
20239
|
runRoot: external_exports.string().min(1).default("/tmp"),
|
|
20223
20240
|
opencodeBin: external_exports.string().min(1).prefault(discoverOpencodeBin),
|
|
20224
20241
|
configSource: external_exports.string().prefault(() => join6(homedir2(), ".config", "opencode", "opencode.jsonc")),
|
|
20225
|
-
roleTimeoutMs: external_exports.number().int().min(5e3).default(6e5)
|
|
20242
|
+
roleTimeoutMs: external_exports.number().int().min(5e3).default(6e5),
|
|
20243
|
+
/** Exam venue: absolute dirs tmpfs-masked from the candidate (grader files,
|
|
20244
|
+
* other runs). Empty = venue runs unsandboxed and SAYS SO on the voice face. */
|
|
20245
|
+
examMaskDirs: external_exports.array(external_exports.string().min(2)).default([])
|
|
20226
20246
|
}).prefault({}),
|
|
20227
20247
|
/** v1.1 E4/F1: seats only from allowed model-id prefixes; deny at launch. */
|
|
20228
20248
|
modelPolicy: external_exports.object({
|
|
@@ -20394,7 +20414,7 @@ async function resolveRun(flags) {
|
|
|
20394
20414
|
if (scenarios.length === 0) return { ok: false, error: "--profile exam needs at least one --scenario" };
|
|
20395
20415
|
const only = splitModelId(modelFlag ?? "") ?? null;
|
|
20396
20416
|
if (only === null) return { ok: false, error: "--profile exam requires --model provider/model" };
|
|
20397
|
-
return { ok: true, run: { runId, runDir, laneCfg, timeoutMs, chamber: null, exam: { scenarios, modelId: modelFlag }, killInjection } };
|
|
20417
|
+
return { ok: true, run: { runId, runDir, laneCfg, timeoutMs, chamber: null, exam: { scenarios, modelId: modelFlag }, killInjection, examMaskDirs: opts?.lane.examMaskDirs ?? [] } };
|
|
20398
20418
|
}
|
|
20399
20419
|
if (profile !== "review") return { ok: false, error: `unknown --profile ${profile}` };
|
|
20400
20420
|
const slots = {
|
|
@@ -20427,7 +20447,7 @@ async function resolveRun(flags) {
|
|
|
20427
20447
|
// wired below (type seam: lane needs cfg, cfg needs lane)
|
|
20428
20448
|
};
|
|
20429
20449
|
chamber.lane = makeIsolatedLane(chamber, laneCfg, timeoutMs);
|
|
20430
|
-
return { ok: true, run: { runId, runDir, laneCfg, timeoutMs, chamber, exam: null, killInjection } };
|
|
20450
|
+
return { ok: true, run: { runId, runDir, laneCfg, timeoutMs, chamber, exam: null, killInjection, examMaskDirs: opts?.lane.examMaskDirs ?? [] } };
|
|
20431
20451
|
}
|
|
20432
20452
|
function slotToId(slot, pool) {
|
|
20433
20453
|
const m = pool[slot] ?? pool["default"];
|
|
@@ -20451,8 +20471,8 @@ function makeIsolatedLane(cfg, laneCfg, timeoutMs) {
|
|
|
20451
20471
|
}
|
|
20452
20472
|
};
|
|
20453
20473
|
}
|
|
20454
|
-
function makeExamDriver(laneCfgBase, runDir, modelId, timeoutMs) {
|
|
20455
|
-
const lc = { runDir, opencodeBin: laneCfgBase.opencodeBin, configSource: laneCfgBase.configSource };
|
|
20474
|
+
function makeExamDriver(laneCfgBase, runDir, modelId, timeoutMs, maskDirs, bwrapProbe) {
|
|
20475
|
+
const lc = { runDir, opencodeBin: laneCfgBase.opencodeBin, configSource: laneCfgBase.configSource, sandboxMaskDirs: maskDirs, bwrapBin: bwrapProbe };
|
|
20456
20476
|
let turnNo = 0;
|
|
20457
20477
|
const run = async (message, resume) => {
|
|
20458
20478
|
turnNo += 1;
|
|
@@ -20469,11 +20489,14 @@ function makeExamDriver(laneCfgBase, runDir, modelId, timeoutMs) {
|
|
|
20469
20489
|
return { facts: { ok: o.ok, rc: o.rc, signal: o.signal, timedOut: o.timedOut }, transcript, sessionId: o.sessionId };
|
|
20470
20490
|
};
|
|
20471
20491
|
return {
|
|
20472
|
-
|
|
20473
|
-
|
|
20492
|
+
sandboxed: maskDirs.length > 0 && existsSync3(bwrapProbe),
|
|
20493
|
+
driver: {
|
|
20494
|
+
start: async (prompt) => await run(prompt),
|
|
20495
|
+
continueTurn: async (sid, message) => await run(message, sid)
|
|
20496
|
+
}
|
|
20474
20497
|
};
|
|
20475
20498
|
}
|
|
20476
|
-
function examVoice(results, runId, runDir) {
|
|
20499
|
+
function examVoice(results, runId, runDir, venueSandbox) {
|
|
20477
20500
|
const lines = ["SIBYL \u2014 ONE CONCLUSION, ONE VOICE [SIBYL-ONE-VOICE]"];
|
|
20478
20501
|
let anyFail = false;
|
|
20479
20502
|
let anyHuman = false;
|
|
@@ -20487,6 +20510,7 @@ function examVoice(results, runId, runDir) {
|
|
|
20487
20510
|
const conclusion = anyFail ? "REJECT" : anyHuman ? "NEEDS_HUMAN" : "APPROVE";
|
|
20488
20511
|
lines.splice(1, 0, `conclusion: ${conclusion} terminal: CONVERGED run: ${runId}`);
|
|
20489
20512
|
lines.push(`honesty: PERFORMANCE-ONLY-IN-LOOP (single-uid box until C-09/C-14 rulings)`);
|
|
20513
|
+
lines.push(`venue sandbox: ${venueSandbox}`);
|
|
20490
20514
|
lines.push(`verify every recorded byte: ${spotcheckCommand(runDir)}`);
|
|
20491
20515
|
return lines.join("\n");
|
|
20492
20516
|
}
|
|
@@ -20550,7 +20574,7 @@ async function main(argv) {
|
|
|
20550
20574
|
return 0;
|
|
20551
20575
|
}
|
|
20552
20576
|
if (run.exam !== null) {
|
|
20553
|
-
const driver = makeExamDriver(run.laneCfg, run.runDir, run.exam.modelId, run.timeoutMs);
|
|
20577
|
+
const { driver, sandboxed } = makeExamDriver(run.laneCfg, run.runDir, run.exam.modelId, run.timeoutMs, run.examMaskDirs, "/usr/bin/bwrap");
|
|
20554
20578
|
const results = [];
|
|
20555
20579
|
const scPaths = typeof flags["scenario"] === "string" ? [flags["scenario"]] : [];
|
|
20556
20580
|
if (flags["allow-unbellied"] !== true) {
|
|
@@ -20564,7 +20588,8 @@ async function main(argv) {
|
|
|
20564
20588
|
}
|
|
20565
20589
|
}
|
|
20566
20590
|
for (const sc of run.exam.scenarios) results.push(await runExamScenario(sc, run.runDir, driver));
|
|
20567
|
-
|
|
20591
|
+
await writeChecksums(run.runDir, await listChecksumTargets(run.runDir));
|
|
20592
|
+
console.log(examVoice(results, run.runId, run.runDir, sandboxed ? `ON (masked ${String(run.examMaskDirs.length)} dir(s))` : run.examMaskDirs.length === 0 ? "OFF (lane.examMaskDirs empty \u2014 grader files are candidate-readable)" : "OFF (bwrap missing at /usr/bin/bwrap)"));
|
|
20568
20593
|
return 0;
|
|
20569
20594
|
}
|
|
20570
20595
|
return 2;
|
package/dist/index.js
CHANGED
|
@@ -18919,7 +18919,10 @@ var pluginOptionsSchema = external_exports.object({
|
|
|
18919
18919
|
runRoot: external_exports.string().min(1).default("/tmp"),
|
|
18920
18920
|
opencodeBin: external_exports.string().min(1).prefault(discoverOpencodeBin),
|
|
18921
18921
|
configSource: external_exports.string().prefault(() => join(homedir(), ".config", "opencode", "opencode.jsonc")),
|
|
18922
|
-
roleTimeoutMs: external_exports.number().int().min(5e3).default(6e5)
|
|
18922
|
+
roleTimeoutMs: external_exports.number().int().min(5e3).default(6e5),
|
|
18923
|
+
/** Exam venue: absolute dirs tmpfs-masked from the candidate (grader files,
|
|
18924
|
+
* other runs). Empty = venue runs unsandboxed and SAYS SO on the voice face. */
|
|
18925
|
+
examMaskDirs: external_exports.array(external_exports.string().min(2)).default([])
|
|
18923
18926
|
}).prefault({}),
|
|
18924
18927
|
/** v1.1 E4/F1: seats only from allowed model-id prefixes; deny at launch. */
|
|
18925
18928
|
modelPolicy: external_exports.object({
|
|
@@ -31968,7 +31971,53 @@ function contentMatches(input2, needle) {
|
|
|
31968
31971
|
}
|
|
31969
31972
|
return false;
|
|
31970
31973
|
}
|
|
31971
|
-
|
|
31974
|
+
var SHELL_TOOLS = ["bash"];
|
|
31975
|
+
var SHELL_KEYS = ["command"];
|
|
31976
|
+
var LINEAGE_CHUNK = 256;
|
|
31977
|
+
var LINEAGE_SAMPLES = 32;
|
|
31978
|
+
var LINEAGE_UNION_RATIO = 0.6;
|
|
31979
|
+
var LINEAGE_MIN_WINDOWS = 3;
|
|
31980
|
+
function sampleWindows(text) {
|
|
31981
|
+
const total = Math.floor(text.length / LINEAGE_CHUNK);
|
|
31982
|
+
if (total < LINEAGE_MIN_WINDOWS) return [];
|
|
31983
|
+
const step = Math.max(1, Math.floor(total / LINEAGE_SAMPLES));
|
|
31984
|
+
const out = [];
|
|
31985
|
+
for (let i = 0; i < total; i += step) out.push(text.slice(i * LINEAGE_CHUNK, (i + 1) * LINEAGE_CHUNK));
|
|
31986
|
+
return out.slice(0, LINEAGE_SAMPLES);
|
|
31987
|
+
}
|
|
31988
|
+
function beginLineage(artifact) {
|
|
31989
|
+
if (artifact.kind !== "path" || artifact.mtimeMs === void 0) return null;
|
|
31990
|
+
const chunks = sampleWindows(artifact.text);
|
|
31991
|
+
if (chunks.length < LINEAGE_MIN_WINDOWS) return null;
|
|
31992
|
+
return {
|
|
31993
|
+
chunks,
|
|
31994
|
+
need: Math.max(2, Math.ceil(chunks.length * LINEAGE_UNION_RATIO)),
|
|
31995
|
+
mtimeMs: artifact.mtimeMs,
|
|
31996
|
+
matched: /* @__PURE__ */ new Map(),
|
|
31997
|
+
copiedAfterMtime: 0,
|
|
31998
|
+
timeless: 0
|
|
31999
|
+
};
|
|
32000
|
+
}
|
|
32001
|
+
function messageTime(m) {
|
|
32002
|
+
const t = m.info?.time?.created;
|
|
32003
|
+
return typeof t === "number" ? t : void 0;
|
|
32004
|
+
}
|
|
32005
|
+
function partCandidates(p) {
|
|
32006
|
+
const shell = p.tool !== void 0 && SHELL_TOOLS.includes(p.tool);
|
|
32007
|
+
if (!shell && !isDraftingTool(p.tool)) return [];
|
|
32008
|
+
const keys = shell ? SHELL_KEYS : CONTENT_KEYS;
|
|
32009
|
+
const out = [];
|
|
32010
|
+
for (const key of keys) {
|
|
32011
|
+
const v = p.input?.[key];
|
|
32012
|
+
if (typeof v === "string" && v.length >= LINEAGE_CHUNK) out.push(v);
|
|
32013
|
+
}
|
|
32014
|
+
return out;
|
|
32015
|
+
}
|
|
32016
|
+
function lineageVerdict(lg, artifact) {
|
|
32017
|
+
const sources = [.../* @__PURE__ */ new Set([...lg.matched.values()])].slice(0, 3).join("; ");
|
|
32018
|
+
return `content-lineage evidence: ${String(lg.matched.size)} of ${String(lg.chunks.length)} verbatim ${String(LINEAGE_CHUNK)}-byte windows of ${artifact.source} appear in convener-chain tool inputs that predate its mtime (${sources}) \u2014 the chain authored this artifact's bytes before/independently of this path`;
|
|
32019
|
+
}
|
|
32020
|
+
function scanMessages(rows, convenerIndex, sid, artifact, lg) {
|
|
31972
32021
|
for (const [mi, m] of rows.entries()) {
|
|
31973
32022
|
for (const p of m.parts ?? []) {
|
|
31974
32023
|
if (p.type === "tool" && isDraftingTool(p.tool)) {
|
|
@@ -31982,6 +32031,26 @@ function scanMessages(rows, convenerIndex, sid, artifact) {
|
|
|
31982
32031
|
if (artifact.kind === "inline" && m.info?.role === "assistant" && p.type === "text" && typeof p.text === "string" && artifact.text.trim().length >= MIN_INLINE_MATCH && p.text.includes(artifact.text.trim())) {
|
|
31983
32032
|
return `drafting evidence: assistant text in convener-chain session ${sid} (chain position ${String(convenerIndex)}, message ${String(mi)}) contains the inline artifact`;
|
|
31984
32033
|
}
|
|
32034
|
+
if (p.type === "tool" && lg !== null) {
|
|
32035
|
+
const t = messageTime(m);
|
|
32036
|
+
if (t === void 0) {
|
|
32037
|
+
lg.timeless += 1;
|
|
32038
|
+
continue;
|
|
32039
|
+
}
|
|
32040
|
+
if (t > lg.mtimeMs) {
|
|
32041
|
+
lg.copiedAfterMtime += 1;
|
|
32042
|
+
continue;
|
|
32043
|
+
}
|
|
32044
|
+
const label = `${p.tool ?? "tool"}@${sid}:${String(mi)}`;
|
|
32045
|
+
for (const cand of partCandidates(p)) {
|
|
32046
|
+
if (artifact.kind === "path" && cand.includes(artifact.text.trim())) {
|
|
32047
|
+
return `${lineageVerdict({ ...lg, matched: /* @__PURE__ */ new Map([[0, label]]) }, artifact)}; full-text containment in ${label}`;
|
|
32048
|
+
}
|
|
32049
|
+
for (const [wi, chunk] of lg.chunks.entries()) {
|
|
32050
|
+
if (!lg.matched.has(wi) && cand.includes(chunk)) lg.matched.set(wi, label);
|
|
32051
|
+
}
|
|
32052
|
+
}
|
|
32053
|
+
}
|
|
31985
32054
|
}
|
|
31986
32055
|
}
|
|
31987
32056
|
return null;
|
|
@@ -32022,6 +32091,7 @@ async function assessIndependence(client, directory, convenerSessionID, artifact
|
|
|
32022
32091
|
seen.add(parent);
|
|
32023
32092
|
current = parent;
|
|
32024
32093
|
}
|
|
32094
|
+
const lg = beginLineage(artifact);
|
|
32025
32095
|
let scanFailed = "";
|
|
32026
32096
|
for (const [i, sid] of chain.entries()) {
|
|
32027
32097
|
const res = await client.session.messages({ path: { id: sid }, query: { directory } });
|
|
@@ -32029,22 +32099,33 @@ async function assessIndependence(client, directory, convenerSessionID, artifact
|
|
|
32029
32099
|
scanFailed = scanFailed.length > 0 ? scanFailed : `messages(${sid}) refused`;
|
|
32030
32100
|
continue;
|
|
32031
32101
|
}
|
|
32032
|
-
const hit = scanMessages(res.data, i, sid, artifact);
|
|
32102
|
+
const hit = scanMessages(res.data, i, sid, artifact, lg);
|
|
32033
32103
|
if (hit !== null) {
|
|
32034
32104
|
return { status: "NOT-INDEPENDENT", convenerChain: chain, evidence: hit };
|
|
32035
32105
|
}
|
|
32036
32106
|
}
|
|
32107
|
+
if (lg !== null && lg.matched.size >= lg.need) {
|
|
32108
|
+
return { status: "NOT-INDEPENDENT", convenerChain: chain, evidence: lineageVerdict(lg, artifact) };
|
|
32109
|
+
}
|
|
32037
32110
|
if (scanFailed.length > 0 || walkIncomplete) {
|
|
32038
32111
|
const why = [walkIncomplete ? walkReason : "", scanFailed].filter((s) => s.length > 0).join("; ");
|
|
32039
32112
|
return { status: "UNVERIFIABLE", convenerChain: chain, evidence: `partial read: ${why} \u2014 independence was NOT established` };
|
|
32040
32113
|
}
|
|
32041
32114
|
const note = walkReason.length > 0 ? `; walk note: ${walkReason}` : "";
|
|
32115
|
+
const lineageNote = lineageFaceNote(lg);
|
|
32042
32116
|
return {
|
|
32043
32117
|
status: "INDEPENDENT",
|
|
32044
32118
|
convenerChain: chain,
|
|
32045
|
-
evidence: `scanned ${String(chain.length)} session(s) of the convener chain to root; no drafting evidence within read scope${note}`
|
|
32119
|
+
evidence: `scanned ${String(chain.length)} session(s) of the convener chain to root; no drafting evidence within read scope${lineageNote}${note}`
|
|
32046
32120
|
};
|
|
32047
32121
|
}
|
|
32122
|
+
function lineageFaceNote(lg) {
|
|
32123
|
+
if (lg === null) return "";
|
|
32124
|
+
const parts = [`${String(lg.matched.size)}/${String(lg.chunks.length)} lineage windows matched (need ${String(lg.need)})`];
|
|
32125
|
+
if (lg.copiedAfterMtime > 0) parts.push(`${String(lg.copiedAfterMtime)} part(s) excluded as post-mtime copies`);
|
|
32126
|
+
if (lg.timeless > 0) parts.push(`${String(lg.timeless)} part(s) lacked timestamps`);
|
|
32127
|
+
return `; content-lineage: ${parts.join(", ")}`;
|
|
32128
|
+
}
|
|
32048
32129
|
function independenceLabel(v) {
|
|
32049
32130
|
return v.status === "INDEPENDENT" ? "independence=INDEPENDENT" : `independence=${v.status} \u2014 ${v.evidence}`;
|
|
32050
32131
|
}
|
|
@@ -32523,7 +32604,7 @@ async function readArtifact(raw, cwd) {
|
|
|
32523
32604
|
} catch (err) {
|
|
32524
32605
|
return { ok: false, error: `artifact "${path}": ${errMessage(err)}` };
|
|
32525
32606
|
}
|
|
32526
|
-
return { ok: true, kind: "path", source: path, text };
|
|
32607
|
+
return { ok: true, kind: "path", source: path, text, mtimeMs: info.mtimeMs };
|
|
32527
32608
|
}
|
|
32528
32609
|
function errMessage(err) {
|
|
32529
32610
|
if (err instanceof Error) return err.message;
|
|
@@ -33855,7 +33936,11 @@ function toEngineClient(sdk) {
|
|
|
33855
33936
|
...m.info.providerID !== void 0 && { providerID: m.info.providerID },
|
|
33856
33937
|
...m.info.modelID !== void 0 && { modelID: m.info.modelID },
|
|
33857
33938
|
...m.info.time !== void 0 && {
|
|
33858
|
-
time: {
|
|
33939
|
+
time: {
|
|
33940
|
+
...m.info.time.completed !== void 0 && { completed: m.info.time.completed },
|
|
33941
|
+
// W1-v2 lineage time gate needs creation stamps too
|
|
33942
|
+
..."created" in m.info.time && m.info.time.created !== void 0 && { created: m.info.time.created }
|
|
33943
|
+
}
|
|
33859
33944
|
},
|
|
33860
33945
|
...m.info.error !== void 0 && { error: m.info.error }
|
|
33861
33946
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sibyl-system",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Sibyl-System: democratic-centralism review chamber (one conclusion, one voice) + fail-closed council voting + dynamic swarm plugin for OpenCode",
|